Move workspace roots onto thread/session state and stop using active permission profile modifications as an overlay for writable roots. Existing app-server threads now preserve their persisted PermissionProfile value across resume, fork, and turn updates; permissions requests on existing threads only update the active named profile after validating it exists. Workspace roots can be updated independently, and SandboxPolicy::WorkspaceWrite no longer stores its own writable_roots.

This commit is contained in:
Michael Bolin
2026-05-11 15:23:16 -07:00
parent 3e10e09e24
commit 6579ec2f9d
141 changed files with 2384 additions and 2210 deletions
+2
View File
@@ -519,6 +519,7 @@ impl TestCodexBuilder {
load_default_config_for_test(home).await
};
config.cwd = cwd_override;
config.workspace_roots = vec![config.cwd.clone()];
config.model_provider = model_provider;
if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") {
config.codex_self_exe = Some(path);
@@ -543,6 +544,7 @@ impl TestCodexBuilder {
for mutator in mutators {
mutator(&mut config);
}
config.workspace_roots = vec![config.cwd.clone()];
ensure_test_model_catalog(&mut config)?;
if config.include_apply_patch_tool {
-4
View File
@@ -827,7 +827,6 @@ fn scenarios() -> Vec<ScenarioSpec> {
use AskForApproval::*;
let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2047,7 +2046,6 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
let server = start_mock_server().await;
let approval_policy = AskForApproval::OnRequest;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2803,7 +2801,6 @@ allow_local_binding = true
)?;
let approval_policy = AskForApproval::OnFailure;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -3083,7 +3080,6 @@ allow_local_binding = true
)?;
let approval_policy = AskForApproval::OnFailure;
let turn_sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
+1
View File
@@ -53,6 +53,7 @@ where
params,
&PermissionProfile::read_only(),
&cwd,
&[],
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
@@ -575,9 +575,13 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let permissions = permissions_texts(&req.single_request());
let normalize_line_endings = |s: &str| s.replace("\r\n", "\n");
let exec_policy = load_exec_policy(&test.config.config_layer_stack).await?;
let sandbox_policy = test.config.legacy_sandbox_policy();
let expected = PermissionsInstructions::from_policy(
&sandbox_policy,
let permission_profile = test
.config
.permissions
.permission_profile()
.materialize_project_roots_with_workspace_roots(&test.config.workspace_roots);
let expected = PermissionsInstructions::from_permission_profile(
&permission_profile,
AskForApproval::OnRequest,
test.config.approvals_reviewer,
&exec_policy,
@@ -63,6 +63,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
forked_from_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
workspace_roots: Vec::new(),
originator: "test_originator".to_string(),
cli_version: "test_version".to_string(),
source: SessionSource::Cli,
@@ -109,6 +110,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
forked_from_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
workspace_roots: Vec::new(),
originator: "test_originator".to_string(),
cli_version: "test_version".to_string(),
source: SessionSource::Cli,
+6 -3
View File
@@ -436,9 +436,12 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(config.cwd.as_path())
.expect("workspace profile should have legacy projection");
let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
&permission_profile.file_system_sandbox_policy(),
permission_profile.network_sandbox_policy(),
config.cwd.as_path(),
);
codex
.submit(Op::OverrideTurnContext {
cwd: None,
@@ -285,7 +285,6 @@ async fn expect_request_permissions_event(
fn workspace_write_excluding_tmp() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -29,11 +29,13 @@ fn resume_history(
turn_id: Some(turn_id.clone()),
trace_id: None,
cwd: config.cwd.to_path_buf(),
workspace_roots: config.workspace_roots.clone(),
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.legacy_sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -141,6 +141,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
forked_from_id: None,
timestamp: "2026-01-27T12:00:00Z".to_string(),
cwd: codex_home.to_path_buf(),
workspace_roots: Vec::new(),
originator: "test".to_string(),
cli_version: "test".to_string(),
source: SessionSource::default(),