Skip to content

Commit 3f61d15

Browse files
feat: add Codex hosted web search
1 parent 11a0ad1 commit 3f61d15

33 files changed

Lines changed: 1973 additions & 95 deletions

docs/plugins/codex-harness.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,9 +505,22 @@ Codex dynamic tools default to `searchable` loading. OpenClaw does not expose
505505
dynamic tools that duplicate Codex-native workspace operations: `read`, `write`,
506506
`edit`, `apply_patch`, `exec`, `process`, and `update_plan`. Most remaining
507507
OpenClaw integration tools such as messaging, media, cron, browser, nodes,
508-
gateway, `heartbeat_respond`, and `web_search` are available through Codex tool
509-
search under the `openclaw` namespace, keeping the initial model context
510-
smaller.
508+
gateway, and `heartbeat_respond` are available through Codex tool search under
509+
the `openclaw` namespace, keeping the initial model context smaller. Web search
510+
uses Codex's hosted `web_search` tool by default when search is enabled and no
511+
managed provider is selected. Native hosted search and OpenClaw's managed
512+
`web_search` dynamic tool are mutually exclusive so managed search cannot bypass
513+
native domain restrictions. OpenClaw uses the managed tool when hosted search is
514+
unavailable, explicitly disabled, or replaced by a selected managed provider.
515+
OpenClaw keeps Codex's standalone `web.run` extension disabled because
516+
production app-server traffic rejects its user-defined `web` namespace.
517+
`tools.web.search.enabled: false` disables both paths, as do tool-disabled
518+
LLM-only runs. Codex treats `"cached"` as a preference and resolves it to live
519+
external access for unrestricted app-server turns. Automatic managed fallback
520+
fails closed when native `allowedDomains` are set so the allowlist cannot be
521+
bypassed. Persistent effective search-policy changes rotate the bound Codex
522+
thread before the next turn. Transient per-turn restrictions use a temporary
523+
restricted thread and preserve the existing binding for later resume.
511524
`sessions_yield` and message-tool-only source replies stay direct because
512525
those are turn-control contracts. `sessions_spawn` stays searchable so Codex's
513526
native `spawn_agent` remains the primary Codex subagent surface, while explicit

