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
This commit is contained in:
sayan-oai
2026-08-05 06:43:23 +00:00
committed by copyberry
parent bac3ef1d8e
commit ed2f985a26
4 changed files with 158 additions and 17 deletions
@@ -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;
@@ -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,
}
}
+23 -5
View File
@@ -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
@@ -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();
+110
View File
@@ -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;
@@ -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;
+19 -3
View File
@@ -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(
@@ -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