Skip to content

Commit 1e0062b

Browse files
feat: add Codex hosted web search (#93446)
Adds Codex as a selectable hosted web-search provider, routes native Codex search safely across model overrides, and isolates bounded hosted-search workers from configured tools.\n\nVerification: focused post-merge regression suite passed 202/202 tests on exact head 23824af.
1 parent 23589d9 commit 1e0062b

48 files changed

Lines changed: 4011 additions & 614 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/tools/web.md

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ local while `web_search` and `x_search` can use xAI Responses under the hood.
6060
<Card title="Brave Search" icon="shield" href="/tools/brave-search">
6161
Structured results with snippets. Supports `llm-context` mode, country/language filters. Free tier available.
6262
</Card>
63+
<Card title="Codex Hosted Search" icon="search" href="/plugins/codex-harness">
64+
AI-synthesized grounded answers through your Codex app-server account.
65+
</Card>
6366
<Card title="DuckDuckGo" icon="bird" href="/tools/duckduckgo-search">
6467
Key-free fallback. No API key needed. Unofficial HTML-based integration.
6568
</Card>
@@ -106,6 +109,7 @@ local while `web_search` and `x_search` can use xAI Responses under the hood.
106109
| Provider | Result style | Filters | API key |
107110
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------- |
108111
| [Brave](/tools/brave-search) | Structured snippets | Country, language, time, `llm-context` mode | `BRAVE_API_KEY` |
112+
| [Codex Hosted Search](/plugins/codex-harness) | AI-synthesized + source URLs | Domains, context size, user location | None; uses Codex/OpenAI sign-in |
109113
| [DuckDuckGo](/tools/duckduckgo-search) | Structured snippets | -- | None (key-free) |
110114
| [Exa](/tools/exa-search) | Structured + extracted | Neural/keyword mode, date, content extraction | `EXA_API_KEY` |
111115
| [Firecrawl](/tools/firecrawl) | Structured snippets | Via `firecrawl_search` tool | `FIRECRAWL_API_KEY` |
@@ -128,20 +132,52 @@ Direct OpenAI Responses models use OpenAI's hosted `web_search` tool automatical
128132

129133
## Native Codex web search
130134

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
135+
The Codex app-server runtime uses Codex's hosted `web_search` tool automatically
136+
when web search is enabled and no managed provider is selected. Native hosted
137+
search and OpenClaw's managed `web_search` dynamic tool are mutually exclusive,
138+
so managed search cannot bypass native domain restrictions. OpenClaw uses the
139+
managed tool when hosted search is unavailable, explicitly disabled, or
140+
replaced by a selected managed provider. OpenClaw keeps Codex's standalone
141+
`web.run` extension disabled because production app-server traffic rejects its
142+
user-defined `web` namespace.
143+
144+
- Configure native search under `tools.web.search.openaiCodex`
145+
- Set `tools.web.search.provider: "codex"` to provision Codex Hosted Search as
146+
the managed `web_search` provider for any parent model. Each call runs a
147+
bounded ephemeral Codex app-server turn and fails if Codex does not emit a
148+
hosted `webSearch` item.
149+
- `mode: "cached"` is the default preference, but Codex resolves it to live
150+
external access for unrestricted app-server turns; set `"live"` to request
151+
live access explicitly
152+
- Set `tools.web.search.provider` to a managed provider such as `brave` to use
153+
OpenClaw's managed `web_search` instead
154+
- Set `tools.web.search.openaiCodex.enabled: false` to opt out of Codex-hosted
155+
search; other managed providers remain available
156+
- Restricting the Codex native tool surface also keeps managed `web_search`
157+
available
158+
- When `allowedDomains` is set, automatic managed fallback fails closed if
159+
hosted search is unavailable so the native allowlist cannot be bypassed
160+
- Tool-disabled LLM-only runs disable both native and managed search
137161
- `tools.web.search.enabled: false` disables both managed and native search
138162

163+
Persistent effective Codex search-policy changes start a fresh bound thread so
164+
an already loaded app-server thread cannot keep stale hosted-search access.
165+
Transient per-turn restrictions use a temporary restricted thread and preserve
166+
the existing binding for later resume.
167+
168+
Direct OpenAI ChatGPT Responses traffic can also use OpenAI's hosted
169+
`web_search` tool. That separate path remains opt-in through
170+
`tools.web.search.openaiCodex.enabled: true` and only applies to eligible
171+
`openai/*` models using `api: "openai-chatgpt-responses"`.
172+
139173
```json5
140174
{
141175
tools: {
142176
web: {
143177
search: {
144178
enabled: true,
179+
// Optional: use Codex Hosted Search from non-Codex parent models too.
180+
provider: "codex",
145181
openaiCodex: {
146182
enabled: true,
147183
mode: "cached",
@@ -159,14 +195,25 @@ Codex-capable models can optionally use the provider-native Responses `web_searc
159195
}
160196
```
161197

162-
If native Codex search is enabled but the current model is not Codex-capable, OpenClaw keeps the normal managed `web_search` behavior.
198+
For runtimes and providers that do not support native Codex search, Codex can
199+
use the managed `web_search` fallback through OpenClaw's dynamic tool namespace.
200+
Use an explicit managed provider when you need OpenClaw's provider-specific
201+
network controls instead of Codex-hosted search.
202+
203+
Selecting `provider: "codex"` enables the bundled `codex` plugin and uses the
204+
same `tools.web.search.openaiCodex` restrictions shown above. Authenticate the
205+
Codex app-server first with `openclaw models auth login --provider openai`.
206+
The parent agent can use any model or runtime; only the bounded search worker
207+
runs through Codex.
163208

164209
## Network safety
165210

166-
Managed `web_search` provider calls use OpenClaw's guarded fetch path. For
211+
Managed HTTP `web_search` provider calls use OpenClaw's guarded fetch path. For
167212
trusted provider API hosts, OpenClaw allows Surge, Clash, and sing-box fake-IP
168213
DNS answers in `198.18.0.0/15` and `fc00::/7` only for that provider hostname.
169214
Other private, loopback, link-local, and metadata destinations remain blocked.
215+
Codex Hosted Search is the exception: its bounded worker delegates network
216+
access to Codex app-server's hosted `web_search` tool.
170217

171218
This automatic allowance does not apply to arbitrary `web_fetch` URLs. For
172219
`web_fetch`, enable `tools.web.fetch.ssrfPolicy.allowRfc2544BenchmarkRange` and
@@ -200,6 +247,7 @@ Key-free fallbacks after that:
200247
12. **DuckDuckGo** -- key-free HTML fallback with no account or API key (order 100)
201248
13. **Ollama Web Search** -- key-free fallback via your configured local Ollama host when it is reachable and signed in with `ollama signin`; can reuse Ollama provider bearer auth when the host needs it, and can call direct `https://ollama.com` search when configured with `OLLAMA_API_KEY` (order 110)
202249
14. **SearXNG** -- `SEARXNG_BASE_URL` or `plugins.entries.searxng.config.webSearch.baseUrl` (order 200)
250+
15. **Codex Hosted Search** -- key-free provider contract that uses the active Codex/OpenAI sign-in (order 900)
203251

