Skip to content

Commit 42aaf0c

Browse files
Prefer Codex native workspace tools (#75308)
Summary: - The PR adds Codex dynamic-tool profile config defaulting to `native-first`, filters duplicate workspace/process/planning tools from Codex app-server thread payloads, keeps managed `web_search`, updates docs/manifest/config baselines/changelog, and adds regression tests. ClawSweeper fixups: - Included follow-up commit: test(codex): pin native-first tool catalog - Included follow-up commit: chore(config): refresh generated schema baseline - Included follow-up commit: chore: add codex native-first changelog - Included follow-up commit: chore: move native-first changelog entry - Included follow-up commit: chore: refresh config baseline after rebase Validation: - ClawSweeper review passed for head 30e5cec. - Required merge gates passed before the squash merge. Prepared head SHA: 30e5cec Review: #75308 (comment) Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: pashpashpash <nik@vault77.ai>
1 parent ec69c07 commit 42aaf0c

11 files changed

Lines changed: 225 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
1515
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
1616
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
1717
- CLI/proxy: add `openclaw proxy validate` so operators can verify effective proxy configuration, proxy reachability, and expected allow/deny destination behavior before deploying proxy-routed OpenClaw commands. (#73438) Thanks @jesse-merhi.
18+
- Agents/Codex: default Codex app-server dynamic tools to native-first, keeping OpenClaw integration tools while leaving file, patch, exec, and process ownership to the Codex harness. (#75308) Thanks @pashpashpash.
1819

1920
### Fixes
2021

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
2197c0110a367c9e2adba959ff8529edad7b4d526894eec602e47189d6930d2f config-baseline.json
1+
1deb67d0a40456e77cb67685f6ae2f14a8ddc2c4be488d4b1a1f1127598982dd config-baseline.json
22
ac7537ed5b5a2d9e7fa50977aa99f5e0babfbe1a93c7c14b93a184b36bb4f539 config-baseline.core.json
33
f3326cd9490169afefe93625f63699266b75db93855ed439c9692e3c286a990c config-baseline.channel.json
4-
4d017161b4dc986fdc6cc68167fedbd1d415ddbcd66125a872e18aa1769cd182 config-baseline.plugin.json
4+
7731a0b93cb335b56fac4c807447ba659fea51ea7a6cd844dc0ef5616669ee75 config-baseline.plugin.json

docs/plugins/codex-harness.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,19 @@ If a deployment needs additional environment isolation, add those variables to
579579

580580
`appServer.clearEnv` only affects the spawned Codex app-server child process.
581581

582+
Codex dynamic tools default to the `native-first` profile. In that mode,
583+
OpenClaw does not expose dynamic tools that duplicate Codex-native workspace
584+
operations: `read`, `write`, `edit`, `apply_patch`, `exec`, `process`, and
585+
`update_plan`. OpenClaw integration tools such as messaging, sessions, media,
586+
cron, browser, nodes, gateway, and `web_search` remain available.
587+
588+
Supported top-level Codex plugin fields:
589+
590+
| Field | Default | Meaning |
591+
| -------------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
592+
| `codexDynamicToolsProfile` | `"native-first"` | Use `"openclaw-compat"` to expose the full OpenClaw dynamic tool set to Codex app-server. |
593+
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
594+
582595
Supported `appServer` fields:
583596

584597
| Field | Default | Meaning |

extensions/codex/openclaw.plugin.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@
3333
"type": "object",
3434
"additionalProperties": false,
3535
"properties": {
36+
"codexDynamicToolsProfile": {
37+
"type": "string",
38+
"enum": ["native-first", "openclaw-compat"],
39+
"default": "native-first"
40+
},
41+
"codexDynamicToolsExclude": {
42+
"type": "array",
43+
"items": { "type": "string" },
44+
"default": []
45+
},
3646
"discovery": {
3747
"type": "object",
3848
"additionalProperties": false,
@@ -141,6 +151,16 @@
141151
}
142152
},
143153
"uiHints": {
154+
"codexDynamicToolsProfile": {
155+
"label": "Dynamic Tools Profile",
156+
"help": "Select which OpenClaw dynamic tools are exposed to Codex app-server. native-first omits tools Codex already owns.",
157+
"advanced": true
158+
},
159+
"codexDynamicToolsExclude": {
160+
"label": "Dynamic Tool Excludes",
161+
"help": "Additional OpenClaw dynamic tool names to omit from Codex app-server turns.",
162+
"advanced": true
163+
},
144164
"discovery": {
145165
"label": "Model Discovery",
146166
"help": "Plugin-owned controls for discovering Codex app-server models."

extensions/codex/src/app-server/config.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@ describe("Codex app-server config", () => {
138138
);
139139
});
140140

141+
it("parses dynamic tool profile controls", () => {
142+
expect(
143+
readCodexPluginConfig({
144+
codexDynamicToolsProfile: "openclaw-compat",
145+
codexDynamicToolsExclude: ["custom_tool"],
146+
}),
147+
).toMatchObject({
148+
codexDynamicToolsProfile: "openclaw-compat",
149+
codexDynamicToolsExclude: ["custom_tool"],
150+
});
151+
});
152+
141153
it("treats configured and environment commands as explicit overrides", () => {
142154
expect(
143155
resolveCodexAppServerRuntimeOptions({

extensions/codex/src/app-server/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure"
1010
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
1111
export type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
1212
export type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
13+
export type CodexDynamicToolsProfile = "native-first" | "openclaw-compat";
1314

1415
export type CodexComputerUseConfig = {
1516
enabled?: boolean;
@@ -55,6 +56,8 @@ export type CodexAppServerRuntimeOptions = {
5556
};
5657

5758
export type CodexPluginConfig = {
59+
codexDynamicToolsProfile?: CodexDynamicToolsProfile;
60+
codexDynamicToolsExclude?: string[];
5861
discovery?: {
5962
enabled?: boolean;
6063
timeoutMs?: number;
@@ -120,6 +123,7 @@ const codexAppServerApprovalPolicySchema = z.enum([
120123
]);
121124
const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]);
122125
const codexAppServerApprovalsReviewerSchema = z.enum(["user", "auto_review", "guardian_subagent"]);
126+
const codexDynamicToolsProfileSchema = z.enum(["native-first", "openclaw-compat"]);
123127
const codexAppServerServiceTierSchema = z
124128
.preprocess(
125129
(value) => (value === null ? null : resolveServiceTier(value)),
@@ -129,6 +133,8 @@ const codexAppServerServiceTierSchema = z
129133

130134
const codexPluginConfigSchema = z
131135
.object({
136+
codexDynamicToolsProfile: codexDynamicToolsProfileSchema.optional(),
137+
codexDynamicToolsExclude: z.array(z.string()).optional(),
132138
discovery: z
133139
.object({
134140
enabled: z.boolean().optional(),

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,20 @@ function createMessageDynamicTool(
304304
};
305305
}
306306

307+
function createNamedDynamicTool(
308+
name: string,
309+
): Parameters<typeof startOrResumeThread>[0]["dynamicTools"][number] {
310+
return {
311+
name,
312+
description: `${name} test tool`,
313+
inputSchema: {
314+
type: "object",
315+
properties: {},
316+
additionalProperties: false,
317+
},
318+
};
319+
}
320+
307321
function extractRelayIdFromThreadRequest(params: unknown): string {
308322
const command = (
309323
params as {
@@ -335,6 +349,94 @@ describe("runCodexAppServerAttempt", () => {
335349
await fs.rm(tempDir, { recursive: true, force: true });
336350
});
337351

352+
it("defaults Codex dynamic tools to the native-first profile", () => {
353+
const tools = [
354+
"read",
355+
"write",
356+
"edit",
357+
"apply_patch",
358+
"exec",
359+
"process",
360+
"update_plan",
361+
"web_search",
362+
"message",
363+
"sessions_spawn",
364+
].map((name) => ({ name }));
365+
366+
expect(__testing.applyCodexDynamicToolProfile(tools, {}).map((tool) => tool.name)).toEqual([
367+
"web_search",
368+
"message",
369+
"sessions_spawn",
370+
]);
371+
});
372+
373+
it("allows Codex dynamic tool filtering to opt back into OpenClaw compatibility", () => {
374+
const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name }));
375+
376+
expect(
377+
__testing
378+
.applyCodexDynamicToolProfile(tools, {
379+
codexDynamicToolsProfile: "openclaw-compat",
380+
codexDynamicToolsExclude: ["custom_tool"],
381+
})
382+
.map((tool) => tool.name),
383+
).toEqual(["read", "exec", "message"]);
384+
});
385+
386+
it("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => {
387+
const sessionFile = path.join(tempDir, "session.jsonl");
388+
const workspaceDir = path.join(tempDir, "workspace");
389+
const appServer = createThreadLifecycleAppServerOptions();
390+
const request = vi.fn(async (method: string, _params: unknown) => {
391+
if (method === "thread/start") {
392+
return threadStartResult();
393+
}
394+
throw new Error(`unexpected method: ${method}`);
395+
});
396+
const dynamicTools = __testing.applyCodexDynamicToolProfile(
397+
[
398+
"read",
399+
"write",
400+
"edit",
401+
"apply_patch",
402+
"exec",
403+
"process",
404+
"update_plan",
405+
"web_search",
406+
"message",
407+
].map(createNamedDynamicTool),
408+
{},
409+
);
410+
411+
await startOrResumeThread({
412+
client: { request } as never,
413+
params: createParams(sessionFile, workspaceDir),
414+
cwd: workspaceDir,
415+
dynamicTools,
416+
appServer,
417+
});
418+
419+
const startRequest = request.mock.calls.find(([method]) => method === "thread/start");
420+
const dynamicToolNames = (
421+
(startRequest?.[1] as { dynamicTools?: Array<{ name: string }> } | undefined)?.dynamicTools ??
422+
[]
423+
).map((tool) => tool.name);
424+
425+
expect(dynamicToolNames).toContain("message");
426+
expect(dynamicToolNames).toContain("web_search");
427+
expect(dynamicToolNames).not.toEqual(
428+
expect.arrayContaining([
429+
"read",
430+
"write",
431+
"edit",
432+
"apply_patch",
433+
"exec",
434+
"process",
435+
"update_plan",
436+
]),
437+
);
438+
});
439+
338440
it("returns a failed dynamic tool response when an app-server tool call exceeds the deadline", async () => {
339441
vi.useFakeTimers();
340442
let capturedSignal: AbortSignal | undefined;

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ import {
4444
} from "./client-factory.js";
4545
import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js";
4646
import { ensureCodexComputerUse } from "./computer-use.js";
47-
import { resolveCodexAppServerRuntimeOptions } from "./config.js";
47+
import {
48+
readCodexPluginConfig,
49+
resolveCodexAppServerRuntimeOptions,
50+
type CodexPluginConfig,
51+
} from "./config.js";
4852
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
4953
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
5054
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
@@ -89,6 +93,15 @@ const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 30_000;
8993
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
9094
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
9195
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
96+
const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [
97+
"read",
98+
"write",
99+
"edit",
100+
"apply_patch",
101+
"exec",
102+
"process",
103+
"update_plan",
104+
] as const;
92105
const LOG_FIELD_MAX_LENGTH = 160;
93106

94107
type OpenClawCodingToolsOptions = NonNullable<
@@ -319,7 +332,8 @@ export async function runCodexAppServerAttempt(
319332
} = {},
320333
): Promise<EmbeddedRunAttemptResult> {
321334
const attemptStartedAt = Date.now();
322-
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: options.pluginConfig });
335+
const pluginConfig = readCodexPluginConfig(options.pluginConfig);
336+
const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig });
323337
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
324338
await fs.mkdir(resolvedWorkspace, { recursive: true });
325339
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
@@ -369,6 +383,7 @@ export async function runCodexAppServerAttempt(
369383
sandbox,
370384
runAbortController,
371385
sessionAgentId,
386+
pluginConfig,
372387
onYieldDetected: () => {
373388
yieldDetected = true;
374389
},
@@ -1317,6 +1332,7 @@ type DynamicToolBuildParams = {
13171332
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>;
13181333
runAbortController: AbortController;
13191334
sessionAgentId: string | undefined;
1335+
pluginConfig: CodexPluginConfig;
13201336
onYieldDetected: () => void;
13211337
};
13221338

@@ -1372,6 +1388,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
13721388
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
13731389
workspaceDir: input.effectiveWorkspace,
13741390
}),
1391+
suppressManagedWebSearch: false,
13751392
currentChannelId: params.currentChannelId,
13761393
currentThreadTs: params.currentThreadTs,
13771394
currentMessageId: params.currentMessageId,
@@ -1390,7 +1407,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
13901407
input.runAbortController.abort("sessions_yield");
13911408
},
13921409
});
1393-
const visionFilteredTools = filterToolsForVisionInputs(allTools, {
1410+
const profiledTools = applyCodexDynamicToolProfile(allTools, input.pluginConfig);
1411+
const visionFilteredTools = filterToolsForVisionInputs(profiledTools, {
13941412
modelHasVision,
13951413
hasInboundImages: (params.images?.length ?? 0) > 0,
13961414
});
@@ -1411,6 +1429,26 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
14111429
});
14121430
}
14131431

1432+
function applyCodexDynamicToolProfile<T extends { name: string }>(
1433+
tools: T[],
1434+
config: CodexPluginConfig,
1435+
): T[] {
1436+
const excludes = new Set<string>();
1437+
const profile = config.codexDynamicToolsProfile ?? "native-first";
1438+
if (profile === "native-first") {
1439+
for (const name of CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES) {
1440+
excludes.add(name);
1441+
}
1442+
}
1443+
for (const name of config.codexDynamicToolsExclude ?? []) {
1444+
const trimmed = name.trim();
1445+
if (trimmed) {
1446+
excludes.add(trimmed);
1447+
}
1448+
}
1449+
return excludes.size === 0 ? tools : tools.filter((tool) => !excludes.has(tool.name));
1450+
}
1451+
14141452
async function withCodexStartupTimeout<T>(params: {
14151453
timeoutMs: number;
14161454
timeoutFloorMs?: number;
@@ -1584,6 +1622,7 @@ export const __testing = {
15841622
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
15851623
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
15861624
buildCodexNativeHookRelayId,
1625+
applyCodexDynamicToolProfile,
15871626
filterToolsForVisionInputs,
15881627
handleDynamicToolCallWithTimeout,
15891628
...createCodexAppServerClientFactoryTestHooks((factory) => {

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ function stabilizeJsonValue(value: JsonValue): JsonValue {
222222
export function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string {
223223
const promptOverlay = renderCodexRuntimePromptOverlay(params);
224224
const sections = [
225-
"You are running inside OpenClaw. Use OpenClaw dynamic tools for messaging, cron, sessions, and host actions when available.",
225+
"You are running inside OpenClaw. Use OpenClaw dynamic tools for OpenClaw-specific integrations such as messaging, cron, sessions, media, gateway, and nodes when available.",
226226
"Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply.",
227227
promptOverlay,
228228
params.extraSystemPrompt,

src/agents/pi-tools.model-provider-collision.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,27 @@ describe("applyModelProviderToolPolicy", () => {
6868
expect(toolNames(filtered)).toEqual(["read", "exec"]);
6969
});
7070

71+
it("can keep managed web_search for Codex app-server dynamic tools", () => {
72+
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
73+
config: {
74+
tools: {
75+
web: {
76+
search: {
77+
enabled: true,
78+
openaiCodex: { enabled: true, mode: "cached" },
79+
},
80+
},
81+
},
82+
},
83+
modelProvider: "gateway",
84+
modelApi: "openai-codex-responses",
85+
modelId: "gpt-5.4",
86+
suppressManagedWebSearch: false,
87+
});
88+
89+
expect(toolNames(filtered)).toEqual(["read", "web_search", "exec"]);
90+
});
91+
7192
it("removes managed web_search for direct Codex models when auth is available", () => {
7293
const filtered = __testing.applyModelProviderToolPolicy(baseTools, {
7394
config: {

0 commit comments

Comments
 (0)