mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
693082f3c4
## Why `selectedCapabilityRoots` belongs to one thread, but MCP contributors previously received only the global Codex config. That left no clean way for a selected executor capability to contribute MCP servers to its own thread. ## What this PR does - Gives MCP contributors a small context containing the config and, for a running thread, its frozen host-seeded inputs. - Uses the same thread inputs during startup, status queries, refreshes, and skill dependency checks. - Keeps threadless MCP operations and the existing hosted Apps behavior unchanged. - Adds coverage showing that two threads resolve independent registrations and that later lifecycle mutations do not change the frozen MCP inputs. This PR does not discover plugin manifests, add MCP servers, or launch anything new. It only establishes the thread-scoped registration boundary. ## Follow-ups - Resolve selected executor plugin roots through their owning environment filesystem. - Convert their stdio MCP declarations into environment-bound registrations and add an executor MCP end-to-end test. ## Verification - `just fmt` - `cargo check --tests -p codex-protocol -p codex-extension-api -p codex-mcp-extension -p codex-core -p codex-app-server` Tests and Clippy were not run.
129 lines
3.8 KiB
Rust
129 lines
3.8 KiB
Rust
use std::any::Any;
|
|
use std::any::TypeId;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::sync::PoisonError;
|
|
|
|
type ErasedData = Arc<dyn Any + Send + Sync>;
|
|
|
|
/// Typed values supplied before an [`ExtensionData`] scope is created.
|
|
///
|
|
/// Hosts may retain a clone when later operations must use the same initial
|
|
/// inputs. Cloning freezes the attachment map and shares each value by `Arc`;
|
|
/// values with interior mutability remain shared. This type does not install
|
|
/// extensions or provide persistence.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct ExtensionDataInit {
|
|
entries: HashMap<TypeId, ErasedData>,
|
|
}
|
|
|
|
impl ExtensionDataInit {
|
|
/// Creates an empty extension data initializer.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Stores `value` as the initial attachment of type `T`.
|
|
pub fn insert<T>(&mut self, value: T) -> Option<Arc<T>>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
self.entries
|
|
.insert(TypeId::of::<T>(), Arc::new(value))
|
|
.map(downcast_data)
|
|
}
|
|
|
|
/// Returns a host-supplied initial attachment without creating a mutable scope.
|
|
pub fn get<T>(&self) -> Option<Arc<T>>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
let value = self.entries.get(&TypeId::of::<T>())?.clone();
|
|
Some(downcast_data(value))
|
|
}
|
|
}
|
|
|
|
/// Typed extension-owned data attached to one host object.
|
|
#[derive(Debug)]
|
|
pub struct ExtensionData {
|
|
level_id: String,
|
|
entries: Mutex<HashMap<TypeId, ErasedData>>,
|
|
}
|
|
|
|
impl ExtensionData {
|
|
/// Creates an empty attachment map for one host-owned scope.
|
|
pub fn new(level_id: impl Into<String>) -> Self {
|
|
Self::new_with_init(level_id, ExtensionDataInit::default())
|
|
}
|
|
|
|
/// Creates an attachment map seeded with host-supplied initial data.
|
|
pub fn new_with_init(level_id: impl Into<String>, init: ExtensionDataInit) -> Self {
|
|
Self {
|
|
level_id: level_id.into(),
|
|
entries: Mutex::new(init.entries),
|
|
}
|
|
}
|
|
|
|
/// Returns the host identity for the scope this data is attached to.
|
|
pub fn level_id(&self) -> &str {
|
|
&self.level_id
|
|
}
|
|
|
|
/// Returns the attached value of type `T`, if one exists.
|
|
pub fn get<T>(&self) -> Option<Arc<T>>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
let value = self.entries().get(&TypeId::of::<T>())?.clone();
|
|
Some(downcast_data(value))
|
|
}
|
|
|
|
/// Returns the attached value of type `T`, inserting one from `init` when absent.
|
|
///
|
|
/// The initializer runs while this map is locked, so it should stay cheap;
|
|
/// heavyweight lazy work belongs inside the attached value itself.
|
|
pub fn get_or_init<T>(&self, init: impl FnOnce() -> T) -> Arc<T>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
let mut entries = self.entries();
|
|
let value = entries
|
|
.entry(TypeId::of::<T>())
|
|
.or_insert_with(|| Arc::new(init()));
|
|
downcast_data(Arc::clone(value))
|
|
}
|
|
|
|
/// Stores `value` as the attachment of type `T`, returning any previous value.
|
|
pub fn insert<T>(&self, value: T) -> Option<Arc<T>>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
self.entries()
|
|
.insert(TypeId::of::<T>(), Arc::new(value))
|
|
.map(downcast_data)
|
|
}
|
|
|
|
/// Removes and returns the attached value of type `T`, if one exists.
|
|
pub fn remove<T>(&self) -> Option<Arc<T>>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
self.entries().remove(&TypeId::of::<T>()).map(downcast_data)
|
|
}
|
|
|
|
fn entries(&self) -> std::sync::MutexGuard<'_, HashMap<TypeId, ErasedData>> {
|
|
self.entries.lock().unwrap_or_else(PoisonError::into_inner)
|
|
}
|
|
}
|
|
|
|
fn downcast_data<T>(value: ErasedData) -> Arc<T>
|
|
where
|
|
T: Any + Send + Sync,
|
|
{
|
|
let Ok(value) = value.downcast::<T>() else {
|
|
unreachable!("typed extension data stored an incompatible value");
|
|
};
|
|
value
|
|
}
|