Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Use turn environment permissions for context and discovery (#37040)
## What changed

- Build filesystem and permission context from the primary turn environment's permission profile, working directory, and workspace roots, falling back to the thread context when no primary environment is available.
- Build capability-discovery sandbox contexts from each environment's permission profile and treat discovery as restricted when any selected environment has restricted filesystem access.

## Testing

- Add regression tests covering environment-specific capability discovery and permission context updates.

GitOrigin-RevId: 6bbbd9b88aafa5e6583971ce4e427fc7557f2d93
  • Loading branch information
sayan-oai authored and copyberry committed Aug 5, 2026
commit ed2f985a26eee9a59cde0fdefd20f69b45bc25f5
15 changes: 6 additions & 9 deletions codex-rs/core/src/context/world_state/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::context::environment_context::NetworkContext;
use crate::context::environment_context::push_xml_escaped_text;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::session::turn_context::TurnContext;
use crate::session::turn_context::TurnEnvironment;
use codex_utils_path_uri::PathUri;
use serde::Deserialize;
use serde::Serialize;
Expand All @@ -29,19 +28,17 @@ impl EnvironmentsState {
environments: &TurnEnvironmentSnapshot,
current_date: Option<String>,
) -> Self {
let workspace_roots = environments
.primary()
.map(TurnEnvironment::workspace_roots)
.unwrap_or_default();
Self {
environments: environment_states(environments),
current_date,
timezone: turn_context.timezone.clone(),
network: network_from_turn_context(turn_context),
filesystem: Some(FileSystemContext::from_permission_profile(
turn_context.config.permissions.permission_profile(),
workspace_roots,
)),
filesystem: environments.primary().map(|environment| {
FileSystemContext::from_permission_profile(
environment.permission_profile(),
environment.workspace_roots(),
)
}),
subagents: None,
}
}
Expand Down
28 changes: 23 additions & 5 deletions codex-rs/core/src/session/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,28 @@ impl Session {
environments: &TurnEnvironmentSnapshot,
windows_sandbox_level: WindowsSandboxLevel,
) -> Option<Arc<ExecutorCapabilityDiscoverySnapshot>> {
let restricted_file_system = !config
.permissions
.file_system_sandbox_policy()
.has_full_disk_read_access();
// Capability roots can currently be selected independently of turn environments, so a
// root may be ready when there is no primary `TurnEnvironment`. Keep using the thread
// policy in that case so restricted discovery fails closed below. Once every selected
// root belongs to a thread/environment attachment whose `EnvironmentConfig` is installed
// before the root becomes ready, discovery can use the root owner's policy and this
// fallback can be removed.
let restricted_file_system = environments.primary().map_or_else(
|| {
!config
.permissions
.file_system_sandbox_policy()
.has_full_disk_read_access()
},
|_| {
environments.turn_environments().any(|environment| {
!environment
.permission_profile()
.file_system_sandbox_policy()
.has_full_disk_read_access()
})
},
);
if !restricted_file_system
&& !config
.features
Expand All @@ -333,7 +351,7 @@ impl Session {
.turn_environments()
.map(|environment| {
let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
config.permissions.permission_profile().clone(),
environment.permission_profile().clone(),
environment.cwd().clone(),
);
sandbox.workspace_roots = environment.workspace_roots().to_vec();
Expand Down
110 changes: 110 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8591,6 +8591,52 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() {
});
}

#[tokio::test]
async fn capability_discovery_uses_environment_permission_profile() {
let (session, mut turn_context) = make_session_and_context().await;
Arc::make_mut(&mut turn_context.config)
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("unrestricted permission profile should be allowed");
let mut environment = turn_context
.environments
.primary()
.expect("primary environment")
.clone();
let mut file_system_policy = PermissionProfile::read_only().file_system_sandbox_policy();
file_system_policy.entries.push(FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::Deny,
missing_path_behavior: None,
});
environment.config.permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::from_runtime_permissions(
&file_system_policy,
NetworkSandboxPolicy::Restricted,
));
let expected_sandbox = turn_context
.file_system_sandbox_context(/*additional_permissions*/ None, &environment);
let environment_id = environment.environment_id.clone();
turn_context.environments.environments[0] = TurnEnvironmentState::Ready(environment);

let discovery = session
.executor_capability_discovery_for_step(
&turn_context.config,
/*ready_selected_capability_roots*/ &[],
&turn_context.environments,
turn_context.windows_sandbox_level,
)
.await
.expect("restricted environment should trigger capability discovery");

assert_eq!(
discovery.sandbox_contexts().get(&environment_id),
Some(&expected_sandbox)
);
}

