Files
codex/codex-rs/protocol/src/capabilities.rs
T
Adam Perry @ OpenAI 9479e1fdb7 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
2026-10-06 22:39:32 +00:00

90 lines
3.2 KiB
Rust

use codex_utils_path_uri::LegacyAppPathString;
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.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SelectedCapabilityRoot {
/// Stable identifier supplied by the capability selection platform.
pub id: String,
/// Where the selected root can be resolved.
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")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum CapabilityRootLocation {
/// A path owned by an execution environment.
Environment {
#[serde(rename = "environmentId")]
#[ts(rename = "environmentId")]
environment_id: String,
/// Absolute path for the root in the selected environment.
#[serde(deserialize_with = "deserialize_path_uri_from_api_path")]
#[schemars(with = "String")]
#[ts(type = "string")]
path: PathUri,
},
}
fn deserialize_path_uri_from_api_path<'de, D>(deserializer: D) -> Result<PathUri, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = LegacyAppPathString::deserialize(deserializer)?;
if let Ok(path_uri) = PathUri::parse(path.as_str()) {
return Ok(path_uri);
}
path.try_into().map_err(serde::de::Error::custom)
}
#[cfg(test)]
#[path = "capabilities_tests.rs"]
mod tests;