Skip to content

Commit 5c34695

Browse files
authored
feat(codex): support app-server network proxy profiles (#93538)
Merged via squash. Prepared head SHA: 9900b14 Co-authored-by: vincentkoc <[email protected]> Co-authored-by: vincentkoc <[email protected]> Reviewed-by: @vincentkoc
1 parent f3ae525 commit 5c34695

15 files changed

Lines changed: 1230 additions & 74 deletions

docs/plugins/codex-harness-reference.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,46 @@ 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 it with `default_permissions` 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` and
111+
`default_permissions` in the Codex thread config so the generated permission
112+
profile can start Codex managed networking. By default, OpenClaw generates a
113+
collision-resistant `openclaw-network-<fingerprint>` profile name from the
114+
profile body; use `profileName` only when a stable local name is required.
115+
116+
```js
117+
export default {
118+
plugins: {
119+
entries: {
120+
codex: {
121+
config: {
122+
appServer: {
123+
sandbox: "workspace-write",
124+
networkProxy: {
125+
enabled: true,
126+
domains: {
127+
"api.openai.com": "allow",
128+
"blocked.example.com": "deny",
129+
},
130+
allowUpstreamProxy: true,
131+
proxyUrl: "http://127.0.0.1:3128",
132+
},
133+
},
134+
},
135+
},
136+
},
137+
},
138+
};
139+
```
140+
141+
If the normal app-server runtime would be `danger-full-access`, enabling
142+
`networkProxy` uses workspace-style filesystem access for the generated
143+
permission profile. Codex managed network enforcement is sandboxed networking,
144+
so a full-access profile would not protect outbound traffic.
145+
108146
The plugin blocks older or unversioned app-server handshakes. Codex app-server
109147
must report stable version `0.125.0` or newer.
110148

docs/plugins/codex-harness.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,52 @@ 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 it with `default_permissions` 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` and
569+
`default_permissions` in the Codex thread config so the generated permission
570+
profile can start Codex managed networking. By default, OpenClaw generates a
571+
collision-resistant `openclaw-network-<fingerprint>` profile name from the
572+
profile body; use `profileName` only when a stable local name is required.
573+
574+
```js
575+
export default {
576+
plugins: {
577+
entries: {
578+
codex: {
579+
config: {
580+
appServer: {
581+
sandbox: "workspace-write",
582+
networkProxy: {
583+
enabled: true,
584+
domains: {
585+
"api.openai.com": "allow",
586+
"blocked.example.com": "deny",
587+
},
588+
unixSockets: {
589+
"/tmp/proxy.sock": "allow",
590+
"/tmp/blocked.sock": "deny",
591+
},
592+
allowUpstreamProxy: true,
593+
proxyUrl: "http://127.0.0.1:3128",
594+
},
595+
},
596+
},
597+
},
598+
},
599+
},
600+
};
601+
```
602+
603+
If the normal app-server runtime would be `danger-full-access`, enabling
604+
`networkProxy` uses workspace-style filesystem access for the generated
605+
permission profile. Codex managed network enforcement is sandboxed networking,
606+
so a full-access profile would not protect outbound traffic.
607+
Domain entries use `allow` or `deny`; Unix socket entries use Codex's
608+
`allow` or `deny` values.
609+
566610
OpenClaw-owned dynamic tool calls are bounded independently from
567611
`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 90 second
568612
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 with default_permissions instead of sandbox fields.",
437+
"advanced": true
438+
},
439+
"appServer.networkProxy.profileName": {
440+
"label": "Network Proxy Profile",
441+
"help": "Optional stable Codex permissions profile name. Leave unset to use a generated openclaw-network fingerprint name.",
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/config.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CODEX_PLUGINS_CONFIG_KEYS,
1111
canUseCodexModelBackedApprovalsReviewerForModel,
1212
codexAppServerStartOptionsKey,
13+
fingerprintCodexAppServerNetworkProxyConfigPatch,
1314
readCodexPluginConfig,
1415
resolveCodexAppServerRuntimeOptions,
1516
resolveCodexComputerUseConfig,
@@ -83,6 +84,21 @@ describe("Codex app-server config", () => {
8384
sandbox: "danger-full-access",
8485
}),
8586
).toBe(false);
87+
expect(
88+
shouldAutoApproveCodexAppServerApprovals({
89+
approvalPolicy: "never",
90+
sandbox: "danger-full-access",
91+
networkProxy: {
92+
profileName: "openclaw-network",
93+
configFingerprint: "network-proxy-v1",
94+
configPatch: {
95+
"features.network_proxy.enabled": true,
96+
default_permissions: "openclaw-network",
97+
permissions: {},
98+
},
99+
},
100+
}),
101+
).toBe(false);
86102
});
87103

88104
it("parses typed plugin config before falling back to environment knobs", () => {
@@ -125,6 +141,102 @@ describe("Codex app-server config", () => {
125141
});
126142
});
127143

144+
it("builds Codex permissions-profile config for app-server network proxy", () => {
145+
const runtime = resolveRuntimeForTest({
146+
pluginConfig: {
147+
appServer: {
148+
sandbox: "workspace-write",
149+
networkProxy: {
150+
enabled: true,
151+
profileName: "mock-proxy",
152+
mode: "limited",
153+
domains: {
154+
" api.openai.com ": "allow",
155+
"blocked.example.com": "deny",
156+
},
157+
unixSockets: {
158+
" /tmp/mock-proxy.sock ": "allow",
159+
"/tmp/blocked.sock": "deny",
160+
},
161+
proxyUrl: "http://127.0.0.1:3128",
162+
socksUrl: "socks5h://127.0.0.1:8081",
163+
enableSocks5: true,
164+
enableSocks5Udp: false,
165+
allowUpstreamProxy: true,
166+
allowLocalBinding: false,
167+
},
168+
},
169+
},
170+
});
171+
172+
const networkProxy = runtime.networkProxy;
173+
if (!networkProxy) {
174+
throw new Error("Expected network proxy runtime config");
175+
}
176+
expect(networkProxy).toEqual({
177+
profileName: "mock-proxy",
178+
configFingerprint: expect.any(String),
179+
configPatch: {
180+
"features.network_proxy.enabled": true,
181+
default_permissions: "mock-proxy",
182+
permissions: {
183+
"mock-proxy": {
184+
filesystem: {
185+
":minimal": "read",
186+
":workspace_roots": {
187+
".": "write",
188+
},
189+
},
190+
network: {
191+
enabled: true,
192+
mode: "limited",
193+
domains: {
194+
"api.openai.com": "allow",
195+
"blocked.example.com": "deny",
196+
},
197+
unix_sockets: {
198+
"/tmp/mock-proxy.sock": "allow",
199+
"/tmp/blocked.sock": "deny",
200+
},
201+
proxy_url: "http://127.0.0.1:3128",
202+
socks_url: "socks5h://127.0.0.1:8081",
203+
enable_socks5: true,
204+
enable_socks5_udp: false,
205+
allow_upstream_proxy: true,
206+
allow_local_binding: false,
207+
},
208+
},
209+
},
210+
},
211+
});
212+
expect(networkProxy.configFingerprint).toBe(
213+
fingerprintCodexAppServerNetworkProxyConfigPatch(networkProxy.configPatch),
214+
);
215+
});
216+
217+
it("uses read-only filesystem rules for read-only network proxy profiles", () => {
218+
const runtime = resolveRuntimeForTest({
219+
pluginConfig: {
220+
appServer: {
221+
sandbox: "read-only",
222+
networkProxy: {
223+
enabled: true,
224+
domains: { "example.com": "allow" },
225+
},
226+
},
227+
},
228+
});
229+
const profileName = runtime.networkProxy?.profileName;
230+
const permissions = runtime.networkProxy?.configPatch.permissions as Record<
231+
string,
232+
{ filesystem: { ":workspace_roots": { ".": string } } }
233+
>;
234+
235+
expect(profileName).toMatch(/^openclaw-network-[a-f0-9]{16}$/u);
236+
expect(runtime.networkProxy?.configPatch.default_permissions).toBe(profileName);
237+
expect(permissions[profileName ?? ""]?.filesystem[":workspace_roots"]["."]).toBe("read");
238+
});
239+
128240
it("clamps oversized app-server timer config", () => {
129241
const runtime = resolveRuntimeForTest({
130242
pluginConfig: {

0 commit comments

Comments
 (0)