mirror of
https://github.com/openai/codex.git
synced 2026-10-27 16:02:59 +00:00
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:
@@ -334,6 +334,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
.expect("local environment"),
|
||||
),
|
||||
PathUri::from_abs_path(&cwd),
|
||||
Vec::new(),
|
||||
/*shell*/ None,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -229,6 +229,7 @@ mod tests {
|
||||
primary.environment_id.clone(),
|
||||
Arc::clone(&primary.environment),
|
||||
PathUri::from_abs_path(&cwd),
|
||||
Vec::new(),
|
||||
primary.shell.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(¤t_cwd) {
|
||||
let mut retargeted_workspace_roots =
|
||||
Vec::with_capacity(next_configuration.workspace_roots.len());
|
||||
for root in &self.workspace_roots {
|
||||
let root = if root == ¤t_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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
}],
|
||||
);
|
||||
|
||||
|
||||
@@ -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)],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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)],
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user