Scope runtime workspace roots to execution environments (#32952)

## Why

Multi-environment turns can run against different filesystems, so a single
thread-wide workspace-root list cannot accurately describe every selected
environment.

## What changed

- Add optional `runtimeWorkspaceRoots` to each app-server environment selection.
  Omitted roots default to that environment's `cwd`, while an empty list selects
  no roots.
- Carry environment-native roots through turn selection, model-visible context,
  permission materialization, and tool sandboxing.
- Keep the top-level `runtimeWorkspaceRoots` field as a compatibility input when
  constructing default environment selections; explicit selections use their
  own roots.

## Testing

- Cover thread and turn API defaults, overrides, empty roots, and schema
  round-tripping.
- Verify foreign roots remain model-visible and remote execution applies the
  target environment's roots before sandbox selection.

GitOrigin-RevId: c6e7a62883c45c676c153055f46a55ee15a323a4
This commit is contained in:
pakrym-oai
2026-07-14 04:41:08 +00:00
committed by copyberry
parent b019a5577c
commit 393f64565a
62 changed files with 769 additions and 446 deletions
+1
View File
@@ -334,6 +334,7 @@ fn resolved_local_environments<const N: usize>(
.expect("local environment"),
),
PathUri::from_abs_path(&cwd),
Vec::new(),
/*shell*/ None,
)
})
-3
View File
@@ -143,7 +143,6 @@ impl ThreadConfigSnapshot {
#[derive(Clone, Default)]
pub struct CodexThreadSettingsOverrides {
pub environments: Option<TurnEnvironmentSelections>,
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
@@ -369,7 +368,6 @@ impl CodexThread {
) -> SessionSettingsUpdate {
let CodexThreadSettingsOverrides {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -396,7 +394,6 @@ impl CodexThread {
SessionSettingsUpdate {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -4,7 +4,7 @@ use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use std::collections::HashSet;
use std::path::PathBuf;
@@ -33,14 +33,18 @@ enum ManagedFileSystemContext {
impl FileSystemContext {
pub(super) fn from_permission_profile(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
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(workspace_roots);
.materialize_project_roots_with_workspace_roots(&materialized_workspace_roots);
let workspace_roots = workspace_roots
.iter()
.map(|root| root.to_string_lossy().into_owned())
.map(PathUri::inferred_native_path_string)
.collect();
let permission_profile = match permission_profile {
PermissionProfile::Managed { file_system, .. } => {
@@ -6,6 +6,7 @@ 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;
@@ -27,14 +28,18 @@ impl EnvironmentsState {
turn_context: &TurnContext,
environments: &TurnEnvironmentSnapshot,
) -> Self {
let workspace_roots = environments
.primary()
.map(TurnEnvironment::workspace_roots)
.unwrap_or_default();
Self {
environments: environment_states(environments),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
network: network_from_turn_context(turn_context),
filesystem: Some(FileSystemContext::from_permission_profile(
&turn_context.permission_profile,
&turn_context.config.effective_workspace_roots(),
turn_context.config.permissions.permission_profile(),
workspace_roots,
)),
subagents: None,
}
@@ -86,7 +86,7 @@ fn serialize_workspace_write_environment_context() {
#[test]
fn serialize_environment_context_with_foreign_windows_cwd() {
let context = environment_state(
let mut context = environment_state(
[environment(
"remote",
PathUri::parse("file:///C:/windows").expect("Windows cwd URI"),
@@ -97,12 +97,17 @@ fn serialize_environment_context_with_foreign_windows_cwd() {
/*network*/ None,
/*subagents*/ None,
);
context.filesystem = Some(FileSystemContext::from_permission_profile(
&PermissionProfile::Disabled,
&[PathUri::parse("file:///D:/workspace").expect("Windows workspace root URI")],
));
assert_eq!(
context.render(),
r#"<environment_context>
<cwd>C:\windows</cwd>
<shell>powershell</shell>
<filesystem><workspace_roots><root>D:\workspace</root></workspace_roots><permission_profile type="disabled"><file_system type="unrestricted" /></permission_profile></filesystem>
</environment_context>"#
);
}
@@ -189,7 +194,10 @@ fn serialize_environment_context_with_full_filesystem_profile() {
);
context.filesystem = Some(FileSystemContext::from_permission_profile(
&workspace_write_permission_profile_with_private_denials(),
&[repo.clone(), other_repo.clone()],
&[
PathUri::from_abs_path(&repo),
PathUri::from_abs_path(&other_repo),
],
));
let expected = format!(
+33 -7
View File
@@ -22,6 +22,7 @@ use crate::shell_snapshot::ShellSnapshot;
pub(crate) fn default_thread_environment_selections(
environment_manager: &EnvironmentManager,
cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
) -> Vec<TurnEnvironmentSelection> {
environment_manager
.default_environment_ids()
@@ -29,6 +30,7 @@ pub(crate) fn default_thread_environment_selections(
.map(|environment_id| TurnEnvironmentSelection {
environment_id,
cwd: PathUri::from_abs_path(cwd),
workspace_roots: workspace_roots.iter().map(PathUri::from_abs_path).collect(),
})
.collect()
}
@@ -175,8 +177,13 @@ impl ThreadEnvironments {
} else {
Some(local_shell)
};
let mut turn_environment =
TurnEnvironment::new(selection.environment_id, environment, selection.cwd, shell);
let mut turn_environment = TurnEnvironment::new(
selection.environment_id,
environment,
selection.cwd,
selection.workspace_roots,
shell,
);
let task = shell_snapshot
.build(turn_environment.clone())
.boxed()
@@ -406,10 +413,11 @@ mod tests {
.await;
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
}]
);
}
@@ -434,15 +442,17 @@ url = "ws://127.0.0.1:8765"
.expect("environment manager");
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
vec![
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri,
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri],
},
]
);
@@ -454,7 +464,7 @@ url = "ws://127.0.0.1:8765"
let manager = EnvironmentManager::without_environments();
assert_eq!(
default_thread_environment_selections(&manager, &cwd),
default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)),
Vec::<TurnEnvironmentSelection>::new()
);
}
@@ -476,6 +486,7 @@ url = "ws://127.0.0.1:8765"
turn_environments.update_selections(&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
}]);
let snapshot = turn_environments.snapshot().await;
@@ -496,6 +507,7 @@ url = "ws://127.0.0.1:8765"
let first = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -505,6 +517,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.join("other").expect("other cwd URI"),
workspace_roots: Vec::new(),
},
],
)
@@ -525,6 +538,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: selected_cwd_uri,
workspace_roots: Vec::new(),
}],
)
.await;
@@ -562,6 +576,7 @@ url = "ws://127.0.0.1:8765"
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
};
let resolved = resolve_turn_environments(
@@ -570,6 +585,7 @@ url = "ws://127.0.0.1:8765"
TurnEnvironmentSelection {
environment_id: "missing".to_string(),
cwd: cwd_uri,
workspace_roots: Vec::new(),
},
local.clone(),
],
@@ -597,6 +613,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = Arc::new(ThreadEnvironments::new(
manager,
@@ -644,10 +661,12 @@ url = "ws://127.0.0.1:8765"
let remote = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: Vec::new(),
};
let local = TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd,
workspace_roots: Vec::new(),
};
let turn_environments = ThreadEnvironments::new(
manager,
@@ -706,6 +725,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -757,6 +777,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let environments = ThreadEnvironments::new(
Arc::clone(&manager),
@@ -813,6 +834,7 @@ url = "ws://127.0.0.1:8765"
let selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: Vec::new(),
};
let inherited_environment = Arc::new(
Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string()))
@@ -822,6 +844,7 @@ url = "ws://127.0.0.1:8765"
selection.environment_id.clone(),
Arc::clone(&inherited_environment),
selection.cwd.clone(),
Vec::new(),
/*shell*/ None,
);
let manager = Arc::new(EnvironmentManager::without_environments());
@@ -865,6 +888,7 @@ url = "ws://127.0.0.1:8765"
&[TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: Vec::new(),
}],
)
.await;
@@ -878,6 +902,7 @@ url = "ws://127.0.0.1:8765"
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment.clone(),
cwd_uri.clone(),
Vec::new(),
/*shell*/ None,
)],
starting: Vec::new(),
@@ -889,6 +914,7 @@ url = "ws://127.0.0.1:8765"
REMOTE_ENVIRONMENT_ID.to_string(),
remote_environment,
cwd_uri,
Vec::new(),
/*shell*/ None,
),
],
+1
View File
@@ -229,6 +229,7 @@ mod tests {
primary.environment_id.clone(),
Arc::clone(&primary.environment),
PathUri::from_abs_path(&cwd),
Vec::new(),
primary.shell.clone(),
);
}
+1
View File
@@ -1169,6 +1169,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
"remote".to_string(),
environment,
secondary_cwd.clone(),
Vec::new(),
/*shell*/ None,
));
-2
View File
@@ -122,7 +122,6 @@ async fn thread_settings_update(
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
@@ -151,7 +150,6 @@ async fn thread_settings_update(
};
SessionSettingsUpdate {
environments,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
-1
View File
@@ -658,7 +658,6 @@ impl Codex {
config.cwd.clone(),
environment_selections,
),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
+27 -31
View File
@@ -80,9 +80,6 @@ pub(crate) struct SessionConfiguration {
/// Sticky thread-level environment selections plus the legacy cwd used
/// when a turn does not select an environment.
pub(super) environments: TurnEnvironmentSelections,
/// Thread-scoped runtime workspace roots for materializing symbolic
/// workspace permissions at session runtime.
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
/// Directory containing all Codex state for this session.
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -119,6 +116,20 @@ impl SessionConfiguration {
&self.environments.environments
}
pub(super) fn primary_workspace_roots(&self) -> Vec<AbsolutePathBuf> {
self.environments
.environments
.first()
.map(|environment| {
environment
.workspace_roots
.iter()
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
.collect()
})
.unwrap_or_default()
}
pub(crate) fn codex_home(&self) -> &AbsolutePathBuf {
&self.codex_home
}
@@ -128,10 +139,12 @@ impl SessionConfiguration {
}
pub(super) fn permission_profile(&self) -> PermissionProfile {
self.permission_profile_state
.permission_profile()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
self.permission_profile_state.permission_profile().clone()
}
fn materialized_permission_profile(&self) -> PermissionProfile {
self.permission_profile()
.materialize_project_roots_with_workspace_roots(&self.primary_workspace_roots())
}
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
@@ -159,7 +172,7 @@ impl SessionConfiguration {
}
pub(super) fn sandbox_policy(&self) -> SandboxPolicy {
let permission_profile = self.permission_profile();
let permission_profile = self.materialized_permission_profile();
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
self.cwd(),
@@ -167,7 +180,8 @@ impl SessionConfiguration {
}
pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile().file_system_sandbox_policy()
self.materialized_permission_profile()
.file_system_sandbox_policy()
}
pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
@@ -183,10 +197,10 @@ impl SessionConfiguration {
service_tier: self.service_tier.clone(),
approval_policy: self.approval_policy.value(),
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.permission_profile(),
permission_profile: self.materialized_permission_profile(),
active_permission_profile: self.active_permission_profile(),
environments: self.environments.clone(),
workspace_roots: self.workspace_roots.clone(),
workspace_roots: self.primary_workspace_roots(),
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
@@ -265,25 +279,8 @@ impl SessionConfiguration {
.environments
.clone()
.unwrap_or_else(|| self.environments.clone());
let cwd_changed = next_environments.legacy_fallback_cwd.as_path() != current_cwd.as_path();
let cwd_changed = next_environments.legacy_fallback_cwd != current_cwd;
next_configuration.environments = next_environments;
if let Some(workspace_roots) = updates.workspace_roots.clone() {
next_configuration.workspace_roots = workspace_roots;
} else if cwd_changed && self.workspace_roots.contains(&current_cwd) {
let mut retargeted_workspace_roots =
Vec::with_capacity(next_configuration.workspace_roots.len());
for root in &self.workspace_roots {
let root = if root == &current_cwd {
next_configuration.cwd().clone()
} else {
root.clone()
};
if !retargeted_workspace_roots.contains(&root) {
retargeted_workspace_roots.push(root);
}
}
next_configuration.workspace_roots = retargeted_workspace_roots;
}
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
@@ -416,7 +413,6 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) environments: Option<TurnEnvironmentSelections>,
pub(crate) workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
@@ -1168,7 +1164,7 @@ impl Session {
service_tier: session_configuration.service_tier.clone(),
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
permission_profile: session_configuration.permission_profile(),
permission_profile: session_configuration.materialized_permission_profile(),
active_permission_profile: session_configuration.active_permission_profile(),
cwd: session_configuration.cwd().clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
+16 -200
View File
@@ -50,9 +50,7 @@ use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::AgentMessageInputContent;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
@@ -182,6 +180,7 @@ use tokio::sync::Semaphore;
use tokio::time::sleep;
use tokio::time::timeout;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use uuid::Uuid;
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
@@ -1084,6 +1083,10 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an
}
impl crate::tools::sandboxing::ToolRuntime<(), ()> for ProbeToolRuntime {
fn workspace_roots<'a>(&self, _req: &'a ()) -> &'a [PathUri] {
&[]
}
async fn run(
&mut self,
_req: &(),
@@ -2962,52 +2965,6 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn session_permission_profile_rebinds_runtime_workspace_roots() -> anyhow::Result<()> {
let codex_home = tempfile::TempDir::new()?;
let cwd = tempfile::TempDir::new()?;
let old_root = test_path_buf("/workspace/old").abs();
let new_root = test_path_buf("/workspace/new").abs();
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(crate::config::ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
additional_writable_roots: vec![old_root.to_path_buf()],
..Default::default()
})
.build()
.await?;
let session_permission_profile_state = session_permission_profile_state_from_config(&config)?;
let stored_file_system_policy = session_permission_profile_state
.permission_profile()
.file_system_sandbox_policy();
assert!(
!stored_file_system_policy
.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()),
"session permission profile state should keep runtime workspace roots symbolic"
);
let mut session_configuration = make_session_configuration_for_tests().await;
session_configuration.environments =
TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new());
session_configuration.workspace_roots = config.workspace_roots.clone();
session_configuration.permission_profile_state = session_permission_profile_state;
let initial_policy = session_configuration.file_system_sandbox_policy();
assert!(initial_policy.can_write_path_with_cwd(old_root.as_path(), config.cwd.as_path()));
let updated = session_configuration.apply(&SessionSettingsUpdate {
workspace_roots: Some(vec![new_root.clone()]),
..Default::default()
})?;
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<()> {
let server = start_mock_server().await;
@@ -3807,7 +3764,6 @@ async fn set_rate_limits_retains_previous_credits() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -3914,7 +3870,6 @@ async fn set_rate_limits_updates_plan_type_when_present() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -4451,7 +4406,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -4609,7 +4563,9 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
)
.expect("set permission profile");
let expected_file_system_sandbox_policy = file_system_sandbox_policy
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
.materialize_project_roots_with_workspace_roots(
&session_configuration.primary_workspace_roots(),
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
@@ -4671,7 +4627,9 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
.expect("permission profile update should succeed");
let mut expected_file_system_policy = requested_file_system_policy
.materialize_project_roots_with_workspace_roots(&session_configuration.workspace_roots);
.materialize_project_roots_with_workspace_roots(
&session_configuration.primary_workspace_roots(),
);
expected_file_system_policy.glob_scan_max_depth = Some(2);
expected_file_system_policy.entries.push(deny_entry);
assert_eq!(
@@ -4727,94 +4685,6 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
);
}
#[tokio::test]
async fn session_configuration_apply_rebinds_symbolic_profile_to_updated_workspace_roots() {
let mut session_configuration = make_session_configuration_for_tests().await;
let old_root = tempfile::tempdir().expect("create old root");
let new_root = tempfile::tempdir().expect("create new root");
let profile_root = tempfile::tempdir().expect("create profile root");
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let profile_root = profile_root.path().abs();
session_configuration.workspace_roots = vec![old_root.clone()];
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
let updated = session_configuration
.apply(&SessionSettingsUpdate {
workspace_roots: Some(vec![new_root.clone()]),
permission_profile: Some(permission_profile),
active_permission_profile: Some(ActivePermissionProfile::new("dev")),
profile_workspace_roots: Some(vec![profile_root.clone()]),
..Default::default()
})
.expect("permission profile update should succeed");
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
assert_eq!(
updated.active_permission_profile(),
Some(ActivePermissionProfile::new("dev"))
);
assert_eq!(updated.profile_workspace_roots(), &[profile_root]);
}
#[tokio::test]
async fn session_configuration_apply_retargets_implicit_workspace_root_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let old_root = tempfile::tempdir().expect("create old root");
let new_root = tempfile::tempdir().expect("create new root");
let extra_root = tempfile::tempdir().expect("create extra root");
let old_root = old_root.path().abs();
let new_root = new_root.path().abs();
let extra_root = extra_root.path().abs();
session_configuration.environments =
TurnEnvironmentSelections::new(old_root.clone(), Vec::new());
session_configuration.workspace_roots = vec![old_root.clone(), extra_root.clone()];
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}]);
let permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
);
session_configuration
.set_permission_profile_for_tests(permission_profile)
.expect("set permission profile");
let updated = session_configuration
.apply(&SessionSettingsUpdate {
environments: Some(TurnEnvironmentSelections::new(new_root.clone(), Vec::new())),
..Default::default()
})
.expect("cwd-only update should succeed");
assert_eq!(
updated.workspace_roots,
vec![new_root.clone(), extra_root.clone()]
);
let updated_policy = updated.file_system_sandbox_policy();
assert!(updated_policy.can_write_path_with_cwd(new_root.as_path(), updated.cwd().as_path()));
assert!(updated_policy.can_write_path_with_cwd(extra_root.as_path(), updated.cwd().as_path()));
assert!(!updated_policy.can_write_path_with_cwd(old_root.as_path(), updated.cwd().as_path()));
}
#[tokio::test]
async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Result<()> {
let codex_home = tempfile::tempdir().expect("create codex home");
@@ -5025,60 +4895,6 @@ enabled = false
);
}
#[tokio::test]
async fn session_configuration_apply_retargets_legacy_workspace_root_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir");
let original_cwd = workspace.path().join("repo-a").abs();
let project_root = workspace.path().join("repo-b").abs();
session_configuration.environments =
TurnEnvironmentSelections::new(original_cwd.clone(), Vec::new());
session_configuration.workspace_roots = vec![session_configuration.cwd().clone()];
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&sandbox_policy,
session_configuration.cwd(),
);
session_configuration
.set_permission_profile_for_tests(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&sandbox_policy),
),
)
.expect("set permission profile");
let updated = session_configuration
.apply(&SessionSettingsUpdate {
environments: Some(TurnEnvironmentSelections::new(
project_root.clone(),
Vec::new(),
)),
..Default::default()
})
.expect("cwd-only update should succeed");
assert_eq!(updated.workspace_roots, vec![project_root.clone()]);
assert!(
updated
.file_system_sandbox_policy()
.can_write_path_with_cwd(project_root.as_path(), updated.cwd().as_path()),
"cwd-only update should keep the new cwd writable"
);
assert!(
!updated
.file_system_sandbox_policy()
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
"cwd-only update should not keep the old implicit cwd writable"
);
}
#[tokio::test]
async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
@@ -5331,7 +5147,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), Vec::new()),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5463,7 +5278,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5717,7 +5531,6 @@ async fn make_session_with_config_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -5825,7 +5638,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -6275,6 +6087,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
"remote".to_string(),
current_environment.environment,
PathUri::from_abs_path(&environment_cwd),
Vec::new(),
current_environment.shell,
);
@@ -6907,6 +6720,7 @@ async fn primary_environment_uses_first_turn_environment() {
"second".to_string(),
Arc::clone(&first_environment.environment),
second_cwd_uri.clone(),
Vec::new(),
/*shell*/ None,
));
@@ -7595,7 +7409,6 @@ where
permission_profile_state: config.permissions.permission_profile_state().clone(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
environments: TurnEnvironmentSelections::new(config.cwd.clone(), default_environments),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
original_config_do_not_use: Arc::clone(&config),
@@ -8148,6 +7961,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
environment.environment_id,
environment.environment,
PathUri::from_abs_path(&cwd),
Vec::new(),
environment.shell,
);
@@ -8207,6 +8021,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
environment.environment_id,
environment.environment,
PathUri::from_abs_path(&test_path_buf("/new-repo").abs()),
Vec::new(),
environment.shell,
);
@@ -8935,6 +8750,7 @@ async fn turn_context_item_stores_local_cwd() {
"remote".to_string(),
environment.environment,
cwd,
Vec::new(),
environment.shell,
);
@@ -303,6 +303,10 @@ async fn guardian_allows_shell_command_additional_permissions_requests_past_poli
.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
.set_permission_profile(codex_protocol::models::PermissionProfile::Disabled)
.expect("test setup should allow disabling the permission profile");
config.codex_linux_sandbox_exe = codex_linux_sandbox_exe_or_skip!();
config
.features
@@ -414,6 +418,10 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
.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
.set_permission_profile(codex_protocol::models::PermissionProfile::Disabled)
.expect("test setup should allow disabling the permission profile");
config.approvals_reviewer = ApprovalsReviewer::User;
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config);
+22 -19
View File
@@ -46,6 +46,7 @@ pub(crate) struct TurnEnvironment {
pub(crate) environment_id: String,
pub(crate) environment: Arc<Environment>,
cwd: PathUri,
workspace_roots: Vec<PathUri>,
pub(crate) shell: Option<shell::Shell>,
pub(crate) shell_snapshot: ShellSnapshotTask,
}
@@ -55,12 +56,14 @@ impl TurnEnvironment {
environment_id: String,
environment: Arc<Environment>,
cwd: PathUri,
workspace_roots: Vec<PathUri>,
shell: Option<shell::Shell>,
) -> Self {
Self {
environment_id,
environment,
cwd,
workspace_roots,
shell,
shell_snapshot: futures::future::ready(None).boxed().shared(),
}
@@ -80,10 +83,15 @@ impl TurnEnvironment {
&self.cwd
}
pub(crate) fn workspace_roots(&self) -> &[PathUri] {
&self.workspace_roots
}
pub(crate) fn selection(&self) -> TurnEnvironmentSelection {
TurnEnvironmentSelection {
environment_id: self.environment_id.clone(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
}
}
}
@@ -94,6 +102,7 @@ impl std::fmt::Debug for TurnEnvironment {
.field("environment_id", &self.environment_id)
.field("environment", &self.environment)
.field("cwd", &self.cwd)
.field("workspace_roots", &self.workspace_roots)
.field("shell", &self.shell)
.finish_non_exhaustive()
}
@@ -317,10 +326,11 @@ impl TurnContext {
pub(crate) fn file_system_sandbox_context(
&self,
additional_permissions: Option<AdditionalPermissionProfile>,
cwd: &PathUri,
environment: &TurnEnvironment,
) -> FileSystemSandboxContext {
let permission_profile = self.config.permissions.permission_profile();
let (base_file_system_sandbox_policy, base_network_sandbox_policy) =
self.permission_profile.to_runtime_permissions();
permission_profile.to_runtime_permissions();
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
&base_file_system_sandbox_policy,
additional_permissions.as_ref(),
@@ -330,19 +340,14 @@ impl TurnContext {
additional_permissions.as_ref(),
);
let permissions = PermissionProfile::from_runtime_permissions_with_enforcement(
self.permission_profile.enforcement(),
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(cwd.clone()),
workspace_roots: self
.config
.effective_workspace_roots()
.iter()
.map(PathUri::from_abs_path)
.collect(),
cwd: Some(environment.cwd().clone()),
workspace_roots: environment.workspace_roots().to_vec(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
@@ -438,10 +443,11 @@ impl Session {
let config = session_configuration.original_config_do_not_use.clone();
let mut per_turn_config = (*config).clone();
per_turn_config.cwd = cwd;
per_turn_config.workspace_roots = session_configuration.workspace_roots.clone();
let workspace_roots = session_configuration.primary_workspace_roots();
per_turn_config.workspace_roots = workspace_roots.clone();
per_turn_config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
.set_workspace_roots(workspace_roots);
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
@@ -476,10 +482,6 @@ impl Session {
Self::build_per_turn_config(session_configuration, session_configuration.cwd().clone());
config.model = Some(session_configuration.collaboration_mode.model().to_string());
config.permissions.approval_policy = session_configuration.approval_policy.clone();
config.workspace_roots = session_configuration.workspace_roots.clone();
config
.permissions
.set_workspace_roots(session_configuration.workspace_roots.clone());
config
}
@@ -531,6 +533,7 @@ impl Session {
per_turn_config.features.enabled(Feature::FastMode),
&model_info,
);
let permission_profile = per_turn_config.permissions.effective_permission_profile();
let per_turn_config = Arc::new(per_turn_config);
let turn_metadata_state = Arc::new(TurnMetadataState::new(
session_id.to_string(),
@@ -541,7 +544,7 @@ impl Session {
session_configuration.thread_source.clone(),
sub_id.clone(),
cwd.clone(),
&session_configuration.permission_profile(),
&permission_profile,
session_configuration.windows_sandbox_level,
network.is_some(),
));
@@ -578,7 +581,7 @@ impl Session {
multi_agent_version,
personality: session_configuration.personality,
approval_policy: session_configuration.approval_policy.clone(),
permission_profile: session_configuration.permission_profile(),
permission_profile,
network,
windows_sandbox_level: session_configuration.windows_sandbox_level,
available_models,
@@ -702,7 +705,7 @@ impl Session {
multi_agent_runtime: TurnMultiAgentRuntime,
) -> Arc<TurnContext> {
let turn_environments = self.services.turn_environments.snapshot().await;
let primary_turn_environment = turn_environments.primary().cloned();
let primary_turn_environment = turn_environments.primary();
// TODO(anp): Migrate per-turn config and legacy TurnContext cwd consumers to PathUri so
// a foreign primary environment does not fall back to the session's host cwd.
let cwd = primary_turn_environment
+26 -5
View File
@@ -510,8 +510,13 @@ impl ThreadManager {
pub fn default_environment_selections(
&self,
cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
) -> Vec<TurnEnvironmentSelection> {
default_thread_environment_selections(self.state.environment_manager.as_ref(), cwd)
default_thread_environment_selections(
self.state.environment_manager.as_ref(),
cwd,
workspace_roots,
)
}
pub fn validate_environment_selections(
@@ -659,6 +664,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
Box::pin(self.start_thread_with_options(StartThreadOptions {
config,
@@ -790,6 +796,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
@@ -836,6 +843,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
Box::pin(self.state.spawn_thread(
config,
@@ -869,6 +877,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let (session_source, thread_source) = initial_history
.get_resumed_session_sources()
@@ -1057,6 +1066,7 @@ impl ThreadManager {
let environments = default_thread_environment_selections(
self.state.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let agent_control = self.agent_control_for_config(&config);
Box::pin(self.state.spawn_thread(
@@ -1373,7 +1383,11 @@ impl ThreadManagerState {
environments: Option<Vec<TurnEnvironmentSelection>>,
) -> CodexResult<NewThread> {
let environments = environments.unwrap_or_else(|| {
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd)
default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
)
});
Box::pin(self.spawn_thread_with_source(
config,
@@ -1412,8 +1426,11 @@ impl ThreadManagerState {
inherited_environments,
inherited_exec_policy,
} = options;
let environments =
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd);
let environments = default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
);
let thread_source = initial_history.get_resumed_thread_source();
Box::pin(self.spawn_thread_with_source(
config,
@@ -1455,7 +1472,11 @@ impl ThreadManagerState {
thread_extension_init: ExtensionDataInit,
) -> CodexResult<NewThread> {
let environments = environments.unwrap_or_else(|| {
default_thread_environment_selections(self.environment_manager.as_ref(), &config.cwd)
default_thread_environment_selections(
self.environment_manager.as_ref(),
&config.cwd,
&config.workspace_roots,
)
});
Box::pin(self.spawn_thread_with_source(
config,
@@ -839,6 +839,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let environments = vec![TurnEnvironmentSelection {
environment_id: "local".to_string(),
cwd: PathUri::from_abs_path(&selected_cwd),
workspace_roots: Vec::new(),
}];
let default_cwd = config.cwd.clone();
let mut source_config = config.clone();
@@ -383,10 +383,8 @@ impl ApplyPatchHandler {
));
};
let fs = turn_environment.environment.get_filesystem();
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd(),
);
let sandbox = turn
.file_system_sandbox_context(/*additional_permissions*/ None, turn_environment);
match codex_apply_patch::verify_apply_patch_args(
args,
turn_environment.cwd(),
@@ -554,7 +552,8 @@ pub(crate) async fn intercept_apply_patch(
call_id: &str,
tool_name: &str,
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
let sandbox = turn.file_system_sandbox_context(/*additional_permissions*/ None, cwd);
let sandbox =
turn.file_system_sandbox_context(/*additional_permissions*/ None, &turn_environment);
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd, fs, Some(&sandbox))
.await
{
@@ -143,7 +143,7 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
.additional_permissions;
let file_system_sandbox_context = invocation
.turn
.file_system_sandbox_context(additional_permissions, environment.cwd());
.file_system_sandbox_context(additional_permissions, environment);
environments.push(ToolEnvironment {
environment_id: environment.environment_id.clone(),
cwd: native_cwd,
@@ -109,6 +109,7 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
.environment,
),
PathUri::from_abs_path(&selected_cwd),
Vec::new(),
Some(selected_shell),
);
let mut expected_env = create_env(
@@ -147,10 +147,8 @@ impl ViewImageHandler {
))
})?;
let model_visible_path = path_uri.inferred_native_path_string();
let sandbox = turn.file_system_sandbox_context(
/*additional_permissions*/ None,
turn_environment.cwd(),
);
let sandbox = turn
.file_system_sandbox_context(/*additional_permissions*/ None, turn_environment);
let fs = turn_environment.environment.get_filesystem();
let metadata = fs
@@ -272,6 +270,7 @@ mod tests {
current.environment_id,
current.environment,
PathUri::from_abs_path(&cwd),
Vec::new(),
current.shell,
);
}
@@ -382,7 +381,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::Disabled;
Arc::make_mut(&mut turn.config)
.permissions
.set_permission_profile(PermissionProfile::Disabled)
.expect("set permission profile");
let turn = Arc::new(turn);
let result = ViewImageHandler::default()
+17 -9
View File
@@ -152,8 +152,17 @@ impl ToolOrchestrator {
// 1) Approval
let mut already_approved = false;
let file_system_sandbox_policy = turn_ctx.file_system_sandbox_policy();
let network_sandbox_policy = turn_ctx.network_sandbox_policy();
let workspace_roots = tool.workspace_roots(req);
let permission_profile = turn_ctx.config.permissions.permission_profile();
let materialized_workspace_roots = workspace_roots
.iter()
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
.collect::<Vec<_>>();
let permissions = permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&materialized_workspace_roots);
let (file_system_sandbox_policy, network_sandbox_policy) =
permissions.to_runtime_permissions();
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});
@@ -252,16 +261,15 @@ impl ToolOrchestrator {
.sandbox_cwd(req)
.cloned()
.unwrap_or_else(|| PathUri::from_abs_path(&turn_ctx.cwd));
let workspace_roots = turn_ctx.config.effective_workspace_roots();
let initial_attempt = SandboxAttempt {
sandbox: initial_sandbox,
sandbox_requested,
permissions: &turn_ctx.permission_profile,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: workspace_roots.as_slice(),
workspace_roots,
codex_linux_sandbox_exe: turn_ctx.config.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -438,12 +446,12 @@ impl ToolOrchestrator {
let retry_attempt = SandboxAttempt {
sandbox: retry_sandbox,
sandbox_requested: retry_sandbox_requested,
permissions: &turn_ctx.permission_profile,
exec_server_permissions: turn_ctx.config.permissions.permission_profile(),
permissions: &permissions,
exec_server_permissions: permission_profile,
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: workspace_roots.as_slice(),
workspace_roots,
codex_linux_sandbox_exe: retry_codex_linux_sandbox_exe,
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -96,11 +96,7 @@ impl ApplyPatchRuntime {
Some(FileSystemSandboxContext {
permissions: permissions.into(),
cwd: Some(attempt.sandbox_cwd.clone()),
workspace_roots: attempt
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
workspace_roots: attempt.workspace_roots.to_vec(),
windows_sandbox_level: attempt.windows_sandbox_level,
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
use_legacy_landlock: attempt.use_legacy_landlock,
@@ -214,6 +210,10 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
}
impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRuntime {
fn workspace_roots<'a>(&self, req: &'a ApplyPatchRequest) -> &'a [PathUri] {
req.turn_environment.workspace_roots()
}
fn sandbox_cwd<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a PathUri> {
Some(&req.action.cwd)
}
@@ -20,6 +20,7 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
environment_id.to_string(),
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
PathUri::from_abs_path(&std::env::temp_dir().abs()),
Vec::new(),
/*shell*/ None,
)
}
@@ -219,7 +220,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&path),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: true,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
@@ -288,7 +289,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&path),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -112,7 +112,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &sandbox_policy_cwd,
workspace_roots: std::slice::from_ref(&native_sandbox_policy_cwd),
workspace_roots: std::slice::from_ref(&sandbox_policy_cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -210,6 +210,10 @@ impl Approvable<ShellRequest> for ShellRuntime {
}
impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
fn workspace_roots<'a>(&self, req: &'a ShellRequest) -> &'a [PathUri] {
req.turn_environment.workspace_roots()
}
fn network_approval_spec(
&self,
req: &ShellRequest,
@@ -13,6 +13,7 @@ async fn approval_key_includes_environment_id() {
"remote".to_string(),
Arc::new(Environment::default_for_tests()),
PathUri::from_abs_path(&cwd),
Vec::new(),
/*shell*/ None,
),
shell_type: None,
@@ -256,6 +256,10 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
}
impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRuntime<'a> {
fn workspace_roots<'b>(&self, req: &'b UnifiedExecRequest) -> &'b [PathUri] {
req.turn_environment.workspace_roots()
}
fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b PathUri> {
Some(&req.sandbox_cwd)
}
@@ -505,6 +509,7 @@ mod tests {
LOCAL_ENVIRONMENT_ID.to_string(),
Arc::new(Environment::default_for_tests()),
cwd,
Vec::new(),
/*shell*/ None,
)
}
+12 -13
View File
@@ -27,7 +27,6 @@ use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_tools::ToolName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::Future;
use futures::future::BoxFuture;
@@ -388,6 +387,8 @@ pub(crate) enum ToolError {
}
pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
fn workspace_roots<'a>(&self, req: &'a Req) -> &'a [PathUri];
fn network_approval_spec(&self, _req: &Req, _ctx: &ToolCtx) -> Option<NetworkApprovalSpec> {
None
}
@@ -414,7 +415,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a PathUri,
pub(crate) workspace_roots: &'a [AbsolutePathBuf],
pub(crate) workspace_roots: &'a [PathUri],
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
@@ -457,10 +458,15 @@ impl<'a> SandboxAttempt<'a> {
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
let workspace_roots = self
.workspace_roots
.iter()
.map(PathUri::to_abs_path)
.collect::<std::io::Result<Vec<_>>>()?;
Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
self.workspace_roots.to_vec(),
workspace_roots,
))
}
@@ -494,21 +500,14 @@ impl<'a> SandboxAttempt<'a> {
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
self.workspace_roots.to_vec(),
);
let mut exec_request =
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, Vec::new());
exec_request.exec_server_managed_network = managed_network;
if self.sandbox_requested {
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
permissions: exec_server_permissions.into(),
cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
workspace_roots: self
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
workspace_roots: self.workspace_roots.to_vec(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
use_legacy_landlock: self.use_legacy_landlock,
+1 -1
View File
@@ -221,7 +221,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
enforce_managed_network: true,
manager: &manager,
sandbox_cwd: &cwd_uri,
workspace_roots: std::slice::from_ref(&cwd),
workspace_roots: std::slice::from_ref(&cwd_uri),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
@@ -592,6 +592,7 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
.expect("remote test environment"),
),
remote_cwd,
Vec::new(),
/*shell*/ None,
),
);
+5 -1
View File
@@ -109,6 +109,7 @@ pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection {
TurnEnvironmentSelection {
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
}
}
@@ -140,6 +141,7 @@ impl TestEnv {
Some(_) => TurnEnvironmentSelection {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&cwd),
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
},
None => local(cwd.clone()),
};
@@ -207,6 +209,7 @@ pub async fn test_env() -> Result<TestEnv> {
let selection = TurnEnvironmentSelection {
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd_uri.clone(),
workspace_roots: vec![cwd_uri.clone()],
};
let cwd = if remote_env == TestEnvironment::WineExec {
// TODO(anp): Convert `Config::cwd` to `LegacyAppPathString` and remove this
@@ -692,7 +695,8 @@ impl TestCodexBuilder {
.await?
}
(None, None) => {
let environments = thread_manager.default_environment_selections(&config.cwd);
let environments = thread_manager
.default_environment_selections(&config.cwd, &config.workspace_roots);
Box::pin(
thread_manager.start_thread_with_options(StartThreadOptions {
config: config.clone(),
@@ -105,9 +105,13 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path());
let environments = TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::parse("file:///C:/codex-home")?,
vec![{
let cwd = PathUri::parse("file:///C:/codex-home")?;
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: cwd.clone(),
workspace_roots: vec![cwd],
}
}],
);
+2
View File
@@ -705,10 +705,12 @@ async fn multi_environment_thread_loads_every_project_and_keeps_creation_snapsho
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
},
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_host_native_path(local_root.path())?,
workspace_roots: vec![PathUri::from_host_native_path(local_root.path())?],
},
],
thread_extension_init: Default::default(),
@@ -1653,6 +1653,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&shared_cwd),
workspace_roots: vec![PathUri::from_abs_path(&shared_cwd)],
},
];
test.codex
@@ -4,12 +4,16 @@ use std::sync::Arc;
use anyhow::Result;
use codex_config::types::Personality;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_features::Feature;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::TurnEnvironmentSelection;
use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
@@ -78,6 +82,64 @@ fn format_environment_context_subagents_snapshot(subagents: &[&str]) -> String {
context_snapshot::format_response_items_snapshot(items.as_slice(), &context_snapshot_options())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_visible_environment_context_preserves_foreign_workspace_roots() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let response_mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
)
.await;
let test = test_codex().build(&server).await?;
let foreign_root = PathUri::parse("file:///C:/workspace")?;
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "inspect the workspace".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![foreign_root],
}],
)),
..Default::default()
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let environment_context = response_mock
.single_request()
.message_input_texts("user")
.into_iter()
.find(|text| text.contains("<environment_context>"))
.expect("model-visible environment context");
assert!(
environment_context.contains("<workspace_roots><root>C:\\workspace</root>"),
"foreign workspace root should remain visible to the model: {environment_context}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -1067,6 +1067,7 @@ async fn approved_network_host_for_one_environment_still_prompts_in_another() ->
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
},
];
+166
View File
@@ -14,10 +14,12 @@ use codex_exec_server::RemoveOptions;
use codex_features::Feature;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval;
@@ -60,6 +62,7 @@ use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local;
use core_test_support::test_codex::test_codex;
use core_test_support::test_codex::test_env;
use core_test_support::test_codex::turn_permission_fields;
use core_test_support::test_docker_container_name;
use core_test_support::test_target_os;
use core_test_support::wait_for_event;
@@ -304,6 +307,7 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
Some(vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
}]),
)
.await?;
@@ -568,6 +572,7 @@ async fn deferred_executor_updates_context_and_tools_after_startup() -> Result<(
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
}],
)),
..Default::default()
@@ -805,6 +810,7 @@ async fn deferred_executor_wait_reports_startup_failure() -> Result<()> {
vec![TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&test.config.cwd),
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
}],
)),
..Default::default()
@@ -1153,6 +1159,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
let remote_selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
};
let multi_env_output = exec_command_routing_output(
&test,
@@ -1191,6 +1198,161 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_exec_materializes_target_roots_before_sandbox_selection() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_target_windows!(
Ok(()),
"sandboxed process launch is not supported by the exec-server Windows backend"
);
skip_if_no_remote_env!(Ok(()));
const SECRET: &str = "target-root-secret";
const SECRET_FILE: &str = "secret.txt";
let server = start_mock_server().await;
let test = unified_exec_test(&server).await?;
let local_cwd = TempDir::new()?;
let remote_cwd = PathBuf::from(format!(
"/tmp/codex-remote-target-roots-{}",
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
))
.abs();
let remote_cwd_uri = PathUri::from_abs_path(&remote_cwd);
test.fs()
.create_directory(
&remote_cwd_uri,
CreateDirectoryOptions { recursive: true },
/*sandbox*/ None,
)
.await?;
test.fs()
.write_file(
&remote_cwd_uri.join(SECRET_FILE)?,
SECRET.as_bytes().to_vec(),
/*sandbox*/ None,
)
.await?;
let call_id = "remote-target-root-sandbox";
let response_mock = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
ev_function_call(
call_id,
"exec_command",
&json!({
"shell": "bash",
"cmd": format!("cat {SECRET_FILE}"),
"login": false,
"yield_time_ms": 1_000,
"environment_id": REMOTE_ENVIRONMENT_ID,
"sandbox_permissions": SandboxPermissions::RequireEscalated,
})
.to_string(),
),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-2", "done"),
ev_completed("resp-2"),
]),
],
)
.await;
let permission_profile = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Deny,
},
]),
NetworkSandboxPolicy::Restricted,
);
let (sandbox_policy, permission_profile) =
turn_permission_fields(permission_profile, test.config.cwd.as_path());
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "try to read the denied remote workspace root".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
environments: Some(TurnEnvironmentSelections::new(
test.config.cwd.clone(),
vec![
TurnEnvironmentSelection {
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&local_cwd.path().abs()),
workspace_roots: Vec::new(),
},
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri.clone(),
workspace_roots: vec![remote_cwd_uri.clone()],
},
],
)),
approval_policy: Some(AskForApproval::Never),
sandbox_policy: Some(sandbox_policy),
permission_profile,
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(),
reasoning_effort: None,
developer_instructions: None,
},
}),
..Default::default()
},
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let request = response_mock
.last_request()
.context("model should receive the denied remote command output")?;
let (output, success) = request
.function_call_output_content_and_success(call_id)
.context("remote command output should be model visible")?;
assert_ne!(success, Some(true));
assert!(
output.is_none_or(|output| !output.contains(SECRET)),
"denied remote workspace contents should not be readable"
);
test.fs()
.remove(
&remote_cwd_uri,
RemoveOptions {
recursive: true,
force: true,
},
/*sandbox*/ None,
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -1309,6 +1471,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
},
],
AskForApproval::OnRequest,
@@ -1450,6 +1613,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
},
]),
)
@@ -1533,6 +1697,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
},
];
let local_patch = format!(
@@ -1734,6 +1899,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: PathUri::from_abs_path(&remote_cwd),
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
},
]),
)
+2 -1
View File
@@ -589,7 +589,8 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
let absolute_image_path = image_path_uri.inferred_native_path_string();
let remote_selection = TurnEnvironmentSelection {
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
cwd: remote_cwd_uri,
cwd: remote_cwd_uri.clone(),
workspace_roots: vec![remote_cwd_uri],
};
let relative_call_id = "call-view-image-relative-multi-env";
let absolute_call_id = "call-view-image-absolute-multi-env";