Files
codex/codex-rs/windows-sandbox-rs/src/identity.rs
T
Michael BolinandGitHub 789f387982 permissions: remove legacy read-only access modes (#19449)
## 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
2026-04-24 17:16:58 -07:00

236 lines
7.9 KiB
Rust

use crate::dpapi;
use crate::logging::debug_log;
use crate::policy::SandboxPolicy;
use crate::setup::gather_read_roots;
use crate::setup::gather_write_roots;
use crate::setup::offline_proxy_settings_from_env;
use crate::setup::run_elevated_setup;
use crate::setup::run_setup_refresh_with_overrides;
use crate::setup::sandbox_users_path;
use crate::setup::setup_marker_path;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
use crate::setup::SetupMarker;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct SandboxIdentity {
username: String,
password: String,
}
#[derive(Debug, Clone)]
pub struct SandboxCreds {
pub username: String,
pub password: String,
}
/// Returns true when the on-disk setup artifacts exist and match the current
/// setup version.
///
/// This is a coarse readiness check; `require_logon_sandbox_creds` performs the
/// additional runtime validation for offline firewall settings.
pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool {
let marker_ok = matches!(load_marker(codex_home), Ok(Some(marker)) if marker.version_matches());
if !marker_ok {
return false;
}
matches!(load_users(codex_home), Ok(Some(users)) if users.version_matches())
}
fn load_marker(codex_home: &Path) -> Result<Option<SetupMarker>> {
let path = setup_marker_path(codex_home);
let marker = match fs::read_to_string(&path) {
Ok(contents) => match serde_json::from_str::<SetupMarker>(&contents) {
Ok(m) => Some(m),
Err(err) => {
debug_log(
&format!("sandbox setup marker parse failed: {err}"),
Some(codex_home),
);
None
}
},
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => {
debug_log(
&format!("sandbox setup marker read failed: {err}"),
Some(codex_home),
);
None
}
};
Ok(marker)
}
fn load_users(codex_home: &Path) -> Result<Option<SandboxUsersFile>> {
let path = sandbox_users_path(codex_home);
let file = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(err) => {
debug_log(
&format!("sandbox users read failed: {err}"),
Some(codex_home),
);
return Ok(None);
}
};
match serde_json::from_str::<SandboxUsersFile>(&file) {
Ok(users) => Ok(Some(users)),
Err(err) => {
debug_log(
&format!("sandbox users parse failed: {err}"),
Some(codex_home),
);
Ok(None)
}
}
}
fn decode_password(record: &SandboxUserRecord) -> Result<String> {
let blob = BASE64_STANDARD
.decode(record.password.as_bytes())
.context("base64 decode password")?;
let decrypted = dpapi::unprotect(&blob)?;
let pwd = String::from_utf8(decrypted).context("sandbox password not utf-8")?;
Ok(pwd)
}
fn select_identity(
network_identity: SandboxNetworkIdentity,
codex_home: &Path,
) -> Result<Option<SandboxIdentity>> {
let _marker = match load_marker(codex_home)? {
Some(m) if m.version_matches() => m,
_ => return Ok(None),
};
let users = match load_users(codex_home)? {
Some(u) if u.version_matches() => u,
_ => return Ok(None),
};
let chosen = match network_identity {
SandboxNetworkIdentity::Offline => users.offline,
SandboxNetworkIdentity::Online => users.online,
};
let password = decode_password(&chosen)?;
Ok(Some(SandboxIdentity {
username: chosen.username,
password,
}))
}
#[allow(clippy::too_many_arguments)]
pub fn require_logon_sandbox_creds(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
read_roots_override: Option<&[PathBuf]>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
deny_write_paths_override: &[PathBuf],
proxy_enforced: bool,
) -> Result<SandboxCreds> {
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
let needed_read = read_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
let needed_write = write_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
let desired_offline_proxy_settings =
offline_proxy_settings_from_env(env_map, network_identity);
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
// granting the sandbox group access to this directory without granting the capability SID.
let mut setup_reason: Option<String> = None;
let mut identity = match load_marker(codex_home)? {
Some(marker) if marker.version_matches() => {
if let Some(reason) =
marker.request_mismatch_reason(network_identity, &desired_offline_proxy_settings)
{
setup_reason = Some(reason);
None
} else {
let selected = select_identity(network_identity, codex_home)?;
if selected.is_none() {
setup_reason =
Some("sandbox users missing or incompatible with marker version".to_string());
}
selected
}
}
_ => {
setup_reason = Some("sandbox setup marker missing or incompatible".to_string());
None
}
};
if identity.is_none() {
if let Some(reason) = &setup_reason {
crate::logging::log_note(
&format!("sandbox setup required: {reason}"),
Some(&sandbox_dir),
);
} else {
crate::logging::log_note("sandbox setup required", Some(&sandbox_dir));
}
run_elevated_setup(
crate::setup::SandboxSetupRequest {
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
proxy_enforced,
},
crate::setup::SetupRootOverrides {
read_roots: Some(needed_read.clone()),
read_roots_include_platform_defaults,
write_roots: Some(needed_write.clone()),
deny_write_paths: Some(deny_write_paths_override.to_vec()),
},
)?;
identity = select_identity(network_identity, codex_home)?;
}
// Always refresh ACLs (non-elevated) for current roots via the setup binary.
run_setup_refresh_with_overrides(
crate::setup::SandboxSetupRequest {
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
proxy_enforced,
},
crate::setup::SetupRootOverrides {
read_roots: Some(needed_read),
read_roots_include_platform_defaults,
write_roots: Some(needed_write),
deny_write_paths: Some(deny_write_paths_override.to_vec()),
},
)?;
let identity = identity.ok_or_else(|| {
anyhow!(
"Windows sandbox setup is missing or out of date; rerun the sandbox setup with elevation"
)
})?;
Ok(SandboxCreds {
username: identity.username,
password: identity.password,
})
}