docs/plugins/sdk-entrypoints.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,16 @@ export default defineToolPlugin({
8282

8383
- `configSchema` is optional. When omitted, OpenClaw uses a strict empty object
8484
schema and the generated manifest still includes `configSchema`.
85-
- `execute` returns a plain string or JSON-serializable value. The helper wraps
86-
it as a text tool result with `details`.
85+
- `execute` returns a plain string, JSON-serializable value, or full OpenClaw
86+
tool result. Full tool results can include advanced fields such as a public
87+
`terminalSummary` for forced terminal replies when the model fails to produce
88+
its own final answer.
89+
- `terminalResultFallback` is optional tool metadata for cases where raw tool
90+
output has a known safe terminal presentation. Omitted metadata does not
91+
expose raw tool output; explicitly set `mode: "safe_text"` or structured
92+
summary metadata only when the tool owns that safety contract.
93+
- Use `terminalResultFallback: { mode: "none" }` to explicitly opt out of
94+
terminal result presentation.
8795
- Tool names are static. `openclaw plugins build` derives `contracts.tools`
8896
from the declared tools, so authors do not duplicate names by hand.
8997
- Runtime loading stays strict. Installed plugins still need

docs/plugins/tool-plugins.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,15 @@ format:
159159
- Return a string when the model should see that exact text.
160160
- Return a JSON-compatible value when you want the model to see formatted JSON
161161
and OpenClaw to keep the original value in `details`.
162+
- Return an OpenClaw tool result when the tool needs advanced fields such as a
163+
public `terminalSummary`. Terminal summaries may be delivered to the user if
164+
the model fails to write a final answer after tool work, so only include text
165+
that is safe to show in the channel.
166+
- Omitted `terminalResultFallback` does not expose raw tool output. When a tool
167+
owns a safe terminal presentation, explicitly set `mode: "safe_text"` or
168+
structured summary metadata.
169+
- Use `terminalResultFallback: { mode: "none" }` to explicitly opt out of
170+
terminal result presentation.
162171

163172
```typescript
164173
tool({

docs/tools/web.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,40 @@ Direct OpenAI Responses models use OpenAI's hosted `web_search` tool automatical
128128

129129
## Native Codex web search
130130

131-
Codex-capable models can optionally use the provider-native Responses `web_search` tool instead of OpenClaw's managed `web_search` function.
132-
133-
- Configure it under `tools.web.search.openaiCodex`
134-
- It only activates for Codex-capable OpenAI models (`openai/*` models using `api: "openai-chatgpt-responses"`)
135-
- Managed `web_search` still applies to non-Codex models
136-
- `mode: "cached"` is the default and recommended setting
131+
The Codex app-server runtime uses Codex's hosted `web_search` tool automatically
132+
when web search is enabled and no managed provider is selected. Native hosted
133+
search and OpenClaw's managed `web_search` dynamic tool are mutually exclusive,
134+
so managed search cannot bypass native domain restrictions. OpenClaw uses the
135+
managed tool when hosted search is unavailable, explicitly disabled, or
136+
replaced by a selected managed provider. OpenClaw keeps Codex's standalone
137+
`web.run` extension disabled because production app-server traffic rejects its
138+
user-defined `web` namespace.
139+
140+
- Configure native search under `tools.web.search.openaiCodex`
141+
- `mode: "cached"` is the default preference, but Codex resolves it to live
142+
external access for unrestricted app-server turns; set `"live"` to request
143+
live access explicitly
144+
- Set `tools.web.search.provider` to a managed provider such as `brave` to use
145+
OpenClaw's managed `web_search` instead
146+
- Set `tools.web.search.openaiCodex.enabled: false` to opt the Codex app-server
147+
runtime out of hosted search while keeping managed search available
148+
- Restricting the Codex native tool surface also keeps managed `web_search`
149+
available
150+
- When `allowedDomains` is set, automatic managed fallback fails closed if
151+
hosted search is unavailable so the native allowlist cannot be bypassed
152+
- Tool-disabled LLM-only runs disable both native and managed search
137153
- `tools.web.search.enabled: false` disables both managed and native search
138154

155+
Persistent effective Codex search-policy changes start a fresh bound thread so
156+
an already loaded app-server thread cannot keep stale hosted-search access.
157+
Transient per-turn restrictions use a temporary restricted thread and preserve
158+
the existing binding for later resume.
159+
160+
Direct OpenAI ChatGPT Responses traffic can also use OpenAI's hosted
161+
`web_search` tool. That separate path remains opt-in through
162+
`tools.web.search.openaiCodex.enabled: true` and only applies to eligible
163+
`openai/*` models using `api: "openai-chatgpt-responses"`.
164+
139165
```json5
140166
{
141167
tools: {
@@ -159,7 +185,10 @@ Codex-capable models can optionally use the provider-native Responses `web_searc
159185
}
160186
```
161187

162-
If native Codex search is enabled but the current model is not Codex-capable, OpenClaw keeps the normal managed `web_search` behavior.
188+
For runtimes and providers that do not support native Codex search, Codex can
189+
use the managed `web_search` fallback through OpenClaw's dynamic tool namespace.
190+
Use an explicit managed provider when you need OpenClaw's provider-specific
191+
network controls instead of Codex-hosted search.
163192

164193
## Network safety
165194

extensions/codex/src/app-server/attempt-startup.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,12 @@ function startThreadWithHarness(
116116
effectiveWorkspace: paths.workspaceDir,
117117
effectiveCwd: paths.cwd,
118118
dynamicTools: [],
119+
webSearchAllowed: false,
119120
developerInstructions: undefined,
120121
finalConfigPatch: undefined,
121122
bundleMcpThreadConfig,
122123
nativeToolSurfaceEnabled: true,
124+
nativeProviderWebSearchSupported: true,
123125
sandboxExecServerEnabled: false,
124126
sandbox: null,
125127
contextEngineProjection: undefined,

extensions/codex/src/app-server/attempt-startup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,15 @@ export async function startCodexAttemptThread(params: {
9696
effectiveWorkspace: string;
9797
effectiveCwd: string;
9898
dynamicTools: CodexDynamicToolSpec[];
99+
persistentWebSearchAllowed?: boolean;
100+
webSearchAllowed: boolean;
99101
developerInstructions: string | undefined;
100102
finalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["finalConfigPatch"];
101103
buildFinalConfigPatch?: Parameters<typeof startOrResumeThread>[0]["buildFinalConfigPatch"];
102104
nativeHookRelayGeneration?: string;
103105
bundleMcpThreadConfig: CodexBundleMcpThreadConfig;
104106
nativeToolSurfaceEnabled: boolean;
107+
nativeProviderWebSearchSupported: boolean;
105108
sandboxExecServerEnabled: boolean;
106109
sandbox: CodexSandboxContext;
107110
contextEngineProjection: CodexContextEngineThreadBootstrapProjection | undefined;
@@ -300,13 +303,16 @@ export async function startCodexAttemptThread(params: {
300303
agentId: params.sessionAgentId,
301304
cwd: startupExecutionCwd,
302305
dynamicTools: params.dynamicTools,
306+
persistentWebSearchAllowed: params.persistentWebSearchAllowed,
307+
webSearchAllowed: params.webSearchAllowed,
303308
appServer: pluginAppServer,
304309
developerInstructions: params.developerInstructions,
305310
config: threadConfig,
306311
finalConfigPatch: params.finalConfigPatch,
307312
buildFinalConfigPatch: params.buildFinalConfigPatch,
308313
nativeHookRelayGeneration: params.nativeHookRelayGeneration,
309314
nativeCodeModeEnabled: params.nativeToolSurfaceEnabled,
315+
nativeProviderWebSearchSupported: params.nativeProviderWebSearchSupported,
310316
nativeCodeModeOnlyEnabled: params.appServer.codeModeOnly,
311317
userMcpServersEnabled: params.nativeToolSurfaceEnabled,
312318
mcpServersFingerprint: params.bundleMcpThreadConfig.fingerprint,

extensions/codex/src/app-server/client-factory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type CodexAppServerClientFactory = (
1818
options?: {
1919
onStartedClient?: (client: CodexAppServerClient) => void;
2020
abandonSignal?: AbortSignal;
21+
timeoutMs?: number;
2122
},
2223
) => Promise<CodexAppServerClient>;
2324

@@ -44,6 +45,7 @@ export const defaultCodexAppServerClientFactory: CodexAppServerClientFactory = (
4445
config,
4546
onStartedClient: options?.onStartedClient,
4647
abandonSignal: options?.abandonSignal,
48+
timeoutMs: options?.timeoutMs,
4749
}),
4850
);
4951

@@ -63,5 +65,6 @@ export const defaultLeasedCodexAppServerClientFactory: CodexAppServerClientFacto
6365
config,
6466
onStartedClient: options?.onStartedClient,
6567
abandonSignal: options?.abandonSignal,
68+
timeoutMs: options?.timeoutMs,
6669
}),
6770
);

extensions/codex/src/app-server/dynamic-tool-build.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,136 @@ describe("Codex app-server dynamic tool build", () => {
171171
]);
172172
});
173173

174+
it("removes managed web_search when domain-restricted Codex hosted search is active", async () => {
175+
const workspaceDir = path.join(tempDir, "workspace");
176+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
177+
params.disableTools = false;
178+
params.config = {
179+
tools: {
180+
web: {
181+
search: { openaiCodex: { allowedDomains: ["example.com"] } },
182+
},
183+
},
184+
} as never;
185+
setOpenClawCodingToolsFactoryForTests(() => [
186+
createRuntimeDynamicTool("web_search"),
187+
createRuntimeDynamicTool("message"),
188+
]);
189+
let webSearchAllowed = false;
190+
191+
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
192+
onWebSearchPolicyResolved: (allowed) => {
193+
webSearchAllowed = allowed;
194+
},
195+
});
196+
197+
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
198+
expect(webSearchAllowed).toBe(true);
199+
});
200+
201+
it("reports hosted search denied when effective tool policy removes web_search", async () => {
202+
const workspaceDir = path.join(tempDir, "workspace");
203+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
204+
params.disableTools = false;
205+
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
206+
let webSearchAllowed = true;
207+
208+
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
209+
onWebSearchPolicyResolved: (allowed) => {
210+
webSearchAllowed = allowed;
211+
},
212+
});
213+
214+
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
215+
expect(webSearchAllowed).toBe(false);
216+
});
217+
218+
it("separates persistent search policy from a runtime toolsAllow restriction", async () => {
219+
const workspaceDir = path.join(tempDir, "workspace");
220+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
221+
params.disableTools = false;
222+
params.toolsAllow = ["message"];
223+
setOpenClawCodingToolsFactoryForTests(() => [
224+
createRuntimeDynamicTool("web_search"),
225+
createRuntimeDynamicTool("message"),
226+
]);
227+
let persistentWebSearchAllowed = false;
228+
let webSearchAllowed = true;
229+
230+
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
231+
onPersistentWebSearchPolicyResolved: (allowed) => {
232+
persistentWebSearchAllowed = allowed;
233+
},
234+
onWebSearchPolicyResolved: (allowed) => {
235+
webSearchAllowed = allowed;
236+
},
237+
});
238+
239+
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
240+
expect(persistentWebSearchAllowed).toBe(true);
241+
expect(webSearchAllowed).toBe(false);
242+
});
243+
244+
it("keeps persistent search denied when runtime toolsAllow also excludes it", async () => {
245+
const workspaceDir = path.join(tempDir, "workspace");
246+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
247+
params.disableTools = false;
248+
params.toolsAllow = ["message"];
249+
setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]);
250+
let persistentWebSearchAllowed = true;
251+
let webSearchAllowed = true;
252+
253+
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
254+
onPersistentWebSearchPolicyResolved: (allowed) => {
255+
persistentWebSearchAllowed = allowed;
256+
},
257+
onWebSearchPolicyResolved: (allowed) => {
258+
webSearchAllowed = allowed;
259+
},
260+
});
261+
262+
expect(tools.map((tool) => tool.name)).toEqual(["message"]);
263+
expect(persistentWebSearchAllowed).toBe(false);
264+
expect(webSearchAllowed).toBe(false);
265+
});
266+
267+
it("keeps managed web_search when a managed provider is explicitly selected", async () => {
268+
const workspaceDir = path.join(tempDir, "workspace");
269+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
270+
params.disableTools = false;
271+
params.config = {
272+
tools: {
273+
web: {
274+
search: { provider: "brave" },
275+
},
276+
},
277+
} as never;
278+
setOpenClawCodingToolsFactoryForTests(() => [
279+
createRuntimeDynamicTool("web_search"),
280+
createRuntimeDynamicTool("message"),
281+
]);
282+
283+
const tools = await buildDynamicToolsForTest(params, workspaceDir);
284+
285+
expect(tools.map((tool) => tool.name)).toEqual(["web_search", "message"]);
286+
});
287+
288+
it("keeps managed web_search when the active Codex provider lacks hosted search", async () => {
289+
const workspaceDir = path.join(tempDir, "workspace");
290+
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
291+
params.disableTools = false;
292+
setOpenClawCodingToolsFactoryForTests(() => [
293+
createRuntimeDynamicTool("web_search"),
294+
createRuntimeDynamicTool("message"),
295+
]);
296+
297+
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
298+
nativeProviderWebSearchSupported: false,
299+
});
300+
301+
expect(tools.map((tool) => tool.name)).toEqual(["web_search", "message"]);
302+
});
303+
174304
it("applies additional Codex dynamic tool excludes without exposing Codex-native tools", () => {
175305
const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name }));
176306

0 commit comments

Comments
 (0)