fix(codex): expose app-server env controls

This commit is contained in:
pashpashpash
2026-04-27 18:27:30 -04:00
committed by Peter Steinberger
parent 09c39463bb
commit aeb007e4e5
5 changed files with 82 additions and 14 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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.",

View File

@@ -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: {

View File

@@ -66,6 +66,7 @@ export type CodexPluginConfig = {
url?: string;
authToken?: string;
headers?: Record<string, string>;
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<string, string> {
);
}
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;