Read turn permissions from the current configuration (#36930)

## What changed

- Remove the cached permission profile from `TurnContext` and derive the
  effective profile, filesystem policy, network policy, and legacy sandbox
  policy from `config.permissions`.
- Update sandbox consumers and tests to use the current turn configuration.
- Verify that role-based agent spawning reapplies runtime permissions and that
  cold-resumed agents retain their disabled permission profile.

GitOrigin-RevId: d67c5d2bcbe6dc76f15b56defb485b155fd1f138
This commit is contained in:
jif
2026-08-04 17:13:13 +00:00
committed by copyberry
parent 7ada37a15e
commit 90314a9207
14 changed files with 67 additions and 40 deletions
+1 -1
View File
@@ -244,7 +244,7 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
fn parent_turn_denied_reads_context(turn: &TurnContext) -> Option<String> {
#[allow(deprecated)]
let cwd = &turn.cwd;
let file_system_policy = turn.permission_profile.file_system_sandbox_policy();
let file_system_policy = turn.file_system_sandbox_policy();
let mut entries = file_system_policy
.get_unreadable_roots_with_cwd(cwd)
.into_iter()
+5 -1
View File
@@ -467,7 +467,7 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re
session.thread_id = fixed_guardian_parent_session_id();
let denied_root = test_path_buf("/repo/private").abs();
let denied_glob = test_path_buf("/repo/private/**").display().to_string();
turn.permission_profile = PermissionProfile::from_runtime_permissions(
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -493,6 +493,10 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re
]),
NetworkSandboxPolicy::Restricted,
);
Arc::make_mut(&mut turn.config)
.permissions
.set_permission_profile(permission_profile)
.expect("test setup should allow updating permission profile");
let session = Arc::new(session);
let turn = Arc::new(turn);
seed_guardian_parent_history(&session, &turn).await;
-1
View File
@@ -229,7 +229,6 @@ mod tests {
fn set_primary_environment_cwd(turn_context: &mut TurnContext, cwd: &Path) {
let cwd = AbsolutePathBuf::try_from(cwd).expect("absolute path");
turn_context.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
let TurnEnvironmentState::Ready(primary) = &mut turn_context.environments.environments[0]
else {
panic!("expected ready primary environment");
+5 -2
View File
@@ -87,7 +87,7 @@ fn approval_metadata(
fn approval_config(turn_context: &TurnContext) -> codex_mcp::McpConfig {
let mut config = (*mcp_config_for_test(&turn_context.config)).clone();
config.permission_profile = turn_context.permission_profile.clone();
config.permission_profile = turn_context.permission_profile();
config
}
@@ -2814,7 +2814,10 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
.approval_policy
.set(AskForApproval::Never)
.expect("test setup should allow updating approval policy");
turn_context.permission_profile = PermissionProfile::Disabled;
Arc::make_mut(&mut turn_context.config)
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("test setup should allow updating permission profile");
let session = Arc::new(session);
let turn_context = Arc::new(turn_context);
+1 -2
View File
@@ -95,7 +95,7 @@ pub(super) async fn spawn_review_thread(
review_turn_id.clone(),
#[allow(deprecated)]
parent_turn_context.cwd.clone(),
&parent_turn_context.permission_profile,
&parent_turn_context.permission_profile(),
parent_turn_context.windows_sandbox_level,
parent_turn_context.network.is_some(),
));
@@ -134,7 +134,6 @@ pub(super) async fn spawn_review_thread(
.clone(),
multi_agent_version: MultiAgentVersion::Disabled,
personality: parent_turn_context.personality,
permission_profile: parent_turn_context.permission_profile(),
network: parent_turn_context.network.clone(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
#[allow(deprecated)]
+16 -5
View File
@@ -9246,11 +9246,15 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy()
async fn turn_context_item_stores_split_file_system_sandbox_policy_when_different() {
let (_session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context);
turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile.enforcement(),
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile().enforcement(),
&file_system_sandbox_policy,
turn_context.network_sandbox_policy(),
);
Arc::make_mut(&mut turn_context.config)
.permissions
.set_permission_profile(permission_profile)
.expect("test setup should allow updating permission profile");
let item = turn_context.to_turn_context_item();
@@ -9409,11 +9413,15 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
{
let (mut session, mut turn_context) = make_session_and_context().await;
let file_system_sandbox_policy = file_system_policy_with_unreadable_glob(&turn_context);
turn_context.permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile.enforcement(),
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
turn_context.permission_profile().enforcement(),
&file_system_sandbox_policy,
turn_context.network_sandbox_policy(),
);
Arc::make_mut(&mut turn_context.config)
.permissions
.set_permission_profile(permission_profile)
.expect("test setup should allow updating permission profile");
let rollout_path = attach_thread_persistence(&mut session).await;
let turn_context = Arc::new(turn_context);
@@ -11219,7 +11227,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
// command. Force DangerFullAccess so this check stays focused on approval
// policy rather than platform-specific sandbox behavior.
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique thread settings Arc");
turn_context_mut.permission_profile = PermissionProfile::Disabled;
Arc::make_mut(&mut turn_context_mut.config)
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("test setup should allow updating permission profile");
let command = session.user_shell().derive_exec_args(
command_script,
@@ -304,7 +304,6 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
.features
.enable(Feature::ExecPermissionApprovals)
.expect("test setup should allow enabling request permissions");
turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
let mut config = (*turn_context_raw.config).clone();
config
.permissions
@@ -420,7 +419,6 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
.approval_policy
.set(AskForApproval::Never)
.expect("test setup should allow updating approval policy");
turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
let mut config = (*turn_context_raw.config).clone();
config
.permissions
+5 -12
View File
@@ -15,7 +15,6 @@ use codex_protocol::protocol::ErrorEvent;
use codex_protocol::protocol::MultiAgentVersion;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_utils_path_uri::PathUri;
use futures::FutureExt;
@@ -136,7 +135,6 @@ pub struct TurnContext {
pub(crate) collaboration_mode_developer_instructions: Option<String>,
pub(crate) multi_agent_version: MultiAgentVersion,
pub(crate) personality: Option<Personality>,
pub(crate) permission_profile: PermissionProfile,
pub(crate) network: Option<NetworkProxy>,
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) available_models: Vec<ModelPreset>,
@@ -190,23 +188,20 @@ impl TurnContext {
}
pub(crate) fn permission_profile(&self) -> PermissionProfile {
self.permission_profile.clone()
self.config.permissions.effective_permission_profile()
}
pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.file_system_sandbox_policy()
self.config.permissions.file_system_sandbox_policy()
}
pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.permission_profile.network_sandbox_policy()
self.config.permissions.network_sandbox_policy()
}
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
compatibility_sandbox_policy_for_permission_profile(
&self.permission_profile,
#[allow(deprecated)]
&self.cwd,
)
#[allow(deprecated)]
self.config.permissions.legacy_sandbox_policy(&self.cwd)
}
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
@@ -313,7 +308,6 @@ impl TurnContext {
.clone(),
multi_agent_version: self.multi_agent_version,
personality: self.personality,
permission_profile: self.permission_profile.clone(),
network: self.network.clone(),
windows_sandbox_level: self.windows_sandbox_level,
available_models,
@@ -581,7 +575,6 @@ impl Session {
.clone(),
multi_agent_version,
personality: session_configuration.personality,
permission_profile,
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
available_models,
@@ -2262,12 +2262,20 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
.expect("approval policy should be set");
let mut config = (*turn.config).clone();
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config
.permissions
.set_permission_profile(expected_permission_profile.clone())
.expect("test setup should allow updating permission profile");
set_turn_config(&mut turn, config);
turn.permission_profile = expected_permission_profile.clone();
let role_name = install_role_with_model_override(&mut turn).await;
let mut role_config = (*turn.config).clone();
crate::agent::role::apply_role_to_config(&mut role_config, Some(role_name.as_str()))
.await
.expect("non-empty role config should apply");
assert_ne!(
role_config.permissions.effective_permission_profile(),
expected_permission_profile,
turn.config.permissions.effective_permission_profile(),
"test requires a runtime profile override that differs from base config"
"role config must discard the runtime permission override before it is reapplied"
);
let invocation = invocation(
@@ -2276,7 +2284,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
"spawn_agent",
function_payload(json!({
"message": "await this command",
"agent_type": "explorer"
"agent_type": role_name
})),
);
let output = SpawnAgentHandler::default()
@@ -4438,7 +4446,10 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
&file_system_sandbox_policy,
network_sandbox_policy,
);
turn.permission_profile = permission_profile.clone();
Arc::make_mut(&mut turn.config)
.permissions
.set_permission_profile(permission_profile)
.expect("permission profile set");
Arc::make_mut(&mut turn.config)
.permissions
.approval_policy
@@ -4464,7 +4475,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
.expect("approval policy set");
expected
.permissions
.set_permission_profile(permission_profile)
.set_permission_profile(turn.permission_profile())
.expect("permission profile set");
assert_eq!(config, expected);
}
@@ -156,7 +156,7 @@ impl ExecCommandHandler {
// permissions config below. Consult the configured platform-sandbox requirement before
// deciding whether parsing may continue without that base path.
let sandbox = SandboxManager::new().select_initial(
&turn.permission_profile,
&turn.permission_profile(),
SandboxablePreference::Auto,
turn.windows_sandbox_level,
turn.network.is_some(),
@@ -316,7 +316,10 @@ mod tests {
replace_primary_environment_cwd(&mut turn, image_cwd.clone());
let image_path = image_cwd.join("image.png");
std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
turn.permission_profile = PermissionProfile::read_only();
Arc::make_mut(&mut turn.config)
.permissions
.set_permission_profile(PermissionProfile::read_only())
.expect("test setup should allow updating permission profile");
let turn = Arc::new(turn);
let result = ViewImageHandler::default()
+3 -2
View File
@@ -443,11 +443,12 @@ impl ToolRegistry {
let tool_name_flat = flat_tool_name(&tool_name);
let call_id_owned = invocation.call_id.clone();
let otel = invocation.turn.session_telemetry.clone();
let permission_profile = invocation.turn.permission_profile();
let base_tool_result_tags = [
(
"sandbox",
permission_profile_sandbox_tag(
&invocation.turn.permission_profile,
&permission_profile,
invocation.turn.windows_sandbox_level,
invocation.turn.network.is_some(),
),
@@ -455,7 +456,7 @@ impl ToolRegistry {
(
"sandbox_policy",
permission_profile_policy_tag(
&invocation.turn.permission_profile,
&permission_profile,
#[allow(deprecated)]
invocation.turn.cwd.as_path(),
),
@@ -550,10 +550,13 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
Arc::make_mut(&mut turn_context.config)
.permissions
.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
turn_context.permission_profile = PermissionProfile::from_runtime_permissions(
&read_only_file_system_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
);
Arc::make_mut(&mut turn_context.config)
.permissions
.set_permission_profile(PermissionProfile::from_runtime_permissions(
&read_only_file_system_sandbox_policy(),
NetworkSandboxPolicy::Restricted,
))
.expect("test setup should allow updating permission profile");
let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?;
let target = std::env::temp_dir().join("execve-hook-short-circuit.txt");
let target_str = target.display().to_string();
@@ -310,6 +310,7 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
assert!(initial_child_request.requests().iter().any(|request| {
request.body_contains_text(INITIAL_TASK)
&& request.body_contains_text(ROLE_DEVELOPER_INSTRUCTIONS)
&& request.body_contains_text("<permission_profile type=\"disabled\">")
&& !request.body_contains_text(SUBAGENT_DEVELOPER_INSTRUCTIONS)
}));
let initial_worker_config = worker_thread.config_snapshot().await;
@@ -492,6 +493,7 @@ async fn cold_root_resume_restores_agent_identity_and_role_on_followup() -> Resu
assert!(followup_child_request.requests().iter().any(|request| {
request.body_contains_text(FOLLOWUP_TASK)
&& request.body_contains_text(ROLE_DEVELOPER_INSTRUCTIONS)
&& request.body_contains_text("<permission_profile type=\"disabled\">")
&& !request.body_contains_text(SUBAGENT_DEVELOPER_INSTRUCTIONS)
}));
let requests = server