Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2856,6 +2856,9 @@ export class CodexSecurity {
runtimePaths,
options.auth,
git,
mode === "deep" && !directDeepScan
? deepScanParentConfig(session.sessionConfig)
Comment on lines +2859 to +2860

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the direct namespace override to the scan turn

For a parent-driven Deep Scan with postScanPrompt, this configuration is attached to the thread for its entire lifetime, so the later follow-up at runPostScan also sees Codex Security tools as direct-only instead of using the caller's original code-mode settings. That needlessly changes routing and exposes the plugin's full direct schemas during caller-supplied post-scan work; run the follow-up with the original configuration or otherwise limit this override to the coordinator turn.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that the coordinator-only setting leaked into the post-scan turn. #2012 resumes the original thread with the caller’s original client for a requested post-scan prompt. In the installed comparison, that prompt could retrieve the sealed scan with the caller’s tools. The serial controls passed; two lookup failures in the first concurrent custom-plugin batch remain unexplained. The follow-up is still under review.

: undefined,
);
const threadOptions: ThreadOptions = {
threadSource: CODEX_SECURITY_THREAD_SOURCES.scan,
Expand Down Expand Up @@ -5810,6 +5813,32 @@ export function classifyConnectionFailure(
return "unknown";
}

function deepScanParentConfig(config: JsonObject): JsonObject {
const resolved = resolveCodexProfile(config);
const features = resolved["features"] ?? {};
if (!isRecord(features)) return resolved;
const mode = features["code_mode"];
const settings = typeof mode === "boolean" ? { enabled: mode } : (mode ?? {});
// Leave malformed user settings for the native config parser to reject.
if (!isRecord(settings)) return resolved;
const direct = settings["direct_only_tool_namespaces"] ?? [];
if (!Array.isArray(direct)) return resolved;
// The native parent waits for direct tool calls before it can end its turn.
// A code-mode cell can yield the still-running coordinator to the model.
return {
...resolved,
features: {
...features,
code_mode: {
...settings,
direct_only_tool_namespaces: [
...new Set([...direct, "mcp__codex_security", "codex_security"]),
Comment on lines +5834 to +5835

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the coordinator's actual callable namespace

When another configured or enterprise-managed MCP server normalizes to codex_security—for example, a dotted codex.security registration—Codex disambiguates the collision by adding a hash suffix to both callable namespaces. Neither hard-coded entry then matches the Codex Security coordinator, so it remains routed through code mode and the early-turn termination this change is meant to prevent can still fail the scan. Derive the collision-resolved namespace from Codex rather than assuming these two base spellings.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced on the merged code. With two other servers advertising the same tool name, the original coordinator was available only inside a yielding cell. In #2012, the native server-name selector keeps the genuine coordinator direct when a free name is available. The installed CLI completed the original scan in one turn, and neither other server was called. The follow-up is still in draft; an empty peer allowlist and alternate plugin declarations have separate regression coverage in progress.

Comment on lines +5834 to +5835

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve direct-only namespaces from native config layers

When /etc/ProgramData, cloud-managed, or another native Codex configuration layer already supplies features.code_mode.direct_only_tool_namespaces, that value is absent from session.sessionConfig, which is built only from the wrapper's defaults and explicit overrides. Supplying this new higher-precedence array therefore replaces rather than extends the native list, silently routing every previously direct-only namespace back through code mode during the scan. Preserve the effective native entries instead of constructing the replacement solely from the wrapper snapshot.

AGENTS.md reference: AGENTS.md:L14-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with an installed CLI and a machine-wide native tool setting: the merged code removed that existing direct tool from the parent menu. #2012 reads the effective native settings with the scan’s ordered overrides, then extends the existing list. The machine-wide tool and the coordinator were both direct in the native comparison; explicit cell-only policy was preserved on both revisions. The fix is in the follow-up draft.

],
},
},
};
}

export function scanRuntimeCodexConfig(
config: JsonObject,
protectedCredentialHome?: string,
Expand Down
29 changes: 27 additions & 2 deletions sdk/typescript/tests-ts/api-deep-scan-runtime-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ test.each([
runtimeNode: null,
route: "direct",
},
{
name: "invalid code mode supplied by the caller",
dependencyNode: null,
runtimeNode: "selected runtime",
route: "parent",
codeMode: "mistyped",
},
])("Deep Scan respects $name", async (scenario) => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
Expand All @@ -59,6 +66,9 @@ test.each([
const runtimeEnvironment: Record<string, string> =
selectedNode === undefined ? {} : { CODEX_MCP_NODE_PATH: selectedNode };
const originalRuntimeEnvironment = { ...runtimeEnvironment };
const codeMode = scenario.codeMode ?? {
direct_only_tool_namespaces: ["existing_tools"],
};
const parentTurn = mock(async () => {
throw new Error("parent route reached");
});
Expand All @@ -75,7 +85,19 @@ test.each([
expect(options.config).toMatchObject({
model: "gpt-6.1-sol",
model_reasoning_effort: "high",
features: { shell_tool: false },
features: {
shell_tool: false,
code_mode:
typeof codeMode === "string" || scenario.route === "direct"
? codeMode
: {
direct_only_tool_namespaces: [
"existing_tools",
"mcp__codex_security",
"codex_security",
],
},
},
});
return {
startThread: () => ({ id: null, runStreamed: parentTurn }),
Expand All @@ -86,7 +108,10 @@ test.each([
codexOverrides: {
model: "gpt-6.1-sol",
model_reasoning_effort: "high",
features: { shell_tool: false },
features: {
shell_tool: false,
code_mode: codeMode,
},
},
},
{
Expand Down
Loading