## Why
Linux managed networking routed proxy connections through filesystem-backed Unix sockets, requiring an extra readable sandbox path and stale socket cleanup.
## What changed
- Transfer loopback TCP listeners from the sandbox namespace to host bridge processes over anonymous Unix socket pairs.
- Validate transferred descriptors, keep bootstrap channels private to each sandbox launch, and close them before running untrusted commands.
- Tie proxy bridge lifetime to the sandbox so cancellation closes active upstream connections.
## Testing
- Cover descriptor validation and interrupted handoffs.
- Verify concurrent sandboxes keep proxy endpoints isolated and do not inherit privileged sockets.
- Exercise managed networking end to end through `app-server`, confirming proxied access succeeds while direct host access remains blocked.
GitOrigin-RevId: 6561e09c83b927c604951bf2aa00ff532d179da5
## Why
Sandboxed descendants can outlive their immediate parent and must be collected by
PID 1 in the Bubblewrap namespace.
## What changed
- Launch `codex-linux-sandbox` with Bubblewrap's `--as-pid-1` option, and fall
back to the bundled Bubblewrap when the system version does not support it.
- Run the sandboxed command as a child, forward signals to it, reap other exited
descendants, and preserve the command's exit status.
- Verify proxy bridge parent identity when arming its parent-death signal.
## Testing
Added Linux sandbox coverage for the filtered namespace reaper, orphan
collection, and fallback from an incompatible system Bubblewrap.
GitOrigin-RevId: 379f08d6c2732ea0a4caeb61f93ae302e16d2458
## Why
Managed proxy helpers can keep captured standard streams open after the sandboxed command exits. Proxy readiness can also fail when inherited standard descriptors are already closed, and zombie owners can leave stale socket directories behind.
## What changed
- Detach bridge and cleanup-worker standard I/O to `/dev/null`.
- Move readiness pipe descriptors above the standard descriptor range.
- Treat zombie processes as exited when cleaning proxy socket directories.
- Move helper lifecycle handling into a dedicated module.
## Testing
Add coverage for output release after command exit, readiness with closed standard descriptors, zombie detection, and stale socket cleanup.
GitOrigin-RevId: 9f4081cba1b73442f02a895473c0383a795fa30e
## What changed
- Forward remote managed-network policy requests to the controller-side decider when Guardian review is enabled.
- Bound callbacks using the configured permission-hook and Guardian review timeouts, restore trusted execution attribution, and recheck live baseline policy before requesting a decision.
- Cancel pending decisions when the process or execution scope ends, and propagate EOF across the sandbox proxy bridge with half-closes.
## Testing
- Cover callback enablement, strict allowlist behavior, live policy updates, trusted attribution, and execution-scope cancellation.
GitOrigin-RevId: 83b47657c8c4546dfe98eec8b272833e96a46b7e
## What changed
- Make the generated Linux proxy socket directory readable inside restricted
`bubblewrap` sandboxes so the proxy bridge can connect.
- Route `WS_PROXY` and `WSS_PROXY` through the Linux managed proxy bridge.
- Remove inherited proxy attribution tokens from unscoped executions while
continuing to replace them with the current token for scoped executions.
## Testing
- Exercise `WSS_PROXY` routing with a minimal filesystem policy.
- Cover attribution-token removal and replacement during environment setup.
GitOrigin-RevId: 2f14ddb26baab0147a786bc354d93eb5aad059f3
## 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>
## Summary
- avoid generating host proxy bridge Unix socket paths that exceed
Linux's `sockaddr_un.sun_path` limit
- fall back from a long `$CODEX_HOME/tmp` path to the system temp
directory, then `/tmp`
- add focused unit coverage for short and overlong parent paths
## Root cause
With a sufficiently long `CODEX_HOME`, the generated
`proxy-route-*.sock` path exceeds Linux's 107-byte pathname limit. The
host bridge child exits before writing its readiness byte, so the parent
reports the indirect error `failed to prepare host proxy routing bridge:
failed to fill whole buffer`.
## Validation
- reproduced the original error with a long `CODEX_HOME` using
`codex-cli 0.138.0-alpha.4`
- `cargo clippy -p codex-linux-sandbox --all-targets`
- `just fix -p codex-linux-sandbox`
- `just fmt`
The Linux-only unit test could not execute locally: the arm64 Docker
build was repeatedly OOM-killed by `rustc` while compiling an unrelated
`codex-app-server-protocol` dependency, before reaching the test.
---------
Co-authored-by: Codex <noreply@openai.com>
## Why
The Linux managed-proxy bridge helpers are long-lived child processes in
the sandbox networking path. Before this change they stayed dumpable and
the network seccomp profile did not block cross-process memory syscalls,
so another same-user process could potentially inspect or modify bridge
memory instead of interacting only through the intended proxy interface.
## What changed
- reuse the shared `codex-process-hardening` helper to mark bridge
helper children non-dumpable before they begin serving
- deny `process_vm_readv` and `process_vm_writev` in the existing
network seccomp filter
## Security impact
Bridge helpers are less exposed to same-user cross-process inspection or
memory writes, which reduces the chance that sandboxed code can
interfere with proxy support processes outside the intended IPC path.
## Verification
- `cargo test -p codex-process-hardening`
- `cargo test -p codex-linux-sandbox`
- attempted `cargo check -p codex-linux-sandbox --target
x86_64-unknown-linux-gnu`; blocked on missing `x86_64-linux-gnu-gcc` on
this macOS host
---------
Co-authored-by: Codex <noreply@openai.com>
## Why
The Bazel-backed `argument-comment-lint` CI path had two gaps:
- Bazel wildcard target expansion skipped inline unit-test crates from
`src/` modules because the generated `*-unit-tests-bin` `rust_test`
targets are tagged `manual`.
- `argument-comment-mismatch` was still only a warning in the Bazel and
packaged-wrapper entrypoints, so a typoed `/*param_name*/` comment could
still pass CI even when the lint detected it.
That left CI blind to real linux-sandbox examples, including the missing
`/*local_port*/` comment in
`codex-rs/linux-sandbox/src/proxy_routing.rs` and typoed argument
comments in `codex-rs/linux-sandbox/src/landlock.rs`.
## What Changed
- Added `tools/argument-comment-lint/list-bazel-targets.sh` so Bazel
lint runs cover `//codex-rs/...` plus the manual `rust_test`
`*-unit-tests-bin` targets.
- Updated `just argument-comment-lint`, `rust-ci.yml`, and
`rust-ci-full.yml` to use that helper.
- Promoted both `argument-comment-mismatch` and
`uncommented-anonymous-literal-argument` to errors in every strict
entrypoint:
- `tools/argument-comment-lint/lint_aspect.bzl`
- `tools/argument-comment-lint/src/bin/argument-comment-lint.rs`
- `tools/argument-comment-lint/wrapper_common.py`
- Added wrapper/bin coverage for the stricter lint flags and documented
the behavior in `tools/argument-comment-lint/README.md`.
- Fixed the now-covered callsites in
`codex-rs/linux-sandbox/src/proxy_routing.rs`,
`codex-rs/linux-sandbox/src/landlock.rs`, and
`codex-rs/core/src/shell_snapshot_tests.rs`.
This keeps the Bazel target expansion narrow while making the Bazel and
prebuilt-linter paths enforce the same strict lint set.
## Verification
- `python3 -m unittest discover -s tools/argument-comment-lint -p
'test_*.py'`
- `cargo +nightly-2025-09-18 test --manifest-path
tools/argument-comment-lint/Cargo.toml`
- `just argument-comment-lint`