#[tokio::test]
async fn step_context_keeps_its_mcp_runtime_for_tools() -> anyhow::Result<()> {
let (session, turn_context) = make_session_and_context().await;
Expand Down Expand Up @@ -8786,6 +8832,70 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
assert!(!environment_update.contains("<environments>"));
}

#[tokio::test]
async fn record_context_updates_use_environment_permission_profile_and_workspace_roots() {
let (session, mut previous_context) = make_session_and_context().await;
Arc::make_mut(&mut previous_context.config)
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("unrestricted permission profile should be allowed");
let mut previous_environment = previous_context
.environments
.primary()
.expect("primary environment")
.clone();
previous_environment.config.permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::Disabled);
previous_context.environments.environments[0] =
TurnEnvironmentState::Ready(previous_environment);
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
let environment = current_context
.environments
.primary()
.expect("primary environment")
.clone();
let cwd = environment.cwd().clone();
let workspace_root = current_context.config.cwd.join("selected-workspace");
let mut environment_config = environment.config;
environment_config.permission_profile =
PermissionProfileSnapshot::legacy(PermissionProfile::workspace_write());
current_context.environments.environments[0] =
TurnEnvironmentState::Ready(TurnEnvironment::new(
environment.environment_id,
environment.environment,
cwd,
vec![PathUri::from_abs_path(&workspace_root)],
environment.shell,
environment_config,
));

let update_items =
record_context_update_items(&session, previous_context, current_context).await;
let permissions_update = developer_input_texts(&update_items)
.into_iter()
.find(|text| text.contains("<permissions instructions>"))
.expect("permissions update should be emitted");
assert!(
permissions_update.contains(workspace_root.to_string_lossy().as_ref()),
"selected workspace root should be visible in permissions: {permissions_update}"
);
let environment_update = user_input_texts(&update_items)
.into_iter()
.find(|text| text.contains("<environment_context>"))
.expect("environment update should be emitted");
assert!(
environment_update.contains("<permission_profile type=\"managed\">")
&& environment_update.contains(workspace_root.to_string_lossy().as_ref()),
"selected environment permissions should be visible: {environment_update}"
);
}

#[tokio::test]
async fn record_context_updates_emits_environment_item_for_time_changes() {
let (session, previous_context) = make_session_and_context().await;
Expand Down
22 changes: 19 additions & 3 deletions codex-rs/core/src/session/world_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,24 @@ impl Session {
));
world_state.add_section(AgentsMdState::new(step_context.loaded_agents_md.as_deref()));
if turn_context.config.include_permissions_instructions {
let permission_profile = turn_context.permission_profile();
let environment = step_context.environments.primary();
let permission_profile = environment
.map(|environment| {
let workspace_roots = environment
.workspace_roots()
.iter()
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
.collect::<Vec<_>>();
environment
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&workspace_roots)
})
.unwrap_or_else(|| turn_context.permission_profile());
#[allow(deprecated)]
let cwd = environment
.and_then(|environment| environment.cwd().to_abs_path().ok())
.unwrap_or_else(|| turn_context.cwd.clone());
let model_messages = turn_context.model_info.model_messages.as_ref();
let exec_policy = self.services.exec_policy.current();
world_state.add_section(PermissionsState::new(
Expand All @@ -126,8 +143,7 @@ impl Session {
model_messages.and_then(|messages| messages.permissions.as_ref()),
),
exec_policy.as_ref(),
#[allow(deprecated)]
&turn_context.cwd,
&cwd,
turn_context
.config
.features
Expand Down
Loading