mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Bind capability roots to environment selections (#51493)
## Why Capability discovery needs the roots captured with each environment snapshot. Threads sharing an executor can select different roots, and changing environment order must preserve executor skill aliases. ## What changed - Attach the receiving thread's capability roots when constructing `TurnEnvironmentSelection` at startup and settings updates, including roots loaded from saved history. - Resolve capability roots from environment snapshots while preserving their original order. Retain startup roots so deselecting and reselecting an environment restores its capabilities. - Match inherited configuration by environment and workspace so differences in capability root metadata do not interrupt configuration propagation. ## Testing Add regression coverage for root ordering across environment states and configuration changes, independent root selections on a shared executor, reselection with startup or saved roots, active turn updates, and pending configuration failures reaching child and grandchild threads. GitOrigin-RevId: 4f2e4bb414d27195a6752406117a6050718a1198
This commit is contained in:
committed by
copyberry
parent
28cd3e2501
commit
9479e1fdb7
@@ -31,6 +31,7 @@ use crate::thread_manager::ForkSnapshot;
|
||||
use crate::thread_manager::StartThreadOptions;
|
||||
use crate::tools::handlers::multi_agents_common::thread_spawn_source;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_extension_api::Instructions;
|
||||
use codex_extension_api::LoadInstructionsFuture;
|
||||
use codex_extension_api::LoadedUserInstructions;
|
||||
@@ -45,6 +46,8 @@ use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
@@ -1925,6 +1928,7 @@ async fn v2_spawn_resolves_reported_effort_without_changing_child_selection(
|
||||
parent.shutdown_and_wait().await.expect("shutdown parent");
|
||||
}
|
||||
|
||||
/// Pending configuration reaches descendants even when their root metadata differs.
|
||||
#[tokio::test]
|
||||
async fn pending_environment_failure_reaches_child_and_grandchild() {
|
||||
let (home, mut config) = test_config().await;
|
||||
@@ -1940,15 +1944,31 @@ async fn pending_environment_failure_reaches_child_and_grandchild() {
|
||||
let harness = AgentControlHarness::new_with_config(home, config).await;
|
||||
let cwd = PathUri::from_abs_path(&harness.config.codex_home);
|
||||
let pending = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: vec![cwd],
|
||||
workspace_roots: vec![cwd.clone()],
|
||||
config: EnvironmentConfigState::Pending,
|
||||
};
|
||||
// Only the second root's environment is selected. Inherited root lists can therefore
|
||||
// give this root a different position without changing which configuration to follow.
|
||||
let roots = ["unselected", codex_exec_server::LOCAL_ENVIRONMENT_ID]
|
||||
.into_iter()
|
||||
.map(|environment_id| SelectedCapabilityRoot {
|
||||
id: environment_id.to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: environment_id.to_string(),
|
||||
path: cwd.clone(),
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
thread_extension_init.insert(roots);
|
||||
let root = harness
|
||||
.manager
|
||||
.start_thread(StartThreadOptions {
|
||||
environments: Some(vec![pending.clone().into_request()]),
|
||||
thread_extension_init,
|
||||
..StartThreadOptions::new(harness.config.clone())
|
||||
})
|
||||
.await
|
||||
@@ -1969,8 +1989,16 @@ async fn pending_environment_failure_reaches_child_and_grandchild() {
|
||||
.get_thread(agent.thread_id)
|
||||
.await
|
||||
.expect("get descendant");
|
||||
assert_eq!(thread.environment_selections().await, vec![pending.clone()]);
|
||||
descendants.push(Arc::clone(&thread));
|
||||
let selections = thread.environment_selections().await;
|
||||
assert_eq!(
|
||||
selections
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::into_request)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![pending.clone().into_request()],
|
||||
);
|
||||
descendants.push((Arc::clone(&thread), selections));
|
||||
parent = thread;
|
||||
}
|
||||
|
||||
@@ -1978,13 +2006,12 @@ async fn pending_environment_failure_reaches_child_and_grandchild() {
|
||||
root.environment_failed(&pending, error.to_string())
|
||||
.await
|
||||
.expect("fail root environment");
|
||||
let failed = TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Failed(error.to_string()),
|
||||
..pending
|
||||
};
|
||||
timeout(Duration::from_secs(/*secs*/ 5), async {
|
||||
for thread in descendants {
|
||||
while thread.environment_selections().await != [failed.clone()] {
|
||||
for (thread, mut expected) in descendants {
|
||||
for selection in &mut expected {
|
||||
selection.config = EnvironmentConfigState::Failed(error.to_string());
|
||||
}
|
||||
while thread.environment_selections().await != expected {
|
||||
sleep(Duration::from_millis(/*millis*/ 10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,7 @@ fn resolved_local_environments<const N: usize>(
|
||||
.map(|(environment_id, cwd)| {
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment_id.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -749,7 +749,8 @@ impl CodexThread {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
},
|
||||
environments: environment_requests.map(TurnEnvironmentRequests::select),
|
||||
environments: environment_requests
|
||||
.map(|requests| requests.select(&self.session.services.selected_capability_roots)),
|
||||
runtime_workspace_roots,
|
||||
profile_workspace_roots,
|
||||
sandbox_policy,
|
||||
|
||||
@@ -426,6 +426,7 @@ fn failure_context_limits_total_detail_bytes_at_utf8_boundaries() {
|
||||
.map(|index| TurnEnvironmentState::Failed {
|
||||
required_skills: Vec::new(),
|
||||
selection: TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: format!("remote-{index}"),
|
||||
cwd: PathUri::parse("file:///workspace").unwrap(),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_exec_server::ExecServerError;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::SelectedCapabilityRootsStatus;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::EnvironmentCapabilityRoots;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
@@ -418,9 +419,7 @@ impl ThreadEnvironments {
|
||||
}
|
||||
let previous = current.iter().find(|environment| {
|
||||
let previous = &environment.selection;
|
||||
previous.environment_id == selected_environment.environment_id
|
||||
&& previous.cwd == selected_environment.cwd
|
||||
&& previous.workspace_roots == selected_environment.workspace_roots
|
||||
previous.has_same_workspace(selected_environment)
|
||||
});
|
||||
let (selected_environment, config_origin) =
|
||||
resolve_selection_config(selected_environment.clone(), thread_defaults);
|
||||
@@ -1058,6 +1057,18 @@ pub struct TurnEnvironmentSnapshot {
|
||||
}
|
||||
|
||||
impl TurnEnvironmentSnapshot {
|
||||
/// Keeps the original root order for child threads and saved history.
|
||||
pub(crate) fn selected_capability_roots(&self) -> Vec<SelectedCapabilityRoot> {
|
||||
EnvironmentCapabilityRoots::collect(self.environments.iter().map(|environment| {
|
||||
let selection = match environment {
|
||||
TurnEnvironmentState::Ready(environment) => &environment.selection,
|
||||
TurnEnvironmentState::Starting(environment) => &environment.selection,
|
||||
TurnEnvironmentState::Failed { selection, .. } => selection,
|
||||
};
|
||||
&selection.selected_capability_roots
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn has_full_access(
|
||||
&self,
|
||||
approval_policy: AskForApproval,
|
||||
@@ -1550,6 +1561,7 @@ url = "ws://127.0.0.1:8765"
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
turn_environments.update_selections(&[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1582,6 +1594,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let manager = Arc::new(EnvironmentManager::default_for_tests());
|
||||
let first = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1593,6 +1606,7 @@ url = "ws://127.0.0.1:8765"
|
||||
&[
|
||||
first.clone(),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.join("other").expect("other cwd URI"),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1615,6 +1629,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let resolved = resolve_turn_environments(
|
||||
Arc::clone(&manager),
|
||||
&[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "local".to_string(),
|
||||
cwd: selected_cwd_uri,
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1655,6 +1670,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let manager = Arc::new(EnvironmentManager::default_for_tests());
|
||||
let local = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1665,6 +1681,7 @@ url = "ws://127.0.0.1:8765"
|
||||
manager,
|
||||
&[
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "missing".to_string(),
|
||||
cwd: cwd_uri,
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1708,6 +1725,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.await,
|
||||
);
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1814,12 +1832,14 @@ url = "ws://127.0.0.1:8765"
|
||||
};
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
let remote = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
};
|
||||
let local = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd,
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -1999,6 +2019,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.await,
|
||||
);
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd")),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2101,6 +2122,7 @@ url = "ws://127.0.0.1:8765"
|
||||
.await,
|
||||
);
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2186,6 +2208,7 @@ url = "ws://127.0.0.1:8765"
|
||||
async fn inherited_environment_reuses_parent_handle_and_uses_child_config() {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2272,6 +2295,7 @@ url = "ws://127.0.0.1:8765"
|
||||
async fn owner_environment_config_is_inherited_and_reset_for_new_cwd() {
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2375,6 +2399,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let local = resolve_turn_environments(
|
||||
Arc::clone(&local_manager),
|
||||
&[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2390,6 +2415,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let remote = TurnEnvironmentSnapshot {
|
||||
environments: vec![TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: remote_cwd_uri.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2404,6 +2430,7 @@ url = "ws://127.0.0.1:8765"
|
||||
environments: vec![
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: remote_cwd_uri,
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2431,6 +2458,7 @@ url = "ws://127.0.0.1:8765"
|
||||
let snapshot = TurnEnvironmentSnapshot {
|
||||
environments: vec![TurnEnvironmentState::Starting(StartingTurnEnvironment {
|
||||
selection: TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -12,10 +12,125 @@ use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Roots stay attached through config changes and retain API ordering across environment states.
|
||||
#[tokio::test]
|
||||
async fn capability_roots_follow_attachments_through_configuration_changes() {
|
||||
let manager = Arc::new(EnvironmentManager::default_for_tests());
|
||||
manager
|
||||
.upsert_environment(
|
||||
"other".to_string(),
|
||||
"http://example.com".to_string(),
|
||||
/*connect_timeout*/ None,
|
||||
)
|
||||
.expect("register other environment");
|
||||
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd"));
|
||||
let mut local = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
config: EnvironmentConfigState::Pending,
|
||||
};
|
||||
let mut other = TurnEnvironmentSelection {
|
||||
environment_id: "other".to_string(),
|
||||
config: EnvironmentConfigState::Failed("offline".to_string()),
|
||||
..local.clone()
|
||||
};
|
||||
let roots = [(&other, "first"), (&local, "second"), (&other, "third")]
|
||||
.into_iter()
|
||||
.map(|(selection, id)| SelectedCapabilityRoot {
|
||||
id: id.to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: selection.environment_id.clone(),
|
||||
path: cwd.clone(),
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
local.selected_capability_roots =
|
||||
EnvironmentCapabilityRoots::for_environment(&local.environment_id, &roots);
|
||||
other.selected_capability_roots =
|
||||
EnvironmentCapabilityRoots::for_environment(&other.environment_id, &roots);
|
||||
let parent = ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
crate::shell::default_user_shell(),
|
||||
ThreadEnvironmentDefaults::new(tests::test_environment_config(), SandboxType::None),
|
||||
ShellSnapshot::disabled(),
|
||||
TurnEnvironmentSnapshot::default(),
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
parent.update_selections(&[local.clone(), other.clone()]);
|
||||
let snapshot = parent.snapshot().await;
|
||||
assert_eq!(snapshot.selected_capability_roots(), roots);
|
||||
assert!(matches!(
|
||||
&snapshot.environments[0],
|
||||
TurnEnvironmentState::Starting(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
&snapshot.environments[1],
|
||||
TurnEnvironmentState::Failed { .. }
|
||||
));
|
||||
|
||||
let child = ThreadEnvironments::new(
|
||||
Arc::clone(&manager),
|
||||
crate::shell::default_user_shell(),
|
||||
ThreadEnvironmentDefaults::new(tests::test_environment_config(), SandboxType::None),
|
||||
ShellSnapshot::disabled(),
|
||||
snapshot,
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
assert_eq!(
|
||||
child.snapshot().await.selected_capability_roots(),
|
||||
vec![roots[1].clone()]
|
||||
);
|
||||
|
||||
local.config = EnvironmentConfigState::Ready(tests::test_environment_config());
|
||||
parent.update_selections(&[local.clone(), other.clone()]);
|
||||
let ready = parent.snapshot().await;
|
||||
assert!(matches!(
|
||||
&ready.environments[0],
|
||||
TurnEnvironmentState::Ready(_)
|
||||
));
|
||||
assert_eq!(ready.selected_capability_roots(), roots);
|
||||
|
||||
local.cwd = cwd.join("changed-cwd").expect("changed cwd");
|
||||
parent.update_selections(&[other.clone(), local.clone()]);
|
||||
assert_eq!(parent.snapshot().await.selected_capability_roots(), roots);
|
||||
|
||||
// A second thread selects different roots on the same executor without changing the parent.
|
||||
let independent_roots = vec![SelectedCapabilityRoot {
|
||||
id: "independent".to_string(),
|
||||
..roots[1].clone()
|
||||
}];
|
||||
local.selected_capability_roots =
|
||||
EnvironmentCapabilityRoots::for_environment(&local.environment_id, &independent_roots);
|
||||
child.update_selections(std::slice::from_ref(&local));
|
||||
assert_eq!(
|
||||
child.snapshot().await.selected_capability_roots(),
|
||||
independent_roots
|
||||
);
|
||||
assert_eq!(parent.snapshot().await.selected_capability_roots(), roots);
|
||||
assert!(Arc::ptr_eq(
|
||||
&child
|
||||
.snapshot()
|
||||
.await
|
||||
.primary()
|
||||
.expect("child executor")
|
||||
.environment,
|
||||
&parent
|
||||
.snapshot()
|
||||
.await
|
||||
.turn_environments()
|
||||
.find(|environment| environment.selection.environment_id == LOCAL_ENVIRONMENT_ID)
|
||||
.expect("parent executor")
|
||||
.environment,
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unpolled_snapshot_does_not_delay_canceling_a_removed_environment() {
|
||||
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("cwd"));
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: vec![cwd],
|
||||
@@ -68,6 +183,7 @@ async fn updating_another_environment_retries_the_executor_without_canceling_pen
|
||||
.unwrap();
|
||||
let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().unwrap());
|
||||
let mut local = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd,
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -159,6 +275,7 @@ async fn credential_refresh_does_not_restore_a_removed_environment() {
|
||||
/*non_blocking_snapshots*/ false,
|
||||
));
|
||||
environments.update_selections(&[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.clone(),
|
||||
workspace_roots: vec![cwd_uri],
|
||||
|
||||
@@ -1303,6 +1303,7 @@ async fn mcp_sandbox_cwd_uses_matching_server_environment_uri() -> anyhow::Resul
|
||||
.environments
|
||||
.push(TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "remote".to_string(),
|
||||
cwd: secondary_cwd.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -190,11 +190,10 @@ impl Session {
|
||||
let mut environments = configuration.environments.clone();
|
||||
let latest_environments = &state.session_configuration.environments;
|
||||
for environment in &mut environments {
|
||||
if let Some(latest) = latest_environments.iter().find(|latest| {
|
||||
latest.environment_id == environment.environment_id
|
||||
&& latest.cwd == environment.cwd
|
||||
&& latest.workspace_roots == environment.workspace_roots
|
||||
}) {
|
||||
if let Some(latest) = latest_environments
|
||||
.iter()
|
||||
.find(|latest| latest.has_same_workspace(environment))
|
||||
{
|
||||
environment.config = latest.config.clone();
|
||||
}
|
||||
}
|
||||
@@ -237,10 +236,12 @@ impl Session {
|
||||
inherited: &TurnEnvironmentSnapshot,
|
||||
selected: &[TurnEnvironmentSelection],
|
||||
) {
|
||||
for starting in inherited
|
||||
.starting()
|
||||
.filter(|starting| selected.contains(&starting.selection))
|
||||
{
|
||||
for starting in inherited.starting().filter(|starting| {
|
||||
selected.iter().any(|selection| {
|
||||
selection.has_same_workspace(&starting.selection)
|
||||
&& matches!(selection.config, EnvironmentConfigState::Pending)
|
||||
})
|
||||
}) {
|
||||
let Some(configuration) = starting.owner_configuration() else {
|
||||
continue;
|
||||
};
|
||||
@@ -285,9 +286,7 @@ impl Session {
|
||||
let mut future = state.session_configuration.environments.clone();
|
||||
let inherited = matches!(source, ConfigUpdateSource::Inherited);
|
||||
let matches = |environment: &TurnEnvironmentSelection| {
|
||||
environment.environment_id == selection.environment_id
|
||||
&& environment.cwd == selection.cwd
|
||||
&& environment.workspace_roots == selection.workspace_roots
|
||||
environment.has_same_workspace(selection)
|
||||
&& (!inherited || matches!(environment.config, EnvironmentConfigState::Pending))
|
||||
};
|
||||
let environments = (
|
||||
|
||||
@@ -501,12 +501,13 @@ impl Session {
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
) -> Vec<ResolvedSelectedCapabilityRoot> {
|
||||
let captured_environments = environments.captured_environments();
|
||||
let thread_root_count = self.services.selected_capability_roots.len();
|
||||
let thread_roots = environments.selected_capability_roots();
|
||||
let thread_root_count = thread_roots.len();
|
||||
let mut root_locations_by_id = HashMap::new();
|
||||
let mut selected_capability_roots = Vec::new();
|
||||
let mut ready_environment_root_count = 0;
|
||||
let combined_roots = combine_selected_capability_roots(
|
||||
&self.services.selected_capability_roots,
|
||||
&thread_roots,
|
||||
environments.turn_environments().map(|environment| {
|
||||
(
|
||||
environment.config_origin,
|
||||
|
||||
@@ -112,6 +112,7 @@ use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
use codex_protocol::approvals::NetworkPolicyRuleAction;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
@@ -586,7 +587,7 @@ impl Session {
|
||||
parent_rollout_thread_trace,
|
||||
parent_trace: _,
|
||||
environment_requests,
|
||||
thread_extension_init,
|
||||
mut thread_extension_init,
|
||||
turn_extension_init,
|
||||
client_mcp_extensions,
|
||||
reserved_thread_id,
|
||||
@@ -606,9 +607,21 @@ impl Session {
|
||||
.get::<codex_extension_api::SessionIsolation>()
|
||||
.map(|policy| *policy)
|
||||
.unwrap_or_default();
|
||||
// Manager-owned threads and inline delegates bind roots at the same startup boundary.
|
||||
let selected_capability_roots =
|
||||
match thread_extension_init.get::<Vec<SelectedCapabilityRoot>>() {
|
||||
Some(roots) => roots.as_ref().clone(),
|
||||
None => {
|
||||
let roots = conversation_history.get_selected_capability_roots();
|
||||
if !roots.is_empty() {
|
||||
thread_extension_init.insert(roots.clone());
|
||||
}
|
||||
roots
|
||||
}
|
||||
};
|
||||
let environment_selections = environment_requests
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::new)
|
||||
.map(|request| TurnEnvironmentSelection::new(request, &selected_capability_roots))
|
||||
.collect::<Vec<_>>();
|
||||
// Enforce snapshot-only instructions for both managed and inline isolated sessions.
|
||||
let instructions = if isolation == codex_extension_api::SessionIsolation::Isolated {
|
||||
|
||||
@@ -986,17 +986,10 @@ impl Session {
|
||||
config.current_time_reminder.as_ref(),
|
||||
external_time_provider,
|
||||
)?;
|
||||
let selected_capability_roots =
|
||||
match thread_extension_init.get::<Vec<SelectedCapabilityRoot>>() {
|
||||
Some(roots) => roots.as_ref().clone(),
|
||||
None => {
|
||||
let roots = initial_history.get_selected_capability_roots();
|
||||
if !roots.is_empty() {
|
||||
thread_extension_init.insert(roots.clone());
|
||||
}
|
||||
roots
|
||||
}
|
||||
};
|
||||
let selected_capability_roots = thread_extension_init
|
||||
.get::<Vec<SelectedCapabilityRoot>>()
|
||||
.map(|roots| roots.as_ref().clone())
|
||||
.unwrap_or_default();
|
||||
thread_extension_init.insert(codex_extension_api::ThreadOriginator(
|
||||
session_configuration.originator.clone(),
|
||||
));
|
||||
|
||||
@@ -275,7 +275,9 @@ impl Session {
|
||||
let environments = environment_requests.map(|requests| {
|
||||
requests
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::new)
|
||||
.map(|request| {
|
||||
TurnEnvironmentSelection::new(request, &self.services.selected_capability_roots)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let updates_step_settings = updates_model_settings || approvals_reviewer.is_some();
|
||||
|
||||
@@ -7858,6 +7858,7 @@ async fn request_permissions_tool_resolves_legacy_paths_against_selected_environ
|
||||
user_home_dir: Some(PathUri::from_abs_path(&environment_home)),
|
||||
..TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "remote".to_string(),
|
||||
cwd: PathUri::from_abs_path(&environment_cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -8537,6 +8538,7 @@ async fn primary_environment_uses_first_turn_environment() {
|
||||
.environments
|
||||
.push(TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "second".to_string(),
|
||||
cwd: second_cwd_uri.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -9579,7 +9581,7 @@ async fn refresh_mcp_servers_uses_latest_state_for_existing_turns() {
|
||||
let rematerialized_old = session
|
||||
.mcp_runtime_for_step(
|
||||
&turn_context,
|
||||
/*selected_capability_roots*/ &[],
|
||||
&[],
|
||||
/*required_servers*/ &[],
|
||||
/*required_plugins*/ &HashSet::new(),
|
||||
)
|
||||
@@ -10025,10 +10027,11 @@ async fn mcp_refresh_detects_shared_auth_manager_changes() {
|
||||
#[tokio::test]
|
||||
async fn capability_roots_require_turn_environment_selection() {
|
||||
let (mut session, turn_context) = make_session_and_context().await;
|
||||
let environment = turn_context
|
||||
let mut environment = turn_context
|
||||
.initial_environments
|
||||
.primary()
|
||||
.expect("ready local environment");
|
||||
.expect("ready local environment")
|
||||
.clone();
|
||||
let root = codex_protocol::capabilities::SelectedCapabilityRoot {
|
||||
id: "selected-root".to_string(),
|
||||
location: codex_protocol::capabilities::CapabilityRootLocation::Environment {
|
||||
@@ -10037,6 +10040,13 @@ async fn capability_roots_require_turn_environment_selection() {
|
||||
},
|
||||
};
|
||||
session.services.selected_capability_roots = vec![root.clone()];
|
||||
environment.selection = TurnEnvironmentSelection::new(
|
||||
environment.selection.into_request(),
|
||||
std::slice::from_ref(&root),
|
||||
);
|
||||
let selected_environments = TurnEnvironmentSnapshot {
|
||||
environments: vec![TurnEnvironmentState::Ready(environment)],
|
||||
};
|
||||
|
||||
let unselected_roots = session
|
||||
.resolve_selected_capability_roots_for_step(&TurnEnvironmentSnapshot::default())
|
||||
@@ -10044,7 +10054,7 @@ async fn capability_roots_require_turn_environment_selection() {
|
||||
assert!(unselected_roots.is_empty());
|
||||
|
||||
let selected_roots = session
|
||||
.resolve_selected_capability_roots_for_step(&turn_context.initial_environments)
|
||||
.resolve_selected_capability_roots_for_step(&selected_environments)
|
||||
.await;
|
||||
assert_eq!(
|
||||
selected_roots
|
||||
@@ -10085,6 +10095,7 @@ async fn conflicting_ready_environment_root_ids_keep_first_location() {
|
||||
environment_config.selected_capability_roots = vec![selected_root.clone()];
|
||||
turn_environments.push(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment_id.clone(),
|
||||
cwd: local_environment.cwd().clone(),
|
||||
workspace_roots: local_environment.workspace_roots().to_vec(),
|
||||
@@ -10375,6 +10386,7 @@ async fn record_context_updates_emits_environment_item_for_cwd_changes() {
|
||||
current_context.initial_environments.environments[0] =
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment.selection.environment_id,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -10436,6 +10448,7 @@ async fn record_context_updates_use_environment_permission_profile_and_workspace
|
||||
current_context.initial_environments.environments[0] =
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment.selection.environment_id,
|
||||
cwd,
|
||||
workspace_roots: vec![PathUri::from_abs_path(&workspace_root)],
|
||||
@@ -10513,6 +10526,7 @@ async fn record_context_updates_omits_environment_item_when_disabled() {
|
||||
current_context.initial_environments.environments[0] =
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment.selection.environment_id,
|
||||
cwd: PathUri::from_abs_path(&test_path_buf("/new-repo").abs()),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -11080,6 +11094,7 @@ async fn turn_context_item_stores_local_cwd() {
|
||||
turn_context.initial_environments.environments[0] =
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "remote".to_string(),
|
||||
cwd,
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::step_settings::StepSettingsUpdate;
|
||||
use crate::WithTurnExtensionData;
|
||||
use crate::config::ConstraintResult;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
|
||||
@@ -34,7 +35,7 @@ pub(super) async fn update(
|
||||
session: &Session,
|
||||
overrides: impl Into<WithTurnExtensionData<ThreadSettingsOverrides>>,
|
||||
) -> ConstraintResult<ThreadSettingsSnapshot> {
|
||||
let updates = prepare_update(overrides);
|
||||
let updates = prepare_update(overrides, &session.services.selected_capability_roots);
|
||||
let commit = session.update_settings(updates).await?;
|
||||
// Standalone settings changes supersede a pending automatic continuation.
|
||||
session.state.lock().await.last_started_turn_id = None;
|
||||
@@ -44,6 +45,7 @@ pub(super) async fn update(
|
||||
/// Converts protocol overrides into the internal settings update shape.
|
||||
pub(super) fn prepare_update(
|
||||
overrides: impl Into<WithTurnExtensionData<ThreadSettingsOverrides>>,
|
||||
roots: &[SelectedCapabilityRoot],
|
||||
) -> SessionSettingsUpdate {
|
||||
let WithTurnExtensionData {
|
||||
request: overrides,
|
||||
@@ -79,8 +81,7 @@ pub(super) fn prepare_update(
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
},
|
||||
environments: environment_requests
|
||||
.map(codex_protocol::protocol::TurnEnvironmentRequests::select),
|
||||
environments: environment_requests.map(|requests| requests.select(roots)),
|
||||
runtime_workspace_roots,
|
||||
profile_workspace_roots,
|
||||
sandbox_policy,
|
||||
|
||||
@@ -107,7 +107,10 @@ impl PreparedTurnInputSettings {
|
||||
{
|
||||
None
|
||||
} else {
|
||||
let updates = thread_settings::prepare_update(thread_settings);
|
||||
let updates = thread_settings::prepare_update(
|
||||
thread_settings,
|
||||
&session.services.selected_capability_roots,
|
||||
);
|
||||
session
|
||||
.preview_settings(&updates)
|
||||
.await
|
||||
|
||||
@@ -417,10 +417,13 @@ async fn prepared_user_updates_merge_with_settings_at_turn_start() {
|
||||
},
|
||||
};
|
||||
session
|
||||
.update_settings(thread_settings::prepare_update(ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(initial.clone()),
|
||||
..Default::default()
|
||||
}))
|
||||
.update_settings(thread_settings::prepare_update(
|
||||
ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(initial.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
&session.services.selected_capability_roots,
|
||||
))
|
||||
.await
|
||||
.expect("set initial model and effort");
|
||||
let prepared =
|
||||
@@ -428,7 +431,10 @@ async fn prepared_user_updates_merge_with_settings_at_turn_start() {
|
||||
.await
|
||||
.expect("prepare partial protocol update");
|
||||
session
|
||||
.update_settings(thread_settings::prepare_update(intervening))
|
||||
.update_settings(thread_settings::prepare_update(
|
||||
intervening,
|
||||
&session.services.selected_capability_roots,
|
||||
))
|
||||
.await
|
||||
.expect("commit intervening settings");
|
||||
|
||||
@@ -466,10 +472,13 @@ async fn automatic_admission_uses_current_candidate_after_plan_preview() {
|
||||
let mut plan_mode = default_mode.clone();
|
||||
plan_mode.mode = ModeKind::Plan;
|
||||
session
|
||||
.update_settings(thread_settings::prepare_update(ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(plan_mode),
|
||||
..Default::default()
|
||||
}))
|
||||
.update_settings(thread_settings::prepare_update(
|
||||
ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(plan_mode),
|
||||
..Default::default()
|
||||
},
|
||||
&session.services.selected_capability_roots,
|
||||
))
|
||||
.await
|
||||
.expect("enter Plan after the initial admission check");
|
||||
let prepared = PreparedTurnInputSettings::prepare(
|
||||
@@ -483,10 +492,13 @@ async fn automatic_admission_uses_current_candidate_after_plan_preview() {
|
||||
.await
|
||||
.expect("validate the patch while the preview is Plan");
|
||||
session
|
||||
.update_settings(thread_settings::prepare_update(ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(default_mode.clone()),
|
||||
..Default::default()
|
||||
}))
|
||||
.update_settings(thread_settings::prepare_update(
|
||||
ThreadSettingsOverrides {
|
||||
collaboration_mode: Some(default_mode.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
&session.services.selected_capability_roots,
|
||||
))
|
||||
.await
|
||||
.expect("leave Plan before atomic admission");
|
||||
|
||||
@@ -642,7 +654,10 @@ async fn automatic_admission_rechecks_plan_mode_without_committing_sparse_settin
|
||||
// The rejected candidate is valid and would have real runtime effects if
|
||||
// accepted by an ordinary settings update.
|
||||
session
|
||||
.update_settings(thread_settings::prepare_update(overrides))
|
||||
.update_settings(thread_settings::prepare_update(
|
||||
overrides,
|
||||
&session.services.selected_capability_roots,
|
||||
))
|
||||
.await
|
||||
.expect("explicit settings update accepts the same patch");
|
||||
assert_eq!(
|
||||
|
||||
@@ -322,6 +322,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() -
|
||||
/*non_blocking_snapshots*/ false,
|
||||
);
|
||||
environments.update_selections(&[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&dir.path().abs()),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -86,8 +86,8 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) thread_extension_data: ExtensionData,
|
||||
/// MCP extensions fixed when this session is created.
|
||||
pub(crate) client_mcp_extensions: ClientMcpExtensions,
|
||||
/// Raw capability selections for this thread. Each model step resolves them against its
|
||||
/// current executor environments before using them.
|
||||
/// Roots supplied when the thread started or loaded from saved history.
|
||||
/// Keep roots for unselected environments so selecting them again restores their capabilities.
|
||||
pub(crate) selected_capability_roots: Vec<SelectedCapabilityRoot>,
|
||||
pub(crate) mcp_thread_init: ExtensionDataInit,
|
||||
pub(crate) agent_control: Arc<dyn AgentControl>,
|
||||
|
||||
@@ -1990,6 +1990,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
|
||||
AbsolutePathBuf::try_from(config.cwd.as_path().join("selected")).expect("absolute path");
|
||||
std::fs::create_dir_all(&selected_cwd).expect("create selected cwd");
|
||||
let environments = vec![TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "local".to_string(),
|
||||
cwd: PathUri::from_abs_path(&selected_cwd),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::collections::HashMap;
|
||||
fn test_turn_environment(environment_id: &str) -> crate::session::turn_context::TurnEnvironment {
|
||||
crate::session::turn_context::TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: environment_id.to_string(),
|
||||
cwd: PathUri::from_abs_path(&std::env::temp_dir().abs()),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -806,6 +806,7 @@ mod tests {
|
||||
fn test_turn_environment(cwd: PathUri) -> TurnEnvironment {
|
||||
TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
cwd,
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn local_request(cwd: AbsolutePathBuf) -> TurnEnvironmentRequest {
|
||||
}
|
||||
|
||||
pub fn local(cwd: AbsolutePathBuf) -> TurnEnvironmentSelection {
|
||||
TurnEnvironmentSelection::new(local_request(cwd))
|
||||
TurnEnvironmentSelection::new(local_request(cwd), &[])
|
||||
}
|
||||
|
||||
/// Builds explicit environment configuration with the test thread's permissions and shell settings.
|
||||
@@ -219,6 +219,7 @@ impl TestEnv {
|
||||
let cwd = local_cwd_temp_dir.abs();
|
||||
let selection = match exec_server_url {
|
||||
Some(_) => TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
|
||||
@@ -301,6 +302,7 @@ pub async fn test_env() -> Result<TestEnv> {
|
||||
)
|
||||
.await?;
|
||||
let selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd_uri.clone(),
|
||||
workspace_roots: vec![cwd_uri.clone()],
|
||||
|
||||
@@ -105,6 +105,7 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
vec![{
|
||||
let cwd = PathUri::parse("file:///C:/codex-home")?;
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: vec![cwd],
|
||||
|
||||
@@ -2337,6 +2337,7 @@ async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Re
|
||||
let environments = vec![
|
||||
local(shared_cwd.clone()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&shared_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&shared_cwd)],
|
||||
|
||||
@@ -181,12 +181,14 @@ async fn two_exec_servers_isolate_workspace_write_roots() -> Result<()> {
|
||||
let second_workspace_uri = PathUri::from_host_native_path(second_workspace.path())?;
|
||||
let environments = vec![
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: FIRST_ENVIRONMENT_ID.to_string(),
|
||||
cwd: first_workspace_uri.clone(),
|
||||
workspace_roots: vec![first_workspace_uri],
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
},
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: SECOND_ENVIRONMENT_ID.to_string(),
|
||||
cwd: second_workspace_uri.clone(),
|
||||
workspace_roots: vec![second_workspace_uri],
|
||||
|
||||
@@ -3065,6 +3065,7 @@ async fn approved_network_host_for_one_environment_still_prompts_in_another() ->
|
||||
let environments = vec![
|
||||
local(local_cwd.path().abs()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
|
||||
@@ -1072,6 +1072,7 @@ async fn active_environment_update_waits_for_a_configured_executor_to_connect(
|
||||
.cwd
|
||||
.join("remote-workspace")?;
|
||||
let mut remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: remote_environment_id.to_string(),
|
||||
cwd: remote_cwd.clone(),
|
||||
workspace_roots: vec![remote_cwd.clone()],
|
||||
@@ -1228,6 +1229,7 @@ async fn deferred_executor_promotes_primary_environment_when_startup_completes()
|
||||
let test = builder.build_with_remote_and_local_env(&server).await?;
|
||||
let local_selection = local(test.config.cwd.clone());
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
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)],
|
||||
@@ -2790,12 +2792,15 @@ async fn ready_before_selection_resolves_resumed_thread_capability_root_after_wa
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let selection = TurnEnvironmentSelection::new(TurnEnvironmentRequest {
|
||||
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)],
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
});
|
||||
let selection = TurnEnvironmentSelection::new(
|
||||
TurnEnvironmentRequest {
|
||||
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)],
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
},
|
||||
std::slice::from_ref(&stale_root),
|
||||
);
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
thread_extension_init.insert(vec![stale_root]);
|
||||
let resumed = test
|
||||
@@ -3070,6 +3075,7 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
let owner_active_profile = ActivePermissionProfile::new("owner-read-only");
|
||||
let owner_profile_workspace_root = test.config.cwd.join("owner-profile-root");
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
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)],
|
||||
@@ -3262,6 +3268,7 @@ async fn deferred_executor_guardian_uses_newly_ready_step_environment(
|
||||
let remote_denied_path = remote_cwd.join("private");
|
||||
let local_denied_path = local_cwd.canonicalize()?.join("private");
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
@@ -3765,6 +3772,7 @@ async fn exec_command_routes_to_selected_remote_environment() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
@@ -4083,6 +4091,7 @@ async fn remote_request_permissions_grant_unblocks_later_remote_exec() -> Result
|
||||
vec![
|
||||
local(local_cwd.path().abs()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
@@ -4240,6 +4249,7 @@ async fn apply_patch_freeform_routes_to_selected_remote_environment() -> Result<
|
||||
Some(vec![
|
||||
local(local_cwd.path().abs()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
@@ -4331,6 +4341,7 @@ async fn apply_patch_approvals_are_remembered_per_environment() -> Result<()> {
|
||||
let environments = vec![
|
||||
local(local_cwd.path().abs()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
@@ -4539,6 +4550,7 @@ async fn apply_patch_intercepted_exec_command_routes_to_selected_remote_environm
|
||||
Some(vec![
|
||||
local(local_cwd.path().abs()),
|
||||
TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&remote_cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&remote_cwd)],
|
||||
|
||||
@@ -1,11 +1,109 @@
|
||||
//! Capability discovery stays within the environments captured by the current turn.
|
||||
|
||||
use super::*;
|
||||
use codex_history::InitialHistory;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::TurnEnvironmentRequests;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Roots supplied at startup or loaded from history work again when their environment is reselected.
|
||||
#[test_case(false; "api roots")]
|
||||
#[test_case(true; "saved roots")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn selected_capability_roots_survive_environment_reselection(
|
||||
from_history: bool,
|
||||
) -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let observed_roots = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut extensions = ExtensionRegistryBuilder::new();
|
||||
extensions.prompt_contributor(Arc::new(ReadyCapabilityRootsTestExtension {
|
||||
observed_roots: Some(Arc::clone(&observed_roots)),
|
||||
}));
|
||||
let test = test_codex()
|
||||
.with_extensions(Arc::new(extensions.build()))
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ExecutorCapabilityDiscovery)
|
||||
.expect("enable discovery");
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
let selection = test
|
||||
.codex
|
||||
.environment_selections()
|
||||
.await
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("selected environment")?;
|
||||
let roots = vec![SelectedCapabilityRoot {
|
||||
id: "selected-root".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: selection.environment_id.clone(),
|
||||
path: selection.cwd.clone(),
|
||||
},
|
||||
}];
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
let initial_history = if from_history {
|
||||
InitialHistory::Forked(vec![RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
selected_capability_roots: roots.clone(),
|
||||
..SessionMeta::default()
|
||||
},
|
||||
git: None,
|
||||
})])
|
||||
} else {
|
||||
thread_extension_init.insert(roots.clone());
|
||||
InitialHistory::New
|
||||
};
|
||||
let thread = test
|
||||
.thread_manager
|
||||
.start_thread(StartThreadOptions {
|
||||
environments: Some(vec![selection.clone().into_request()]),
|
||||
initial_history,
|
||||
thread_extension_init,
|
||||
..StartThreadOptions::new(test.config.clone())
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
let response_mock = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![ev_completed("selected")]),
|
||||
sse(vec![ev_completed("removed")]),
|
||||
sse(vec![ev_completed("reselected")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
// Observe the startup selection before a settings update can rebuild it.
|
||||
for environment_requests in [None, Some(Vec::new()), Some(vec![selection.into_request()])] {
|
||||
thread
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "continue".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
environments: environment_requests.map(|requests| {
|
||||
TurnEnvironmentRequests::new(test.config.cwd.clone(), requests)
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&thread, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
}
|
||||
let mut actual_roots = observed_roots.lock().expect("observed roots").clone();
|
||||
// Prompt contributors can run more than once per turn.
|
||||
actual_roots.dedup();
|
||||
assert_eq!(actual_roots, vec![roots.clone(), Vec::new(), roots]);
|
||||
assert_eq!(response_mock.requests().len(), 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(PermissionProfile::Disabled; "full access")]
|
||||
#[test_case(PermissionProfile::workspace_write(); "workspace write")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -3112,6 +3112,7 @@ async fn production_turn_keeps_rebalanced_catalogs_stable_after_compaction_and_r
|
||||
/*connect_timeout*/ None,
|
||||
)?;
|
||||
let pending_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "skills-executor".to_string(),
|
||||
cwd: PathUri::from_abs_path(&test.config.cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
use super::*;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::EnvironmentCapabilityRoots;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::models::PermissionProfileSnapshot;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::TurnEnvironmentRequests;
|
||||
@@ -10,9 +14,13 @@ use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use core_test_support::submit_thread_settings;
|
||||
use core_test_support::test_codex::environment_config_for_selection;
|
||||
use pretty_assertions::assert_eq;
|
||||
use test_case::test_case;
|
||||
|
||||
/// Active updates restore selected roots even when the request only names the environment.
|
||||
#[test_case(false; "without roots")]
|
||||
#[test_case(true; "with roots")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn current_turn_environment_selections_follow_active_updates() -> Result<()> {
|
||||
async fn current_turn_environment_selections_follow_active_updates(with_roots: bool) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -22,13 +30,44 @@ async fn current_turn_environment_selections_follow_active_updates() -> Result<(
|
||||
)
|
||||
.await;
|
||||
let test = step_settings_test().build_with_auto_env(&server).await?;
|
||||
let thread = &test.codex;
|
||||
let selection = test.executor_environment().selection().clone();
|
||||
let roots = if with_roots {
|
||||
vec![SelectedCapabilityRoot {
|
||||
id: "selected-root".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: selection.environment_id.clone(),
|
||||
path: selection.cwd.clone(),
|
||||
},
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut thread_extension_init = ExtensionDataInit::new();
|
||||
thread_extension_init.insert(roots.clone());
|
||||
let thread = test
|
||||
.thread_manager
|
||||
.start_thread(StartThreadOptions {
|
||||
environments: Some(vec![selection.into_request()]),
|
||||
thread_extension_init,
|
||||
..StartThreadOptions::new(test.config.clone())
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
let thread = &thread;
|
||||
let turn_id = start_paused_turn(thread).await?.turn_id;
|
||||
let initial = thread
|
||||
.active_turn_environment_selections()
|
||||
.await
|
||||
.expect("running turn");
|
||||
assert_eq!(initial.len(), 1);
|
||||
assert_eq!(
|
||||
EnvironmentCapabilityRoots::collect(
|
||||
initial
|
||||
.iter()
|
||||
.map(|selection| &selection.selected_capability_roots)
|
||||
),
|
||||
roots,
|
||||
);
|
||||
assert_eq!(
|
||||
thread.current_turn_environment_selections(&turn_id).await,
|
||||
Some(initial.clone())
|
||||
@@ -41,17 +80,16 @@ async fn current_turn_environment_selections_follow_active_updates() -> Result<(
|
||||
);
|
||||
|
||||
for environments in [vec![], initial.clone()] {
|
||||
let requested = environments
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::into_request)
|
||||
.collect();
|
||||
apply_turn_settings(
|
||||
thread,
|
||||
&turn_id,
|
||||
TurnSettingsUpdate {
|
||||
environments: Some(
|
||||
environments
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::into_request)
|
||||
.collect(),
|
||||
),
|
||||
environments: Some(requested),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -751,6 +751,7 @@ async fn view_image_routes_to_selected_remote_environment() -> anyhow::Result<()
|
||||
.await?;
|
||||
let absolute_image_path = image_path_uri.inferred_native_path_string();
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: remote_cwd_uri.clone(),
|
||||
workspace_roots: vec![remote_cwd_uri],
|
||||
|
||||
@@ -1158,6 +1158,7 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
|
||||
|
||||
let turn_store = ExtensionData::new("turn-1");
|
||||
let turn_environment = TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "env-1".to_string(),
|
||||
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -2380,6 +2381,7 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
turn_id: "turn-1",
|
||||
environments: &[TurnEnvironmentSelection {
|
||||
selected_capability_roots: Default::default(),
|
||||
environment_id: "env-1".to_string(),
|
||||
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
|
||||
workspace_roots: Vec::new(),
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// A user-selected root that can expose one or more runtime capabilities.
|
||||
@@ -16,6 +17,43 @@ pub struct SelectedCapabilityRoot {
|
||||
pub location: CapabilityRootLocation,
|
||||
}
|
||||
|
||||
/// Roots selected for one environment, retaining their order in the thread's root list.
|
||||
///
|
||||
/// The index is each root's position in the original thread list. Executor skills assign
|
||||
/// aliases (`e0`, `e1`, ...) in root order. Keeping the index lets us split roots across
|
||||
/// environments and combine them again without changing those aliases.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct EnvironmentCapabilityRoots(Arc<[(usize, SelectedCapabilityRoot)]>);
|
||||
|
||||
impl EnvironmentCapabilityRoots {
|
||||
/// Selects this environment's roots without losing their original positions.
|
||||
pub fn for_environment(environment_id: &str, roots: &[SelectedCapabilityRoot]) -> Self {
|
||||
let mut selected = Vec::new();
|
||||
for (index, root) in roots.iter().enumerate() {
|
||||
let CapabilityRootLocation::Environment {
|
||||
environment_id: root_environment_id,
|
||||
..
|
||||
} = &root.location;
|
||||
if root_environment_id == environment_id {
|
||||
selected.push((index, root.clone()));
|
||||
}
|
||||
}
|
||||
Self(selected.into())
|
||||
}
|
||||
|
||||
/// Restores root order so changing environment order does not change skill aliases.
|
||||
pub fn collect<'a>(
|
||||
selections: impl IntoIterator<Item = &'a Self>,
|
||||
) -> Vec<SelectedCapabilityRoot> {
|
||||
let mut roots = Vec::new();
|
||||
for selection in selections {
|
||||
roots.extend(selection.0.iter());
|
||||
}
|
||||
roots.sort_by_key(|(index, _)| *index);
|
||||
roots.into_iter().map(|(_, root)| root.clone()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Location used to resolve a selected capability root.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Environment requests, thread selections, and attachment authority.
|
||||
//! Input APIs accept requests; runtime code uses selections constructed by the thread.
|
||||
//! Selections bind requests to the thread's capability roots before runtime use.
|
||||
|
||||
use crate::capabilities::EnvironmentCapabilityRoots;
|
||||
use crate::capabilities::SelectedCapabilityRoot;
|
||||
use crate::config_types::ShellEnvironmentPolicy;
|
||||
use crate::config_types::WindowsSandboxLevel;
|
||||
@@ -14,10 +15,11 @@ use codex_network_proxy::EnvironmentNetworkPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
/// An environment requested by a caller, before construction of thread-owned state.
|
||||
/// An environment requested by a caller, before attaching the receiving thread's roots.
|
||||
///
|
||||
/// Input APIs accept requests so the receiving thread has a construction boundary for
|
||||
/// attaching its state before passing a selection to runtime code.
|
||||
/// Callers choose the environment and paths, but startup may still need to load roots from
|
||||
/// saved history or inherited attachments. Keeping this input separate lets the receiving
|
||||
/// thread construct a complete selection instead of filling in missing roots later.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TurnEnvironmentRequest {
|
||||
pub environment_id: String,
|
||||
@@ -26,20 +28,30 @@ pub struct TurnEnvironmentRequest {
|
||||
pub config: EnvironmentConfigState,
|
||||
}
|
||||
|
||||
/// An environment selected by a thread for runtime use.
|
||||
/// Constructed from caller input at the receiving thread's startup or settings boundary.
|
||||
/// An environment selected for a thread, with its capability roots already attached.
|
||||
///
|
||||
/// Runtime snapshots carry this value so capability discovery uses the roots captured
|
||||
/// with the environment. Construct it from a request and the receiving thread's roots.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TurnEnvironmentSelection {
|
||||
pub environment_id: String,
|
||||
pub cwd: PathUri,
|
||||
pub workspace_roots: Vec<PathUri>,
|
||||
pub config: EnvironmentConfigState,
|
||||
/// Roots selected by the client for this environment when the thread started.
|
||||
pub selected_capability_roots: EnvironmentCapabilityRoots,
|
||||
}
|
||||
|
||||
impl TurnEnvironmentSelection {
|
||||
/// Constructs runtime state at the receiving thread boundary.
|
||||
pub fn new(request: TurnEnvironmentRequest) -> Self {
|
||||
/// Attaches this environment's roots before the selection enters runtime state.
|
||||
/// The caller supplies the receiving thread's roots so reselection restores them and
|
||||
/// reusing an environment from another thread does not reuse that thread's root choices.
|
||||
pub fn new(request: TurnEnvironmentRequest, roots: &[SelectedCapabilityRoot]) -> Self {
|
||||
Self {
|
||||
selected_capability_roots: EnvironmentCapabilityRoots::for_environment(
|
||||
&request.environment_id,
|
||||
roots,
|
||||
),
|
||||
environment_id: request.environment_id,
|
||||
cwd: request.cwd,
|
||||
workspace_roots: request.workspace_roots,
|
||||
@@ -47,6 +59,15 @@ impl TurnEnvironmentSelection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether these selections refer to the same environment and workspace.
|
||||
/// Unlike full selection equality, this ignores configuration and capability roots:
|
||||
/// those can differ between threads that follow the same owner's configuration.
|
||||
pub fn has_same_workspace(&self, other: &Self) -> bool {
|
||||
self.environment_id == other.environment_id
|
||||
&& self.cwd == other.cwd
|
||||
&& self.workspace_roots == other.workspace_roots
|
||||
}
|
||||
|
||||
/// Requests this environment again; the receiving thread supplies its own roots.
|
||||
/// This explicit conversion discards thread-owned state. Test inputs should instead
|
||||
/// construct requests directly so they do not depend on runtime selection fields.
|
||||
@@ -92,7 +113,7 @@ impl TurnEnvironmentSelections {
|
||||
}
|
||||
|
||||
/// Environment input supplied together with its fallback working directory.
|
||||
/// The receiving thread constructs selections before capturing these environments.
|
||||
/// The receiving thread attaches roots in `select` before capturing these environments.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TurnEnvironmentRequests {
|
||||
pub legacy_fallback_cwd: AbsolutePathBuf,
|
||||
@@ -110,14 +131,15 @@ impl TurnEnvironmentRequests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the selections captured by the receiving thread.
|
||||
pub fn select(self) -> TurnEnvironmentSelections {
|
||||
/// Constructs selections with the receiving thread's roots, including roots retained
|
||||
/// while an environment was deselected. Snapshots can then carry complete selections.
|
||||
pub fn select(self, roots: &[SelectedCapabilityRoot]) -> TurnEnvironmentSelections {
|
||||
TurnEnvironmentSelections {
|
||||
legacy_fallback_cwd: self.legacy_fallback_cwd,
|
||||
environments: self
|
||||
.environment_requests
|
||||
.into_iter()
|
||||
.map(TurnEnvironmentSelection::new)
|
||||
.map(|request| TurnEnvironmentSelection::new(request, roots))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user