## Summary
Some improvements to Windows process-management issues from
https://github.com/openai/codex/pull/15578
- bound the elevated runner pipe-connect handshake instead of waiting
forever on blocking pipe connects
- terminate the spawned runner if that handshake fails, so timeout/error
paths do not leave a stray `codex-command-runner.exe`
- loop on partial `WriteFile` results when forwarding stdin in the
elevated runner, so input is not silently truncated
- fix the concrete HANDLE/SID cleanup paths in the runner setup code
- keep draining driver-backed stdout/stderr after exit until the backend
closes, instead of dropping the tail after a fixed 200ms grace period
- reuse `LocalSid` for SID ownership and add more explanatory comments
around the ownership/concurrency-sensitive code paths
## Why
The original PR fixed a lot of Windows session plumbing, but there were
still a few sharp process-lifecycle edges:
- some elevated runner handshakes could block forever
- the new timeout path could still orphan the spawned runner process
- stdin forwarding still assumed a single `WriteFile` consumed the whole
buffer
- a few raw HANDLE/SID error paths still leaked
- driver-backed output could still lose the last chunk of stdout/stderr
on slower backends
## Validation
- `cargo fmt -p codex-windows-sandbox -p codex-utils-pty`
- `cargo test -p codex-utils-pty`
- `cargo test -p codex-windows-sandbox finish_driver_spawn`
- `cargo test -p codex-windows-sandbox runner_`
Ran a local test matrix of unified-exec and shell_tool tests, all
passing
## Why
`ReadOnlyAccess` was a transitional legacy shape on `SandboxPolicy`:
`FullAccess` meant the historical read-only/workspace-write modes could
read the full filesystem, while `Restricted` tried to carry partial
readable roots. The partial-read model now belongs in
`FileSystemSandboxPolicy` and `PermissionProfile`, so keeping it on
`SandboxPolicy` makes every legacy projection reintroduce lossy
read-root bookkeeping and creates unnecessary noise in the rest of the
permissions migration.
This PR makes the legacy policy model narrower and explicit:
`SandboxPolicy::ReadOnly` and `SandboxPolicy::WorkspaceWrite` represent
the old full-read sandbox modes only. Split readable roots, deny-read
globs, and platform-default/minimal read behavior stay in the runtime
permissions model.
## What changed
- Removes `ReadOnlyAccess` from
`codex_protocol::protocol::SandboxPolicy`, including the generated
`access` and `readOnlyAccess` API fields.
- Updates legacy policy/profile conversions so restricted filesystem
reads are represented only by `FileSystemSandboxPolicy` /
`PermissionProfile` entries.
- Keeps app-server v2 compatible with legacy `fullAccess` read-access
payloads by accepting and ignoring that no-op shape, while rejecting
legacy `restricted` read-access payloads instead of silently widening
them to full-read legacy policies.
- Carries Windows sandbox platform-default read behavior with an
explicit override flag instead of depending on
`ReadOnlyAccess::Restricted`.
- Refreshes generated app-server schema/types and updates tests/docs for
the simplified legacy policy shape.
## Verification
- `cargo check -p codex-app-server-protocol --tests`
- `cargo check -p codex-windows-sandbox --tests`
- `cargo test -p codex-app-server-protocol sandbox_policy_`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19449).
* #19395
* #19394
* #19393
* #19392
* #19391
* __->__ #19449
## Summary
This is the runtime/foundation half of the Windows sandbox unified-exec
work.
- add Windows sandbox `unified_exec` session support in
`windows-sandbox-rs` for both:
- the legacy restricted-token backend
- the elevated runner backend
- extend the PTY/process runtime so driver-backed sessions can support:
- stdin streaming
- stdout/stderr separation
- exit propagation
- PTY resize hooks
- add Windows sandbox runtime coverage in `codex-windows-sandbox` /
`codex-utils-pty`
This PR does **not** enable Windows sandbox `UnifiedExec` for product
callers yet because hooking this up to app-server comes in the next PR.
Windows sandbox advertising is intentionally kept aligned with `main`,
so sandboxed Windows callers still fall back to `ShellCommand`.
This PR isolates the runtime/session layer so it can be reviewed
independently from product-surface enablement.
---------
Co-authored-by: jif-oai <jif@openai.com>
Co-authored-by: Codex <noreply@openai.com>
## Stack
1. Base PR: #18443 stops granting ACLs on `USERPROFILE`.
2. This PR: filters additional SSH-owned profile roots discovered from
SSH config.
## Bug
The base PR removes the broadest bad grant: `USERPROFILE` itself.
That still leaves one important case. A user profile child can be
SSH-owned even when its name is not one of our fixed exclusions.
For example:
```sshconfig
Host devbox
IdentityFile ~/.keys/devbox
CertificateFile ~/.certs/devbox-cert.pub
UserKnownHostsFile ~/.known_hosts_custom
Include ~/.ssh/conf.d/*.conf
```
After profile expansion, the sandbox might see these as normal profile
children:
```text
C:\Users\me\.keys
C:\Users\me\.certs
C:\Users\me\.known_hosts_custom
C:\Users\me\.ssh
```
Those paths have another owner: OpenSSH and the tools that manage SSH
identity and host-key state. Codex should not add sandbox ACLs to them.
OpenSSH describes this dependency tree in
[`ssh_config(5)`](https://man.openbsd.org/ssh_config.5), and the client
parser follows the same shape in `readconf.c`:
- `Include` recursively reads more config files and expands globs
- `IdentityFile` and `CertificateFile` name authentication files
- `UserKnownHostsFile`, `GlobalKnownHostsFile`, and `RevokedHostKeys`
name host-key files
- `ControlPath` and `IdentityAgent` can name profile-owned sockets or
control files
- these path directives can use forms such as `~`, `%d`, and `${HOME}`
## Change
This PR adds a small SSH config dependency scanner.
It starts at:
```text
~/.ssh/config
```
Then it returns concrete paths named by `Include` and by path-valued SSH
config directives:
```text
IdentityFile
CertificateFile
UserKnownHostsFile
GlobalKnownHostsFile
RevokedHostKeys
ControlPath
IdentityAgent
```
For example:
```sshconfig
IdentityFile ~/.keys/devbox
CertificateFile ~/.certs/devbox-cert.pub
Include ~/.ssh/conf.d/*.conf
```
returns paths like:
```text
C:\Users\me\.keys\devbox
C:\Users\me\.certs\devbox-cert.pub
C:\Users\me\.ssh\conf.d\devbox.conf
```
The setup code then maps those paths back to their top-level
`USERPROFILE` child and filters matching sandbox roots out of both the
writable and readable root lists.
## Why this shape
The parser reports what SSH config references. The sandbox setup code
decides which `USERPROFILE` roots are unsafe to grant.
That keeps the policy simple:
1. expand broad profile grants
2. remove the profile root
3. remove fixed sensitive profile folders
4. remove profile folders referenced by SSH config dependencies
If a path has two possible owners, the sandbox steps back. SSH keeps
control of SSH config, keys, certificates, known-hosts files, sockets,
and included config files.
## Tests
- `cargo test -p codex-windows-sandbox --lib`
- `just bazel-lock-check`
- `just fix -p codex-windows-sandbox`
- `git diff --check`
## Summary
This updates the Windows elevated sandbox setup/refresh path to include
the legacy `compute_allow_paths(...).deny` protected children in the
same deny-write payload pipe added for split filesystem carveouts.
Concretely, elevated setup and elevated refresh now both build
deny-write payload paths from:
- explicit split-policy deny-write paths, preserving missing paths so
setup can materialize them before applying ACLs
- legacy `compute_allow_paths(...).deny`, which includes existing
`.git`, `.codex`, and `.agents` children under writable roots
This lets the elevated backend protect `.git` consistently with the
unelevated/restricted-token path, and removes the old janky hard-coded
`.codex` / `.agents` elevated setup helpers in favor of the shared
payload path.
## Root Cause
The landed split-carveout PR threaded a `deny_write_paths` pipe through
elevated setup/refresh, but the legacy workspace-write deny set from
`compute_allow_paths(...).deny` was not included in that payload. As a
result, elevated workspace-write did not apply the intended deny-write
ACLs for existing protected children like `<cwd>/.git`.
## Notes
The legacy protected children still only enter the deny set if they
already exist, because `compute_allow_paths` filters `.git`, `.codex`,
and `.agents` with `exists()`. Missing explicit split-policy deny paths
are preserved separately because setup intentionally materializes those
before applying ACLs.
## Validation
- `cargo fmt --check -p codex-windows-sandbox`
- `cargo test -p codex-windows-sandbox`
- `cargo build -p codex-cli -p codex-windows-sandbox --bins`
- Elevated `codex exec` smoke with `windows.sandbox='elevated'`: fresh
git repo, attempted append to `.git/config`, observed `Access is
denied`, marker not written, Deny ACE present on `.git`
- Unelevated `codex exec` smoke with `windows.sandbox='unelevated'`:
fresh git repo, attempted append to `.git/config`, observed `Access is
denied`, marker not written, Deny ACE present on `.git`
## Summary
- reduce public module visibility across Rust crates, preferring private
or crate-private modules with explicit crate-root public exports
- update external call sites and tests to use the intended public crate
APIs instead of reaching through module trees
- add the module visibility guideline to AGENTS.md
## Validation
- `cargo check --workspace --all-targets --message-format=short` passed
before the final fix/format pass
- `just fix` completed successfully
- `just fmt` completed successfully
- `git diff --check` passed
## Why
Follow-up to #16345, the Bazel clippy rollout in #15955, and the cleanup
pass in #16353.
`cargo clippy` was enforcing the workspace deny-list from
`codex-rs/Cargo.toml` because the member crates opt into `[lints]
workspace = true`, but Bazel clippy was only using `rules_rust` plus
`clippy.toml`. That left the Bazel lane vulnerable to drift:
`clippy.toml` can tune lint behavior, but it cannot set
allow/warn/deny/forbid levels.
This PR now closes both sides of the follow-up. It keeps `.bazelrc` in
sync with `[workspace.lints.clippy]`, and it fixes the real clippy
violations that the newly-synced Windows Bazel lane surfaced once that
deny-list started matching Cargo.
## What Changed
- added `.github/scripts/verify_bazel_clippy_lints.py`, a Python check
that parses `codex-rs/Cargo.toml` with `tomllib`, reads the Bazel
`build:clippy` `clippy_flag` entries from `.bazelrc`, and reports
missing, extra, or mismatched lint levels
- ran that verifier from the lightweight `ci.yml` workflow so the sync
check does not depend on a Rust toolchain being installed first
- expanded the `.bazelrc` comment to explain the Cargo `workspace =
true` linkage and why Bazel needs the deny-list duplicated explicitly
- fixed the Windows-only `codex-windows-sandbox` violations that Bazel
clippy reported after the sync, using the same style as #16353: inline
`format!` args, method references instead of trivial closures, removed
redundant clones, and replaced SID conversion `unwrap` and `expect`
calls with proper errors
- cleaned up the remaining cross-platform violations the Bazel lane
exposed in `codex-backend-client` and `core_test_support`
## Testing
Key new test introduced by this PR:
`python3 .github/scripts/verify_bazel_clippy_lints.py`
## Why
The initial `argument-comment-lint` rollout left Windows on
default-target coverage because there were still Windows-only callsites
failing under `--all-targets`. This follow-up cleans up those remaining
Windows-specific violations so the Windows CI lane can enforce the same
stricter coverage, leaving Linux as the remaining platform-specific
follow-up.
## What changed
- switched the Windows `rust-ci` argument-comment-lint step back to the
default wrapper invocation so it runs full-target coverage again
- added the required `/*param_name*/` annotations at Windows-gated
literal callsites in:
- `codex-rs/windows-sandbox-rs/src/lib.rs`
- `codex-rs/windows-sandbox-rs/src/elevated_impl.rs`
- `codex-rs/tui_app_server/src/multi_agents.rs`
- `codex-rs/network-proxy/src/proxy.rs`
## Validation
- Windows `argument comment lint` CI on this PR
## Why
`codex-utils-pty` and `codex-windows-sandbox` were the remaining crates
in `codex-rs` that still overrode the workspace's Rust 2024 edition.
Moving them forward in a separate PR keeps the baseline edition update
isolated from the follow-on Bazel clippy workflow in #15955, while
making linting and formatting behavior consistent with the rest of the
workspace.
This PR also needs Cargo and Bazel to agree on the edition for
`codex-windows-sandbox`. Without the Bazel-side sync, the experimental
Bazel app-server builds fail once they compile `windows-sandbox-rs`.
## What changed
- switch `codex-rs/utils/pty` and `codex-rs/windows-sandbox-rs` to
`edition = "2024"`
- update `codex-utils-pty` callsites and tests to use the collapsed `if
let` form that Clippy expects under the new edition
- fix the Rust 2024 fallout in `windows-sandbox-rs`, including the
reserved `gen` identifier, `unsafe extern` requirements, and new Clippy
findings that surfaced under the edition bump
- keep the edition bump separate from a larger unsafe cleanup by
temporarily allowing `unsafe_op_in_unsafe_fn` in the Windows entrypoint
modules that now report it under Rust 2024
- update `codex-rs/windows-sandbox-rs/BUILD.bazel` to `crate_edition =
"2024"` so Bazel compiles the crate with the same edition as Cargo
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/15954).
* #15976
* #15955
* __->__ #15954
## Summary
This PR makes Windows sandbox proxying enforceable by routing proxy-only
runs through the existing `offline` sandbox user and reserving direct
network access for the existing `online` sandbox user.
In brief:
- if a Windows sandbox run should be proxy-enforced, we run it as the
`offline` user
- the `offline` user gets firewall rules that block direct outbound
traffic and only permit the configured localhost proxy path
- if a Windows sandbox run should have true direct network access, we
run it as the `online` user
- no new sandbox identity is introduced
This brings Windows in line with the intended model: proxy use is not
just env-based, it is backed by OS-level egress controls. Windows
already has two sandbox identities:
- `offline`: intended to have no direct network egress
- `online`: intended to have full network access
This PR makes proxy-enforced runs use that model directly.
### Proxy-enforced runs
When proxy enforcement is active:
- the run is assigned to the `offline` identity
- setup extracts the loopback proxy ports from the sandbox env
- Windows setup programs firewall rules for the `offline` user that:
- block all non-loopback outbound traffic
- block loopback UDP
- block loopback TCP except for the configured proxy ports
- optionally allow broader localhost access when `allow_local_binding=1`
So the sandboxed process can only talk to the local proxy. It cannot
open direct outbound sockets or do local UDP-based DNS on its own.The
proxy then performs the real outbound network access outside that
restricted sandbox identity.
### Direct-network runs
When proxy enforcement is not active and full network access is allowed:
- the run is assigned to the `online` identity
- no proxy-only firewall restrictions are applied
- the process gets normal direct network access
### Unelevated vs elevated
The restricted-token / unelevated path cannot enforce per-identity
firewall policy by itself.
So for Windows proxy-enforced runs, we transparently use the logon-user
sandbox path under the hood, even if the caller started from the
unelevated mode. That keeps enforcement real instead of best-effort.
---------
Co-authored-by: Codex <noreply@openai.com>
## Summary
- keep legacy Windows restricted-token sandboxing as the supported
baseline
- support the split-policy subset that restricted-token can enforce
directly today
- support full-disk read, the same writable root set as legacy
`WorkspaceWrite`, and extra read-only carveouts under those writable
roots via additional deny-write ACLs
- continue to fail closed for unsupported split-only shapes, including
explicit unreadable (`none`) carveouts, reopened writable descendants
under read-only carveouts, and writable root sets that do not match the
legacy workspace roots
## Example
Given a filesystem policy like:
```toml
":root" = "read"
":cwd" = "write"
"./docs" = "read"
```
the restricted-token backend can keep the workspace writable while
denying writes under `docs` by layering an extra deny-write carveout on
top of the legacy workspace-write roots.
A policy like:
```toml
"/workspace" = "write"
"/workspace/docs" = "read"
"/workspace/docs/tmp" = "write"
```
still fails closed, because the unelevated backend cannot reopen the
nested writable descendant safely.
## Stack
-> fix: support split carveouts in windows restricted-token sandbox
#14172
fix: support split carveouts in windows elevated sandbox #14568
## Summary
- support legacy `ReadOnlyAccess::Restricted` on Windows in the elevated
setup/runner backend
- keep the unelevated restricted-token backend on the legacy full-read
model only, and fail closed for restricted read-only policies there
- keep the legacy full-read Windows path unchanged while deriving
narrower read roots only for elevated restricted-read policies
- honor `include_platform_defaults` by adding backend-managed Windows
system roots only when requested, while always keeping helper roots and
the command `cwd` readable
- preserve `workspace-write` semantics by keeping writable roots
readable when restricted read access is in use in the elevated backend
- document the current Windows boundary: legacy `SandboxPolicy` is
supported on both backends, while richer split-only carveouts still fail
closed instead of running with weaker enforcement
## Testing
- `cargo test -p codex-windows-sandbox`
- `cargo check -p codex-windows-sandbox --tests --target
x86_64-pc-windows-msvc`
- `cargo clippy -p codex-windows-sandbox --tests --target
x86_64-pc-windows-msvc -- -D warnings`
- `cargo test -p codex-core windows_restricted_token_`
## Notes
- local `cargo test -p codex-windows-sandbox` on macOS only exercises
the non-Windows stubs; the Windows-targeted compile and clippy runs
provide the local signal, and GitHub Windows CI exercises the runtime
path
# Summary
This PR introduces the Windows sandbox runner IPC foundation that later
unified_exec work will build on.
The key point is that this is intentionally infrastructure-only. The new
IPC transport, runner plumbing, and ConPTY helpers are added here, but
the active elevated Windows sandbox path still uses the existing
request-file bootstrap. In other words, this change prepares the
transport and module layout we need for unified_exec without switching
production behavior over yet.
Part of this PR is also a source-layout cleanup: some Windows sandbox
files are moved into more explicit `elevated/`, `conpty/`, and shared
locations so it is clearer which code is for the elevated sandbox flow,
which code is legacy/direct-spawn behavior, and which helpers are shared
between them. That reorganization is intentional in this first PR so
later behavioral changes do not also have to carry a large amount of
file-move churn.
# Why This Is Needed For unified_exec
Windows elevated sandboxed unified_exec needs a long-lived,
bidirectional control channel between the CLI and a helper process
running under the sandbox user. That channel has to support:
- starting a process and reporting structured spawn success/failure
- streaming stdout/stderr back incrementally
- forwarding stdin over time
- terminating or polling a long-lived process
- supporting both pipe-backed and PTY-backed sessions
The existing elevated one-shot path is built around a request-file
bootstrap and does not provide those primitives cleanly. Before we can
turn on Windows sandbox unified_exec, we need the underlying runner
protocol and transport layer that can carry those lifecycle events and
streams.
# Why Windows Needs More Machinery Than Linux Or macOS
Linux and macOS can generally build unified_exec on top of the existing
sandbox/process model: the parent can spawn the child directly, retain
normal ownership of stdio or PTY handles, and manage the lifetime of the
sandboxed process without introducing a second control process.
Windows elevated sandboxing is different. To run inside the sandbox
boundary, we cross into a different user/security context and then need
to manage a long-lived process from outside that boundary. That means we
need an explicit helper process plus an IPC transport to carry spawn,
stdin, output, and exit events back and forth. The extra code here is
mostly that missing Windows sandbox infrastructure, not a conceptual
difference in unified_exec itself.
# What This PR Adds
- the framed IPC message types and transport helpers for parent <->
runner communication
- the renamed Windows command runner with both the existing request-file
bootstrap and the dormant IPC bootstrap
- named-pipe helpers for the elevated runner path
- ConPTY helpers and process-thread attribute plumbing needed for
PTY-backed sessions
- shared sandbox/process helpers that later PRs will reuse when
switching live execution paths over
- early file/module moves so later PRs can focus on behavior rather than
layout churn
# What This PR Does Not Yet Do
- it does not switch the active elevated one-shot path over to IPC yet
- it does not enable Windows sandbox unified_exec yet
- it does not remove the existing request-file bootstrap yet
So while this code compiles and the new path has basic validation, it is
not yet the exercised production path. That is intentional for this
first PR: the goal here is to land the transport and runner foundation
cleanly before later PRs start routing real command execution through
it.
# Follow-Ups
Planned follow-up PRs will:
1. switch elevated one-shot Windows sandbox execution to the new runner
IPC path
2. layer Windows sandbox unified_exec sessions on top of the same
transport
3. remove the legacy request-file path once the IPC-based path is live
# Validation
- `cargo build -p codex-windows-sandbox`
## Summary
- launch Windows sandboxed children on a private desktop instead of
`Winsta0\Default`
- make private desktop the default while keeping
`windows.sandbox_private_desktop=false` as the escape hatch
- centralize process launch through the shared
`create_process_as_user(...)` path
- scope the private desktop ACL to the launching logon SID
## Why
Today sandboxed Windows commands run on the visible shared desktop. That
leaves an avoidable same-desktop attack surface for window interaction,
spoofing, and related UI/input issues. This change moves sandboxed
commands onto a dedicated per-launch desktop by default so the sandbox
no longer shares `Winsta0\Default` with the user session.
The implementation stays conservative on security with no silent
fallback back to `Winsta0\Default`
If private-desktop setup fails on a machine, users can still opt out
explicitly with `windows.sandbox_private_desktop=false`.
## Validation
- `cargo build -p codex-cli`
- elevated-path `codex exec` desktop-name probe returned
`CodexSandboxDesktop-*`
- elevated-path `codex exec` smoke sweep for shell commands, nested
`pwsh`, jobs, and hidden `notepad` launch
- unelevated-path full private-desktop compatibility sweep via `codex
exec` with `-c windows.sandbox=unelevated`
We do this for codex-command-runner.exe as well for the same reason.
Windows sandbox users cannot execute binaries in the WindowsApp/
installed directory for the Codex App. This causes apply-patch to fail
because it tries to execute codex.exe as the sandbox user.
• Keep Windows sandbox runner launches working from packaged installs by
running the helper from a user-owned runtime location.
On some Windows installs, the packaged helper location is difficult to
use reliably for sandboxed runner launches even though the binaries are
present. This change works around that by copying codex-
command-runner.exe into CODEX_HOME/.sandbox-bin/, reusing that copy
across launches, and falling back to the existing packaged-path lookup
if anything goes wrong.
The runtime copy lives in a dedicated directory with tighter ACLs than
.sandbox: sandbox users can read and execute the runner there, but they
cannot modify it. This keeps the workaround focused on the
command runner, leaves the setup helper on its trusted packaged path,
and adds logging so it is clear which runner path was selected at
launch.
The Mac and Linux implementations of the sandbox recently added write
protections for `.codex` and `.agents` subdirectories in all writable
roots. When adding documentation for this, I noticed that this change
was never made for the Windows sandbox.
Summary
- make compute_allow_paths treat .codex/.agents as protected alongside
.git, and cover their behavior in new tests
- wire protect_workspace_agents_dir through the sandbox lib and setup
path to apply deny ACEs when `.agents` exists
- factor shared ACL logic for workspace subdirectories
There is an edge case where a directory is not readable by the sandbox.
In practice, we've seen very little of it, but it can happen so this
slash command unlocks users when it does.
Future idea is to make this a tool that the agent knows about so it can
be more integrated.
`SandboxPolicy::ReadOnly` previously implied broad read access and could
not express a narrower read surface.
This change introduces an explicit read-access model so we can support
user-configurable read restrictions in follow-up work, while preserving
current behavior today.
It also ensures unsupported backends fail closed for restricted-read
policies instead of silently granting broader access than intended.
## What
- Added `ReadOnlyAccess` in protocol with:
- `Restricted { include_platform_defaults, readable_roots }`
- `FullAccess`
- Updated `SandboxPolicy` to carry read-access configuration:
- `ReadOnly { access: ReadOnlyAccess }`
- `WorkspaceWrite { ..., read_only_access: ReadOnlyAccess }`
- Preserved existing behavior by defaulting current construction paths
to `ReadOnlyAccess::FullAccess`.
- Threaded the new fields through sandbox policy consumers and call
sites across `core`, `tui`, `linux-sandbox`, `windows-sandbox`, and
related tests.
- Updated Seatbelt policy generation to honor restricted read roots by
emitting scoped read rules when full read access is not granted.
- Added fail-closed behavior on Linux and Windows backends when
restricted read access is requested but not yet implemented there
(`UnsupportedOperation`).
- Regenerated app-server protocol schema and TypeScript artifacts,
including `ReadOnlyAccess`.
## Compatibility / rollout
- Runtime behavior remains unchanged by default (`FullAccess`).
- API/schema changes are in place so future config wiring can enable
restricted read access without another policy-shape migration.
1. Move Windows Sandbox NUX to right after trust directory screen
2. Don't offer read-only as an option in Sandbox NUX.
Elevated/Legacy/Quit
3. Don't allow new untrusted directories. It's trust or quit
4. move experimental sandbox features to `[windows]
sandbox="elevated|unelevatd"`
5. Copy tweaks = elevated -> default, non-elevated -> non-admin
calculated a hashed user ID from either auth user id or API key
Also correctly populates OS.
These will make our metrics more useful and powerful for analysis.
Today, there is a single capability SID that allows the sandbox to write
to
* workspace (cwd)
* tmp directories if enabled
* additional writable roots
This change splits those up, so that each workspace has its own
capability SID, while tmp and additional roots, which are
installation-wide, are still governed by the "generic" capability SID
This isolates workspaces from each other in terms of sandbox write
access.
Also allows us to protect <cwd>/.codex when codex runs in a specific
<cwd>
This fixes a bug where the elevated sandbox setup encrypts sandbox user
passwords as an admin user, but normal command execution attempts to
decrypt them as a different user.
Machine scope allows all users to encyrpt/decrypt
this PR also moves the encrypted file to a different location
.codex/.sandbox-secrets which the sandbox users cannot read.
Elevated Sandbox NUX:
* prompt for elevated sandbox setup when agent mode is selected (via
/approvals or at startup)
* prompt for degraded sandbox if elevated setup is declined or fails
* introduce /elevate-sandbox command to upgrade from degraded
experience.
The elevated sandbox creates two new Windows users - CodexSandboxOffline
and CodexSandboxOnline. This is necessary, so this PR does all that it
can to "hide" those users. It uses the registry plus directory flags (on
their home directories) to get them to show up as little as possible.
The elevated setup synchronously applies read/write ACLs to any
workspace roots.
However, until we apply *read* permission to the full path, powershell
cannot use some roots as a cwd as it needs access to all parts of the
path in order to apply it as the working directory for a command.
The solution is, while the async read-ACL part of setup is running, use
a "junction" that lives in C:\Users\CodexSandbox{Offline|Online} that
points to the cwd.
Once the read ACLs are applied, we stop using the junction.
-----
this PR also removes some dead code and overly-verbose logging, and has
some light refactoring to the ACL-related functions
## Description
Introduced `ExternalSandbox` policy to cover use case when sandbox
defined by outside environment, effectively it translates to
`SandboxMode#DangerFullAccess` for file system (since sandbox configured
on container level) and configurable `network_access` (either Restricted
or Enabled by outside environment).
as example you can configure `ExternalSandbox` policy as part of
`sendUserTurn` v1 app_server API:
```
{
"conversationId": <id>,
"cwd": <cwd>,
"approvalPolicy": "never",
"sandboxPolicy": {
"type": ""external-sandbox",
"network_access": "enabled"/"restricted"
},
"model": <model>,
"effort": <effort>,
....
}
```
- Batch read ACL creation for online/offline sandbox user
- creates a new ACL helper process that is long-lived and runs in the
background
- uses a mutex so that only one helper process is running at a time.
a few fixes based on testing feedback:
* ensure cap_sid file is always written by elevated setup.
* always log to same file whether using elevated sandbox or not
* process potentially slow ACE write operations in parallel
* dedupe write roots so we don't double process any
* don't try to create read/write ACEs on the same directories, due to
race condition
clean up the code for scanning for world writable directories
One path (selecting a sandbox mode from /approvals) was using an
incorrect method that did not use the new method of creating deny aces
to prevent writing to those directories. Now all paths are the same.
Our Restricted Token contains 3 SIDs (Logon, Everyone, {WorkspaceWrite
Capability || ReadOnly Capability})
because it must include Everyone, that left us vulnerable to directories
that allow writes to Everyone. Even though those directories do not have
ACEs that enable our capability SIDs to write to them, they could still
be written to even in ReadOnly mode, or even in WorkspaceWrite mode if
they are outside of a writable root.
A solution to this is to explicitly add *Deny* ACEs to these
directories, always for the ReadOnly Capability SID, and for the
WorkspaceWrite SID if the directory is outside of a workspace root.
Under a restricted token, Windows always checks Deny ACEs before Allow
ACEs so even though our restricted token would allow a write to these
directories due to the Everyone SID, it fails first because of the Deny
ACE on the capability SID
The `cap_sid` file contains the IDs of the two custom SIDs that the
Windows sandbox creates/manages to implement read-only and
workspace-write sandbox policies.
It previously lived in `<cwd>/.codex` which means that the sandbox could
write to it, which could degrade the efficacy of the sandbox. This
change moves it to `~/.codex/` (or wherever `CODEX_HOME` points to) so
that it is outside the workspace.
3 improvements:
1. show up to 3 actual paths that are world-writable
2. do the scan/warning for Read-Only mode too, because it also applies
there
3. remove the "Cancel" option since it doesn't always apply (like on
startup)
1. scan many more directories since it's much faster than the original
implementation
2. limit overall scan time to 2s
3. skip some directories that are noisy - ApplicationData, Installer,
etc.
- Added the new codex-windows-sandbox crate that builds both a library
entry point (run_windows_sandbox_capture) and a CLI executable to launch
commands inside a Windows restricted-token sandbox, including ACL
management, capability SID provisioning, network lockdown, and output
capture
(windows-sandbox-rs/src/lib.rs:167, windows-sandbox-rs/src/main.rs:54).
- Introduced the experimental WindowsSandbox feature flag and wiring so
Windows builds can opt into the sandbox:
SandboxType::WindowsRestrictedToken, the in-process execution path, and
platform sandbox selection now honor the flag (core/src/features.rs:47,
core/src/config.rs:1224, core/src/safety.rs:19,
core/src/sandboxing/mod.rs:69, core/src/exec.rs:79,
core/src/exec.rs:172).
- Updated workspace metadata to include the new crate and its
Windows-specific dependencies so the core crate can link against it
(codex-rs/
Cargo.toml:91, core/Cargo.toml:86).
- Added a PowerShell bootstrap script that installs the Windows
toolchain, required CLI utilities, and builds the workspace to ease
development
on the platform (scripts/setup-windows.ps1:1).
- Landed a Python smoke-test suite that exercises
read-only/workspace-write policies, ACL behavior, and network denial for
the Windows sandbox
binary (windows-sandbox-rs/sandbox_smoketests.py:1).