mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Honor per-environment permission profiles (#38673)
## What changed - Add a resolved `permission_profile` to each `EnvironmentConfig` and use the complete attachment config for execution and capability-root selection. - Let `Ready` environment configurations override thread permissions while `FromThread` selections continue to inherit them. - Restrict inherited Guardian environment profiles to read-only permissions. ## Testing - Add coverage proving that a read-only environment blocks writes even when the thread permits workspace writes. - Update environment inheritance, Guardian review, and capability-root tests for the resolved attachment configuration. GitOrigin-RevId: 1a313b9e4892b1a579a0e880e322a782b4f6c0a7
This commit is contained in:
1 parent
15fde8c1f2
commit
1873e947f8
17 files changed
+245
-145
No files matched your search
@@ -5,7 +5,6 @@ use crate::context::ContextualUserFragment;
|
|||||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||||
use crate::environment_selection::TurnEnvironmentState;
|
use crate::environment_selection::TurnEnvironmentState;
|
||||||
use crate::session::turn_context::TurnEnvironment;
|
use crate::session::turn_context::TurnEnvironment;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use codex_config::ConfigLayerEntry;
|
use codex_config::ConfigLayerEntry;
|
||||||
use codex_config::ConfigLayerStack;
|
use codex_config::ConfigLayerStack;
|
||||||
use codex_config::ConfigRequirements;
|
use codex_config::ConfigRequirements;
|
||||||
@@ -23,6 +22,7 @@ use codex_exec_server::RemoveOptions;
|
|||||||
use codex_extension_api::UserInstructions;
|
use codex_extension_api::UserInstructions;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
@@ -345,12 +345,12 @@ fn resolved_local_environments<const N: usize>(
|
|||||||
.expect("local environment"),
|
.expect("local environment"),
|
||||||
),
|
),
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ use tokio_util::task::AbortOnDropHandle;
|
|||||||
|
|
||||||
use crate::session::turn_context::ShellSnapshotTask;
|
use crate::session::turn_context::ShellSnapshotTask;
|
||||||
use crate::session::turn_context::TurnEnvironment;
|
use crate::session::turn_context::TurnEnvironment;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::shell::Shell;
|
use crate::shell::Shell;
|
||||||
use crate::shell_snapshot::ShellSnapshot;
|
use crate::shell_snapshot::ShellSnapshot;
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ struct ResolvedEnvironment {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct SelectedTurnEnvironment {
|
struct SelectedTurnEnvironment {
|
||||||
selection: TurnEnvironmentSelection,
|
selection: TurnEnvironmentSelection,
|
||||||
config: TurnEnvironmentConfig,
|
config: EnvironmentConfig,
|
||||||
environment: Arc<Environment>,
|
environment: Arc<Environment>,
|
||||||
// Selection clones share one listener; the final handle drop aborts it.
|
// Selection clones share one listener; the final handle drop aborts it.
|
||||||
connection_events_task: Option<Arc<AbortOnDropHandle<()>>>,
|
connection_events_task: Option<Arc<AbortOnDropHandle<()>>>,
|
||||||
@@ -77,7 +76,7 @@ struct SelectedTurnEnvironment {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct StartingTurnEnvironment {
|
pub(crate) struct StartingTurnEnvironment {
|
||||||
pub(crate) selection: TurnEnvironmentSelection,
|
pub(crate) selection: TurnEnvironmentSelection,
|
||||||
config: TurnEnvironmentConfig,
|
config: EnvironmentConfig,
|
||||||
resolution: TurnEnvironmentResolution,
|
resolution: TurnEnvironmentResolution,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,17 +84,11 @@ impl SelectedTurnEnvironment {
|
|||||||
fn apply_configuration(
|
fn apply_configuration(
|
||||||
&mut self,
|
&mut self,
|
||||||
selection_config: EnvironmentConfigState,
|
selection_config: EnvironmentConfigState,
|
||||||
thread_config: &TurnEnvironmentConfig,
|
thread_config: &EnvironmentConfig,
|
||||||
) {
|
) {
|
||||||
self.config = match &selection_config {
|
self.config = match &selection_config {
|
||||||
EnvironmentConfigState::FromThread => thread_config.clone(),
|
EnvironmentConfigState::FromThread => thread_config.clone(),
|
||||||
EnvironmentConfigState::Ready(config) => TurnEnvironmentConfig {
|
EnvironmentConfigState::Ready(config) => config.clone(),
|
||||||
allow_login_shell: config.allow_login_shell,
|
|
||||||
// temp read from thread_config; will go away once perms on EnvironmentConfig,
|
|
||||||
// then we can just assign passed config directly
|
|
||||||
permission_profile: thread_config.permission_profile.clone(),
|
|
||||||
selected_capability_roots: Some(config.selected_capability_roots.clone()),
|
|
||||||
},
|
|
||||||
EnvironmentConfigState::Pending => {
|
EnvironmentConfigState::Pending => {
|
||||||
unreachable!("pending environment configuration is not supported yet")
|
unreachable!("pending environment configuration is not supported yet")
|
||||||
}
|
}
|
||||||
@@ -133,7 +126,7 @@ impl ThreadEnvironments {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
environment_manager: Arc<EnvironmentManager>,
|
environment_manager: Arc<EnvironmentManager>,
|
||||||
local_shell: Shell,
|
local_shell: Shell,
|
||||||
thread_environment_config: TurnEnvironmentConfig,
|
thread_environment_config: EnvironmentConfig,
|
||||||
shell_snapshot: ShellSnapshot,
|
shell_snapshot: ShellSnapshot,
|
||||||
current: TurnEnvironmentSnapshot,
|
current: TurnEnvironmentSnapshot,
|
||||||
non_blocking_snapshots: bool,
|
non_blocking_snapshots: bool,
|
||||||
@@ -186,7 +179,7 @@ impl ThreadEnvironments {
|
|||||||
pub(crate) fn update_selections(
|
pub(crate) fn update_selections(
|
||||||
&self,
|
&self,
|
||||||
environments: &[TurnEnvironmentSelection],
|
environments: &[TurnEnvironmentSelection],
|
||||||
thread_environment_config: &TurnEnvironmentConfig,
|
thread_environment_config: &EnvironmentConfig,
|
||||||
) {
|
) {
|
||||||
let previous = self.environments.load();
|
let previous = self.environments.load();
|
||||||
let mut seen_environment_ids = HashSet::with_capacity(environments.len());
|
let mut seen_environment_ids = HashSet::with_capacity(environments.len());
|
||||||
@@ -306,7 +299,7 @@ impl ThreadEnvironments {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_environment_configs(&self, config: &TurnEnvironmentConfig) {
|
pub(crate) fn update_environment_configs(&self, config: &EnvironmentConfig) {
|
||||||
let environments = self
|
let environments = self
|
||||||
.environments
|
.environments
|
||||||
.load()
|
.load()
|
||||||
@@ -353,9 +346,8 @@ impl ThreadEnvironments {
|
|||||||
let environments = self.environments.load();
|
let environments = self.environments.load();
|
||||||
let mut selected_capability_roots = thread_selected_capability_roots.to_vec();
|
let mut selected_capability_roots = thread_selected_capability_roots.to_vec();
|
||||||
for environment in environments.iter() {
|
for environment in environments.iter() {
|
||||||
if let Some(roots) = &environment.config.selected_capability_roots {
|
selected_capability_roots
|
||||||
selected_capability_roots.extend(roots.iter().cloned());
|
.extend(environment.config.selected_capability_roots.iter().cloned());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let mut seen_root_ids = HashSet::with_capacity(selected_capability_roots.len());
|
let mut seen_root_ids = HashSet::with_capacity(selected_capability_roots.len());
|
||||||
selected_capability_roots.retain(|root| seen_root_ids.insert(root.id.clone()));
|
selected_capability_roots.retain(|root| seen_root_ids.insert(root.id.clone()));
|
||||||
@@ -706,11 +698,11 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn test_environment_config() -> TurnEnvironmentConfig {
|
fn test_environment_config() -> EnvironmentConfig {
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,14 +866,14 @@ url = "ws://127.0.0.1:8765"
|
|||||||
shell_type: crate::shell::ShellType::Zsh,
|
shell_type: crate::shell::ShellType::Zsh,
|
||||||
shell_path: std::path::PathBuf::from("/configured/zsh"),
|
shell_path: std::path::PathBuf::from("/configured/zsh"),
|
||||||
};
|
};
|
||||||
let expected_config = TurnEnvironmentConfig {
|
let expected_config = EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
ActivePermissionProfile::read_only(),
|
ActivePermissionProfile::read_only(),
|
||||||
vec![cwd.join("profile-root")],
|
vec![cwd.join("profile-root")],
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
let turn_environments = ThreadEnvironments::new(
|
let turn_environments = ThreadEnvironments::new(
|
||||||
Arc::new(EnvironmentManager::default_for_tests()),
|
Arc::new(EnvironmentManager::default_for_tests()),
|
||||||
@@ -1075,14 +1067,14 @@ url = "ws://127.0.0.1:8765"
|
|||||||
.await,
|
.await,
|
||||||
);
|
);
|
||||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||||
let expected_config = TurnEnvironmentConfig {
|
let expected_config = EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
ActivePermissionProfile::read_only(),
|
ActivePermissionProfile::read_only(),
|
||||||
vec![cwd.join("profile-root")],
|
vec![cwd.join("profile-root")],
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
let cwd = PathUri::from_abs_path(&cwd);
|
let cwd = PathUri::from_abs_path(&cwd);
|
||||||
let remote = TurnEnvironmentSelection {
|
let remote = TurnEnvironmentSelection {
|
||||||
@@ -1244,7 +1236,7 @@ url = "ws://127.0.0.1:8765"
|
|||||||
/*connect_timeout*/ None,
|
/*connect_timeout*/ None,
|
||||||
)
|
)
|
||||||
.expect("replacement environment");
|
.expect("replacement environment");
|
||||||
let next_config = TurnEnvironmentConfig {
|
let next_config = EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
..test_environment_config()
|
..test_environment_config()
|
||||||
};
|
};
|
||||||
@@ -1394,14 +1386,14 @@ url = "ws://127.0.0.1:8765"
|
|||||||
/*connect_timeout*/ None,
|
/*connect_timeout*/ None,
|
||||||
)
|
)
|
||||||
.expect("replacement environment");
|
.expect("replacement environment");
|
||||||
let child_config = TurnEnvironmentConfig {
|
let child_config = EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
ActivePermissionProfile::read_only(),
|
ActivePermissionProfile::read_only(),
|
||||||
vec![cwd.join("child-profile-root")],
|
vec![cwd.join("child-profile-root")],
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
let environments = ThreadEnvironments::new(
|
let environments = ThreadEnvironments::new(
|
||||||
manager,
|
manager,
|
||||||
@@ -1423,7 +1415,7 @@ url = "ws://127.0.0.1:8765"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn installed_environment_config_is_inherited_and_reset_for_new_cwd() {
|
async fn owner_environment_config_is_inherited_and_reset_for_new_cwd() {
|
||||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||||
let selection = TurnEnvironmentSelection {
|
let selection = TurnEnvironmentSelection {
|
||||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||||
@@ -1441,17 +1433,16 @@ url = "ws://127.0.0.1:8765"
|
|||||||
let manager = Arc::new(EnvironmentManager::default_for_tests());
|
let manager = Arc::new(EnvironmentManager::default_for_tests());
|
||||||
let parent =
|
let parent =
|
||||||
resolve_turn_environments(Arc::clone(&manager), std::slice::from_ref(&selection)).await;
|
resolve_turn_environments(Arc::clone(&manager), std::slice::from_ref(&selection)).await;
|
||||||
|
let parent_owner_config = EnvironmentConfig {
|
||||||
|
allow_login_shell: false,
|
||||||
|
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||||
|
selected_capability_roots: vec![root("parent-root")],
|
||||||
|
};
|
||||||
parent
|
parent
|
||||||
.environment_ready(
|
.environment_ready(&selection, parent_owner_config.clone())
|
||||||
&selection,
|
|
||||||
EnvironmentConfig {
|
|
||||||
allow_login_shell: false,
|
|
||||||
selected_capability_roots: vec![root("parent-root")],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("install environment config");
|
.expect("install environment config");
|
||||||
|
|
||||||
let child_thread_config = TurnEnvironmentConfig {
|
let child_thread_config = EnvironmentConfig {
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
PermissionProfile::workspace_write(),
|
PermissionProfile::workspace_write(),
|
||||||
),
|
),
|
||||||
@@ -1468,14 +1459,12 @@ url = "ws://127.0.0.1:8765"
|
|||||||
let child_snapshot = child.snapshot().await;
|
let child_snapshot = child.snapshot().await;
|
||||||
let child_environment = child_snapshot.primary().expect("child environment");
|
let child_environment = child_snapshot.primary().expect("child environment");
|
||||||
|
|
||||||
|
let cleared_parent_owner_config = EnvironmentConfig {
|
||||||
|
selected_capability_roots: Vec::new(),
|
||||||
|
..parent_owner_config.clone()
|
||||||
|
};
|
||||||
parent
|
parent
|
||||||
.environment_ready(
|
.environment_ready(&selection, cleared_parent_owner_config.clone())
|
||||||
&selection,
|
|
||||||
EnvironmentConfig {
|
|
||||||
allow_login_shell: false,
|
|
||||||
selected_capability_roots: Vec::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("clear parent roots");
|
.expect("clear parent roots");
|
||||||
let cleared_snapshot = parent.snapshot().await;
|
let cleared_snapshot = parent.snapshot().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1483,20 +1472,9 @@ url = "ws://127.0.0.1:8765"
|
|||||||
.primary()
|
.primary()
|
||||||
.expect("environment with cleared roots")
|
.expect("environment with cleared roots")
|
||||||
.config,
|
.config,
|
||||||
TurnEnvironmentConfig {
|
cleared_parent_owner_config
|
||||||
allow_login_shell: false,
|
|
||||||
selected_capability_roots: Some(Vec::new()),
|
|
||||||
..test_environment_config()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
child_environment.config,
|
|
||||||
TurnEnvironmentConfig {
|
|
||||||
allow_login_shell: false,
|
|
||||||
selected_capability_roots: Some(vec![root("parent-root")]),
|
|
||||||
..child_thread_config
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
assert_eq!(child_environment.config, parent_owner_config);
|
||||||
|
|
||||||
let changed_selection = TurnEnvironmentSelection {
|
let changed_selection = TurnEnvironmentSelection {
|
||||||
cwd: PathUri::from_abs_path(&cwd.join("changed")),
|
cwd: PathUri::from_abs_path(&cwd.join("changed")),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use codex_protocol::models::BaseInstructionsProvenance;
|
|||||||
use codex_protocol::models::ContentItem;
|
use codex_protocol::models::ContentItem;
|
||||||
use codex_protocol::models::ImageDetail;
|
use codex_protocol::models::ImageDetail;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::models::PermissionProfileSnapshot;
|
||||||
use codex_protocol::models::ResponseInputItem;
|
use codex_protocol::models::ResponseInputItem;
|
||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::openai_models::InputModality;
|
use codex_protocol::openai_models::InputModality;
|
||||||
@@ -28,6 +29,7 @@ use codex_protocol::openai_models::ModelMessages;
|
|||||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::CodexErrorInfo;
|
use codex_protocol::protocol::CodexErrorInfo;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::ErrorEvent;
|
use codex_protocol::protocol::ErrorEvent;
|
||||||
use codex_protocol::protocol::Event;
|
use codex_protocol::protocol::Event;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
@@ -1050,8 +1052,23 @@ async fn run_review_on_session(
|
|||||||
.total_token_usage()
|
.total_token_usage()
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let guardian_permission_profile = params.spawn_config.permissions.permission_profile().clone();
|
let guardian_permission_snapshot = params
|
||||||
let parent_turn_environments = params.parent_context.environments().to_selections();
|
.spawn_config
|
||||||
|
.permissions
|
||||||
|
.permission_profile_state()
|
||||||
|
.snapshot();
|
||||||
|
let mut parent_turn_environments = params.parent_context.environments().to_selections();
|
||||||
|
// FromThread inherits the Guardian profile below. Once Core normalizes FromThread
|
||||||
|
// to Ready(config), every selection will carry explicit config and this conditional can be
|
||||||
|
// removed.
|
||||||
|
for selection in &mut parent_turn_environments {
|
||||||
|
if let EnvironmentConfigState::Ready(config) = &mut selection.config {
|
||||||
|
config.permission_profile =
|
||||||
|
PermissionProfileSnapshot::legacy(read_only_guardian_permission_profile(
|
||||||
|
config.permission_profile.permission_profile(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
// TODO(anp): Migrate guardian review thread settings to a PathUri fallback cwd so foreign
|
// TODO(anp): Migrate guardian review thread settings to a PathUri fallback cwd so foreign
|
||||||
// parent environments do not fall back to the host-native config cwd.
|
// parent environments do not fall back to the host-native config cwd.
|
||||||
let parent_turn_legacy_fallback_cwd = params
|
let parent_turn_legacy_fallback_cwd = params
|
||||||
@@ -1071,7 +1088,7 @@ async fn run_review_on_session(
|
|||||||
)),
|
)),
|
||||||
approval_policy: Some(AskForApproval::Never),
|
approval_policy: Some(AskForApproval::Never),
|
||||||
sandbox_policy: None,
|
sandbox_policy: None,
|
||||||
permission_profile: Some(guardian_permission_profile),
|
permission_profile: Some(guardian_permission_snapshot.permission_profile().clone()),
|
||||||
summary: Some(params.reasoning_summary),
|
summary: Some(params.reasoning_summary),
|
||||||
personality: params.personality,
|
personality: params.personality,
|
||||||
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
|
collaboration_mode: Some(codex_protocol::config_types::CollaborationMode {
|
||||||
@@ -1275,6 +1292,16 @@ fn event_matches_turn(event: &Event, expected_turn_id: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_only_guardian_permission_profile(
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
) -> PermissionProfile {
|
||||||
|
permission_profile
|
||||||
|
.intersect_with_read_only()
|
||||||
|
.unwrap_or(PermissionProfile::External {
|
||||||
|
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_guardian_review_session_config(
|
pub(crate) fn build_guardian_review_session_config(
|
||||||
parent_config: &Config,
|
parent_config: &Config,
|
||||||
live_network_config: Option<codex_network_proxy::NetworkProxyConfig>,
|
live_network_config: Option<codex_network_proxy::NetworkProxyConfig>,
|
||||||
@@ -1303,13 +1330,8 @@ pub(crate) fn build_guardian_review_session_config(
|
|||||||
guardian_config.notify = None;
|
guardian_config.notify = None;
|
||||||
guardian_config.developer_instructions = None;
|
guardian_config.developer_instructions = None;
|
||||||
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||||
let guardian_permission_profile = parent_config
|
let guardian_permission_profile =
|
||||||
.permissions
|
read_only_guardian_permission_profile(parent_config.permissions.permission_profile());
|
||||||
.permission_profile()
|
|
||||||
.intersect_with_read_only()
|
|
||||||
.unwrap_or(PermissionProfile::External {
|
|
||||||
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
|
||||||
});
|
|
||||||
guardian_config
|
guardian_config
|
||||||
.permissions
|
.permissions
|
||||||
.set_permission_profile(guardian_permission_profile)
|
.set_permission_profile(guardian_permission_profile)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use crate::session::tests::make_session_and_context;
|
|||||||
use crate::session::tests::make_session_and_context_with_rx;
|
use crate::session::tests::make_session_and_context_with_rx;
|
||||||
use crate::session::tests::mcp_config_for_test;
|
use crate::session::tests::mcp_config_for_test;
|
||||||
use crate::session::turn_context::TurnEnvironment;
|
use crate::session::turn_context::TurnEnvironment;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::state::ActiveTurn;
|
use crate::state::ActiveTurn;
|
||||||
use crate::test_support::models_manager_with_provider;
|
use crate::test_support::models_manager_with_provider;
|
||||||
use crate::tools::hook_names::HookToolName;
|
use crate::tools::hook_names::HookToolName;
|
||||||
@@ -26,6 +25,7 @@ use codex_hooks::HooksConfig;
|
|||||||
use codex_model_provider::create_model_provider;
|
use codex_model_provider::create_model_provider;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::GranularApprovalConfig;
|
use codex_protocol::protocol::GranularApprovalConfig;
|
||||||
@@ -1116,14 +1116,14 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
|
|||||||
},
|
},
|
||||||
environment,
|
environment,
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: turn_context
|
permission_profile: turn_context
|
||||||
.config
|
.config
|
||||||
.permissions
|
.permissions
|
||||||
.permission_profile_state()
|
.permission_profile_state()
|
||||||
.snapshot(),
|
.snapshot(),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
|||||||
@@ -445,13 +445,11 @@ impl Session {
|
|||||||
.selected_capability_roots
|
.selected_capability_roots
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.chain(environments.turn_environments().flat_map(|environment| {
|
.chain(
|
||||||
environment
|
environments
|
||||||
.config
|
.turn_environments()
|
||||||
.selected_capability_roots
|
.flat_map(|environment| environment.config.selected_capability_roots.clone()),
|
||||||
.clone()
|
)
|
||||||
.unwrap_or_else(|| environment.environment.selected_capability_roots())
|
|
||||||
}))
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
if let Some(kept_location) = root_locations_by_id.get(&root.id) {
|
if let Some(kept_location) = root_locations_by_id.get(&root.id) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use crate::agents_md_manager::AgentsMdManager;
|
|||||||
use crate::config::ConstraintError;
|
use crate::config::ConstraintError;
|
||||||
use crate::environment_selection::ThreadEnvironments;
|
use crate::environment_selection::ThreadEnvironments;
|
||||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::shell_snapshot::ShellSnapshot;
|
use crate::shell_snapshot::ShellSnapshot;
|
||||||
use crate::state::ActiveTurn;
|
use crate::state::ActiveTurn;
|
||||||
use codex_extension_api::ExtensionDataInit;
|
use codex_extension_api::ExtensionDataInit;
|
||||||
@@ -20,6 +19,7 @@ use codex_protocol::config_types::ServiceTier;
|
|||||||
use codex_protocol::mcp::ClientMcpExtensions;
|
use codex_protocol::mcp::ClientMcpExtensions;
|
||||||
use codex_protocol::permissions::FileSystemPath;
|
use codex_protocol::permissions::FileSystemPath;
|
||||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::HookCompletedEvent;
|
use codex_protocol::protocol::HookCompletedEvent;
|
||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
@@ -136,14 +136,14 @@ impl SessionConfiguration {
|
|||||||
&self.permission_profile_state
|
&self.permission_profile_state
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn turn_environment_config(&self) -> TurnEnvironmentConfig {
|
pub(super) fn turn_environment_config(&self) -> EnvironmentConfig {
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: self
|
allow_login_shell: self
|
||||||
.original_config_do_not_use
|
.original_config_do_not_use
|
||||||
.permissions
|
.permissions
|
||||||
.allow_login_shell,
|
.allow_login_shell,
|
||||||
permission_profile: self.permission_profile_state.snapshot(),
|
permission_profile: self.permission_profile_state.snapshot(),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1070,7 +1070,6 @@ impl Session {
|
|||||||
let turn_environments = Arc::new(ThreadEnvironments::new(
|
let turn_environments = Arc::new(ThreadEnvironments::new(
|
||||||
environment_manager,
|
environment_manager,
|
||||||
default_shell.clone(),
|
default_shell.clone(),
|
||||||
// Temporary: preserve thread-level behavior until environments supply config.
|
|
||||||
session_configuration.turn_environment_config(),
|
session_configuration.turn_environment_config(),
|
||||||
shell_snapshot,
|
shell_snapshot,
|
||||||
inherited_environments.unwrap_or_default(),
|
inherited_environments.unwrap_or_default(),
|
||||||
|
|||||||
@@ -8598,25 +8598,10 @@ async fn mcp_refresh_updates_plugin_auth_mode_before_checking_pending_state() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingNoiseConnectProvider;
|
|
||||||
|
|
||||||
impl codex_exec_server::NoiseRendezvousConnectProvider for PendingNoiseConnectProvider {
|
|
||||||
fn connect_bundle(
|
|
||||||
&self,
|
|
||||||
_: codex_exec_server::NoiseChannelPublicKey,
|
|
||||||
) -> futures::future::BoxFuture<
|
|
||||||
'_,
|
|
||||||
Result<codex_exec_server::NoiseRendezvousConnectBundle, codex_exec_server::ExecServerError>,
|
|
||||||
> {
|
|
||||||
Box::pin(futures::future::pending())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[tracing_test::traced_test]
|
#[tracing_test::traced_test]
|
||||||
async fn conflicting_ready_environment_root_ids_keep_first_location() {
|
async fn conflicting_ready_environment_root_ids_keep_first_location() {
|
||||||
let (session, turn_context) = make_session_and_context().await;
|
let (session, turn_context) = make_session_and_context().await;
|
||||||
let environment_manager = session.services.turn_environments.environment_manager();
|
|
||||||
let selected_root =
|
let selected_root =
|
||||||
|environment_id: &str, path: &str| codex_protocol::capabilities::SelectedCapabilityRoot {
|
|environment_id: &str, path: &str| codex_protocol::capabilities::SelectedCapabilityRoot {
|
||||||
id: "shared-root".to_string(),
|
id: "shared-root".to_string(),
|
||||||
@@ -8639,29 +8624,21 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() {
|
|||||||
environment_id,
|
environment_id,
|
||||||
..
|
..
|
||||||
} = &selected_root.location;
|
} = &selected_root.location;
|
||||||
let provider = Arc::new(PendingNoiseConnectProvider);
|
let mut environment_config = local_environment.config.clone();
|
||||||
let environment = environment_manager
|
environment_config.selected_capability_roots = vec![selected_root.clone()];
|
||||||
.materialize_pending_noise_environment(environment_id.clone(), provider.clone())
|
|
||||||
.expect("materialize deferred environment");
|
|
||||||
environment_manager
|
|
||||||
.report_environment_provisioning_status(
|
|
||||||
environment_id.clone(),
|
|
||||||
Ok(codex_exec_server::EnvironmentReadyInfo {
|
|
||||||
selected_capability_roots: vec![selected_root.clone()],
|
|
||||||
}),
|
|
||||||
provider,
|
|
||||||
)
|
|
||||||
.expect("report environment ready");
|
|
||||||
turn_environments.push(TurnEnvironment::new(
|
turn_environments.push(TurnEnvironment::new(
|
||||||
TurnEnvironmentSelection {
|
TurnEnvironmentSelection {
|
||||||
environment_id: environment_id.clone(),
|
environment_id: environment_id.clone(),
|
||||||
cwd: local_environment.cwd().clone(),
|
cwd: local_environment.cwd().clone(),
|
||||||
workspace_roots: local_environment.workspace_roots().to_vec(),
|
workspace_roots: local_environment.workspace_roots().to_vec(),
|
||||||
config: EnvironmentConfigState::FromThread,
|
config: EnvironmentConfigState::Ready(environment_config.clone()),
|
||||||
},
|
},
|
||||||
environment,
|
Arc::new(
|
||||||
|
codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)
|
||||||
|
.expect("create test environment"),
|
||||||
|
),
|
||||||
local_environment.shell.clone(),
|
local_environment.shell.clone(),
|
||||||
local_environment.config.clone(),
|
environment_config,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let environments = TurnEnvironmentSnapshot {
|
let environments = TurnEnvironmentSnapshot {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use codex_file_system::FileSystemSandboxContext;
|
|||||||
use codex_model_provider::SharedModelProvider;
|
use codex_model_provider::SharedModelProvider;
|
||||||
use codex_protocol::SessionId;
|
use codex_protocol::SessionId;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::ErrorEvent;
|
use codex_protocol::protocol::ErrorEvent;
|
||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::ThreadHistoryMode;
|
use codex_protocol::protocol::ThreadHistoryMode;
|
||||||
@@ -31,21 +31,12 @@ use tracing::instrument;
|
|||||||
|
|
||||||
pub(crate) type ShellSnapshotTask = Shared<BoxFuture<'static, Option<Arc<ShellSnapshotFile>>>>;
|
pub(crate) type ShellSnapshotTask = Shared<BoxFuture<'static, Option<Arc<ShellSnapshotFile>>>>;
|
||||||
|
|
||||||
/// Effective per-environment config; fields move here as executor config is migrated.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub(crate) struct TurnEnvironmentConfig {
|
|
||||||
pub(crate) allow_login_shell: bool,
|
|
||||||
pub(crate) permission_profile: PermissionProfileSnapshot,
|
|
||||||
/// None preserves legacy executor roots; Some, including empty, is owner-installed.
|
|
||||||
pub(crate) selected_capability_roots: Option<Vec<SelectedCapabilityRoot>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct TurnEnvironment {
|
pub(crate) struct TurnEnvironment {
|
||||||
pub(crate) selection: TurnEnvironmentSelection,
|
pub(crate) selection: TurnEnvironmentSelection,
|
||||||
pub(crate) environment: Arc<Environment>,
|
pub(crate) environment: Arc<Environment>,
|
||||||
pub(crate) shell: Option<shell::Shell>,
|
pub(crate) shell: Option<shell::Shell>,
|
||||||
pub(crate) config: TurnEnvironmentConfig,
|
pub(crate) config: EnvironmentConfig,
|
||||||
pub(crate) shell_snapshot: ShellSnapshotTask,
|
pub(crate) shell_snapshot: ShellSnapshotTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +45,7 @@ impl TurnEnvironment {
|
|||||||
selection: TurnEnvironmentSelection,
|
selection: TurnEnvironmentSelection,
|
||||||
environment: Arc<Environment>,
|
environment: Arc<Environment>,
|
||||||
shell: Option<shell::Shell>,
|
shell: Option<shell::Shell>,
|
||||||
config: TurnEnvironmentConfig,
|
config: EnvironmentConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
selection,
|
selection,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use crate::sandboxing::SandboxPermissions;
|
|||||||
use crate::session::step_context::StepContext;
|
use crate::session::step_context::StepContext;
|
||||||
use crate::session::tests::make_session_and_context;
|
use crate::session::tests::make_session_and_context;
|
||||||
use crate::session::turn_context::TurnEnvironment;
|
use crate::session::turn_context::TurnEnvironment;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::shell::Shell;
|
use crate::shell::Shell;
|
||||||
use crate::shell::ShellType;
|
use crate::shell::ShellType;
|
||||||
use crate::tools::context::FunctionToolOutput;
|
use crate::tools::context::FunctionToolOutput;
|
||||||
@@ -25,6 +24,7 @@ use crate::tools::handlers::ShellCommandHandler;
|
|||||||
use crate::tools::hook_names::HookToolName;
|
use crate::tools::hook_names::HookToolName;
|
||||||
use crate::tools::registry::CoreToolRuntime;
|
use crate::tools::registry::CoreToolRuntime;
|
||||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
use codex_shell_command::is_safe_command::is_known_safe_command;
|
use codex_shell_command::is_safe_command::is_known_safe_command;
|
||||||
@@ -119,13 +119,13 @@ async fn shell_command_handler_to_exec_params_uses_selected_environment() {
|
|||||||
.environment,
|
.environment,
|
||||||
),
|
),
|
||||||
Some(selected_shell),
|
Some(selected_shell),
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::active(
|
permission_profile: PermissionProfileSnapshot::active(
|
||||||
permission_profile,
|
permission_profile,
|
||||||
active_permission_profile.clone(),
|
active_permission_profile.clone(),
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut expected_env = create_env(
|
let mut expected_env = create_env(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::PermissionProfileSnapshot;
|
use crate::config::PermissionProfileSnapshot;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::tools::sandboxing::SandboxAttempt;
|
use crate::tools::sandboxing::SandboxAttempt;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
use codex_protocol::models::FileSystemPermissions;
|
use codex_protocol::models::FileSystemPermissions;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::GranularApprovalConfig;
|
use codex_protocol::protocol::GranularApprovalConfig;
|
||||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
@@ -28,10 +28,10 @@ fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::
|
|||||||
},
|
},
|
||||||
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
|
std::sync::Arc::new(codex_exec_server::Environment::default_for_tests()),
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::PermissionProfileSnapshot;
|
use crate::config::PermissionProfileSnapshot;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::tools::approvals::ApprovalCacheKey;
|
use crate::tools::approvals::ApprovalCacheKey;
|
||||||
use codex_exec_server::Environment;
|
use codex_exec_server::Environment;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
@@ -25,12 +25,12 @@ async fn approval_key_uses_path_uri_and_includes_environment_id() {
|
|||||||
},
|
},
|
||||||
Arc::new(Environment::default_for_tests()),
|
Arc::new(Environment::default_for_tests()),
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
shell_type: None,
|
shell_type: None,
|
||||||
|
|||||||
@@ -515,11 +515,11 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::PermissionProfileSnapshot;
|
use crate::config::PermissionProfileSnapshot;
|
||||||
use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS;
|
use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS;
|
||||||
use crate::session::turn_context::TurnEnvironmentConfig;
|
|
||||||
use crate::tools::sandboxing::ToolRuntime;
|
use crate::tools::sandboxing::ToolRuntime;
|
||||||
use codex_exec_server::Environment;
|
use codex_exec_server::Environment;
|
||||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||||
use codex_tools::ZshForkConfig;
|
use codex_tools::ZshForkConfig;
|
||||||
@@ -540,12 +540,12 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Arc::new(Environment::default_for_tests()),
|
Arc::new(Environment::default_for_tests()),
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
TurnEnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: PermissionProfileSnapshot::legacy(
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
PermissionProfile::read_only(),
|
PermissionProfile::read_only(),
|
||||||
),
|
),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -917,14 +917,14 @@ async fn zsh_fork_unified_exec_keeps_shell_parameter_when_remote_environment_ava
|
|||||||
.expect("remote test environment"),
|
.expect("remote test environment"),
|
||||||
),
|
),
|
||||||
/*shell*/ None,
|
/*shell*/ None,
|
||||||
crate::session::turn_context::TurnEnvironmentConfig {
|
codex_protocol::protocol::EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
permission_profile: turn
|
permission_profile: turn
|
||||||
.config
|
.config
|
||||||
.permissions
|
.permissions
|
||||||
.permission_profile_state()
|
.permission_profile_state()
|
||||||
.snapshot(),
|
.snapshot(),
|
||||||
selected_capability_roots: None,
|
selected_capability_roots: Vec::new(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ use codex_login::CodexAuth;
|
|||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::models::PermissionProfileSnapshot;
|
||||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||||
use codex_protocol::openai_models::ModelsResponse;
|
use codex_protocol::openai_models::ModelsResponse;
|
||||||
use codex_protocol::permissions::FileSystemAccessMode;
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
@@ -33,6 +34,8 @@ use codex_protocol::permissions::FileSystemSandboxEntry;
|
|||||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfig;
|
||||||
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
use codex_protocol::protocol::SandboxPolicy;
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
@@ -511,6 +514,19 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let mut parent_environments = local_selections(test.config.cwd.clone());
|
||||||
|
let parent_environment_config = EnvironmentConfig {
|
||||||
|
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||||
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
|
test.config.permissions.permission_profile().clone(),
|
||||||
|
),
|
||||||
|
selected_capability_roots: Vec::new(),
|
||||||
|
};
|
||||||
|
parent_environments
|
||||||
|
.environments
|
||||||
|
.first_mut()
|
||||||
|
.expect("local environment selection")
|
||||||
|
.config = EnvironmentConfigState::Ready(parent_environment_config);
|
||||||
|
|
||||||
test.codex
|
test.codex
|
||||||
.start_or_steer_turn(
|
.start_or_steer_turn(
|
||||||
@@ -519,7 +535,7 @@ async fn guardian_session_is_reused_for_consecutive_tool_reviews_without_prewarm
|
|||||||
text_elements: Vec::new(),
|
text_elements: Vec::new(),
|
||||||
}])
|
}])
|
||||||
.with_thread_settings(ThreadSettingsOverrides {
|
.with_thread_settings(ThreadSettingsOverrides {
|
||||||
environments: Some(local_selections(test.config.cwd.clone())),
|
environments: Some(parent_environments),
|
||||||
approval_policy: Some(approval_policy),
|
approval_policy: Some(approval_policy),
|
||||||
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use codex_mcp::McpResourceClient;
|
|||||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::models::PermissionProfileSnapshot;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
use codex_protocol::protocol::Op;
|
use codex_protocol::protocol::Op;
|
||||||
@@ -478,6 +479,9 @@ async fn root_reconciliation_reuses_pending_apps_startup() -> Result<()> {
|
|||||||
&selection,
|
&selection,
|
||||||
EnvironmentConfig {
|
EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
|
test.config.permissions.permission_profile().clone(),
|
||||||
|
),
|
||||||
selected_capability_roots: vec![SelectedCapabilityRoot {
|
selected_capability_roots: vec![SelectedCapabilityRoot {
|
||||||
id: "calendar-root".to_string(),
|
id: "calendar-root".to_string(),
|
||||||
location: CapabilityRootLocation::Environment {
|
location: CapabilityRootLocation::Environment {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ use codex_protocol::config_types::ModeKind;
|
|||||||
use codex_protocol::config_types::Settings;
|
use codex_protocol::config_types::Settings;
|
||||||
use codex_protocol::models::FileSystemPermissions;
|
use codex_protocol::models::FileSystemPermissions;
|
||||||
use codex_protocol::models::PermissionProfile;
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::models::PermissionProfileSnapshot;
|
||||||
use codex_protocol::models::SandboxPermissions;
|
use codex_protocol::models::SandboxPermissions;
|
||||||
use codex_protocol::permissions::FileSystemAccessMode;
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
use codex_protocol::permissions::FileSystemPath;
|
use codex_protocol::permissions::FileSystemPath;
|
||||||
@@ -435,6 +436,105 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn ready_environment_permissions_override_thread_permissions() -> Result<()> {
|
||||||
|
const CALL_ID: &str = "attachment-specific-permissions";
|
||||||
|
const FILE_NAME: &str = "attachment-read-only-marker.txt";
|
||||||
|
|
||||||
|
skip_if_target_windows!(
|
||||||
|
Ok(()),
|
||||||
|
"Windows sandbox enforcement is covered by the platform-specific suite"
|
||||||
|
);
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let mut builder = test_codex().with_config(|config| {
|
||||||
|
config.use_experimental_unified_exec_tool = true;
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.enable(Feature::UnifiedExec)
|
||||||
|
.expect("test config should allow feature update");
|
||||||
|
config
|
||||||
|
.permissions
|
||||||
|
.set_permission_profile(PermissionProfile::workspace_write())
|
||||||
|
.expect("thread should allow workspace writes");
|
||||||
|
});
|
||||||
|
let test = builder.build_with_auto_env(&server).await?;
|
||||||
|
let mut selection = test.executor_environment().selection().clone();
|
||||||
|
let marker = selection.cwd.join(FILE_NAME)?;
|
||||||
|
selection.config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
|
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||||
|
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||||
|
selected_capability_roots: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (shell, command) = match test_target_os() {
|
||||||
|
TestTargetOs::Linux => (
|
||||||
|
"bash",
|
||||||
|
format!(
|
||||||
|
"if printf blocked > {FILE_NAME}; then echo WRITE_SUCCEEDED; else echo WRITE_DENIED; fi"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestTargetOs::MacOs => (
|
||||||
|
"zsh",
|
||||||
|
format!(
|
||||||
|
"if printf blocked > {FILE_NAME}; then echo WRITE_SUCCEEDED; else echo WRITE_DENIED; fi"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TestTargetOs::Windows => (
|
||||||
|
"powershell",
|
||||||
|
format!(
|
||||||
|
"try {{ Set-Content -Path '{FILE_NAME}' -Value blocked -ErrorAction Stop; Write-Output WRITE_SUCCEEDED }} catch {{ Write-Output WRITE_DENIED }}"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let arguments = serde_json::to_string(&json!({
|
||||||
|
"cmd": command,
|
||||||
|
"shell": shell,
|
||||||
|
"login": false,
|
||||||
|
"yield_time_ms": 10_000,
|
||||||
|
"sandbox_permissions": SandboxPermissions::UseDefault,
|
||||||
|
}))?;
|
||||||
|
let response_mock = mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
vec![
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-1"),
|
||||||
|
ev_function_call(CALL_ID, "exec_command", &arguments),
|
||||||
|
ev_completed("resp-1"),
|
||||||
|
]),
|
||||||
|
sse(vec![
|
||||||
|
ev_response_created("resp-2"),
|
||||||
|
ev_assistant_message("msg-1", "done"),
|
||||||
|
ev_completed("resp-2"),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
test.submit_turn_with_environments("try to write a file", Some(vec![selection]))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let output = response_mock
|
||||||
|
.last_request()
|
||||||
|
.context("model should receive the command output")?
|
||||||
|
.function_call_output_text(CALL_ID)
|
||||||
|
.context("shell tool result should be present")?;
|
||||||
|
assert!(
|
||||||
|
output.contains("WRITE_DENIED"),
|
||||||
|
"unexpected output: {output}"
|
||||||
|
);
|
||||||
|
assert!(!output.contains("WRITE_SUCCEEDED"));
|
||||||
|
assert!(
|
||||||
|
test.fs()
|
||||||
|
.read_file_text(&marker, /*sandbox*/ None)
|
||||||
|
.await
|
||||||
|
.is_err(),
|
||||||
|
"read-only attachment unexpectedly wrote {FILE_NAME}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn step_world_state_gates_deferred_prompt_independently_of_host_config() -> Result<()> {
|
async fn step_world_state_gates_deferred_prompt_independently_of_host_config() -> Result<()> {
|
||||||
for deferred_executor_enabled in [false, true] {
|
for deferred_executor_enabled in [false, true] {
|
||||||
@@ -1007,11 +1107,14 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
|||||||
path: selection.cwd.clone(),
|
path: selection.cwd.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
let permission_profile =
|
||||||
|
PermissionProfileSnapshot::legacy(test.config.permissions.permission_profile().clone());
|
||||||
|
|
||||||
for config in [
|
for config in [
|
||||||
EnvironmentConfigState::Pending,
|
EnvironmentConfigState::Pending,
|
||||||
EnvironmentConfigState::Ready(EnvironmentConfig {
|
EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
|
permission_profile: permission_profile.clone(),
|
||||||
selected_capability_roots: vec![root("duplicate"), root("duplicate")],
|
selected_capability_roots: vec![root("duplicate"), root("duplicate")],
|
||||||
}),
|
}),
|
||||||
] {
|
] {
|
||||||
@@ -1045,6 +1148,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
|||||||
environments: Some(vec![TurnEnvironmentSelection {
|
environments: Some(vec![TurnEnvironmentSelection {
|
||||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
allow_login_shell: true,
|
allow_login_shell: true,
|
||||||
|
permission_profile: permission_profile.clone(),
|
||||||
selected_capability_roots: vec![root("startup-root"), root("second-root")],
|
selected_capability_roots: vec![root("startup-root"), root("second-root")],
|
||||||
}),
|
}),
|
||||||
..selection.clone()
|
..selection.clone()
|
||||||
@@ -1062,6 +1166,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
|||||||
vec![TurnEnvironmentSelection {
|
vec![TurnEnvironmentSelection {
|
||||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
|
permission_profile: permission_profile.clone(),
|
||||||
selected_capability_roots: vec![root("first-root")],
|
selected_capability_roots: vec![root("first-root")],
|
||||||
}),
|
}),
|
||||||
..selection.clone()
|
..selection.clone()
|
||||||
@@ -1118,6 +1223,7 @@ async fn shared_executor_keeps_ready_capability_roots_scoped_to_each_attachment(
|
|||||||
vec![TurnEnvironmentSelection {
|
vec![TurnEnvironmentSelection {
|
||||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
allow_login_shell: false,
|
allow_login_shell: false,
|
||||||
|
permission_profile: permission_profile.clone(),
|
||||||
selected_capability_roots: vec![root("first-updated-root")],
|
selected_capability_roots: vec![root("first-updated-root")],
|
||||||
}),
|
}),
|
||||||
..selection.clone()
|
..selection.clone()
|
||||||
@@ -1283,7 +1389,7 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte
|
|||||||
.report_environment_provisioning_status(
|
.report_environment_provisioning_status(
|
||||||
REMOTE_ENVIRONMENT_ID.to_string(),
|
REMOTE_ENVIRONMENT_ID.to_string(),
|
||||||
Ok(EnvironmentReadyInfo {
|
Ok(EnvironmentReadyInfo {
|
||||||
selected_capability_roots: vec![ready_root],
|
selected_capability_roots: Vec::new(),
|
||||||
}),
|
}),
|
||||||
Arc::new(ReadyNoiseConnectProvider {
|
Arc::new(ReadyNoiseConnectProvider {
|
||||||
websocket_url: format!("{rendezvous_url}/relay?role=harness"),
|
websocket_url: format!("{rendezvous_url}/relay?role=harness"),
|
||||||
@@ -1327,7 +1433,13 @@ async fn ready_before_selection_exposes_remote_tools_and_capability_context_afte
|
|||||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||||
cwd: PathUri::from_abs_path(&test.config.cwd),
|
cwd: PathUri::from_abs_path(&test.config.cwd),
|
||||||
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
||||||
config: EnvironmentConfigState::FromThread,
|
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||||
|
allow_login_shell: true,
|
||||||
|
permission_profile: PermissionProfileSnapshot::legacy(
|
||||||
|
test.config.permissions.permission_profile().clone(),
|
||||||
|
),
|
||||||
|
selected_capability_roots: vec![ready_root],
|
||||||
|
}),
|
||||||
}]),
|
}]),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::capabilities::SelectedCapabilityRoot;
|
use crate::capabilities::SelectedCapabilityRoot;
|
||||||
|
use crate::models::PermissionProfileSnapshot;
|
||||||
|
|
||||||
/// Configuration supplied for a thread's selected environment.
|
/// Configuration supplied for a thread's selected environment.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@@ -11,11 +12,13 @@ pub enum EnvironmentConfigState {
|
|||||||
Ready(EnvironmentConfig),
|
Ready(EnvironmentConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved configuration supplied by the owner of a thread/environment attachment.
|
/// Resolved configuration for a thread/environment attachment.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct EnvironmentConfig {
|
pub struct EnvironmentConfig {
|
||||||
/// Whether shell tools may start login shells in this environment.
|
/// Whether shell tools may start login shells in this environment.
|
||||||
pub allow_login_shell: bool,
|
pub allow_login_shell: bool,
|
||||||
|
/// Resolved permissions for this thread's environment attachment.
|
||||||
|
pub permission_profile: PermissionProfileSnapshot,
|
||||||
/// Capability roots selected for this thread's environment attachment.
|
/// Capability roots selected for this thread's environment attachment.
|
||||||
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
pub selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user