diff --git a/CHANGELOG.md b/CHANGELOG.md index c076b1d14dc..b95b090c6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai - Dependencies: refresh workspace pins and patch targets, including ACPX `@agentclientprotocol/claude-agent-acp` `0.33.1`, Codex ACP `0.14.0`, Baileys `7.0.0-rc10`, Google GenAI `2.0.1`, OpenAI `6.37.0`, AWS SDK `3.1045.0`, Kysely `0.29.0`, Tlon skill `0.3.6`, Aimock `1.19.5`, and tsdown `0.22.0`. - Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle. - CLI/onboarding: improve setup, onboarding, configure, and channel command wayfinding so terminal flows explain the next useful command instead of relying on terse setup labels. +- Agents/Codex: remove the configurable Codex dynamic-tools profile so Codex app-server always owns workspace, edit, patch, exec, process, and plan tools while OpenClaw integration tools remain available. ### Fixes diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 0ab8b3b4d6e..f11e536be75 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -31ba805dfaa665fe16735d7c2f3a5e9fb0c349aeb5e301881b5da65cee62b1f8 config-baseline.json +f10281b32feb151b97952fda0722bae46c66456783e82ccaa7f39f11306287fa config-baseline.json 5552e7871057e05d5699f90f536ce58e62d5b8cfc6020d3e7106be7915fed041 config-baseline.core.json 80f0f51caedf14dc2138d975b62852ff7c5cf085df1c734c9de279f5859a7eeb config-baseline.channel.json -3a08e5422ce1422d9e2b75feac7e44cdcd0c3dc1eea594f664bceec13cbe3f58 config-baseline.plugin.json +dba159f639977bb96d79f0b78de2c6de48d25ed6ba1590f55812affb7ca6e4b0 config-baseline.plugin.json diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index 74dd558b3e6..24bf60d421c 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -67,6 +67,7 @@ Notes: - Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing. - Doctor warns when no command owner is configured. The command owner is the human operator account allowed to run owner-only commands and approve dangerous actions. DM pairing only lets someone talk to the bot; if you approved a sender before first-owner bootstrap existed, set `commands.ownerAllowFrom` explicitly. - Doctor warns when Codex-mode agents are configured and personal Codex CLI assets exist in the operator's Codex home. Local Codex app-server launches use isolated per-agent homes, so use `openclaw migrate codex --dry-run` to inventory assets that should be promoted deliberately. +- Doctor removes retired `plugins.entries.codex.config.codexDynamicToolsProfile`; Codex app-server always keeps Codex-native workspace tools native. - Doctor warns when skills allowed for the default agent are unavailable in the current runtime environment because bins, env vars, config, or OS requirements are missing. `doctor --fix` can disable those unavailable skills with `skills.entries..enabled=false`; install/configure the missing requirement instead when you want to keep the skill active. - If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`). - If legacy sandbox registry files (`~/.openclaw/sandbox/containers.json` or `~/.openclaw/sandbox/browsers.json`) are present, doctor reports them; `openclaw doctor --fix` migrates valid entries into sharded registry directories and quarantines invalid legacy files. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 20ccc5e3ac2..001c9a39559 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -223,6 +223,7 @@ That stages grounded durable candidates into the short-term dreaming store while - `browser.profiles.*.driver: "extension"` → `"existing-session"` - remove `browser.relayBindHost` (legacy extension relay setting) - legacy `models.providers.*.api: "openai"` → `"openai-completions"` (gateway startup also skips providers whose `api` is set to a future or unknown enum value rather than failing closed) + - remove `plugins.entries.codex.config.codexDynamicToolsProfile`; Codex app-server always keeps Codex-native workspace tools native Doctor warnings also include account-default guidance for multi-account channels: diff --git a/docs/plugins/codex-harness-reference.md b/docs/plugins/codex-harness-reference.md index 354a584d338..525393e98c6 100644 --- a/docs/plugins/codex-harness-reference.md +++ b/docs/plugins/codex-harness-reference.md @@ -42,7 +42,6 @@ Supported top-level fields: | -------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `discovery` | enabled | Model discovery settings for Codex app-server `model/list`. | | `appServer` | managed stdio app-server | Transport, command, auth, approval, sandbox, and timeout settings. | -| `codexDynamicToolsProfile` | `"native-first"` | Use `"openclaw-compat"` to expose the full OpenClaw dynamic tool set to Codex app-server. | | `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. | | `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. | | `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. See [Native Codex plugins](/plugins/codex-native-plugins). | @@ -211,9 +210,8 @@ isolation on local launches. ## Dynamic tools -Codex dynamic tools default to the `native-first` profile and `searchable` -loading. In that mode, OpenClaw does not expose dynamic tools that duplicate -Codex-native workspace operations: +Codex dynamic tools default to `searchable` loading. OpenClaw does not expose +dynamic tools that duplicate Codex-native workspace operations: - `read` - `write` diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index 6a6f24ad3ed..a39e2b19138 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -377,6 +377,128 @@ Get the thread id from the completed `/diagnostics` reply, `/codex binding`, or For upload mechanics and runtime-level diagnostics boundaries, see [Codex harness runtime](/plugins/codex-harness-runtime#codex-feedback-upload). +Auth is selected in this order: + +1. An explicit OpenClaw Codex auth profile for the agent. +2. The app-server's existing account in that agent's Codex home. +3. For local stdio app-server launches only, `CODEX_API_KEY`, then + `OPENAI_API_KEY`, when no app-server account is present and OpenAI auth is + still required. + +When OpenClaw sees a ChatGPT subscription-style Codex auth profile, it removes +`CODEX_API_KEY` and `OPENAI_API_KEY` from the spawned Codex child process. That +keeps Gateway-level API keys available for embeddings or direct OpenAI models +without making native Codex app-server turns bill through the API by accident. +Explicit Codex API-key profiles and local stdio env-key fallback use app-server +login instead of inherited child-process env. WebSocket app-server connections +do not receive Gateway env API-key fallback; use an explicit auth profile or the +remote app-server's own account. + +If a deployment needs additional environment isolation, add those variables to +`appServer.clearEnv`: + +```json5 +{ + plugins: { + entries: { + codex: { + enabled: true, + config: { + appServer: { + clearEnv: ["CODEX_API_KEY", "OPENAI_API_KEY"], + }, + }, + }, + }, + }, +} +``` + +`appServer.clearEnv` only affects the spawned Codex app-server child process. + +Codex dynamic tools default to `searchable` loading. OpenClaw does not expose +dynamic tools that duplicate Codex-native workspace operations: `read`, `write`, +`edit`, `apply_patch`, `exec`, `process`, and `update_plan`. Remaining OpenClaw +integration tools such as messaging, sessions, media, cron, browser, nodes, +gateway, `heartbeat_respond`, and `web_search` are available through Codex tool +search under the `openclaw` namespace, keeping the initial model context +smaller. +`sessions_yield` and message-tool-only source replies stay direct because those +are turn-control contracts. Heartbeat collaboration instructions tell Codex to +search for `heartbeat_respond` before ending a heartbeat turn when the tool is +not already loaded. + +Set `codexDynamicToolsLoading: "direct"` only when connecting to a custom Codex +app-server that cannot search deferred dynamic tools or when debugging the full +tool payload. + +Supported top-level Codex plugin fields: + +| Field | Default | Meaning | +| -------------------------- | -------------- | ---------------------------------------------------------------------------------------- | +| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. | +| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. | +| `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. | + +Supported `appServer` fields: + +| Field | Default | Meaning | +| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. | +| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. | +| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. | +| `url` | unset | WebSocket app-server URL. | +| `authToken` | unset | Bearer token for WebSocket transport. | +| `headers` | `{}` | Extra WebSocket headers. | +| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. `CODEX_HOME` and `HOME` are reserved for OpenClaw's per-agent Codex isolation on local launches. | +| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | +| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. | +| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. | +| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. | +| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. | +| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. | +| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. | + +OpenClaw-owned dynamic tool calls are bounded independently from +`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 30 second +OpenClaw watchdog by default. A positive per-call `timeoutMs` argument extends +or shortens that specific tool budget. The `image_generate` tool also uses +`agents.defaults.imageGenerationModel.timeoutMs` when the tool call does not +provide its own timeout, and the media-understanding `image` tool uses +`tools.media.image.timeoutSeconds` or its 60 second media default. Dynamic tool +budgets are capped at 600000 ms. On timeout, OpenClaw aborts the tool signal +where supported and returns a failed dynamic-tool response to Codex so the turn +can continue instead of leaving the session in `processing`. + +After OpenClaw responds to a Codex turn-scoped app-server request, the harness +also expects Codex to finish the native turn with `turn/completed`. If the +app-server goes quiet for `appServer.turnCompletionIdleTimeoutMs` after that +response, OpenClaw best-effort interrupts the Codex turn, records a diagnostic +timeout, and releases the OpenClaw session lane so follow-up chat messages are +not queued behind a stale native turn. Any non-terminal notification for the +same turn, including `rawResponseItem/completed`, disarms that short watchdog +because Codex has proven the turn is still alive; the longer terminal watchdog +continues to protect genuinely stuck turns. Timeout diagnostics include the +last app-server notification method and, for raw assistant response items, the +item type, role, id, and a bounded assistant text preview. + +Environment overrides remain available for local testing: + +- `OPENCLAW_CODEX_APP_SERVER_BIN` +- `OPENCLAW_CODEX_APP_SERVER_ARGS` +- `OPENCLAW_CODEX_APP_SERVER_MODE=yolo|guardian` +- `OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY` +- `OPENCLAW_CODEX_APP_SERVER_SANDBOX` + +`OPENCLAW_CODEX_APP_SERVER_BIN` bypasses the managed binary when +`appServer.command` is unset. + +`OPENCLAW_CODEX_APP_SERVER_GUARDIAN=1` was removed. Use +`plugins.entries.codex.config.appServer.mode: "guardian"` instead, or +`OPENCLAW_CODEX_APP_SERVER_MODE=guardian` for one-off local testing. Config is +preferred for repeatable deployments because it keeps the plugin behavior in the +same reviewed file as the rest of the Codex harness setup. + ## Native Codex plugins Native Codex plugin support uses Codex app-server's own app and plugin diff --git a/extensions/codex/doctor-contract-api.test.ts b/extensions/codex/doctor-contract-api.test.ts new file mode 100644 index 00000000000..ab47e18d400 --- /dev/null +++ b/extensions/codex/doctor-contract-api.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract-api.js"; + +describe("codex doctor contract", () => { + it("reports the retired dynamic tools profile config key", () => { + expect( + legacyConfigRules[0]?.match({ + codexDynamicToolsProfile: "openclaw-compat", + codexDynamicToolsLoading: "direct", + }), + ).toBe(true); + expect(legacyConfigRules[0]?.match({ codexDynamicToolsLoading: "direct" })).toBe(false); + }); + + it("removes the retired dynamic tools profile without dropping other Codex config", () => { + const original = { + plugins: { + entries: { + codex: { + enabled: true, + config: { + codexDynamicToolsProfile: "openclaw-compat", + codexDynamicToolsLoading: "direct", + codexDynamicToolsExclude: ["custom_tool"], + appServer: { mode: "guardian" }, + }, + }, + }, + }, + }; + + const result = normalizeCompatibilityConfig({ cfg: original }); + + expect(result.changes).toEqual([ + "Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.", + ]); + expect(result.config.plugins?.entries?.codex?.config).toEqual({ + codexDynamicToolsLoading: "direct", + codexDynamicToolsExclude: ["custom_tool"], + appServer: { mode: "guardian" }, + }); + expect(original.plugins.entries.codex.config).toHaveProperty("codexDynamicToolsProfile"); + }); +}); diff --git a/extensions/codex/doctor-contract-api.ts b/extensions/codex/doctor-contract-api.ts index 1ef0ab52183..6f5dc1fa757 100644 --- a/extensions/codex/doctor-contract-api.ts +++ b/extensions/codex/doctor-contract-api.ts @@ -1,5 +1,61 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import type { DoctorSessionRouteStateOwner } from "openclaw/plugin-sdk/runtime-doctor"; +type LegacyConfigRule = { + path: string[]; + message: string; + match: (value: unknown) => boolean; +}; + +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null; +} + +function hasRetiredDynamicToolsProfile(value: unknown): boolean { + return Object.prototype.hasOwnProperty.call(asRecord(value) ?? {}, "codexDynamicToolsProfile"); +} + +export const legacyConfigRules: LegacyConfigRule[] = [ + { + path: ["plugins", "entries", "codex", "config"], + message: + 'plugins.entries.codex.config.codexDynamicToolsProfile is retired; Codex app-server always keeps Codex-native workspace tools native. Run "openclaw doctor --fix".', + match: hasRetiredDynamicToolsProfile, + }, +]; + +export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }): { + config: OpenClawConfig; + changes: string[]; +} { + const rawEntry = asRecord(cfg.plugins?.entries?.codex); + const rawPluginConfig = asRecord(rawEntry?.config); + if (!rawPluginConfig || !hasRetiredDynamicToolsProfile(rawPluginConfig)) { + return { config: cfg, changes: [] }; + } + + const nextConfig = structuredClone(cfg) as OpenClawConfig & { + plugins?: Record; + }; + const nextPlugins = asRecord(nextConfig.plugins); + const nextEntries = asRecord(nextPlugins?.entries); + const nextEntry = asRecord(nextEntries?.codex); + const nextPluginConfig = asRecord(nextEntry?.config); + if (!nextPluginConfig) { + return { config: cfg, changes: [] }; + } + + delete nextPluginConfig.codexDynamicToolsProfile; + return { + config: nextConfig, + changes: [ + "Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.", + ], + }; +} + export const sessionRouteStateOwners: DoctorSessionRouteStateOwner[] = [ { id: "codex", diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index a8173798225..bfaa9d35a7f 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -33,11 +33,6 @@ "type": "object", "additionalProperties": false, "properties": { - "codexDynamicToolsProfile": { - "type": "string", - "enum": ["native-first", "openclaw-compat"], - "default": "native-first" - }, "codexDynamicToolsLoading": { "type": "string", "enum": ["searchable", "direct"], @@ -197,11 +192,6 @@ } }, "uiHints": { - "codexDynamicToolsProfile": { - "label": "Dynamic Tools Profile", - "help": "Select which OpenClaw dynamic tools are exposed to Codex app-server. native-first omits tools Codex already owns.", - "advanced": true - }, "codexDynamicToolsLoading": { "label": "Dynamic Tools Loading", "help": "Use searchable to defer OpenClaw dynamic tools behind Codex tool search, or direct to expose them in the initial context.", diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index 766a7461257..bb57dec7fb0 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -453,18 +453,25 @@ allowed_sandbox_modes = ["read-only", "workspace-write"] ); }); - it("parses dynamic tool profile controls", () => { + it("parses dynamic tool controls", () => { + expect( + readCodexPluginConfig({ + codexDynamicToolsLoading: "direct", + codexDynamicToolsExclude: ["custom_tool"], + }), + ).toEqual({ + codexDynamicToolsLoading: "direct", + codexDynamicToolsExclude: ["custom_tool"], + }); + }); + + it("rejects the retired dynamic tool profile key", () => { expect( readCodexPluginConfig({ codexDynamicToolsProfile: "openclaw-compat", codexDynamicToolsLoading: "direct", - codexDynamicToolsExclude: ["custom_tool"], }), - ).toMatchObject({ - codexDynamicToolsProfile: "openclaw-compat", - codexDynamicToolsLoading: "direct", - codexDynamicToolsExclude: ["custom_tool"], - }); + ).toEqual({}); }); it("parses native Codex plugin policy without treating wildcard as supported config", () => { diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 4eab3ad3d39..906e6b3ee70 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -31,7 +31,6 @@ export type CodexAppServerEffectiveApprovalPolicy = export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access"; type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent"; type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env"; -type CodexDynamicToolsProfile = "native-first" | "openclaw-compat"; export type CodexDynamicToolsLoading = "searchable" | "direct"; export type CodexPluginDestructivePolicy = boolean; @@ -110,7 +109,6 @@ export type CodexAppServerRuntimeOptions = { }; export type CodexPluginConfig = { - codexDynamicToolsProfile?: CodexDynamicToolsProfile; codexDynamicToolsLoading?: CodexDynamicToolsLoading; codexDynamicToolsExclude?: string[]; discovery?: { @@ -194,7 +192,6 @@ const codexAppServerApprovalPolicySchema = z.enum([ ]); const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]); const codexAppServerApprovalsReviewerSchema = z.enum(["user", "auto_review", "guardian_subagent"]); -const codexDynamicToolsProfileSchema = z.enum(["native-first", "openclaw-compat"]); const codexDynamicToolsLoadingSchema = z.enum(["searchable", "direct"]); const codexAppServerServiceTierSchema = z .preprocess( @@ -222,7 +219,6 @@ const codexPluginsConfigSchema = z const codexPluginConfigSchema = z .object({ - codexDynamicToolsProfile: codexDynamicToolsProfileSchema.optional(), codexDynamicToolsLoading: codexDynamicToolsLoadingSchema.optional(), codexDynamicToolsExclude: z.array(z.string()).optional(), discovery: z diff --git a/extensions/codex/src/app-server/dynamic-tool-profile.ts b/extensions/codex/src/app-server/dynamic-tool-profile.ts index e6dc0797759..33671d7d0b5 100644 --- a/extensions/codex/src/app-server/dynamic-tool-profile.ts +++ b/extensions/codex/src/app-server/dynamic-tool-profile.ts @@ -1,6 +1,6 @@ import type { CodexPluginConfig } from "./config.js"; -export const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [ +export const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [ "read", "write", "edit", @@ -20,16 +20,13 @@ export function normalizeCodexDynamicToolName(name: string): string { return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized; } -export function applyCodexDynamicToolProfile( +export function filterCodexDynamicTools( tools: T[], - config: Pick, + config: Pick, ): T[] { const excludes = new Set(); - const profile = config.codexDynamicToolsProfile ?? "native-first"; - if (profile === "native-first") { - for (const name of CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES) { - excludes.add(name); - } + for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) { + excludes.add(name); } for (const name of config.codexDynamicToolsExclude ?? []) { const trimmed = normalizeCodexDynamicToolName(name); diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 6ef2baf46b2..746aa04ea7a 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -544,7 +544,7 @@ describe("runCodexAppServerAttempt", () => { await fs.rm(tempDir, { recursive: true, force: true }); }); - it("defaults Codex dynamic tools to the native-first profile", () => { + it("filters Codex-native dynamic tools from app-server tool exposure", () => { const tools = [ "read", "write", @@ -559,7 +559,7 @@ describe("runCodexAppServerAttempt", () => { "sessions_spawn", ].map((name) => ({ name })); - expect(__testing.applyCodexDynamicToolProfile(tools, {}).map((tool) => tool.name)).toEqual([ + expect(__testing.filterCodexDynamicTools(tools, {}).map((tool) => tool.name)).toEqual([ "web_search", "message", "heartbeat_respond", @@ -567,17 +567,16 @@ describe("runCodexAppServerAttempt", () => { ]); }); - it("allows Codex dynamic tool filtering to opt back into OpenClaw compatibility", () => { + it("applies additional Codex dynamic tool excludes without exposing Codex-native tools", () => { const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name })); expect( __testing - .applyCodexDynamicToolProfile(tools, { - codexDynamicToolsProfile: "openclaw-compat", + .filterCodexDynamicTools(tools, { codexDynamicToolsExclude: ["custom_tool"], }) .map((tool) => tool.name), - ).toEqual(["read", "exec", "message"]); + ).toEqual(["message"]); }); it("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => { @@ -590,7 +589,7 @@ describe("runCodexAppServerAttempt", () => { } throw new Error(`unexpected method: ${method}`); }); - const dynamicTools = __testing.applyCodexDynamicToolProfile( + const dynamicTools = __testing.filterCodexDynamicTools( [ "read", "write", diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index a567a22cdc0..fe8336a4340 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -73,10 +73,7 @@ import { type CodexPluginConfig, } from "./config.js"; import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js"; -import { - applyCodexDynamicToolProfile, - normalizeCodexDynamicToolName, -} from "./dynamic-tool-profile.js"; +import { filterCodexDynamicTools, normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js"; import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js"; import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js"; import { CodexAppServerEventProjector } from "./event-projector.js"; @@ -1907,8 +1904,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) { input.runAbortController.abort("sessions_yield"); }, }); - const profiledTools = applyCodexDynamicToolProfile(allTools, input.pluginConfig); - const visionFilteredTools = filterToolsForVisionInputs(profiledTools, { + const codexFilteredTools = filterCodexDynamicTools(allTools, input.pluginConfig); + const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, { modelHasVision, hasInboundImages: (params.images?.length ?? 0) > 0, }); @@ -2369,7 +2366,7 @@ export const __testing = { CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS, CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS, buildCodexNativeHookRelayId, - applyCodexDynamicToolProfile, + filterCodexDynamicTools, buildDynamicTools, filterCodexDynamicToolsForAllowlist, filterToolsForVisionInputs, diff --git a/extensions/codex/test-api.ts b/extensions/codex/test-api.ts index 8b0ad5c8994..732c5be2487 100644 --- a/extensions/codex/test-api.ts +++ b/extensions/codex/test-api.ts @@ -7,7 +7,7 @@ import { resolveCodexAppServerRuntimeOptions, } from "./src/app-server/config.js"; import type { CodexPluginConfig } from "./src/app-server/config.js"; -import { applyCodexDynamicToolProfile } from "./src/app-server/dynamic-tool-profile.js"; +import { filterCodexDynamicTools } from "./src/app-server/dynamic-tool-profile.js"; import { createCodexDynamicToolBridge } from "./src/app-server/dynamic-tools.js"; import type { CodexDynamicToolSpec, JsonObject } from "./src/app-server/protocol.js"; import { @@ -70,15 +70,12 @@ export function buildCodexHarnessPromptSnapshot(params: { export function createCodexDynamicToolSpecsForPromptSnapshot(params: { tools: AnyAgentTool[]; - pluginConfig?: Pick< - CodexPluginConfig, - "codexDynamicToolsProfile" | "codexDynamicToolsLoading" | "codexDynamicToolsExclude" - >; + pluginConfig?: Pick; directToolNames?: Iterable; }): CodexDynamicToolSpec[] { - const profiledTools = applyCodexDynamicToolProfile(params.tools, params.pluginConfig ?? {}); + const filteredTools = filterCodexDynamicTools(params.tools, params.pluginConfig ?? {}); return createCodexDynamicToolBridge({ - tools: profiledTools, + tools: filteredTools, signal: new AbortController().signal, loading: params.pluginConfig?.codexDynamicToolsLoading ?? "searchable", directToolNames: params.directToolNames, diff --git a/scripts/tsdown-build.mjs b/scripts/tsdown-build.mjs index 76531768410..10800a411d5 100644 --- a/scripts/tsdown-build.mjs +++ b/scripts/tsdown-build.mjs @@ -19,6 +19,7 @@ const ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g"); const HASHED_ROOT_JS_RE = /^(?.+)-[A-Za-z0-9_-]+\.js$/u; const DEFAULT_CAPTURE_BYTES = 8 * 1024 * 1024; const DEFAULT_HEARTBEAT_MS = 30_000; +const DEFAULT_TSDOWN_NODE_OPTIONS = "--max-old-space-size=6144"; const TERMINATION_GRACE_MS = 5_000; const TSDOWN_OUTPUT_ROOTS = ["dist", "dist-runtime"]; const GENERATED_SOURCE_DECLARATION_PATHSPEC = ":(glob)extensions/**/*.d.ts"; @@ -199,6 +200,19 @@ function parseNonNegativeInteger(value) { return Math.trunc(parsed); } +function resolveTsdownEnv(env) { + const nodeOptions = env.NODE_OPTIONS?.trim() ?? ""; + if (/(?:^|\s)--max-old-space-size(?:=|\s+)/u.test(nodeOptions)) { + return env; + } + return { + ...env, + NODE_OPTIONS: nodeOptions + ? `${nodeOptions} ${DEFAULT_TSDOWN_NODE_OPTIONS}` + : DEFAULT_TSDOWN_NODE_OPTIONS, + }; +} + export function createTsdownOutputScanner(params = {}) { const maxCaptureBytes = params.maxCaptureBytes ?? DEFAULT_CAPTURE_BYTES; let captured = ""; @@ -242,7 +256,7 @@ export function createTsdownOutputScanner(params = {}) { } export function resolveTsdownBuildInvocation(params = {}) { - const env = params.env ?? process.env; + const env = resolveTsdownEnv(params.env ?? process.env); const runner = resolvePnpmRunner({ pnpmArgs: [ "exec", diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index d8f49671d8a..b417d65f3e4 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -82,7 +82,10 @@ type CodexPromptSnapshotApi = { }; createCodexDynamicToolSpecsForPromptSnapshot: (params: { tools: AnyAgentTool[]; - pluginConfig?: { codexDynamicToolsProfile?: "native-first" | "openclaw-compat" }; + pluginConfig?: { + codexDynamicToolsLoading?: "searchable" | "direct"; + codexDynamicToolsExclude?: string[]; + }; directToolNames?: string[]; }) => CodexDynamicToolSpec[]; }; @@ -359,7 +362,6 @@ function createDynamicTools(params: { }); return codexApi.createCodexDynamicToolSpecsForPromptSnapshot({ tools: normalized.filter((tool) => HAPPY_PATH_TOOL_NAMES.has(tool.name)), - pluginConfig: { codexDynamicToolsProfile: "native-first" }, directToolNames: ["message"], }); } @@ -670,9 +672,7 @@ function renderScenarioSnapshot(scenario: PromptScenario): string { scenario, sessionKey: scenario.ctx.SessionKey ?? `agent:main:${scenario.id}`, }); - const appServer = codexApi.resolveCodexPromptSnapshotAppServerOptions({ - codexDynamicToolsProfile: "native-first", - }); + const appServer = codexApi.resolveCodexPromptSnapshotAppServerOptions(); const codexSnapshot = codexApi.buildCodexHarnessPromptSnapshot({ attempt, cwd: WORKSPACE_DIR, diff --git a/test/scripts/tsdown-build.test.ts b/test/scripts/tsdown-build.test.ts index 7757937d29b..1039a573050 100644 --- a/test/scripts/tsdown-build.test.ts +++ b/test/scripts/tsdown-build.test.ts @@ -54,11 +54,21 @@ describe("resolveTsdownBuildInvocation", () => { stdio: ["ignore", "pipe", "pipe"], shell: false, windowsVerbatimArguments: undefined, - env: {}, + env: { NODE_OPTIONS: "--max-old-space-size=6144" }, }, }); }); + it("preserves explicit tsdown heap settings", () => { + const result = resolveTsdownBuildInvocation({ + nodeExecPath: "/usr/bin/node", + npmExecPath: "/tmp/pnpm.cjs", + env: { NODE_OPTIONS: "--trace-warnings --max-old-space-size=8192" }, + }); + + expect(result.options.env.NODE_OPTIONS).toBe("--trace-warnings --max-old-space-size=8192"); + }); + it("keeps source-checkout prune best-effort", () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); const rmSync = vi.spyOn(fs, "rmSync");