Skip to content

Commit 31a0b7b

Browse files
committed
feat: add Codex app-server controls
1 parent 0f08916 commit 31a0b7b

22 files changed

Lines changed: 1162 additions & 93 deletions

docs/plugins/codex-harness.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,106 @@ fallback catalog:
255255
}
256256
```
257257

258+
## App-server connection and policy
259+
260+
By default, the plugin starts Codex locally with:
261+
262+
```bash
263+
codex app-server --listen stdio://
264+
```
265+
266+
You can keep that default and only tune Codex native policy:
267+
268+
```json5
269+
{
270+
plugins: {
271+
entries: {
272+
codex: {
273+
enabled: true,
274+
config: {
275+
appServer: {
276+
approvalPolicy: "on-request",
277+
sandbox: "workspace-write",
278+
serviceTier: "priority",
279+
},
280+
},
281+
},
282+
},
283+
},
284+
}
285+
```
286+
287+
For an already-running app-server, use WebSocket transport:
288+
289+
```json5
290+
{
291+
plugins: {
292+
entries: {
293+
codex: {
294+
enabled: true,
295+
config: {
296+
appServer: {
297+
transport: "websocket",
298+
url: "ws://127.0.0.1:39175",
299+
authToken: "${CODEX_APP_SERVER_TOKEN}",
300+
requestTimeoutMs: 60000,
301+
},
302+
},
303+
},
304+
},
305+
},
306+
}
307+
```
308+
309+
Supported `appServer` fields:
310+
311+
| Field | Default | Meaning |
312+
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
313+
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
314+
| `command` | `"codex"` | Executable for stdio transport. |
315+
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
316+
| `url` | unset | WebSocket app-server URL. |
317+
| `authToken` | unset | Bearer token for WebSocket transport. |
318+
| `headers` | `{}` | Extra WebSocket headers. |
319+
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
320+
| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. |
321+
| `sandbox` | `"workspace-write"` | Native Codex sandbox mode sent to thread start/resume. |
322+
| `approvalsReviewer` | `"user"` | Use `"guardian_subagent"` to let Codex guardian review native approvals. |
323+
| `serviceTier` | unset | Optional Codex service tier, for example `"priority"`. |
324+
325+
The older environment variables still work as fallbacks for local testing when
326+
the matching config field is unset:
327+
328+
- `OPENCLAW_CODEX_APP_SERVER_BIN`
329+
- `OPENCLAW_CODEX_APP_SERVER_ARGS`
330+
- `OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY`
331+
- `OPENCLAW_CODEX_APP_SERVER_SANDBOX`
332+
- `OPENCLAW_CODEX_APP_SERVER_GUARDIAN=1`
333+
334+
Config is preferred for repeatable deployments.
335+
336+
## Codex command
337+
338+
The bundled plugin registers `/codex` as an authorized slash command. It is
339+
generic and works on any channel that supports OpenClaw text commands.
340+
341+
Common forms:
342+
343+
- `/codex status` shows live app-server connectivity, models, account, rate limits, MCP servers, and skills.
344+
- `/codex models` lists live Codex app-server models.
345+
- `/codex threads [filter]` lists recent Codex threads.
346+
- `/codex resume <thread-id>` attaches the current OpenClaw session to an existing Codex thread.
347+
- `/codex compact` asks Codex app-server to compact the attached thread.
348+
- `/codex review` starts Codex native review for the attached thread.
349+
- `/codex account` shows account and rate-limit status.
350+
- `/codex mcp` lists Codex app-server MCP server status.
351+
- `/codex skills` lists Codex app-server skills.
352+
353+
`/codex resume` writes the same sidecar binding file that the harness uses for
354+
normal turns. On the next message, OpenClaw resumes that Codex thread, passes the
355+
currently selected OpenClaw `codex/*` model into app-server, and keeps extended
356+
history enabled.
357+
258358
## Tools, media, and compaction
259359

260360
The Codex harness changes the low-level embedded agent executor only.
@@ -286,6 +386,9 @@ reports version `0.118.0` or newer.
286386
**Model discovery is slow:** lower `plugins.entries.codex.config.discovery.timeoutMs`
287387
or disable discovery.
288388

389+
**WebSocket transport fails immediately:** check `appServer.url`, `authToken`,
390+
and that the remote app-server speaks the same Codex app-server protocol version.
391+
289392
**A non-Codex model uses PI:** that is expected. The Codex harness only claims
290393
`codex/*` model refs.
291394

docs/tools/slash-commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Bundled plugins can add more slash commands. Current bundled commands in this re
152152
- `/phone status|arm <camera|screen|writes|all> [duration]|disarm` temporarily arms high-risk phone node commands.
153153
- `/voice status|list [limit]|set <voiceId|name>` manages Talk voice config. On Discord, the native command name is `/talkvoice`.
154154
- `/card ...` sends LINE rich card presets. See [LINE](/channels/line).
155+
- `/codex status|models|threads|resume|compact|review|account|mcp|skills` inspects and controls the bundled Codex app-server harness. See [Codex Harness](/plugins/codex-harness).
155156
- QQBot-only commands:
156157
- `/bot-ping`
157158
- `/bot-version`

extensions/codex/harness.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function createCodexAppServerAgentHarness(options?: {
1818
id?: string;
1919
label?: string;
2020
providerIds?: Iterable<string>;
21+
pluginConfig?: unknown;
2122
}): AgentHarness {
2223
const providerIds = new Set(
2324
[...(options?.providerIds ?? DEFAULT_CODEX_HARNESS_PROVIDER_IDS)].map((id) =>
@@ -37,8 +38,10 @@ export function createCodexAppServerAgentHarness(options?: {
3738
reason: `provider is not one of: ${[...providerIds].toSorted().join(", ")}`,
3839
};
3940
},
40-
runAttempt: runCodexAppServerAttempt,
41-
compact: maybeCompactCodexAppServerSession,
41+
runAttempt: (params) =>
42+
runCodexAppServerAttempt(params, { pluginConfig: options?.pluginConfig }),
43+
compact: (params) =>
44+
maybeCompactCodexAppServerSession(params, { pluginConfig: options?.pluginConfig }),
4245
reset: async (params) => {
4346
if (params.sessionFile) {
4447
await clearCodexAppServerBinding(params.sessionFile);

extensions/codex/index.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("codex plugin", () => {
1414

1515
it("registers the codex provider and agent harness", () => {
1616
const registerAgentHarness = vi.fn();
17+
const registerCommand = vi.fn();
1718
const registerProvider = vi.fn();
1819

1920
plugin.register(
@@ -25,6 +26,7 @@ describe("codex plugin", () => {
2526
pluginConfig: {},
2627
runtime: {} as never,
2728
registerAgentHarness,
29+
registerCommand,
2830
registerProvider,
2931
}),
3032
);
@@ -34,5 +36,9 @@ describe("codex plugin", () => {
3436
id: "codex",
3537
label: "Codex agent harness",
3638
});
39+
expect(registerCommand.mock.calls[0]?.[0]).toMatchObject({
40+
name: "codex",
41+
description: "Inspect and control the Codex app-server harness",
42+
});
3743
});
3844
});

extensions/codex/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
22
import { createCodexAppServerAgentHarness } from "./harness.js";
33
import { buildCodexProvider } from "./provider.js";
4+
import { createCodexCommand } from "./src/commands.js";
45

56
export default definePluginEntry({
67
id: "codex",
78
name: "Codex",
89
description: "Codex app-server harness and Codex-managed GPT model catalog.",
910
register(api) {
10-
api.registerAgentHarness(createCodexAppServerAgentHarness());
11+
api.registerAgentHarness(createCodexAppServerAgentHarness({ pluginConfig: api.pluginConfig }));
1112
api.registerProvider(buildCodexProvider({ pluginConfig: api.pluginConfig }));
13+
api.registerCommand(createCodexCommand({ pluginConfig: api.pluginConfig }));
1214
},
1315
});

extensions/codex/openclaw.plugin.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
"name": "Codex",
44
"description": "Codex app-server harness and Codex-managed GPT model catalog.",
55
"providers": ["codex"],
6+
"commandAliases": [
7+
{
8+
"name": "codex",
9+
"kind": "runtime-slash",
10+
"cliCommand": "plugins"
11+
}
12+
],
613
"configSchema": {
714
"type": "object",
815
"additionalProperties": false,
@@ -18,6 +25,57 @@
1825
"default": 2500
1926
}
2027
}
28+
},
29+
"appServer": {
30+
"type": "object",
31+
"additionalProperties": false,
32+
"properties": {
33+
"transport": {
34+
"type": "string",
35+
"enum": ["stdio", "websocket"],
36+
"default": "stdio"
37+
},
38+
"command": {
39+
"type": "string",
40+
"default": "codex"
41+
},
42+
"args": {
43+
"oneOf": [
44+
{
45+
"type": "array",
46+
"items": { "type": "string" }
47+
},
48+
{ "type": "string" }
49+
]
50+
},
51+
"url": { "type": "string" },
52+
"authToken": { "type": "string" },
53+
"headers": {
54+
"type": "object",
55+
"additionalProperties": { "type": "string" }
56+
},
57+
"requestTimeoutMs": {
58+
"type": "number",
59+
"minimum": 1,
60+
"default": 60000
61+
},
62+
"approvalPolicy": {
63+
"type": "string",
64+
"enum": ["never", "on-request", "on-failure", "untrusted"],
65+
"default": "never"
66+
},
67+
"sandbox": {
68+
"type": "string",
69+
"enum": ["read-only", "workspace-write", "danger-full-access"],
70+
"default": "workspace-write"
71+
},
72+
"approvalsReviewer": {
73+
"type": "string",
74+
"enum": ["user", "guardian_subagent"],
75+
"default": "user"
76+
},
77+
"serviceTier": { "type": "string" }
78+
}
2179
}
2280
}
2381
},
@@ -34,6 +92,62 @@
3492
"label": "Discovery Timeout",
3593
"help": "Maximum time to wait for Codex app-server model discovery before falling back to the bundled model list.",
3694
"advanced": true
95+
},
96+
"appServer": {
97+
"label": "App Server",
98+
"help": "Runtime controls for connecting to Codex app-server.",
99+
"advanced": true
100+
},
101+
"appServer.transport": {
102+
"label": "Transport",
103+
"help": "Use stdio to spawn Codex locally, or websocket to connect to an already-running app-server.",
104+
"advanced": true
105+
},
106+
"appServer.command": {
107+
"label": "Command",
108+
"help": "Executable used for stdio transport.",
109+
"advanced": true
110+
},
111+
"appServer.args": {
112+
"label": "Arguments",
113+
"help": "Arguments used for stdio transport. Defaults to app-server --listen stdio://.",
114+
"advanced": true
115+
},
116+
"appServer.url": {
117+
"label": "WebSocket URL",
118+
"help": "Codex app-server WebSocket URL when transport is websocket.",
119+
"advanced": true
120+
},
121+
"appServer.authToken": {
122+
"label": "Auth Token",
123+
"help": "Bearer token sent to the WebSocket app-server.",
124+
"sensitive": true,
125+
"advanced": true
126+
},
127+
"appServer.requestTimeoutMs": {
128+
"label": "Request Timeout",
129+
"help": "Maximum time to wait for Codex app-server control-plane requests.",
130+
"advanced": true
131+
},
132+
"appServer.approvalPolicy": {
133+
"label": "Approval Policy",
134+
"help": "Codex native approval policy sent to thread start, resume, and turns.",
135+
"advanced": true
136+
},
137+
"appServer.sandbox": {
138+
"label": "Sandbox",
139+
"help": "Codex native sandbox mode sent to thread start and resume.",
140+
"advanced": true
141+
},
142+
"appServer.approvalsReviewer": {
143+
"label": "Approvals Reviewer",
144+
"help": "Use user approvals or the Codex guardian subagent for native app-server approvals.",
145+
"advanced": true
146+
},
147+
"appServer.serviceTier": {
148+
"label": "Service Tier",
149+
"help": "Optional Codex service tier passed when starting or resuming threads.",
150+
"advanced": true
37151
}
38152
}
39153
}

extensions/codex/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "OpenClaw Codex harness and model provider plugin",
55
"type": "module",
66
"dependencies": {
7-
"@mariozechner/pi-coding-agent": "0.65.2"
7+
"@mariozechner/pi-coding-agent": "0.65.2",
8+
"ws": "^8.20.0"
89
},
910
"devDependencies": {
1011
"@openclaw/plugin-sdk": "workspace:*"

extensions/codex/provider.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ describe("codex provider", () => {
2929
pluginConfig: { discovery: { timeoutMs: 1234 } },
3030
});
3131

32-
expect(listModels).toHaveBeenCalledWith({ limit: 100, timeoutMs: 1234 });
32+
expect(listModels).toHaveBeenCalledWith(
33+
expect.objectContaining({ limit: 100, timeoutMs: 1234 }),
34+
);
3335
expect(result.provider).toMatchObject({
3436
auth: "token",
3537
api: "openai-codex-responses",

0 commit comments

Comments
 (0)