Skip to content

Commit 8ed092e

Browse files
committed
feat(codex): support app-server secret refs
1 parent 01e5621 commit 8ed092e

12 files changed

Lines changed: 397 additions & 22 deletions

docs/plugins/codex-harness-reference.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ Supported `appServer` fields:
9191
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. |
9292
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
9393
| `url` | unset | WebSocket app-server URL. |
94-
| `authToken` | unset | Bearer token for WebSocket transport. |
95-
| `headers` | `{}` | Extra WebSocket headers. |
94+
| `authToken` | unset | Bearer token for WebSocket transport. Accepts a literal string or SecretInput such as `${CODEX_APP_SERVER_TOKEN}`. |
95+
| `headers` | `{}` | Extra WebSocket headers. Header values accept literal strings or SecretInput values, for example `x-codex-client-session-token: "${CODEX_CLIENT_SESSION_TOKEN}"`. |
9696
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
9797
| `remoteWorkspaceRoot` | unset | Remote Codex app-server workspace root. When set, OpenClaw infers the local workspace root from the resolved OpenClaw workspace, preserves the current cwd suffix under this remote root, and sends only the final app-server cwd to Codex. If the cwd is outside the resolved OpenClaw workspace root, OpenClaw fails closed instead of sending a gateway-local path to the remote app-server. |
9898
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
@@ -149,11 +149,15 @@ must report stable version `0.125.0` or newer.
149149

150150
OpenClaw treats non-loopback WebSocket app-server URLs as remote and requires
151151
identity-bearing WebSocket auth through `appServer.authToken` or an
152-
`Authorization` header. When native Codex plugins are configured, OpenClaw uses
153-
the connected app-server's plugin control plane to install or refresh those
154-
plugins and then refreshes app inventory so plugin-owned apps are visible to the
155-
Codex thread. Only connect OpenClaw to remote app-servers that are trusted to
156-
accept OpenClaw-managed plugin installs and app inventory refreshes.
152+
`Authorization` header. `appServer.authToken` and each `appServer.headers.*`
153+
value can be a SecretInput; the secrets runtime resolves SecretRefs and env
154+
shorthand before OpenClaw builds app-server start options, and unresolved
155+
structured SecretRefs fail before any token or header is sent. When native Codex
156+
plugins are configured, OpenClaw uses the connected app-server's plugin control
157+
plane to install or refresh those plugins and then refreshes app inventory so
158+
plugin-owned apps are visible to the Codex thread. Only connect OpenClaw to
159+
remote app-servers that are trusted to accept OpenClaw-managed plugin installs
160+
and app inventory refreshes.
157161

158162
## Approval and sandbox modes
159163

docs/plugins/codex-harness.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,8 +552,8 @@ Supported `appServer` fields:
552552
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
553553
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
554554
| `url` | unset | WebSocket app-server URL. |
555-
| `authToken` | unset | Bearer token for WebSocket transport. |
556-
| `headers` | `{}` | Extra WebSocket headers. |
555+
| `authToken` | unset | Bearer token for WebSocket transport. Accepts a literal string or SecretInput such as `${CODEX_APP_SERVER_TOKEN}`. |
556+
| `headers` | `{}` | Extra WebSocket headers. Header values accept literal strings or SecretInput values, for example `x-codex-client-session-token: "${CODEX_CLIENT_SESSION_TOKEN}"`. |
557557
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
558558
| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. |
559559
| `remoteWorkspaceRoot` | unset | Remote Codex app-server workspace root. When set, OpenClaw infers the local workspace root from the resolved OpenClaw workspace, preserves the current cwd suffix under this remote root, and sends only the final app-server cwd to Codex. If the cwd is outside the resolved OpenClaw workspace root, OpenClaw fails closed instead of sending a gateway-local path to the remote app-server. |

docs/reference/secretref-credential-surface.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Scope intent:
4343
- `tools.web.fetch.firecrawl.apiKey`
4444
- `plugins.entries.acpx.config.mcpServers.*.env.*`
4545
- `plugins.entries.brave.config.webSearch.apiKey`
46+
- `plugins.entries.codex.config.appServer.authToken`
47+
- `plugins.entries.codex.config.appServer.headers.*`
4648
- `plugins.entries.exa.config.webSearch.apiKey`
4749
- `plugins.entries.google-meet.config.realtime.providers.*.apiKey`
4850
- `plugins.entries.google.config.webSearch.apiKey`

