Skip to content

Commit 9850eb6

Browse files
committed
feat(codex): support app-server network proxy profiles
1 parent 6382536 commit 9850eb6

12 files changed

Lines changed: 729 additions & 20 deletions

docs/plugins/codex-harness-reference.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,45 @@ Supported `appServer` fields:
103103
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. |
104104
| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. |
105105
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. |
106+
| `networkProxy` | disabled | Opt into Codex permissions-profile networking for app-server commands. OpenClaw defines the selected `permissions.<profile>.network` config and selects that profile on thread start or resume instead of sending `sandbox`. |
106107
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
107108

109+
`appServer.networkProxy` is explicit because it changes the Codex sandbox
110+
contract. When enabled, OpenClaw also sets `features.network_proxy.enabled` in
111+
the Codex thread config so the generated permission profile can start Codex
112+
managed networking. The default generated profile is `openclaw-network`; use
113+
`profileName` to choose another local name.
114+
115+
```js
116+
export default {
117+
plugins: {
118+
entries: {
119+
codex: {
120+
config: {
121+
appServer: {
122+
sandbox: "workspace-write",
123+
networkProxy: {
124+
enabled: true,
125+
domains: {
126+
"api.openai.com": "allow",
127+
"blocked.example.com": "deny",
128+
},
129+
allowUpstreamProxy: true,
130+
proxyUrl: "http://127.0.0.1:3128",
131+
},
132+
},
133+
},
134+
},
135+
},
136+
},
137+
};
138+
```
139+
140+
If the normal app-server runtime would be `danger-full-access`, enabling
141+
`networkProxy` uses workspace-style filesystem access for the generated
142+
permission profile. Codex managed network enforcement is sandboxed networking,
143+
so a full-access profile would not protect outbound traffic.
144+
108145
The plugin blocks older or unversioned app-server handshakes. Codex app-server
109146
must report stable version `0.125.0` or newer.
110147

docs/plugins/codex-harness.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,45 @@ Supported `appServer` fields:
561561
| `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"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. |
562562
| `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. |
563563
| `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"`. |
564+
| `networkProxy` | disabled | Opt into Codex permissions-profile networking for app-server commands. OpenClaw defines the selected `permissions.<profile>.network` config and selects that profile on thread start or resume instead of sending `sandbox`. |
564565
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
565566

