diff --git a/extensions/openai/native-web-search.ts b/extensions/openai/native-web-search.ts index b002c25e8fad..00d4a5f5bb2e 100644 --- a/extensions/openai/native-web-search.ts +++ b/extensions/openai/native-web-search.ts @@ -92,7 +92,10 @@ export function createOpenAINativeWebSearchWrapper( ): StreamFn { return createPayloadPatchStreamWrapper( baseStreamFn, - ({ payload }) => { + ({ payload, options }) => { + ( + options as { openclawCodeModeAllowedHostedToolTypes?: Set } | undefined + )?.openclawCodeModeAllowedHostedToolTypes?.add(OPENAI_WEB_SEARCH_TOOL.type); patchOpenAINativeWebSearchPayload(payload); }, { diff --git a/extensions/openai/openai-provider.test.ts b/extensions/openai/openai-provider.test.ts index 2891f7009830..1f2e4afd9a2b 100644 --- a/extensions/openai/openai-provider.test.ts +++ b/extensions/openai/openai-provider.test.ts @@ -192,6 +192,11 @@ function runWrappedPayloadCase(params: { agentId?: string; nativeWebSearchAllowedByToolPolicy?: boolean; payload?: Record; + context?: Context; + streamOptions?: SimpleStreamOptions & { + openclawCodeModeToolSurface?: boolean; + openclawCodeModeAllowedHostedToolTypes?: Set; + }; }) { const payload = params.payload ?? { store: false }; let capturedOptions: SimpleStreamOptions | undefined; @@ -212,8 +217,7 @@ function runWrappedPayloadCase(params: { streamFn: baseStreamFn, } as never); - const context: Context = { messages: [] }; - void streamFn?.(params.model, context, {}); + void streamFn?.(params.model, params.context ?? { messages: [] }, params.streamOptions ?? {}); return { payload, @@ -2338,6 +2342,53 @@ describe("buildOpenAIProvider", () => { ]); }); + it("authorizes native OpenAI web search through the code mode wrapper chain", () => { + const provider = buildOpenAIProvider(); + const wrap = provider.wrapStreamFn; + if (!wrap) { + throw new Error("expected OpenAI wrapper"); + } + const allowedHostedToolTypes = new Set(); + + const result = runWrappedPayloadCase({ + wrap, + provider: "openai", + modelId: "gpt-5.4", + model: { + api: "openai-responses", + provider: "openai", + id: "gpt-5.4", + baseUrl: "https://api.openai.com/v1", + } as Model<"openai-responses">, + context: { + messages: [], + tools: [ + { name: "exec", description: "", parameters: {} }, + { name: "wait", description: "", parameters: {} }, + ], + }, + streamOptions: { + openclawCodeModeToolSurface: true, + openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes, + }, + payload: { + tools: [ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + { type: "function", name: "rogue" }, + { type: "function", name: "web_search" }, + ], + }, + }); + + expect(result.payload.tools).toEqual([ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + { type: "web_search" }, + ]); + expect(allowedHostedToolTypes).toEqual(new Set(["web_search"])); + }); + it("keeps one native OpenAI web search tool when the payload is already patched", () => { const provider = buildOpenAIProvider(); const wrap = provider.wrapStreamFn; @@ -2373,12 +2424,17 @@ describe("buildOpenAIProvider", () => { throw new Error("expected OpenAI wrapper"); } + const allowedHostedToolTypes = new Set(); const result = runWrappedPayloadCase({ wrap, provider: "openai", modelId: "gpt-5.4", agentId: "main", nativeWebSearchAllowedByToolPolicy: false, + streamOptions: { + openclawCodeModeToolSurface: true, + openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes, + }, cfg: { agents: { list: [ @@ -2407,6 +2463,7 @@ describe("buildOpenAIProvider", () => { { type: "function", name: "read" }, { type: "function", name: "web_search" }, ]); + expect(allowedHostedToolTypes).toEqual(new Set()); }); it("raises minimal reasoning when native OpenAI web search is injected", () => { @@ -2444,11 +2501,15 @@ describe("buildOpenAIProvider", () => { throw new Error("expected OpenAI wrapper"); } + const disabledAllowedHostedToolTypes = new Set(); const disabled = runWrappedPayloadCase({ wrap, provider: "openai", modelId: "gpt-5.4", cfg: { tools: { web: { search: { enabled: false } } } }, + streamOptions: { + openclawCodeModeAllowedHostedToolTypes: disabledAllowedHostedToolTypes, + }, model: { api: "openai-responses", provider: "openai", @@ -2457,6 +2518,7 @@ describe("buildOpenAIProvider", () => { } as Model<"openai-responses">, payload: { tools: [{ type: "function", name: "web_search" }] }, }); + const proxiedAllowedHostedToolTypes = new Set(); const proxied = runWrappedPayloadCase({ wrap, provider: "openai", @@ -2467,11 +2529,16 @@ describe("buildOpenAIProvider", () => { id: "gpt-5.4", baseUrl: "https://example-proxy.invalid/v1", } as Model<"openai-responses">, + streamOptions: { + openclawCodeModeAllowedHostedToolTypes: proxiedAllowedHostedToolTypes, + }, payload: { tools: [{ type: "function", name: "web_search" }] }, }); expect(disabled.payload.tools).toEqual([{ type: "function", name: "web_search" }]); expect(proxied.payload.tools).toEqual([{ type: "function", name: "web_search" }]); + expect(disabledAllowedHostedToolTypes).toEqual(new Set()); + expect(proxiedAllowedHostedToolTypes).toEqual(new Set()); }); it("keeps managed web_search when another search provider is configured", () => { @@ -2482,11 +2549,15 @@ describe("buildOpenAIProvider", () => { throw new Error("expected OpenAI wrapper"); } + const allowedHostedToolTypes = new Set(); const result = runWrappedPayloadCase({ wrap, provider: "openai", modelId: "gpt-5.4", cfg: { tools: { web: { search: { enabled: true, provider: "brave" } } } }, + streamOptions: { + openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes, + }, model: { api: "openai-responses", provider: "openai", @@ -2497,6 +2568,7 @@ describe("buildOpenAIProvider", () => { }); expect(result.payload.tools).toEqual([{ type: "function", name: "web_search" }]); + expect(allowedHostedToolTypes).toEqual(new Set()); }); it("preserves explicit OpenAI responses transport overrides", () => { diff --git a/packages/ai/src/provider-options.ts b/packages/ai/src/provider-options.ts index 5e569a0a165a..ff534e6d83cb 100644 --- a/packages/ai/src/provider-options.ts +++ b/packages/ai/src/provider-options.ts @@ -60,6 +60,7 @@ export type BaseOpenAIStreamOptions = StreamOptions & { firstEventTimeoutMs?: number; onFirstEventTimeout?: (reason: Error) => void; openclawCodeModeToolSurface?: boolean; + openclawCodeModeAllowedHostedToolTypes?: Set; frequencyPenalty?: number; presencePenalty?: number; seed?: number; diff --git a/packages/ai/src/transports/openai-responses-client.ts b/packages/ai/src/transports/openai-responses-client.ts index bb71d4ef7a69..6519582b5b34 100644 --- a/packages/ai/src/transports/openai-responses-client.ts +++ b/packages/ai/src/transports/openai-responses-client.ts @@ -255,8 +255,9 @@ function createResponsesTransportExecutor(config: ResponsesTransportExecutorOpti ?.openclawCodeModeToolSurface === true ) { const visibleToolNames = resolveCodeModeResponsesVisibleToolNames(context); - enforceCodeModeResponsesToolSurface(params, visibleToolNames); - assertCodeModeResponsesToolSurface(params, visibleToolNames); + const allowedHostedToolTypes = responsesOptions?.openclawCodeModeAllowedHostedToolTypes; + enforceCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes); + assertCodeModeResponsesToolSurface(params, visibleToolNames, allowedHostedToolTypes); } return params; }; diff --git a/packages/ai/src/transports/openai-responses-prompt-observer.test.ts b/packages/ai/src/transports/openai-responses-prompt-observer.test.ts index 3305476bb051..eb78cc41702e 100644 --- a/packages/ai/src/transports/openai-responses-prompt-observer.test.ts +++ b/packages/ai/src/transports/openai-responses-prompt-observer.test.ts @@ -416,6 +416,7 @@ describe("OpenAI Responses provider prompt observer", () => { context: createContext(prompt, { tools: [tool("exec"), tool("wait")] as never }), options: { openclawCodeModeToolSurface: true, + openclawCodeModeAllowedHostedToolTypes: new Set(["web_search"]), onPayload: async () => { await Promise.resolve(); return { @@ -430,7 +431,13 @@ describe("OpenAI Responses provider prompt observer", () => { content: [{ type: "input_image", image_url: "data:image/png;base64,invalid!" }], }, ], - tools: [tool("exec"), tool("wait"), tool("rogue")], + tools: [ + tool("exec"), + tool("wait"), + tool("rogue"), + { type: "web_search" }, + { type: "file_search" }, + ], }; }, }, @@ -439,7 +446,7 @@ describe("OpenAI Responses provider prompt observer", () => { expect(run.order).toEqual(["observe", "openai.create"]); expect(run.observations[0]?.matchesAssembledPrompt).toBe(true); expect(run.requests[0]?.metadata).toEqual({ caller: "kept", host: "added" }); - expect(run.requests[0]?.tools).toEqual([tool("exec"), tool("wait")]); + expect(run.requests[0]?.tools).toEqual([tool("exec"), tool("wait"), { type: "web_search" }]); expect(JSON.stringify(run.requests[0]?.input)).toContain("omitted image payload"); }); diff --git a/packages/ai/src/transports/openai-transport-params.code-mode.test.ts b/packages/ai/src/transports/openai-transport-params.code-mode.test.ts index 02a059b94e41..b2700d3b7a17 100644 --- a/packages/ai/src/transports/openai-transport-params.code-mode.test.ts +++ b/packages/ai/src/transports/openai-transport-params.code-mode.test.ts @@ -98,7 +98,7 @@ describe("OpenAI Code Mode payload tool filtering", () => { it("keeps the existing Responses and Completions enforcement contract", () => { const payload = { - tools: [{ name: "exec" }, { name: "web_search" }, { name: "wait" }], + tools: [{ name: "exec" }, { name: "web_search" }, { type: "web_search" }, { name: "wait" }], }; enforceCodeModeResponsesToolSurface(payload, visibleToolNames); @@ -107,6 +107,34 @@ describe("OpenAI Code Mode payload tool filtering", () => { expect(() => assertCodeModeResponsesToolSurface(payload, visibleToolNames)).not.toThrow(); }); + it("keeps only explicitly allowed hosted tools alongside visible client functions", () => { + const allowedHostedToolTypes = new Set(["web_search"]); + const payload = { + tools: [ + { type: "function", name: "exec" }, + { type: "function", name: "rogue" }, + { type: "web_search" }, + { type: "file_search" }, + { type: "web_search", name: "exec" }, + { type: "web_search", function: { name: "wait" } }, + { type: "web_search", functionDeclarations: [{ name: "exec" }] }, + { type: "function", name: "wait" }, + ], + }; + + filterCodeModePayloadTools(payload, visibleToolNames, allowedHostedToolTypes); + enforceCodeModeResponsesToolSurface(payload, visibleToolNames, allowedHostedToolTypes); + + expect(payload.tools).toEqual([ + { type: "function", name: "exec" }, + { type: "web_search" }, + { type: "function", name: "wait" }, + ]); + expect(() => + assertCodeModeResponsesToolSurface(payload, visibleToolNames, allowedHostedToolTypes), + ).not.toThrow(); + }); + it.each(["functionDeclarations", "function_declarations"] as const)( "rejects and removes grouped %s from final OpenAI payloads", (field) => { @@ -139,6 +167,24 @@ describe("OpenAI Code Mode payload tool filtering", () => { ); }); + it.each([ + [ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + { type: "web_search" }, + { type: "web_search" }, + ], + [ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + { type: "file_search" }, + ], + ])("rejects duplicate or undeclared hosted tools", (...tools) => { + expect(() => + assertCodeModeResponsesToolSurface({ tools }, visibleToolNames, new Set(["web_search"])), + ).toThrow(/tool surface violation/); + }); + it("fails closed when the asserted payload tools getter throws", () => { expect(() => assertCodeModeResponsesToolSurface(withThrowingGetter("tools"), visibleToolNames), diff --git a/packages/ai/src/transports/openai-transport-params.ts b/packages/ai/src/transports/openai-transport-params.ts index 14547f4ff215..e6848ec42e86 100644 --- a/packages/ai/src/transports/openai-transport-params.ts +++ b/packages/ai/src/transports/openai-transport-params.ts @@ -42,9 +42,40 @@ export function readCodeModePayloadToolName(tool: unknown): string | undefined { return typeof fnName === "string" ? fnName : undefined; } +function readCodeModePayloadToolIdentity( + tool: unknown, + visibleToolNames: ReadonlySet, + allowedHostedToolTypes?: ReadonlySet, +): string | false | undefined { + if (!isRecord(tool)) { + return undefined; + } + const type = readToolPayloadField(tool, "type"); + if (typeof type === "string" && allowedHostedToolTypes?.has(type)) { + try { + if ( + Object.hasOwn(tool, "name") || + Object.hasOwn(tool, "function") || + Object.hasOwn(tool, "functionDeclarations") || + Object.hasOwn(tool, "function_declarations") + ) { + return false; + } + } catch { + return false; + } + return `hosted:${type}`; + } + const name = readCodeModePayloadToolName(tool); + return typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames) + ? `client:${name}` + : undefined; +} + export function filterCodeModePayloadTools( payload: unknown, visibleToolNames: ReadonlySet, + allowedHostedToolTypes?: ReadonlySet, ): void { if (!isRecord(payload)) { return; @@ -54,10 +85,17 @@ export function filterCodeModePayloadTools( return; } payload.tools = tools.flatMap((tool) => { - const name = readCodeModePayloadToolName(tool); - if (typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames)) { + const identity = readCodeModePayloadToolIdentity( + tool, + visibleToolNames, + allowedHostedToolTypes, + ); + if (identity) { return [tool]; } + if (identity === false) { + return []; + } if (!isRecord(tool)) { return []; } @@ -95,6 +133,7 @@ export function resolveCodeModeResponsesVisibleToolNames( export function enforceCodeModeResponsesToolSurface( payload: unknown, visibleToolNames: ReadonlySet, + allowedHostedToolTypes?: ReadonlySet, ): void { if (!isRecord(payload)) { return; @@ -103,31 +142,36 @@ export function enforceCodeModeResponsesToolSurface( if (!Array.isArray(tools)) { return; } - payload.tools = tools.filter((tool) => { - const name = readCodeModePayloadToolName(tool); - return typeof name === "string" && isCodeModeModelVisibleToolName(name, visibleToolNames); - }); + payload.tools = tools.filter((tool) => + Boolean(readCodeModePayloadToolIdentity(tool, visibleToolNames, allowedHostedToolTypes)), + ); } export function assertCodeModeResponsesToolSurface( payload: unknown, visibleToolNames: ReadonlySet, + allowedHostedToolTypes?: ReadonlySet, ): void { const tools = isRecord(payload) ? readToolPayloadField(payload, "tools") : undefined; if (!Array.isArray(tools)) { throw new Error("Code mode payload tool surface violation: expected exec,wait; got no tools"); } - const names = tools - .map(readCodeModePayloadToolName) - .filter((name): name is string => typeof name === "string" && name.length > 0) + const identities = tools.map((tool) => + readCodeModePayloadToolIdentity(tool, visibleToolNames, allowedHostedToolTypes), + ); + const names = identities + .flatMap((identity) => + typeof identity === "string" && identity.startsWith("client:") + ? [identity.slice("client:".length)] + : [], + ) .toSorted((left, right) => left.localeCompare(right)); if ( names.length >= 2 && - names.length === tools.length && - new Set(names).size === names.length && + identities.every((identity): identity is string => typeof identity === "string") && + new Set(identities).size === identities.length && names.includes("exec") && - names.includes("wait") && - names.every((name) => isCodeModeModelVisibleToolName(name, visibleToolNames)) + names.includes("wait") ) { return; } diff --git a/src/llm/providers/stream-wrappers/openai.test.ts b/src/llm/providers/stream-wrappers/openai.test.ts index b947e0431853..edf8f7d9e83b 100644 --- a/src/llm/providers/stream-wrappers/openai.test.ts +++ b/src/llm/providers/stream-wrappers/openai.test.ts @@ -116,9 +116,11 @@ describe("createOpenAICompletionsToolsCompatWrapper", () => { }); describe("createCodexNativeWebSearchWrapper", () => { - it("does not inject native web_search when code mode owns the tool surface", () => { + it("keeps native_active web_search alongside the code mode tool surface", () => { + let observedOptions: Parameters[2]; const payloads: Array> = []; const baseStreamFn: StreamFn = (model, context, options) => { + observedOptions = options; const payload: Record = { model: model.id, tools: [ @@ -178,7 +180,12 @@ describe("createCodexNativeWebSearchWrapper", () => { expect(payloads[0]?.tools).toEqual([ { type: "function", name: "exec" }, { type: "function", name: "wait" }, + { type: "web_search" }, ]); + expect( + (observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set } | undefined) + ?.openclawCodeModeAllowedHostedToolTypes, + ).toEqual(new Set(["web_search"])); }); it("filters async replacement payloads when code mode owns the tool surface", async () => { @@ -189,12 +196,22 @@ describe("createCodexNativeWebSearchWrapper", () => { }; const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { codeModeToolSurfaceEnabled: true, + config: { + tools: { + web: { + search: { + enabled: true, + openaiCodex: { enabled: true, mode: "cached" }, + }, + }, + }, + }, }); const model = { - api: "openai-responses", - provider: "openai", + api: "openai-chatgpt-responses", + provider: "gateway", id: "gpt-5.5", - } as Model<"openai-responses">; + } as Model<"openai-chatgpt-responses">; void wrapped( model, @@ -224,6 +241,7 @@ describe("createCodexNativeWebSearchWrapper", () => { }, { type: "function", name: "wait" }, { type: "web_search" }, + { type: "file_search" }, ], }), }, @@ -236,8 +254,66 @@ describe("createCodexNativeWebSearchWrapper", () => { { type: "function", name: "sessions_yield" }, { type: "function", name: "structured_output" }, { type: "function", name: "wait" }, + { type: "web_search" }, ], }); + expect( + (observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set } | undefined) + ?.openclawCodeModeAllowedHostedToolTypes, + ).toEqual(new Set(["web_search"])); + }); + + it("does not authorize hosted search when runtime tool policy denies it in code mode", () => { + let observedOptions: Parameters[2]; + const payloads: Array> = []; + const baseStreamFn: StreamFn = (model, _context, options) => { + observedOptions = options; + const payload = { + tools: [ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + { type: "web_search" }, + ], + }; + options?.onPayload?.(payload, model); + payloads.push(structuredClone(payload)); + return createAssistantMessageEventStream(); + }; + const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { + codeModeToolSurfaceEnabled: true, + nativeWebSearchAllowedByToolPolicy: false, + config: { + tools: { + web: { + search: { + enabled: true, + openaiCodex: { enabled: true, mode: "cached" }, + }, + }, + }, + }, + }); + + void wrapped( + codexModel, + { + messages: [], + tools: [ + { name: "exec", description: "", parameters: {} }, + { name: "wait", description: "", parameters: {} }, + ], + }, + {}, + ); + + expect(payloads[0]?.tools).toEqual([ + { type: "function", name: "exec" }, + { type: "function", name: "wait" }, + ]); + expect( + (observedOptions as { openclawCodeModeAllowedHostedToolTypes?: Set } | undefined) + ?.openclawCodeModeAllowedHostedToolTypes, + ).toEqual(new Set()); }); it("does not enable code-mode transport enforcement when config is on but controls are inactive", () => { diff --git a/src/llm/providers/stream-wrappers/openai.ts b/src/llm/providers/stream-wrappers/openai.ts index 6d4c362fcc12..db620ce773d6 100644 --- a/src/llm/providers/stream-wrappers/openai.ts +++ b/src/llm/providers/stream-wrappers/openai.ts @@ -3,7 +3,6 @@ import { supportsOpenAIReasoningEffort, } from "@openclaw/ai/internal/openai"; import { - emitModelTransportDebug, filterCodeModePayloadTools, isCodeModeModelVisibleToolName, readCodeModePayloadToolName, @@ -49,6 +48,7 @@ type OpenAIServiceTier = "auto" | "default" | "flex" | "priority"; type DynamicFastMode = boolean | (() => boolean | undefined); type OpenClawSimpleStreamOptions = SimpleStreamOptions & { openclawCodeModeToolSurface?: boolean; + openclawCodeModeAllowedHostedToolTypes?: Set; }; type OpenAIResponsesReplayOptions = Parameters[2] & { replayResponsesItemIds?: boolean; @@ -146,9 +146,10 @@ function filterCodeModePayloadHookResult( payload: unknown, nextPayload: unknown, visibleToolNames: ReadonlySet, + allowedHostedToolTypes: ReadonlySet, ): unknown { const finalPayload = nextPayload === undefined ? payload : nextPayload; - filterCodeModePayloadTools(finalPayload, visibleToolNames); + filterCodeModePayloadTools(finalPayload, visibleToolNames, allowedHostedToolTypes); return nextPayload === undefined ? undefined : finalPayload; } @@ -648,31 +649,81 @@ export function createCodexNativeWebSearchWrapper( const codeModeSurfaceFromOptions = (options as OpenClawSimpleStreamOptions | undefined)?.openclawCodeModeToolSurface === true; const codeModeVisibleToolNames = resolveCodeModeVisibleToolNames(context); + const resolveNativeSearchActivation = () => + resolveCodexNativeSearchActivation({ + config: params.config, + modelProvider: readStringValue(model.provider), + modelApi: readStringValue(model.api), + modelId: readStringValue(model.id), + agentId: params.agentId, + sessionKey: params.sessionKey, + sandboxToolPolicy: params.sandboxToolPolicy, + messageProvider: params.messageProvider, + agentAccountId: params.agentAccountId, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + spawnedBy: params.spawnedBy, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + agentDir: params.agentDir, + }); if ( (params.codeModeToolSurfaceEnabled === true || codeModeSurfaceFromOptions || isCodeModeEnabled(params.config)) && codeModeVisibleToolNames ) { - emitModelTransportDebug( - log, - `skipping Codex native web search because code mode owns the model tool surface for ${ - model.provider ?? "unknown" - }/${model.id ?? "unknown"}`, - ); + // Every spread below must retain this request-scoped Set so the provider policy owner + // and final Responses egress agree on the same hosted-tool authorization fact. + const allowedHostedToolTypes = + (options as OpenClawSimpleStreamOptions | undefined) + ?.openclawCodeModeAllowedHostedToolTypes ?? new Set(); + const activation = + params.nativeWebSearchAllowedByToolPolicy === false + ? undefined + : resolveNativeSearchActivation(); + if (activation?.state === "native_active") { + allowedHostedToolTypes.add("web_search"); + } + if (activation?.state === "native_active" || activation?.codexNativeEnabled) { + const outcome = + activation.state === "native_active" + ? `activating (${activation.codexMode})` + : `skipping (${activation.inactiveReason ?? "inactive"})`; + log.debug( + `${outcome} Codex native web search alongside code mode for ${model.provider ?? "unknown"}/${model.id ?? "unknown"}`, + ); + } const originalOnPayload = options?.onPayload; const codeModeOptions: OpenClawSimpleStreamOptions = { ...options, openclawCodeModeToolSurface: true, + openclawCodeModeAllowedHostedToolTypes: allowedHostedToolTypes, onPayload: (payload) => { - filterCodeModePayloadTools(payload, codeModeVisibleToolNames); + if (activation?.state === "native_active") { + patchCodexNativeWebSearchPayload({ payload, config: params.config }); + } + filterCodeModePayloadTools(payload, codeModeVisibleToolNames, allowedHostedToolTypes); const nextPayload = originalOnPayload?.(payload, model); if (isPromiseLike(nextPayload)) { return Promise.resolve(nextPayload).then((resolvedPayload) => - filterCodeModePayloadHookResult(payload, resolvedPayload, codeModeVisibleToolNames), + filterCodeModePayloadHookResult( + payload, + resolvedPayload, + codeModeVisibleToolNames, + allowedHostedToolTypes, + ), ); } - return filterCodeModePayloadHookResult(payload, nextPayload, codeModeVisibleToolNames); + return filterCodeModePayloadHookResult( + payload, + nextPayload, + codeModeVisibleToolNames, + allowedHostedToolTypes, + ); }, }; return underlying(model, context, codeModeOptions); @@ -687,26 +738,7 @@ export function createCodexNativeWebSearchWrapper( return underlying(model, context, options); } - const activation = resolveCodexNativeSearchActivation({ - config: params.config, - modelProvider: readStringValue(model.provider), - modelApi: readStringValue(model.api), - modelId: readStringValue(model.id), - agentId: params.agentId, - sessionKey: params.sessionKey, - sandboxToolPolicy: params.sandboxToolPolicy, - messageProvider: params.messageProvider, - agentAccountId: params.agentAccountId, - groupId: params.groupId, - groupChannel: params.groupChannel, - groupSpace: params.groupSpace, - spawnedBy: params.spawnedBy, - senderId: params.senderId, - senderName: params.senderName, - senderUsername: params.senderUsername, - senderE164: params.senderE164, - agentDir: params.agentDir, - }); + const activation = resolveNativeSearchActivation(); if (activation.state !== "native_active") { if (activation.codexNativeEnabled) {