Make sandbox read access configurable with ReadOnlyAccess

## What

This change introduces a new `ReadOnlyAccess` model and threads it through
sandbox policy consumers so read access is explicit instead of implicit.

- Added `ReadOnlyAccess` to protocol:
  - `Restricted { include_platform_defaults, readable_roots }`
  - `FullAccess`
- Changed `SandboxPolicy` shape:
  - `ReadOnly` is now `ReadOnly { access: ReadOnlyAccess }`
  - `WorkspaceWrite` now carries `read_only_access: ReadOnlyAccess`
- Kept existing behavior for now by defaulting to `ReadOnlyAccess::FullAccess`
  in constructors and current config/app-server mappings.
- Added helper methods to compute effective readable roots (including optional
  platform defaults + cwd) and to detect full read access.
- Updated seatbelt policy generation to honor restricted read roots by emitting
  scoped `(allow file-read* ...)` entries when full read access is not granted.
- Updated Linux backends (`bwrap`, legacy landlock path) to fail closed with an
  explicit `UnsupportedOperation` when restricted read access is requested but
  not yet implemented there.
- Updated Windows sandbox backends (standard, elevated, and runner paths) to
  fail closed in the same way for restricted read access.
- Updated all call sites/tests/pattern matches for the new structured variants
  and regenerated app-server protocol schema/types.

## Why

The previous `SandboxPolicy::ReadOnly` implied full-disk read access and left
no way to express a narrower read surface.

This refactor establishes the policy model needed to support user-configurable
read restrictions in a follow-up without changing current runtime behavior.

It also ensures we do not silently ignore future restricted-read policies on
platform backends that do not support them yet. Failing closed keeps sandbox
semantics predictable and avoids accidental over-permission.

## Compatibility and rollout notes

- Existing behavior is preserved by default (`FullAccess`).
- Existing config/app-server flows continue to serialize/deserialize cleanly.
- New schema artifacts are included to keep generated protocol outputs in sync.

## Validation

- `just fmt`
- `just fix -p codex-protocol -p codex-core -p codex-linux-sandbox -p codex-windows-sandbox -p codex-app-server-protocol`
- `cargo check -p codex-windows-sandbox`
- Targeted crate/test runs were executed during development for protocol/core/
  sandbox-related crates.
This commit is contained in:
Michael Bolin
2026-02-10 21:41:43 -08:00
parent 641d5268fa
commit e428b13c8a
72 changed files with 1126 additions and 289 deletions
+1 -1
View File
@@ -123,7 +123,7 @@ impl ExecTool {
.await
.clone()
.unwrap_or_else(|| SandboxState {
sandbox_policy: SandboxPolicy::ReadOnly,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
codex_linux_sandbox_exe: None,
sandbox_cwd: PathBuf::from(&params.workdir),
use_linux_sandbox_bwrap: false,
+2 -1
View File
@@ -88,7 +88,7 @@ where
S: Service<RoleClient> + ClientHandler,
{
let sandbox_state = SandboxState {
sandbox_policy: SandboxPolicy::ReadOnly,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
codex_linux_sandbox_exe,
sandbox_cwd: sandbox_cwd.as_ref().to_path_buf(),
use_linux_sandbox_bwrap: false,
@@ -110,6 +110,7 @@ where
// Note that sandbox_cwd will already be included as a writable root
// when the sandbox policy is expanded.
writable_roots: vec![],
read_only_access: Default::default(),
network_access: false,
// Disable writes to temp dir because this is a test, so
// writable_folder is likely also under /tmp and we want to be