feat: make sandbox read access configurable with ReadOnlyAccess (#11387)

`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.
This commit is contained in:
Michael Bolin
2026-02-11 18:31:14 -08:00
committed by GitHub
parent 572ab66496
commit abbd74e2be
79 changed files with 1797 additions and 188 deletions

View File

@@ -92,7 +92,7 @@ fn session_configured_produces_thread_started_event() {
model: "codex-mini-latest".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
cwd: PathBuf::from("/home/user/project"),
reasoning_effort: None,
history_log_id: 0,

View File

@@ -69,7 +69,7 @@ async fn spawn_command_under_sandbox(
async fn linux_sandbox_test_env() -> Option<HashMap<String, String>> {
let command_cwd = std::env::current_dir().ok()?;
let sandbox_cwd = command_cwd.clone();
let policy = SandboxPolicy::ReadOnly;
let policy = SandboxPolicy::new_read_only_policy();
if can_apply_linux_sandbox_policy(&policy, &command_cwd, sandbox_cwd.as_path(), HashMap::new())
.await
@@ -134,6 +134,7 @@ async fn python_multiprocessing_lock_works_under_sandbox() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots,
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -194,7 +195,7 @@ async fn python_getpwuid_works_under_sandbox() {
return;
}
let policy = SandboxPolicy::ReadOnly;
let policy = SandboxPolicy::new_read_only_policy();
let command_cwd = std::env::current_dir().expect("should be able to get current dir");
let sandbox_cwd = command_cwd.clone();
@@ -247,6 +248,7 @@ async fn sandbox_distinguishes_command_and_policy_cwds() {
// is under a writable root.
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -387,7 +389,7 @@ fn unix_sock_body() {
async fn allow_unix_socketpair_recvfrom() {
run_code_under_sandbox(
"allow_unix_socketpair_recvfrom",
&SandboxPolicy::ReadOnly,
&SandboxPolicy::new_read_only_policy(),
|| async { unix_sock_body() },
)
.await