mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Honor environment-resolved workspace roots (#40912)
## What changed - Add effective workspace roots to `EnvironmentConfig` and use them when materializing permission profiles and filesystem context. - Preserve selection roots for thread-owned configurations while allowing a ready environment attachment to supply its resolved roots. - Propagate resolved roots to child environments and validate both command and `apply_patch` writes in owner-provided secondary workspaces. GitOrigin-RevId: 1832c168ce1fc37fa67efc2a472998ab4f448c70
This commit is contained in:
@@ -455,10 +455,10 @@ impl AgentControl {
|
||||
let owner_environment = parent_environments
|
||||
.turn_environments()
|
||||
.find(|environment| {
|
||||
environment.selection.environment_id == *environment_id
|
||||
&& environment.cwd() == &selection.cwd
|
||||
&& environment.workspace_roots()
|
||||
== selection.workspace_roots.as_slice()
|
||||
let parent_selection = &environment.selection;
|
||||
parent_selection.environment_id == selection.environment_id
|
||||
&& parent_selection.cwd == selection.cwd
|
||||
&& parent_selection.workspace_roots == selection.workspace_roots
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
invalid_environment("no longer matches a ready parent environment")
|
||||
@@ -493,8 +493,8 @@ impl AgentControl {
|
||||
let cwd = selection.cwd.to_abs_path().map_err(|_| {
|
||||
invalid_environment("working directory is not a local absolute path")
|
||||
})?;
|
||||
let roots = selection
|
||||
.workspace_roots
|
||||
let roots = owner_environment
|
||||
.workspace_roots()
|
||||
.iter()
|
||||
.map(PathUri::to_abs_path)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
|
||||
@@ -368,6 +368,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
|
||||
@@ -34,13 +34,9 @@ impl FileSystemContext {
|
||||
permission_profile: &PermissionProfile,
|
||||
workspace_roots: &[PathUri],
|
||||
) -> Self {
|
||||
let materialized_workspace_roots = workspace_roots
|
||||
.iter()
|
||||
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
|
||||
.collect::<Vec<_>>();
|
||||
let permission_profile = permission_profile
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&materialized_workspace_roots);
|
||||
.materialize_project_roots_with_path_uris(workspace_roots);
|
||||
let workspace_roots = workspace_roots
|
||||
.iter()
|
||||
.map(PathUri::inferred_native_path_string)
|
||||
|
||||
@@ -168,7 +168,10 @@ fn resolve_selection_config(
|
||||
) -> (TurnEnvironmentSelection, EnvironmentConfigOrigin) {
|
||||
let (config, origin) = match selection.config {
|
||||
EnvironmentConfigState::FromThread => (
|
||||
EnvironmentConfigState::Ready(thread_config.clone()),
|
||||
EnvironmentConfigState::Ready(thread_config_for_selection(
|
||||
&selection.workspace_roots,
|
||||
thread_config,
|
||||
)),
|
||||
EnvironmentConfigOrigin::Thread,
|
||||
),
|
||||
config @ (EnvironmentConfigState::Ready(_)
|
||||
@@ -179,10 +182,23 @@ fn resolve_selection_config(
|
||||
(selection, origin)
|
||||
}
|
||||
|
||||
fn thread_config_for_selection(
|
||||
workspace_roots: &[PathUri],
|
||||
thread_config: &EnvironmentConfig,
|
||||
) -> EnvironmentConfig {
|
||||
EnvironmentConfig {
|
||||
workspace_roots: workspace_roots.to_vec(),
|
||||
..thread_config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTurnEnvironment {
|
||||
fn refresh_thread_config(&mut self, config: &EnvironmentConfig) {
|
||||
if self.config_origin == EnvironmentConfigOrigin::Thread {
|
||||
self.selection.config = EnvironmentConfigState::Ready(config.clone());
|
||||
self.selection.config = EnvironmentConfigState::Ready(thread_config_for_selection(
|
||||
&self.selection.workspace_roots,
|
||||
config,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -916,6 +932,7 @@ mod tests {
|
||||
fn test_environment_config() -> EnvironmentConfig {
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
@@ -1093,6 +1110,7 @@ url = "ws://127.0.0.1:8765"
|
||||
};
|
||||
let expected_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
@@ -1344,6 +1362,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let expected_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
@@ -1701,6 +1720,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.expect("replacement environment");
|
||||
let child_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
@@ -1764,6 +1784,7 @@ url = "ws://127.0.0.1:8765"
|
||||
resolve_turn_environments(Arc::clone(&manager), std::slice::from_ref(&selection)).await;
|
||||
let parent_owner_config = EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
|
||||
@@ -649,7 +649,7 @@ async fn build_guardian_prompt_includes_parent_turn_denied_reads() -> anyhow::Re
|
||||
};
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(environment_permission_profile);
|
||||
environment.selection.workspace_roots = vec![
|
||||
environment.config_mut().workspace_roots = vec![
|
||||
PathUri::from_abs_path(&workspace_root),
|
||||
PathUri::from_abs_path(&second_workspace_root),
|
||||
];
|
||||
|
||||
@@ -286,6 +286,7 @@ mod tests {
|
||||
};
|
||||
primary.selection.cwd = PathUri::from_abs_path(&cwd);
|
||||
primary.selection.workspace_roots.clear();
|
||||
primary.config_mut().workspace_roots.clear();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1178,6 +1178,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
.config
|
||||
|
||||
@@ -142,6 +142,7 @@ impl SessionConfiguration {
|
||||
pub(super) fn inferred_environment_config(&self) -> EnvironmentConfig {
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: self.allow_login_shell,
|
||||
workspace_roots: Vec::new(),
|
||||
permission_profile: self.permission_profile_state.snapshot(),
|
||||
shell_environment_policy: self.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
@@ -167,6 +168,21 @@ impl SessionConfiguration {
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots)
|
||||
}
|
||||
|
||||
fn effective_permission_profile(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
) -> PermissionProfile {
|
||||
ThreadEnvironments::primary_config_for(environments)
|
||||
.map(|config| {
|
||||
config
|
||||
.permission_profile
|
||||
.permission_profile()
|
||||
.clone()
|
||||
.materialize_project_roots_with_path_uris(&config.workspace_roots)
|
||||
})
|
||||
.unwrap_or_else(|| self.materialized_permission_profile(environments))
|
||||
}
|
||||
|
||||
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
|
||||
self.permission_profile_state.active_permission_profile()
|
||||
}
|
||||
@@ -227,10 +243,7 @@ impl SessionConfiguration {
|
||||
service_tier: self.step_settings.service_tier.clone(),
|
||||
approval_policy: self.step_settings.approval_policy.value(),
|
||||
approvals_reviewer: self.step_settings.approvals_reviewer,
|
||||
permission_profile: permission_profile
|
||||
.permission_profile()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots),
|
||||
permission_profile: self.effective_permission_profile(&environment_selections),
|
||||
active_permission_profile: permission_profile.active_permission_profile(),
|
||||
environments: TurnEnvironmentSelections::new(
|
||||
self.legacy_fallback_cwd.clone(),
|
||||
@@ -314,13 +327,7 @@ impl SessionConfiguration {
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
) -> StepSettingsConstraints<'_> {
|
||||
let permission_profile = ThreadEnvironments::primary_config_for(environments)
|
||||
.map(|config| config.permission_profile.permission_profile())
|
||||
.unwrap_or_else(|| self.permission_profile_state.permission_profile())
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(
|
||||
&ThreadEnvironments::primary_workspace_roots_for(environments),
|
||||
);
|
||||
let permission_profile = self.effective_permission_profile(environments);
|
||||
StepSettingsConstraints {
|
||||
requirements: self
|
||||
.original_config_do_not_use
|
||||
|
||||
@@ -9432,6 +9432,7 @@ async fn record_context_updates_use_environment_permission_profile_and_workspace
|
||||
let cwd = environment.cwd().clone();
|
||||
let workspace_root = current_context.config.cwd.join("selected-workspace");
|
||||
let mut environment_config = environment.config().clone();
|
||||
environment_config.workspace_roots = vec![PathUri::from_abs_path(&workspace_root)];
|
||||
environment_config.permission_profile =
|
||||
PermissionProfileSnapshot::legacy(PermissionProfile::workspace_write());
|
||||
current_context.environments.environments[0] =
|
||||
|
||||
@@ -104,7 +104,7 @@ impl TurnEnvironment {
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_roots(&self) -> &[PathUri] {
|
||||
&self.selection.workspace_roots
|
||||
&self.config().workspace_roots
|
||||
}
|
||||
|
||||
pub(crate) fn permission_profile(&self) -> &PermissionProfile {
|
||||
@@ -160,7 +160,7 @@ impl std::fmt::Debug for TurnEnvironment {
|
||||
.field("environment_id", &self.selection.environment_id)
|
||||
.field("environment", &self.environment)
|
||||
.field("cwd", &self.selection.cwd)
|
||||
.field("workspace_roots", &self.selection.workspace_roots)
|
||||
.field("workspace_roots", &self.config().workspace_roots)
|
||||
.field("temporary_directories", &self.temporary_directories)
|
||||
.field("shell", &self.shell)
|
||||
.field("config", self.config())
|
||||
|
||||
@@ -1062,6 +1062,7 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
||||
reviewer_environment.config =
|
||||
EnvironmentConfigState::Ready(codex_protocol::protocol::EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: reviewer_environment.workspace_roots.clone(),
|
||||
permission_profile: config.permissions.permission_profile_state().snapshot(),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
|
||||
|
||||
@@ -284,12 +284,13 @@ mod tests {
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
fn replace_primary_environment_cwd(turn: &mut crate::TurnContext, cwd: AbsolutePathBuf) {
|
||||
let current = turn
|
||||
let mut current = turn
|
||||
.environments
|
||||
.turn_environments()
|
||||
.next()
|
||||
.cloned()
|
||||
.expect("default local turn environment");
|
||||
current.config_mut().workspace_roots.clear();
|
||||
let mut selection = current.selection;
|
||||
selection.cwd = PathUri::from_abs_path(&cwd);
|
||||
selection.workspace_roots.clear();
|
||||
|
||||
@@ -27,6 +27,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
|
||||
@@ -553,6 +553,7 @@ mod tests {
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: true,
|
||||
use_legacy_landlock: false,
|
||||
|
||||
@@ -637,7 +637,7 @@ async fn internal_guardian_sessions_require_managed_secondary_environments() {
|
||||
else {
|
||||
panic!("secondary environment should be ready");
|
||||
};
|
||||
secondary.selection.workspace_roots = vec![secondary_workspace_root];
|
||||
secondary.config_mut().workspace_roots = vec![secondary_workspace_root];
|
||||
secondary.config_mut().permission_profile =
|
||||
codex_protocol::models::PermissionProfileSnapshot::legacy(secondary_profile);
|
||||
let turn = Arc::new(turn);
|
||||
@@ -1086,6 +1086,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
||||
config: EnvironmentConfigState::Ready(
|
||||
codex_protocol::protocol::EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: Vec::new(),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn
|
||||
.config
|
||||
|
||||
@@ -310,8 +310,22 @@ async fn v2_residency_reload_preserves_inherited_environment_and_tools(
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
let mut owner_workspace_roots = child_environment.workspace_roots.clone();
|
||||
let owner_workspace_root = test.workspace_path_uri("owner-only-root")?;
|
||||
test.fs()
|
||||
.create_directory(
|
||||
&owner_workspace_root,
|
||||
CreateDirectoryOptions {
|
||||
recursive: true,
|
||||
follow_symlinks: true,
|
||||
},
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
owner_workspace_roots.push(owner_workspace_root);
|
||||
child_environment.config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: owner_workspace_roots,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(child_permissions),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
|
||||
@@ -716,6 +716,7 @@ async fn environment_command_restrictions_override_saved_prefix_approvals() -> R
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -745,6 +746,7 @@ async fn environment_command_restrictions_override_saved_prefix_approvals() -> R
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -853,6 +855,7 @@ async fn environment_command_policy_changes_invalidate_session_approvals() -> Re
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::Disabled,
|
||||
),
|
||||
|
||||
@@ -824,6 +824,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
|
||||
let mut parent_environments = local_selections(test.config.cwd.clone());
|
||||
let parent_environment_config = EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: parent_environments.environments[0].workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
|
||||
@@ -398,6 +398,7 @@ impl ExecutorStopHookFixture {
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
self.test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
|
||||
@@ -362,6 +362,7 @@ async fn skill_mcp_dependency_oauth_uses_configured_http_client() -> Result<()>
|
||||
let mut environments = local_selections(fixture.config.cwd.clone());
|
||||
environments.environments[0].config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: fixture.config.permissions.allow_login_shell,
|
||||
workspace_roots: environments.environments[0].workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&fixture.config),
|
||||
|
||||
@@ -489,6 +489,7 @@ async fn root_reconciliation_reuses_pending_apps_startup() -> Result<()> {
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
test.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
|
||||
@@ -213,6 +213,7 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() -
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
permission_profile: Some(PermissionProfile::workspace_write()),
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
test.config.cwd.clone(),
|
||||
vec![TurnEnvironmentSelection {
|
||||
@@ -241,6 +242,10 @@ async fn model_visible_environment_context_preserves_foreign_workspace_roots() -
|
||||
environment_context.contains("<workspace_roots><root>C:\\workspace</root>"),
|
||||
"foreign workspace root should remain visible to the model: {environment_context}"
|
||||
);
|
||||
assert!(
|
||||
environment_context.contains("<entry access=\"write\"><path>C:\\workspace</path>"),
|
||||
"foreign workspace root should retain its permissions: {environment_context}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2020,6 +2020,7 @@ async fn owner_network_policy_follows_the_selected_remote_command() -> Result<()
|
||||
proxy_config.set_allowed_domains(vec![allowed_domain.to_string()]);
|
||||
let owner_config = EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: remote.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(if restricted {
|
||||
PermissionProfile::workspace_write()
|
||||
} else {
|
||||
@@ -2042,6 +2043,7 @@ async fn owner_network_policy_follows_the_selected_remote_command() -> Result<()
|
||||
};
|
||||
let mut primary = local(test.cwd.path().abs());
|
||||
primary.config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
workspace_roots: primary.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
network_policy: None,
|
||||
..owner_config.clone()
|
||||
|
||||
@@ -557,6 +557,7 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
|
||||
vec![TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: owner_permission_profile,
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1322,6 +1323,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
EnvironmentConfigState::Pending,
|
||||
EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1366,6 +1368,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
environments: Some(vec![TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1394,6 +1397,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
vec![TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1461,6 +1465,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
||||
vec![TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: false,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1536,6 +1541,7 @@ async fn owner_network_policy_rejects_unsupported_environment_authority() -> Res
|
||||
.context("thread should select its executor environment")?;
|
||||
let owner_config = EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: test.config.permissions.shell_environment_policy.clone(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1614,6 +1620,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
config: EnvironmentConfigState::Pending,
|
||||
..selection.clone()
|
||||
};
|
||||
let owner_workspace_root = selection.cwd.join("owner-workspace")?;
|
||||
let root = |id: &str| SelectedCapabilityRoot {
|
||||
id: id.to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
@@ -1623,6 +1630,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
};
|
||||
let owner_config = |id: &str, allow_login_shell: bool| EnvironmentConfig {
|
||||
allow_login_shell,
|
||||
workspace_roots: vec![selection.cwd.clone(), owner_workspace_root.clone()],
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
@@ -1642,6 +1650,7 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
let waiting = timeout(Duration::from_secs(5), start_pending_thread())
|
||||
.await
|
||||
.context("pending thread startup should not block")??;
|
||||
let requested_workspace_roots = waiting.thread.config_snapshot().await.workspace_roots;
|
||||
let independent = start_pending_thread().await?;
|
||||
let failed = start_pending_thread().await?;
|
||||
|
||||
@@ -1757,22 +1766,19 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
config: EnvironmentConfigState::Ready(waiting_config.clone()),
|
||||
..pending_selection.clone()
|
||||
};
|
||||
submit_thread_settings(
|
||||
&waiting.thread,
|
||||
ThreadSettingsOverrides {
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
test.config.cwd.clone(),
|
||||
vec![ready_selection.clone()],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
waiting
|
||||
.thread
|
||||
.environment_ready(&pending_selection, waiting_config)
|
||||
.await?;
|
||||
wait_for_response_request_count(&response_mock, /*expected_count*/ 2).await;
|
||||
assert_eq!(
|
||||
waiting.thread.environment_selections().await,
|
||||
vec![ready_selection]
|
||||
);
|
||||
assert_eq!(
|
||||
waiting.thread.config_snapshot().await.workspace_roots,
|
||||
requested_workspace_roots
|
||||
);
|
||||
|
||||
let ready_request = response_mock
|
||||
.last_request()
|
||||
@@ -1800,6 +1806,13 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
.rfind(|text| text.contains("<ready_capability_roots>")),
|
||||
Some("<ready_capability_roots>waiting-root</ready_capability_roots>".to_string())
|
||||
);
|
||||
assert!(
|
||||
ready_request
|
||||
.message_input_texts("user")
|
||||
.iter()
|
||||
.any(|text| text.contains(&owner_workspace_root.inferred_native_path_string())),
|
||||
"waiting turn should observe owner-resolved workspace roots"
|
||||
);
|
||||
|
||||
let recovered_selection = TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(owner_config(
|
||||
@@ -2276,6 +2289,7 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||
PermissionProfile::read_only(),
|
||||
owner_active_profile.clone(),
|
||||
|
||||
@@ -801,6 +801,7 @@ async fn environment_mcp_policy_filters_runtime_config_and_model_tools(
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
fixture.config.permissions.permission_profile().clone(),
|
||||
),
|
||||
@@ -1743,14 +1744,11 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
|
||||
.into_iter()
|
||||
.find(|selection| selection.environment_id == remote_aware_environment_id())
|
||||
.context("thread should select the MCP server's executor environment")?;
|
||||
let selection = TurnEnvironmentSelection {
|
||||
workspace_roots: vec![PathUri::parse(if cfg!(windows) {
|
||||
"file:///foreign/workspace"
|
||||
} else {
|
||||
"file:///C:/workspace"
|
||||
})?],
|
||||
..selection
|
||||
};
|
||||
let workspace_roots = vec![PathUri::parse(if cfg!(windows) {
|
||||
"file:///foreign/workspace"
|
||||
} else {
|
||||
"file:///C:/workspace"
|
||||
})?];
|
||||
submit_thread_settings(
|
||||
&fixture.codex,
|
||||
ThreadSettingsOverrides {
|
||||
@@ -1765,13 +1763,13 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta(
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let workspace_roots = selection.workspace_roots.clone();
|
||||
fixture
|
||||
.codex
|
||||
.environment_ready(
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: fixture.config.permissions.allow_login_shell,
|
||||
workspace_roots: workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
owner_permission_profile.clone(),
|
||||
),
|
||||
|
||||
@@ -391,6 +391,7 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: selection.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::Disabled),
|
||||
shell_environment_policy: ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_core::EnvironmentConfig;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::RemoveOptions;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::PermissionProfileSnapshot;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -23,6 +27,7 @@ use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_target_os;
|
||||
use serde_json::json;
|
||||
use test_case::test_case;
|
||||
use wiremock::MockServer;
|
||||
|
||||
const PATCH_CALL_ID: &str = "workspace-root-patch";
|
||||
@@ -194,9 +199,14 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
|
||||
remove_files(&test, &[&patch_path, &command_path]).await
|
||||
}
|
||||
|
||||
#[test_case(false; "thread-owned secondary root")]
|
||||
#[test_case(true; "owner-resolved secondary root")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn workspace_roots_allow_apply_patch_in_secondary_root() -> Result<()> {
|
||||
async fn workspace_roots_allow_file_and_command_writes_in_secondary_root(
|
||||
owner_resolved_roots: bool,
|
||||
) -> Result<()> {
|
||||
const SECONDARY_ROOT_NAME: &str = "secondary-workspace-root";
|
||||
const COMMAND_CONTENTS: &str = "secondary root command";
|
||||
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
@@ -205,7 +215,7 @@ async fn workspace_roots_allow_apply_patch_in_secondary_root() -> Result<()> {
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex()
|
||||
.with_config(|config| {
|
||||
.with_config(move |config| {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
config.cwd = dunce::canonicalize(config.cwd.as_path())
|
||||
@@ -220,7 +230,10 @@ async fn workspace_roots_allow_apply_patch_in_secondary_root() -> Result<()> {
|
||||
&config.cwd,
|
||||
SECONDARY_ROOT_NAME,
|
||||
));
|
||||
config.workspace_roots = vec![config.cwd.clone(), secondary_root];
|
||||
config.workspace_roots = vec![config.cwd.clone()];
|
||||
if !owner_resolved_roots {
|
||||
config.workspace_roots.push(secondary_root);
|
||||
}
|
||||
config.set_windows_sandbox_enabled(/*value*/ true);
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
@@ -245,31 +258,54 @@ async fn workspace_roots_allow_apply_patch_in_secondary_root() -> Result<()> {
|
||||
.parent()
|
||||
.context("workspace should have a parent")?
|
||||
.join(&secondary_root_name)?;
|
||||
if owner_resolved_roots {
|
||||
let selection = test
|
||||
.codex
|
||||
.environment_selections()
|
||||
.await
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("thread should select its executor environment")?;
|
||||
test.codex
|
||||
.environment_ready(
|
||||
&selection,
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
workspace_roots: vec![selection.cwd.clone(), secondary_root.clone()],
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
workspace_roots_profile(),
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&test.config),
|
||||
windows_sandbox_private_desktop: test
|
||||
.config
|
||||
.permissions
|
||||
.windows_sandbox_private_desktop,
|
||||
use_legacy_landlock: test.config.features.use_legacy_landlock(),
|
||||
exec_policy: None,
|
||||
mcp_policy: None,
|
||||
network_policy: None,
|
||||
selected_capability_roots: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let patch = format!(
|
||||
"*** Begin Patch\n*** Add File: ../{secondary_root_name}/secondary-root-patch.txt\n+secondary root\n*** End Patch\n"
|
||||
);
|
||||
let response_mock = mount_sse_sequence(
|
||||
let response_mock = mount_patch_and_command_calls(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_custom_tool_call(PATCH_CALL_ID, &patch),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
],
|
||||
&patch,
|
||||
&format!("../{secondary_root_name}/secondary-root-command.txt"),
|
||||
COMMAND_CONTENTS,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
submit_workspace_turn(&test, "write in the secondary workspace root").await?;
|
||||
|
||||
let request = response_mock
|
||||
.last_request()
|
||||
.context("model should receive the apply_patch result")?;
|
||||
.context("model should receive the apply_patch and command results")?;
|
||||
let (patch_output, patch_success) = request
|
||||
.custom_tool_call_output_content_and_success(PATCH_CALL_ID)
|
||||
.context("patch result should be present")?;
|
||||
@@ -282,6 +318,15 @@ async fn workspace_roots_allow_apply_patch_in_secondary_root() -> Result<()> {
|
||||
);
|
||||
let patched_file = secondary_root.join("secondary-root-patch.txt")?;
|
||||
assert_eq!(read_file(&test, &patched_file).await?, "secondary root\n");
|
||||
let (_, command_success) = request
|
||||
.function_call_output_content_and_success(COMMAND_CALL_ID)
|
||||
.context("command result should be present")?;
|
||||
assert_ne!(command_success, Some(false));
|
||||
let command_file = secondary_root.join("secondary-root-command.txt")?;
|
||||
assert_eq!(
|
||||
read_file(&test, &command_file).await?.trim_end(),
|
||||
COMMAND_CONTENTS
|
||||
);
|
||||
|
||||
test.fs()
|
||||
.remove(
|
||||
|
||||
@@ -153,6 +153,7 @@ async fn read_only_permissions_preserve_parent_environments_and_denied_reads() -
|
||||
.expect("parent should have an execution environment");
|
||||
environment.config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: true,
|
||||
workspace_roots: environment.workspace_roots.clone(),
|
||||
permission_profile: PermissionProfileSnapshot::legacy(parent_profile.clone()),
|
||||
shell_environment_policy: Default::default(),
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(&parent_config),
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::mcp_policy::EnvironmentMcpPolicy;
|
||||
use crate::models::PermissionProfileSnapshot;
|
||||
use codex_execpolicy::RequirementsExecPolicy;
|
||||
use codex_network_proxy::EnvironmentNetworkPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
/// Configuration supplied for a thread's selected environment.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -25,6 +26,8 @@ pub enum EnvironmentConfigState {
|
||||
pub struct EnvironmentConfig {
|
||||
/// Whether shell tools may start login shells in this environment.
|
||||
pub allow_login_shell: bool,
|
||||
/// Effective workspace roots resolved for this environment attachment.
|
||||
pub workspace_roots: Vec<PathUri>,
|
||||
/// Resolved permissions for this thread's environment attachment.
|
||||
pub permission_profile: PermissionProfileSnapshot,
|
||||
/// Controls which environment variables shell commands may inherit.
|
||||
@@ -50,6 +53,7 @@ impl std::fmt::Debug for EnvironmentConfig {
|
||||
formatter
|
||||
.debug_struct("EnvironmentConfig")
|
||||
.field("allow_login_shell", &self.allow_login_shell)
|
||||
.field("workspace_roots", &self.workspace_roots)
|
||||
.field("permission_profile", &self.permission_profile)
|
||||
.field("shell_environment_policy", &"<redacted>")
|
||||
.field("windows_sandbox_level", &self.windows_sandbox_level)
|
||||
|
||||
Reference in New Issue
Block a user