Skip to content
Closed
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
9 changes: 9 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,15 @@ code-mode `exec` has the call converted into the matching `tools.<helper>(...)`
`exec`. A catalog that genuinely declares the bare goal tool keeps it, and a catalog that declares
neither the tool nor `exec` still rejects the call as undeclared.

On routed conversions with a verified freeform code-mode `exec` catalog, structured calls
sent directly to
`mcp__<server>__<tool>` (including a provider-added `default.` prefix) are also
wrapped as nested host-tool calls. This avoids a retry
caused solely by a model omitting the `exec` wrapper. Explicitly declared MCP tools
keep their normal behavior; an ordinary JSON function named `exec` does not enable
this repair. Unknown tools still fail at the host. Tool-call records printed as
ordinary answer text are not executed by this compatibility rule.

For routed Responses turns, an explicit tool-enforcement policy also rejects client tool calls if
the request's declared-tool catalog is unavailable. An empty declared catalog rejects every client
tool call; Chat and Anthropic clients retain their own tool-validation responsibility.
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,7 @@
"responses-bare-echo-helper-fence.test.ts": "responses",
"responses-canonical-only-top-level-fields.test.ts": "responses",
"responses-code-mode-goal-helpers.test.ts": "responses",
"responses-code-mode-mcp-direct.test.ts": "responses",
"responses-code-mode-patch-compile.test.ts": "responses",
"responses-code-mode-shell-compile.test.ts": "responses",
"responses-compact-handoff-admission.test.ts": "responses",
Expand Down
4 changes: 3 additions & 1 deletion src/bridge/response-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function buildResponseJSONWithBudget(
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
/** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
declaredToolNames?: ReadonlySet<string>;
/** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
bareCustomToolNames?: ReadonlySet<string>;
/** See `bridgeToResponsesSSE`: enforcement is separate from normalization (#4735). */
enforceDeclaredToolNames?: boolean;
/** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
Expand Down Expand Up @@ -451,7 +453,7 @@ function buildResponseJSONWithBudget(
rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
}
flushToolCall();
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
if (
(options?.enforceDeclaredToolNames === true || options?.declaredToolNames != null)
&& options?.enforceDeclaredToolNames !== false
Expand Down
4 changes: 3 additions & 1 deletion src/bridge/sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export function bridgeToResponsesSSE(
onUsage?: (usage: OcxUsage | undefined) => void;
/** Request-visible tool names. Required for client calls when enforcement is explicitly enabled. */
declaredToolNames?: ReadonlySet<string>;
/** Bare custom declarations; unlike freeformToolNames, excludes foreign namespace children. */
bareCustomToolNames?: ReadonlySet<string>;
/**
* Whether `declaredToolNames` is an authorization boundary this proxy enforces, or only the
* catalog used to normalize provider-invented names back to declared ones.
Expand Down Expand Up @@ -1013,7 +1015,7 @@ export function bridgeToResponsesSSE(
rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
}
if (currentToolCall) closeCurrentToolCall();
const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames, undefined, options?.bareCustomToolNames);
const codeModeHelperName = effectiveName === "exec" && event.name !== effectiveName
? event.name
: undefined;
Expand Down
15 changes: 14 additions & 1 deletion src/responses/code-mode-helper-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import {
normalizeApplyPatchDelimiters,
unwrapFreeformToolInput,
} from "./apply-patch-envelope";
import { declaresCodeModeExec } from "../types/tools";
import { declaresCodeModeExec, isCodeModeMcpDirectName } from "../types/tools";
import { parseCodeModeShellInput } from "./code-mode-shell-input";

function isPlainObject(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}

/** A nested host tool reachable as `tools.<name>` when the flattened name is one identifier. */
const CODE_MODE_IDENTIFIER_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;

/**
* Convert a nested Code Mode helper call into unified-exec JavaScript.
*
Expand Down Expand Up @@ -95,6 +98,16 @@ export function compileCodeModeHelperInput(
if (helperName === "create_goal" || helperName === "get_goal" || helperName === "update_goal") {
return `const result = await tools.${helperName}(${JSON.stringify(args)});\ntext(result);`;
}
if (isCodeModeMcpDirectName(helperName)) {
// Direct `mcp__<server>__<tool>` call under a code-mode catalog: the emitted name IS the
// nested host tool's name, so compile to the same `tools.<name>(args)` the model could
// have written. Dot access when the name is a clean identifier (the common case); bracket
// access otherwise, so a hyphenated server name still addresses the same tool.
const target = CODE_MODE_IDENTIFIER_NAME.test(helperName)
? `tools.${helperName}`
: `tools[${JSON.stringify(helperName)}]`;
return `const result = await ${target}(${JSON.stringify(args)});\ntext(result);`;
}
return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
}

Expand Down
2 changes: 1 addition & 1 deletion src/responses/custom-tool-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function routedCustomToolTargetName(
if (wireName === undefined) return undefined;
if (names.has(wireName)) return wireName;
if (!isPlainObject(value) || typeof value.namespace === "string") return undefined;
const normalized = normalizeDeclaredToolName(wireName, declaredNames);
const normalized = normalizeDeclaredToolName(wireName, declaredNames, undefined, names);
return normalized !== wireName && names.has(normalized) ? normalized : undefined;
}

Expand Down
1 change: 1 addition & 0 deletions src/server/inference/client-encoder-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface ClientEncodedDelivery {
declaredToolNames?: ReadonlySet<string>;
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
freeformToolNames?: Set<string>;
bareCustomToolNames?: ReadonlySet<string>;
toolSearchToolNames?: Set<string>;
};
stallTimeoutSec?: number;
Expand Down
49 changes: 43 additions & 6 deletions src/server/responses-undeclared-tool-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,35 @@ export function collectDeclaredBareWireToolNames(body: unknown): Set<string> {
return names;
}

/** Custom declarations with no foreign namespace, for code-mode exec recovery. */
export function collectDeclaredBareCustomWireToolNames(body: unknown): Set<string> {
const names = new Set<string>();
if (!isPlainObject(body)) return names;
const specGroups: unknown[] = [body.tools];
if (Array.isArray(body.input)) {
for (const item of body.input) {
if (isPlainObject(item) && (item.type === "additional_tools" || item.type === "tool_search_output")) {
specGroups.push(item.tools);
}
}
}
const add = (tool: unknown): void => {
if (isPlainObject(tool) && tool.type === "custom" && typeof tool.name === "string") names.add(tool.name);
};
for (const specs of specGroups) {
if (!Array.isArray(specs)) continue;
for (const spec of specs) {
if (!isPlainObject(spec)) continue;
if (spec.type === "namespace") {
if (spec.name === BUILTIN_FUNCTIONS_NAMESPACE && Array.isArray(spec.tools)) {
for (const inner of spec.tools) add(inner);
}
} else add(spec);
}
}
return names;
}

function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
if (!Array.isArray(specs)) return;
for (const spec of specs) {
Expand Down Expand Up @@ -349,6 +378,7 @@ export function hasExplicitWireToolCatalog(body: unknown): boolean {
* @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
* @param providerExecutedCallTypes - Call types executed by the provider.
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
* @returns The undeclared tool call name if unauthorized, or undefined if permitted.
*/
function undeclaredNameInItem(
Expand All @@ -357,6 +387,7 @@ function undeclaredNameInItem(
declaredNamelessClientCallTypes: ReadonlySet<string>,
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
declaredBare?: ReadonlySet<string>,
declaredCustom?: ReadonlySet<string>,
): string | undefined {
if (!isPlainObject(item)) return undefined;
if (typeof item.type !== "string") return undefined;
Expand Down Expand Up @@ -396,7 +427,7 @@ function undeclaredNameInItem(
) return undefined;
return name;
}
const effectiveName = normalizeDeclaredToolName(name, declared, declaredBare);
const effectiveName = normalizeDeclaredToolName(name, declared, declaredBare, declaredCustom);
if (declared.has(effectiveName)) return undefined;
return name;
}
Expand All @@ -409,6 +440,7 @@ function undeclaredNameInItem(
* @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
* @param providerExecutedCallTypes - Call types executed by the provider.
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
* @returns The name of the first undeclared tool call, or undefined.
*/
export function undeclaredToolCallName(
Expand All @@ -417,18 +449,19 @@ export function undeclaredToolCallName(
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
declaredBare?: ReadonlySet<string>,
declaredCustom?: ReadonlySet<string>,
): string | undefined {
if (!isPlainObject(payload)) return undefined;
if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
}
if (payload.type === "response.function_call_arguments.done" && typeof payload.name === "string") {
const fakeItem = { type: "function_call", name: payload.name, namespace: payload.namespace };
return undeclaredNameInItem(fakeItem, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
return undeclaredNameInItem(fakeItem, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
}
// Sparse gateways skip incremental items and only ever ship the terminal snapshot.
if (payload.type === "response.completed" || payload.type === "response.incomplete") {
return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
}
return undefined;
}
Expand All @@ -441,6 +474,7 @@ export function undeclaredToolCallName(
* @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
* @param providerExecutedCallTypes - Call types executed by the provider.
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
* @returns The name of the first undeclared tool call, or undefined.
*/
export function undeclaredToolCallNameInResponse(
Expand All @@ -449,10 +483,11 @@ export function undeclaredToolCallNameInResponse(
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
declaredBare?: ReadonlySet<string>,
declaredCustom?: ReadonlySet<string>,
): string | undefined {
if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
for (const item of response.output) {
const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
if (name !== undefined) return name;
}
return undefined;
Expand Down Expand Up @@ -667,13 +702,15 @@ function failedBlocks(name: string, newline: string): readonly string[] {
* @param declaredNamelessClientCallTypes - Nameless client call types declared by the request.
* @param providerExecutedCallTypes - Call types executed by the provider.
* @param declaredBare - Explicitly declared bare tool names without namespace provenance.
* @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
* @returns An SSE block rewrite function.
*/
export function createUndeclaredToolCallGuardBlockRewrite(
declared: ReadonlySet<string>,
declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
declaredBare?: ReadonlySet<string>,
declaredCustom?: ReadonlySet<string>,
): SseBlockRewrite {
let tripped = false;
return (block: string) => {
Expand All @@ -686,7 +723,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
} catch {
return [block];
}
const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare);
const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
if (name !== undefined) {
tripped = true;
return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
Expand Down
10 changes: 6 additions & 4 deletions src/server/responses/adapter-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function deliverAdapterResponse(
continuation: fetchGuardedEmptyCompletionRetry,
})
: eventStream;
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
// One completion owner for both deliveries: the bridge calls it from its terminal, the
// direct client encoder from the fold of the same events.
const onCompletedResponse = (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
Expand Down Expand Up @@ -153,7 +153,7 @@ export async function deliverAdapterResponse(
fold: {
replayCacheScope: parsed._reasoningReplayScope,
hideThinkingSummary: parsed.options.hideThinkingSummary,
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames,
toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames,
},
stallTimeoutSec: config.stallTimeoutSec,
localUpstream,
Expand All @@ -176,8 +176,9 @@ export async function deliverAdapterResponse(
localUpstream,
hideThinkingSummary: parsed.options.hideThinkingSummary,
declaredToolNames,
bareCustomToolNames,
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
toolParameterSchemas,
toolParameterSchemas,
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
...(routedCompaction ? { compaction: true } : {}),
// Same grok-surface split as the runTurn branch above.
Expand Down Expand Up @@ -236,14 +237,15 @@ export async function deliverAdapterResponse(
} finally {
cleanupUpstreamAbort();
}
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
let providerState: OcxProviderContinuationState | undefined;
const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
translatorBudget,
replayCacheScope: parsed._reasoningReplayScope,
hideThinkingSummary: parsed.options.hideThinkingSummary,
toolNsMap,
declaredToolNames,
bareCustomToolNames,
enforceDeclaredToolNames: options.inboundWire !== "chat" && options.inboundWire !== "anthropic",
toolParameterSchemas,
freeformToolNames,
Expand Down
4 changes: 4 additions & 0 deletions src/server/responses/passthrough-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ export async function deliverPassthroughResponse(
| "declaredNamelessClientCallTypes"
| "providerExecutedCallTypes"
| "declaredBareWireToolNames"
| "recoverableBareCustomWireToolNames"
| "rememberPassthroughResponse"
| "noteInspectedPayload"
| "normalizeFunctionCompletionJson"
Expand All @@ -336,6 +337,7 @@ export async function deliverPassthroughResponse(
declaredNamelessClientCallTypes,
providerExecutedCallTypes,
declaredBareWireToolNames,
recoverableBareCustomWireToolNames,
rememberPassthroughResponse,
noteInspectedPayload,
normalizeFunctionCompletionJson,
Expand Down Expand Up @@ -732,6 +734,7 @@ export async function deliverPassthroughResponse(
declaredNamelessClientCallTypes,
providerExecutedCallTypes,
declaredBareWireToolNames,
recoverableBareCustomWireToolNames,
)
: undefined,
grokUpstreamEchoEnabled
Expand Down Expand Up @@ -998,6 +1001,7 @@ export async function deliverPassthroughResponse(
declaredNamelessClientCallTypes,
providerExecutedCallTypes,
declaredBareWireToolNames,
recoverableBareCustomWireToolNames,
);
} catch {
return undefined;
Expand Down
Loading
Loading