docs/reference/secretref-user-supplied-credentials-matrix.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,20 @@
554554
"secretShape": "secret_input",
555555
"optIn": true
556556
},
557+
{
558+
"id": "plugins.entries.codex.config.appServer.authToken",
559+
"configFile": "openclaw.json",
560+
"path": "plugins.entries.codex.config.appServer.authToken",
561+
"secretShape": "secret_input",
562+
"optIn": true
563+
},
564+
{
565+
"id": "plugins.entries.codex.config.appServer.headers.*",
566+
"configFile": "openclaw.json",
567+
"path": "plugins.entries.codex.config.appServer.headers.*",
568+
"secretShape": "secret_input",
569+
"optIn": true
570+
},
557571
{
558572
"id": "plugins.entries.exa.config.webSearch.apiKey",
559573
"configFile": "openclaw.json",

extensions/codex/openclaw.plugin.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@
152152
]
153153
},
154154
"url": { "type": "string" },
155-
"authToken": { "type": "string" },
155+
"authToken": { "type": ["string", "object"] },
156156
"headers": {
157157
"type": "object",
158-
"additionalProperties": { "type": "string" }
158+
"additionalProperties": { "type": ["string", "object"] }
159159
},
160160
"clearEnv": {
161161
"type": "array",
@@ -254,6 +254,14 @@
254254
}
255255
}
256256
},
257+
"configContracts": {
258+
"secretInputs": {
259+
"paths": [
260+
{ "path": "appServer.authToken", "expected": "string" },
261+
{ "path": "appServer.headers.*", "expected": "string" }
262+
]
263+
}
264+
},
257265
"uiHints": {
258266
"codexDynamicToolsLoading": {
259267
"label": "Dynamic Tools Loading",
@@ -382,6 +390,7 @@
382390
"appServer.headers": {
383391
"label": "Headers",
384392
"help": "Additional headers sent to the WebSocket app-server.",
393+
"sensitive": true,
385394
"advanced": true
386395
},
387396
"appServer.clearEnv": {

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

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ function resolveRuntimeForTest(params: RuntimeOptionsParams = {}) {
2828
return resolveCodexAppServerRuntimeOptions({ env: {}, requirementsToml: null, ...params });
2929
}
3030

31+
function envRef(id: string) {
32+
return { source: "env" as const, provider: "default", id };
33+
}
34+
3135
function requireRecord(value: unknown, label: string): Record<string, unknown> {
3236
if (!value || typeof value !== "object" || Array.isArray(value)) {
3337
throw new Error(`Expected ${label}`);
@@ -413,6 +417,65 @@ describe("Codex app-server config", () => {
413417
});
414418
});
415419

420+
it("passes resolved app-server SecretInput strings through to auth token and headers", () => {
421+
const runtime = resolveRuntimeForTest({
422+
pluginConfig: {
423+
appServer: {
424+
transport: "websocket",
425+
url: "wss://codex-app-server.example.internal/ws",
426+
authToken: " resolved-capability-token ",
427+
headers: {
428+
" x-codex-client-session-token ": " resolved-session-token ",
429+
Authorization: " Bearer explicit-token ",
430+
},
431+
},
432+
},
433+
});
434+
435+
expectFields(runtime.start, "runtime start", {
436+
authToken: "resolved-capability-token",
437+
headers: {
438+
"x-codex-client-session-token": "resolved-session-token",
439+
Authorization: "Bearer explicit-token",
440+
},
441+
});
442+
});
443+
444+
it("rejects unresolved app-server auth token SecretRefs at runtime option resolution", () => {
445+
expect(() =>
446+
resolveRuntimeForTest({
447+
pluginConfig: {
448+
appServer: {
449+
transport: "websocket",
450+
url: "wss://codex-app-server.example.internal/ws",
451+
authToken: envRef("CODEX_APP_SERVER_TOKEN"),
452+
},
453+
},
454+
}),
455+
).toThrow(
456+
'plugins.entries.codex.config.appServer.authToken: unresolved SecretRef "env:default:CODEX_APP_SERVER_TOKEN"',
457+
);
458+
});
459+
460+
it("rejects unresolved app-server header SecretRefs at runtime option resolution", () => {
461+
expect(() =>
462+
resolveRuntimeForTest({
463+
pluginConfig: {
464+
appServer: {
465+
transport: "websocket",
466+
url: "wss://codex-app-server.example.internal/ws",
467+
authToken: "capability-token",
468+
headers: {
469+
"x-codex-client-session-token": envRef("CODEX_CLIENT_SESSION_TOKEN"),
470+
},
471+
},
472+
},
473+
}),
474+
).toThrow(
475+
'plugins.entries.codex.config.appServer.headers.x-codex-client-session-token: unresolved SecretRef "env:default:CODEX_CLIENT_SESSION_TOKEN"',
476+
);
477+
});
478+
416479
it("treats IPv6 loopback websocket app-servers as local loopback", () => {
417480
const runtime = resolveRuntimeForTest({
418481
pluginConfig: {
@@ -2314,6 +2377,47 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
23142377
expect(second).not.toContain("sk-second");
23152378
});
23162379

2380+
it("derives distinct shared-client keys for distinct headers without exposing them", () => {
2381+
const first = codexAppServerStartOptionsKey({
2382+
transport: "websocket",
2383+
command: "codex",
2384+
args: [],
2385+
url: "ws://127.0.0.1:39175",
2386+
headers: {
2387+
Authorization: "Bearer first",
2388+
"x-codex-client-session-token": "session-first",
2389+
},
2390+
});
2391+
const second = codexAppServerStartOptionsKey({
2392+
transport: "websocket",
2393+
command: "codex",
2394+
args: [],
2395+
url: "ws://127.0.0.1:39175",
2396+
headers: {
2397+
Authorization: "Bearer second",
2398+
"x-codex-client-session-token": "session-second",
2399+
},
2400+
});
2401+
2402+
expect(first).not.toEqual(second);
2403+
expect(
2404+
codexAppServerStartOptionsKey({
2405+
transport: "websocket",
2406+
command: "codex",
2407+
args: [],
2408+
url: "ws://127.0.0.1:39175",
2409+
headers: {
2410+
Authorization: "Bearer first",
2411+
"x-codex-client-session-token": "session-first",
2412+
},
2413+
}),
2414+
).toEqual(first);
2415+
expect(first).not.toContain("Bearer first");
2416+
expect(first).not.toContain("session-first");
2417+
expect(second).not.toContain("Bearer second");
2418+
expect(second).not.toContain("session-second");
2419+
});
2420+
23172421
it("keeps secret-derived shared-client keys stable across module reloads", async () => {
23182422
const startOptions = {
23192423
transport: "websocket" as const,

0 commit comments

Comments
 (0)