diff --git a/CHANGELOG.md b/CHANGELOG.md index 512aba182b0..813cb1a4706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Providers/Codex: pass agent and workspace directories into provider stream wrappers so Codex native `web_search` activation can evaluate the correct auth context, and smoke-test the built status-message runtime by resolving the emitted bundle name. Carries forward #67843; refs #65909. Thanks @neilofneils404. - Cron/models: keep `payload.model` as a per-job primary that can use configured fallbacks, while still letting `payload.fallbacks: []` make cron runs strict and avoid hidden agent-primary retries. Refs #73023. Thanks @pavelyortho-cyber. - Models/fallbacks: treat user-selected session models as exact choices, so `/model ollama/...` and model-picker switches fail visibly when the selected provider is unreachable instead of answering from an unrelated configured fallback. Fixes #73023. Thanks @pavelyortho-cyber. +- Codex harness: expose `appServer.clearEnv` in the plugin config schema so deployments can keep Gateway-level `OPENAI_API_KEY` for embeddings and direct OpenAI models while removing it from the spawned native Codex app-server process. Fixes #73057. Thanks @holgergruenhagen. - CLI/model probes: fail local `infer model run` probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber. - CLI/Ollama: run local `infer model run` through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native `/api/chat` request. Fixes #72851. Thanks @TotalRes2020. - Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk. diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index ed8eb98fbfb..ecfe5bdb3a0 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -508,22 +508,47 @@ For an already-running app-server, use WebSocket transport: } ``` +Stdio app-server launches inherit OpenClaw's process environment by default. +When the Gateway needs `OPENAI_API_KEY` for embeddings or direct OpenAI models +but Codex should use the local ChatGPT login, clear that variable only for the +Codex child: + +```json5 +{ + plugins: { + entries: { + codex: { + enabled: true, + config: { + appServer: { + clearEnv: ["OPENAI_API_KEY"], + }, + }, + }, + }, + }, +} +``` + +`appServer.clearEnv` only affects the spawned Codex app-server child process. + 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. | -| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | -| `mode` | `"yolo"` | Preset for YOLO or guardian-reviewed execution. | -| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. | -| `sandbox` | `"danger-full-access"` | Native Codex sandbox mode sent to thread start/resume. | -| `approvalsReviewer` | `"user"` | Use `"auto_review"` to let Codex review native approval prompts. `guardian_subagent` remains a legacy alias. | -| `serviceTier` | unset | Optional Codex app-server service tier: `"fast"`, `"flex"`, or `null`. Invalid legacy values are ignored. | +| 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` | `[]` | Environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. | +| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. | +| `mode` | `"yolo"` | Preset for YOLO or guardian-reviewed execution. | +| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. | +| `sandbox` | `"danger-full-access"` | Native Codex sandbox mode sent to thread start/resume. | +| `approvalsReviewer` | `"user"` | Use `"auto_review"` to let Codex review native approval prompts. `guardian_subagent` remains a legacy alias. | +| `serviceTier` | unset | Optional Codex app-server service tier: `"fast"`, `"flex"`, or `null`. Invalid legacy values are ignored. | Environment overrides remain available for local testing: diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index a0da7789df6..4b8d5794046 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -109,6 +109,10 @@ "type": "object", "additionalProperties": { "type": "string" } }, + "clearEnv": { + "type": "array", + "items": { "type": "string" } + }, "requestTimeoutMs": { "type": "number", "minimum": 1, @@ -234,6 +238,11 @@ "help": "Additional headers sent to the WebSocket app-server.", "advanced": true }, + "appServer.clearEnv": { + "label": "Clear Environment", + "help": "Environment variable names removed from the spawned stdio app-server process after overrides are applied.", + "advanced": true + }, "appServer.requestTimeoutMs": { "label": "Request Timeout", "help": "Maximum time to wait for Codex app-server control-plane requests.", diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index 79bf770f823..aade1d88c88 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -18,6 +18,7 @@ describe("Codex app-server config", () => { transport: "websocket", url: "ws://127.0.0.1:39175", headers: { "X-Test": "yes" }, + clearEnv: ["OPENAI_API_KEY"], approvalPolicy: "on-request", sandbox: "danger-full-access", approvalsReviewer: "guardian_subagent", @@ -40,11 +41,29 @@ describe("Codex app-server config", () => { transport: "websocket", url: "ws://127.0.0.1:39175", headers: { "X-Test": "yes" }, + clearEnv: ["OPENAI_API_KEY"], }), }), ); }); + it("normalizes app-server environment variables to clear", () => { + const runtime = resolveCodexAppServerRuntimeOptions({ + pluginConfig: { + appServer: { + clearEnv: [" OPENAI_API_KEY ", "", " "], + }, + }, + env: {}, + }); + + expect(runtime.start).toEqual( + expect.objectContaining({ + clearEnv: ["OPENAI_API_KEY"], + }), + ); + }); + it("drops invalid legacy service tiers without discarding the rest of the config", () => { const runtime = resolveCodexAppServerRuntimeOptions({ pluginConfig: { diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index a1fc3184dc1..2f643d66720 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -66,6 +66,7 @@ export type CodexPluginConfig = { url?: string; authToken?: string; headers?: Record; + clearEnv?: string[]; requestTimeoutMs?: number; approvalPolicy?: CodexAppServerApprovalPolicy; sandbox?: CodexAppServerSandboxMode; @@ -83,6 +84,7 @@ export const CODEX_APP_SERVER_CONFIG_KEYS = [ "url", "authToken", "headers", + "clearEnv", "requestTimeoutMs", "approvalPolicy", "sandbox", @@ -152,6 +154,7 @@ const codexPluginConfigSchema = z url: z.string().optional(), authToken: z.string().optional(), headers: z.record(z.string(), z.string()).optional(), + clearEnv: z.array(z.string()).optional(), requestTimeoutMs: z.number().positive().optional(), approvalPolicy: codexAppServerApprovalPolicySchema.optional(), sandbox: codexAppServerSandboxSchema.optional(), @@ -188,6 +191,7 @@ export function resolveCodexAppServerRuntimeOptions( : "managed"; const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS); const headers = normalizeHeaders(config.headers); + const clearEnv = normalizeStringList(config.clearEnv); const authToken = readNonEmptyString(config.authToken); const url = readNonEmptyString(config.url); const policyMode = @@ -210,6 +214,7 @@ export function resolveCodexAppServerRuntimeOptions( ...(url ? { url } : {}), ...(authToken ? { authToken } : {}), headers, + ...(clearEnv.length > 0 ? { clearEnv } : {}), }, requestTimeoutMs: normalizePositiveNumber(config.requestTimeoutMs, 60_000), approvalPolicy: @@ -373,6 +378,15 @@ function normalizeHeaders(value: unknown): Record { ); } +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((entry) => readNonEmptyString(entry)) + .filter((entry): entry is string => entry !== undefined); +} + function readBooleanEnv(value: string | undefined): boolean | undefined { if (value === undefined) { return undefined;