567+
`appServer.networkProxy` is explicit because it changes the Codex sandbox
568+
contract. When enabled, OpenClaw also sets `features.network_proxy.enabled` in
569+
the Codex thread config so the generated permission profile can start Codex
570+
managed networking. The default generated profile is `openclaw-network`; use
571+
`profileName` to choose another local name.
572+
573+
```js
574+
export default {
575+
plugins: {
576+
entries: {
577+
codex: {
578+
config: {
579+
appServer: {
580+
sandbox: "workspace-write",
581+
networkProxy: {
582+
enabled: true,
583+
domains: {
584+
"api.openai.com": "allow",
585+
"blocked.example.com": "deny",
586+
},
587+
allowUpstreamProxy: true,
588+
proxyUrl: "http://127.0.0.1:3128",
589+
},
590+
},
591+
},
592+
},
593+
},
594+
},
595+
};
596+
```
597+
598+
If the normal app-server runtime would be `danger-full-access`, enabling
599+
`networkProxy` uses workspace-style filesystem access for the generated
600+
permission profile. Codex managed network enforcement is sandboxed networking,
601+
so a full-access profile would not protect outbound traffic.
602+
566603
OpenClaw-owned dynamic tool calls are bounded independently from
567604
`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 90 second
568605
OpenClaw watchdog by default. A positive per-call `timeoutMs` argument extends

extensions/codex/openclaw.plugin.json

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,47 @@
193193
"enum": ["user", "auto_review", "guardian_subagent"]
194194
},
195195
"serviceTier": { "type": ["string", "null"] },
196+
"networkProxy": {
197+
"type": "object",
198+
"additionalProperties": false,
199+
"properties": {
200+
"enabled": {
201+
"type": "boolean",
202+
"default": false
203+
},
204+
"profileName": { "type": "string" },
205+
"baseProfile": {
206+
"type": "string",
207+
"enum": ["read-only", "workspace"]
208+
},
209+
"mode": {
210+
"type": "string",
211+
"enum": ["limited", "full"]
212+
},
213+
"domains": {
214+
"type": "object",
215+
"additionalProperties": {
216+
"type": "string",
217+
"enum": ["allow", "deny"]
218+
}
219+
},
220+
"unixSockets": {
221+
"type": "object",
222+
"additionalProperties": {
223+
"type": "string",
224+
"enum": ["allow", "deny"]
225+
}
226+
},
227+
"proxyUrl": { "type": "string" },
228+
"socksUrl": { "type": "string" },
229+
"enableSocks5": { "type": "boolean" },
230+
"enableSocks5Udp": { "type": "boolean" },
231+
"allowUpstreamProxy": { "type": "boolean" },
232+
"allowLocalBinding": { "type": "boolean" },
233+
"dangerouslyAllowNonLoopbackProxy": { "type": "boolean" },
234+
"dangerouslyAllowAllUnixSockets": { "type": "boolean" }
235+
}
236+
},
196237
"defaultWorkspaceDir": {
197238
"type": "string"
198239
},
@@ -385,6 +426,81 @@
385426
"help": "Optional Codex app-server service tier. Use priority, flex, or null. Legacy fast is accepted as priority.",
386427
"advanced": true
387428
},
429+
"appServer.networkProxy": {
430+
"label": "Network Proxy",
431+
"help": "Enable Codex permissions-profile networking for app-server commands.",
432+
"advanced": true
433+
},
434+
"appServer.networkProxy.enabled": {
435+
"label": "Network Proxy Enabled",
436+
"help": "When enabled, OpenClaw defines a Codex permissions profile and selects it on thread start or resume instead of sandbox fields.",
437+
"advanced": true
438+
},
439+
"appServer.networkProxy.profileName": {
440+
"label": "Network Proxy Profile",
441+
"help": "Codex permissions profile name generated for app-server network access.",
442+
"advanced": true
443+
},
444+
"appServer.networkProxy.baseProfile": {
445+
"label": "Network Proxy Base",
446+
"help": "Filesystem access used by the generated profile. Defaults to read-only for read-only sandboxes and workspace otherwise.",
447+
"advanced": true
448+
},
449+
"appServer.networkProxy.domains": {
450+
"label": "Network Domains",
451+
"help": "Domain allow and deny rules for Codex sandboxed networking.",
452+
"advanced": true
453+
},
454+
"appServer.networkProxy.unixSockets": {
455+
"label": "Unix Sockets",
456+
"help": "Unix socket allow and deny rules for Codex sandboxed networking.",
457+
"advanced": true
458+
},
459+
"appServer.networkProxy.proxyUrl": {
460+
"label": "HTTP Proxy URL",
461+
"help": "HTTP listener URL used by Codex sandboxed networking.",
462+
"advanced": true
463+
},
464+
"appServer.networkProxy.socksUrl": {
465+
"label": "SOCKS Proxy URL",
466+
"help": "SOCKS listener URL used by Codex sandboxed networking.",
467+
"advanced": true
468+
},
469+
"appServer.networkProxy.enableSocks5": {
470+
"label": "Enable SOCKS5",
471+
"help": "Expose SOCKS5 support for the generated Codex permissions profile.",
472+
"advanced": true
473+
},
474+
"appServer.networkProxy.enableSocks5Udp": {
475+
"label": "Enable SOCKS5 UDP",
476+
"help": "Allow UDP over the SOCKS5 listener when SOCKS5 is enabled.",
477+
"advanced": true
478+
},
479+
"appServer.networkProxy.allowUpstreamProxy": {
480+
"label": "Allow Upstream Proxy",
481+
"help": "Allow Codex sandboxed networking to chain through inherited HTTP(S)_PROXY or ALL_PROXY settings.",
482+
"advanced": true
483+
},
484+
"appServer.networkProxy.allowLocalBinding": {
485+
"label": "Allow Local Binding",
486+
"help": "Permit broader local and private-network access through Codex sandboxed networking.",
487+
"advanced": true
488+
},
489+
"appServer.networkProxy.mode": {
490+
"label": "Network Mode",
491+
"help": "Codex sandboxed networking mode for subprocess traffic.",
492+
"advanced": true
493+
},
494+
"appServer.networkProxy.dangerouslyAllowNonLoopbackProxy": {
495+
"label": "Allow Non-Loopback Proxy",
496+
"help": "Permit non-loopback bind addresses for Codex sandboxed networking listeners.",
497+
"advanced": true
498+
},
499+
"appServer.networkProxy.dangerouslyAllowAllUnixSockets": {
500+
"label": "Allow All Unix Sockets",
501+
"help": "Bypass Codex's Unix socket allowlist for tightly controlled environments.",
502+
"advanced": true
503+
},
388504
"appServer.defaultWorkspaceDir": {
389505
"label": "Default Workspace",
390506
"help": "Workspace used by /codex bind when --cwd is omitted.",

extensions/codex/src/app-server/bounded-turn.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,17 @@ function resolveBoundedThreadConfig(
218218
params: CodexBoundedTurnParams,
219219
workspace: { codexHome?: string },
220220
): JsonObject {
221-
const boundedConfig = mergeCodexThreadConfigs(CODEX_BOUNDED_THREAD_CONFIG, params.threadConfig);
222-
return workspace.codexHome
223-
? mergeCodexThreadConfigs(boundedConfig, CODEX_PRIVATE_BOUNDED_THREAD_CONFIG)
224-
: boundedConfig;
221+
const boundedConfig = mergeCodexThreadConfigs(
222+
CODEX_BOUNDED_THREAD_CONFIG,
223+
params.threadConfig,
224+
) ?? { ...CODEX_BOUNDED_THREAD_CONFIG };
225+
if (!workspace.codexHome) {
226+
return boundedConfig;
227+
}
228+
return mergeCodexThreadConfigs(boundedConfig, CODEX_PRIVATE_BOUNDED_THREAD_CONFIG) ?? {
229+
...boundedConfig,
230+
...CODEX_PRIVATE_BOUNDED_THREAD_CONFIG,
231+
};
225232
}
226233

227234
function buildPrivateCodexAppServerStartOptions(

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,89 @@ describe("Codex app-server config", () => {
125125
});
126126
});
127127

128+
it("builds Codex permissions-profile config for app-server network proxy", () => {
129+
const runtime = resolveRuntimeForTest({
130+
pluginConfig: {
131+
appServer: {
132+
sandbox: "workspace-write",
133+
networkProxy: {
134+
enabled: true,
135+
profileName: "mock-proxy",
136+
mode: "limited",
137+
domains: {
138+
" api.openai.com ": "allow",
139+
"blocked.example.com": "deny",
140+
},
141+
unixSockets: {
142+
" /tmp/mock-proxy.sock ": "allow",
143+
},
144+
proxyUrl: "http://127.0.0.1:3128",
145+
socksUrl: "socks5h://127.0.0.1:8081",
146+
enableSocks5: true,
147+
enableSocks5Udp: false,
148+
allowUpstreamProxy: true,
149+
allowLocalBinding: false,
150+
},
151+
},
152+
},
153+
});
154+
155+
expect(runtime.networkProxy).toEqual({
156+
profileName: "mock-proxy",
157+
configPatch: {
158+
"features.network_proxy.enabled": true,
159+
permissions: {
160+
"mock-proxy": {
161+
filesystem: {
162+
":minimal": "read",
163+
":workspace_roots": {
164+
".": "write",
165+
},
166+
},
167+
network: {
168+
enabled: true,
169+
mode: "limited",
170+
domains: {
171+
"api.openai.com": "allow",
172+
"blocked.example.com": "deny",
173+
},
174+
unix_sockets: {
175+
"/tmp/mock-proxy.sock": "allow",
176+
},
177+
proxy_url: "http://127.0.0.1:3128",
178+
socks_url: "socks5h://127.0.0.1:8081",
179+
enable_socks5: true,
180+
enable_socks5_udp: false,
181+
allow_upstream_proxy: true,
182+
allow_local_binding: false,
183+
},
184+
},
185+
},
186+
},
187+
});
188+
});
189+
190+
it("uses read-only filesystem rules for read-only network proxy profiles", () => {
191+
const runtime = resolveRuntimeForTest({
192+
pluginConfig: {
193+
appServer: {
194+
sandbox: "read-only",
195+
networkProxy: {
196+
enabled: true,
197+
domains: { "example.com": "allow" },
198+
},
199+
},
200+
},
201+
});
202+
const permissions = runtime.networkProxy?.configPatch.permissions as Record<
203+
string,
204+
{ filesystem: { ":workspace_roots": { ".": string } } }
205+
>;
206+
207+
expect(runtime.networkProxy?.profileName).toBe("openclaw-network");
208+
expect(permissions["openclaw-network"]?.filesystem[":workspace_roots"]["."]).toBe("read");
209+
});
210+
128211
it("clamps oversized app-server timer config", () => {
129212
const runtime = resolveRuntimeForTest({
130213
pluginConfig: {

0 commit comments

Comments
 (0)