## Why Managed-network commands within one Codex conversation share the same HTTP and SOCKS proxy ingress. When several exec calls run concurrently, the proxy sees the requested destination but cannot tell which exec opened the connection. For example: ```text exec A: curl https://example.com/a ─┐ ├─> conversation proxy ─> Guardian exec B: curl https://example.com/b ─┘ host: example.com trigger: unknown ```. Three parallel network execs reached Guardian without their triggering call IDs or commands. Guardian denied the requests, but Codex could not safely associate those outcomes with the individual tool calls. ## What changes Keep the shared proxy ingress and tag each connection at the existing trusted Linux bridge: ```text exec A ─> existing Linux bridge ─> [token A][proxy bytes] ─┐ ├─> shared HTTP/SOCKS ingress exec B ─> existing Linux bridge ─> [token B][proxy bytes] ─┘ │ token A ─> exec A ─────┤ token B ─> exec B ─────┘ ``` The complete path is: ```text active exec registration │ ├─ registers its UUID as a short-lived attribution token ├─ passes the token to the Linux sandbox helper ├─ helper removes the token before launching the user command ├─ existing host bridge prepends the token to each proxy connection ├─ shared proxy consumes the bounded attribution frame └─ proxy attaches the matching execution-scoped state ├─ Guardian receives the exact call ID and command └─ a denial finishes/cancels the matching tool call ``` Dropping the active or deferred exec registration removes the token. Connections that were already accepted retain their resolved attribution; new connections using an expired token fail closed. ## Before and after Before, Guardian could receive only the network destination: ```json { "tool": "network_access", "host": "www.17track.net", "port": 443, "protocol": "https" } ``` After, the same request includes the action that caused it: ```json { "tool": "network_access", "host": "www.17track.net", "port": 443, "protocol": "https", "trigger": { "callId": "exec-network-first", "command": ["/bin/sh", "-c", "curl https://www.17track.net"] } } ``` ## Listener accounting This PR does **not** create proxy listeners per exec. ```text Existing topology: one conversation -> one HTTP listener + optional one SOCKS listener Discarded per-exec approach: one conversation -> existing listener pair + up to one additional listener pair per active exec This PR: one conversation -> existing listener pair only + one small token-map entry per active exec ``` The Linux sandbox already creates a trusted routing bridge for each sandboxed command. This PR adds a short frame write to that bridge rather than introducing another listener, task, or proxy process. The existing conversation-scoped listener pair remains. Making a single proxy service shared across multiple conversations would be a separate multi-tenant architecture change involving per-conversation policy, configuration, audit, and Guardian routing. ## Keeping the implementation small The attribution is bound once, when the TCP connection enters the proxy. The ingress installs an execution-scoped clone of the existing `NetworkProxyState`, so the established HTTP, SOCKS, MITM, policy, audit, and blocked-request paths continue using their existing state lookup. This avoids plumbing a new request-context type through every protocol handler. Outside the two ingress wrappers, protocol-specific request handling is unchanged. ## Security behavior - Tokens are generated from the existing random execution registration IDs. - The trusted Linux helper consumes and removes the token before executing user code. - Attribution frames have a fixed magic prefix, bounded token length, and bounded read timeout. - Unknown or expired tokens close the connection. - A token presented to a proxy for another environment closes the connection. - Existing unframed callers preserve the current conservative attribution behavior. ## Platform scope Exact bridge attribution is enabled on Linux. macOS and Windows retain their current shared-proxy behavior. ## Test coverage The concurrent end-to-end test starts two managed-network execs together and synchronizes them so both are active before either connects. It then inspects the two Guardian requests and compares the complete attribution pairs: ```text (exec-network-first, exact first command) (exec-network-second, exact second command) ``` Focused proxy coverage verifies the bounded frame and that a registered framed connection receives the matching execution and environment state. ## Scope This fixes the Linux network-to-exec attribution path and records a denial against the exact matching tool call. It intentionally does not change: - delivery of an entirely unattributed denial to the parent turn; - how parallel denials count toward the Guardian circuit breaker; - how the UI displays the rejection reason or completed-turn state. Those remain separate concerns from attribution. ## Relationship to #29456 and #29668 #29456 made the proxy environment and sandbox policy come from the same prepared network context. This PR adds the execution token to that prepared launch and consumes it at the shared ingress. This follows #29668's shared-ingress framing direction, but completes the production registration, Linux bridge, core call mapping, denial mapping, and concurrent end-to-end path. It also keeps attribution in the existing per-connection proxy state instead of introducing request-context plumbing through every HTTP, SOCKS, and MITM handler. This PR is intended to supersede #29668 for the Linux attribution fix. --------- Co-authored-by: viyatb-oai <viyatb@openai.com> Co-authored-by: Codex <noreply@openai.com>
codex-linux-sandbox
This crate is responsible for producing:
- a
codex-linux-sandboxstandalone executable for Linux that is bundled with the Node.js version of the Codex CLI - a lib crate that exposes the business logic of the executable as
run_main()so that- the
codex-execCLI can check if its arg0 iscodex-linux-sandboxand, if so, execute as if it werecodex-linux-sandbox - this should also be true of the
codexmultitool CLI
- the
On Linux, Codex prefers the first bwrap found on PATH
outside the current working directory whenever it is available. If bwrap is
present but too old to support
--argv0, the helper keeps using system bubblewrap and switches to a
no---argv0 compatibility path for the inner re-exec. If bwrap is missing,
the helper falls back to the bundled codex-resources/bwrap binary shipped
with Codex.
Codex also surfaces a startup warning when bwrap is missing so users know it
is falling back to the bundled helper. Codex surfaces the same startup warning
path when bubblewrap cannot create user namespaces. WSL2 follows the normal
Linux bubblewrap path. WSL1 is not supported for bubblewrap sandboxing because
it cannot create the required user namespaces, so Codex rejects sandboxed shell
commands that would enter the bubblewrap path.
Current Behavior
-
Legacy
SandboxPolicy/sandbox_modeconfigs remain supported. -
Bubblewrap is the default filesystem sandbox.
-
If
bwrapis present onPATHoutside the current working directory, the helper uses it. -
If
bwrapis present but too old to support--argv0, the helper uses a no---argv0compatibility path for the inner re-exec. -
If
bwrapis missing, the helper falls back to the bundledcodex-resources/bwrappath. -
If
bwrapis missing, Codex also surfaces a startup warning instead of printing directly from the sandbox helper. -
If bubblewrap cannot create user namespaces, Codex surfaces a startup warning instead of waiting for a runtime sandbox failure.
-
WSL2 uses the normal Linux bubblewrap path.
-
WSL1 is not supported for bubblewrap sandboxing; Codex rejects sandboxed shell commands that would require the bubblewrap path before invoking
bwrap. -
Legacy Landlock + mount protections remain available as an explicit legacy fallback path.
-
Set
features.use_legacy_landlock = true(or CLI-c use_legacy_landlock=true) to force the legacy Landlock fallback. -
The legacy Landlock fallback is used only when the split filesystem policy is sandbox-equivalent to the legacy model after
cwdresolution. -
Split-only filesystem policies that do not round-trip through the legacy
SandboxPolicymodel stay on bubblewrap so nested read-only or denied carveouts are preserved. -
When bubblewrap is active, the helper applies
PR_SET_NO_NEW_PRIVSand a seccomp network filter in-process. -
When bubblewrap is active, the filesystem is read-only by default via
--ro-bind / /. -
When bubblewrap is active, writable roots are layered with
--bind <root> <root>. -
When bubblewrap is active, protected subpaths under writable roots (for example
.git, resolvedgitdir:, and.codex) are re-applied as read-only via--ro-bind. -
When bubblewrap is active, overlapping split-policy entries are applied in path-specificity order so narrower writable children can reopen broader read-only or denied parents while narrower denied subpaths still win. For example,
/repo = write,/repo/a = none,/repo/a/b = writekeeps/repowritable, denies/repo/a, and reopens/repo/a/bas writable again. -
When bubblewrap is active, unreadable glob entries are expanded before launching the sandbox and matching files are masked in bubblewrap:
Prefer: rg --files --hidden --no-ignore --glob <pattern> -- <search-root> Fallback: internal globset walker when rg is not installed Failure: any other rg failure aborts sandbox constructionUsers can cap the scan depth per permissions profile:
[permissions.workspace.filesystem] glob_scan_max_depth = 2 [permissions.workspace.filesystem.":workspace_roots"] "**/*.env" = "none" -
When bubblewrap is active, symlink-in-path and non-existent protected paths inside writable roots are blocked by mounting
/dev/nullon the symlink or first missing component. -
When bubblewrap is active, the helper explicitly isolates the user namespace via
--unshare-userand the PID namespace via--unshare-pid. -
When bubblewrap is active and network is restricted without proxy routing, the helper also isolates the network namespace via
--unshare-net. -
In managed proxy mode, the helper uses
--unshare-netplus an internal TCP->UDS->TCP routing bridge so tool traffic reaches only configured proxy endpoints. -
In managed proxy mode, after the bridge is live, seccomp blocks new AF_UNIX/socketpair creation for the user command.
-
When bubblewrap is active, it mounts a fresh
/procvia--proc /procby default, but you can skip this in restrictive container environments with--no-proc.
Notes
- The CLI surface is
codex sandbox; the host OS selects the sandbox backend.