204252
When no API-backed provider is configured, OpenClaw defaults to **Parallel
205253
Search (Free)**, so `web_search` works without an API key.

extensions/codex/index.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ describe("codex plugin", () => {
3232
expect(manifest.enabledByDefault).toBeUndefined();
3333
});
3434

35-
it("registers the codex provider and agent harness", () => {
35+
it("registers the codex provider, agent harness, and hosted web search", () => {
3636
const registerAgentHarness = vi.fn();
3737
const registerCommand = vi.fn();
3838
const registerMediaUnderstandingProvider = vi.fn();
3939
const registerMigrationProvider = vi.fn();
4040
const registerProvider = vi.fn();
41+
const registerWebSearchProvider = vi.fn();
4142
const on = vi.fn();
4243
const onConversationBindingResolved = vi.fn();
4344

@@ -54,6 +55,7 @@ describe("codex plugin", () => {
5455
registerMediaUnderstandingProvider,
5556
registerMigrationProvider,
5657
registerProvider,
58+
registerWebSearchProvider,
5759
on,
5860
onConversationBindingResolved,
5961
}),
@@ -82,6 +84,13 @@ describe("codex plugin", () => {
8284
expect(mediaProviderRegistration?.defaultModels).toEqual({ image: "gpt-5.5" });
8385
expect(typeof mediaProviderRegistration?.describeImage).toBe("function");
8486
expect(typeof mediaProviderRegistration?.describeImages).toBe("function");
87+
const webSearchRegistration = mockCallArg(registerWebSearchProvider) as
88+
| Record<string, unknown>
89+
| undefined;
90+
expect(webSearchRegistration?.id).toBe("codex");
91+
expect(webSearchRegistration?.label).toBe("Codex Hosted Search");
92+
expect(webSearchRegistration?.requiresCredential).toBe(false);
93+
expect(typeof webSearchRegistration?.createTool).toBe("function");
8594
const commandRegistration = mockCallArg(registerCommand) as Record<string, unknown> | undefined;
8695
expect(commandRegistration?.name).toBe("codex");
8796
expect(commandRegistration?.description).toBe(

extensions/codex/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
resumeCodexCliSessionOnNode,
2424
resolveCodexCliSessionForBindingOnNode,
2525
} from "./src/node-cli-sessions.js";
26+
import { createCodexWebSearchProvider } from "./src/web-search-provider.js";
2627

2728
export default definePluginEntry({
2829
id: "codex",
@@ -46,6 +47,9 @@ export default definePluginEntry({
4647
api.registerMediaUnderstandingProvider(
4748
buildCodexMediaUnderstandingProvider({ pluginConfig: api.pluginConfig }),
4849
);
50+
api.registerWebSearchProvider(
51+
createCodexWebSearchProvider({ resolvePluginConfig: resolveCurrentPluginConfig }),
52+
);
4953
api.registerMigrationProvider(buildCodexMigrationProvider({ runtime: api.runtime }));
5054
for (const command of createCodexCliSessionNodeHostCommands()) {
5155
api.registerNodeHostCommand(command);

extensions/codex/media-understanding-provider.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ describe("codex media understanding provider", () => {
229229
undefined,
230230
"/tmp/openclaw-agent",
231231
cfg,
232+
{ timeoutMs: 30_000 },
232233
);
233234
expect(requests[1]?.params).toEqual({
234235
model: "gpt-5.4",
@@ -240,8 +241,14 @@ describe("codex media understanding provider", () => {
240241
developerInstructions:
241242
"You are OpenClaw's bounded image-understanding worker. Describe only the provided image content. Do not call tools, edit files, or ask follow-up questions.",
242243
config: {
244+
"features.apps": false,
243245
"features.code_mode": false,
244246
"features.code_mode_only": false,
247+
"features.image_generation": false,
248+
"features.multi_agent": false,
249+
"features.plugins": false,
250+
"features.standalone_web_search": false,
251+
web_search: "disabled",
245252
},
246253
environments: [],
247254
dynamicTools: [],
@@ -279,11 +286,51 @@ describe("codex media understanding provider", () => {
279286
agentDir: " ",
280287
});
281288

282-
expect(clientFactory).toHaveBeenCalledWith(expect.any(Object), undefined, undefined, cfg);
289+
expect(clientFactory).toHaveBeenCalledWith(expect.any(Object), undefined, undefined, cfg, {
290+
timeoutMs: 30_000,
291+
});
283292
expect(requests[1]?.params).toEqual(expect.objectContaining({ cwd: process.cwd() }));
284293
expect(requests[2]?.params).toEqual(expect.objectContaining({ cwd: process.cwd() }));
285294
});
286295

296+
it("preserves configured WebSocket transport for media turns", async () => {
297+
const { client, requests } = createFakeClient();
298+
const clientFactory = vi.fn(async () => client);
299+
const provider = buildCodexMediaUnderstandingProvider({
300+
pluginConfig: {
301+
appServer: {
302+
transport: "websocket",
303+
url: "ws://127.0.0.1:4501",
304+
},
305+
},
306+
clientFactory,
307+
});
308+
309+
await provider.describeImage?.({
310+
buffer: Buffer.from("image-bytes"),
311+
fileName: "image.png",
312+
mime: "image/png",
313+
provider: "codex",
314+
model: "gpt-5.4",
315+
timeoutMs: 30_000,
316+
cfg: {},
317+
agentDir: "/tmp/openclaw-agent",
318+
});
319+
320+
expect(clientFactory).toHaveBeenCalledWith(
321+
expect.objectContaining({
322+
transport: "websocket",
323+
url: "ws://127.0.0.1:4501",
324+
}),
325+
undefined,
326+
"/tmp/openclaw-agent",
327+
{},
328+
{ timeoutMs: 30_000 },
329+
);
330+
expect(requests[1]?.params).toEqual(expect.objectContaining({ cwd: "/tmp/openclaw-agent" }));
331+
expect(requests[2]?.params).toEqual(expect.objectContaining({ cwd: "/tmp/openclaw-agent" }));
332+
});
333+
287334
it("passes the scoped auth store into isolated app-server startup", async () => {
288335
const { client } = createFakeClient();
289336
sharedClientMocks.createIsolatedCodexAppServerClient.mockResolvedValue(client);
@@ -486,8 +533,14 @@ describe("codex media understanding provider", () => {
486533
developerInstructions:
487534
"You are OpenClaw's bounded structured-extraction worker. Return only the requested extraction. Do not call tools, edit files, ask follow-up questions, or include secrets.",
488535
config: {
536+
"features.apps": false,
489537
"features.code_mode": false,
490538
"features.code_mode_only": false,
539+
"features.image_generation": false,
540+
"features.multi_agent": false,
541+
"features.plugins": false,
542+
"features.standalone_web_search": false,
543+
web_search: "disabled",
491544
},
492545
environments: [],
493546
dynamicTools: [],

0 commit comments

Comments
 (0)