mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
refactor(plugins): split host capability projection base
This commit is contained in:
Generated
-1
@@ -3990,7 +3990,6 @@ dependencies = [
|
||||
name = "codex-skills-extension"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-core-plugins",
|
||||
"codex-core-skills",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
|
||||
@@ -801,7 +801,6 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact {
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
@@ -1683,7 +1682,6 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
@@ -1988,7 +1986,6 @@ async fn compaction_event_ingests_custom_fact() {
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
@@ -2119,7 +2116,6 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
|
||||
@@ -349,8 +349,6 @@ pub struct InProcessClientStartArgs {
|
||||
pub experimental_api: bool,
|
||||
/// Whether MCP servers may send `openai/form` elicitation requests.
|
||||
pub mcp_server_openai_form_elicitation: bool,
|
||||
/// Host features available to runtime plugins.
|
||||
pub host_capabilities: Vec<String>,
|
||||
/// Notification methods this client opts out of receiving.
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
/// Queue capacity for command/event channels (clamped to at least 1).
|
||||
@@ -376,8 +374,6 @@ impl InProcessClientStartArgs {
|
||||
Some(self.opt_out_notification_methods.clone())
|
||||
},
|
||||
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
||||
host_capabilities: (!self.host_capabilities.is_empty())
|
||||
.then(|| self.host_capabilities.clone()),
|
||||
};
|
||||
|
||||
InitializeParams {
|
||||
@@ -1049,7 +1045,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity,
|
||||
})
|
||||
@@ -1244,7 +1239,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 8,
|
||||
}
|
||||
@@ -1263,20 +1257,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_initialize_params_forward_host_capabilities() {
|
||||
let mut args = test_remote_connect_args("ws://localhost/rpc".to_string());
|
||||
args.host_capabilities = vec!["codex.inline_visualization".to_string()];
|
||||
|
||||
assert_eq!(
|
||||
args.initialize_params()
|
||||
.capabilities
|
||||
.expect("initialize capabilities")
|
||||
.host_capabilities,
|
||||
Some(vec!["codex.inline_visualization".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn typed_request_roundtrip_works() {
|
||||
let client = start_test_client(SessionSource::Exec).await;
|
||||
@@ -1548,7 +1528,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 8,
|
||||
})
|
||||
@@ -1638,7 +1617,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 8,
|
||||
})
|
||||
@@ -1659,7 +1637,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: 8,
|
||||
})
|
||||
@@ -2264,22 +2241,18 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: true,
|
||||
host_capabilities: vec!["codex.inline_visualization".to_string()],
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
}
|
||||
.into_runtime_start_args();
|
||||
|
||||
assert_eq!(runtime_args.config, config);
|
||||
let initialize_capabilities = runtime_args
|
||||
.initialize
|
||||
.capabilities
|
||||
.as_ref()
|
||||
.expect("initialize capabilities");
|
||||
assert!(initialize_capabilities.mcp_server_openai_form_elicitation);
|
||||
assert_eq!(
|
||||
initialize_capabilities.host_capabilities,
|
||||
Some(vec!["codex.inline_visualization".to_string()])
|
||||
assert!(
|
||||
runtime_args
|
||||
.initialize
|
||||
.capabilities
|
||||
.expect("initialize capabilities")
|
||||
.mcp_server_openai_form_elicitation
|
||||
);
|
||||
assert!(Arc::ptr_eq(
|
||||
&runtime_args.environment_manager,
|
||||
@@ -2317,7 +2290,6 @@ mod tests {
|
||||
client_version: "0.0.0-test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ pub struct RemoteAppServerConnectArgs {
|
||||
pub client_version: String,
|
||||
pub experimental_api: bool,
|
||||
pub mcp_server_openai_form_elicitation: bool,
|
||||
pub host_capabilities: Vec<String>,
|
||||
pub opt_out_notification_methods: Vec<String>,
|
||||
pub channel_capacity: usize,
|
||||
}
|
||||
@@ -102,8 +101,6 @@ impl RemoteAppServerConnectArgs {
|
||||
Some(self.opt_out_notification_methods.clone())
|
||||
},
|
||||
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
||||
host_capabilities: (!self.host_capabilities.is_empty())
|
||||
.then(|| self.host_capabilities.clone()),
|
||||
};
|
||||
|
||||
InitializeParams {
|
||||
|
||||
@@ -1279,16 +1279,6 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostCapabilities": {
|
||||
"description": "Host features available to runtime plugins on this connection.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
-10
@@ -2923,16 +2923,6 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostCapabilities": {
|
||||
"description": "Host features available to runtime plugins on this connection.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
-10
@@ -7532,16 +7532,6 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostCapabilities": {
|
||||
"description": "Host features available to runtime plugins on this connection.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -30,16 +30,6 @@
|
||||
"description": "Opt into receiving experimental API methods and fields.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostCapabilities": {
|
||||
"description": "Host features available to runtime plugins on this connection.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"mcpServerOpenaiFormElicitation": {
|
||||
"description": "Allow downstream MCP servers to request OpenAI extended form elicitations.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -18,10 +18,6 @@ requestAttestation: boolean,
|
||||
* Allow downstream MCP servers to request OpenAI extended form elicitations.
|
||||
*/
|
||||
mcpServerOpenaiFormElicitation?: boolean,
|
||||
/**
|
||||
* Host features available to runtime plugins on this connection.
|
||||
*/
|
||||
hostCapabilities?: Array<string> | null,
|
||||
/**
|
||||
* Exact notification method names that should be suppressed for this
|
||||
* connection (for example `thread/started`).
|
||||
|
||||
@@ -2209,7 +2209,6 @@ mod tests {
|
||||
experimental_api: true,
|
||||
request_attestation: true,
|
||||
mcp_server_openai_form_elicitation: true,
|
||||
host_capabilities: Some(vec!["codex.inline_visualization".to_string()]),
|
||||
opt_out_notification_methods: Some(vec![
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
@@ -2232,7 +2231,6 @@ mod tests {
|
||||
"experimentalApi": true,
|
||||
"requestAttestation": true,
|
||||
"mcpServerOpenaiFormElicitation": true,
|
||||
"hostCapabilities": ["codex.inline_visualization"],
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
@@ -2260,7 +2258,6 @@ mod tests {
|
||||
"experimentalApi": true,
|
||||
"requestAttestation": true,
|
||||
"mcpServerOpenaiFormElicitation": true,
|
||||
"hostCapabilities": ["codex.inline_visualization"],
|
||||
"optOutNotificationMethods": [
|
||||
"thread/started",
|
||||
"item/agentMessage/delta"
|
||||
@@ -2283,7 +2280,6 @@ mod tests {
|
||||
experimental_api: true,
|
||||
request_attestation: true,
|
||||
mcp_server_openai_form_elicitation: true,
|
||||
host_capabilities: Some(vec!["codex.inline_visualization".to_string()]),
|
||||
opt_out_notification_methods: Some(vec![
|
||||
"thread/started".to_string(),
|
||||
"item/agentMessage/delta".to_string(),
|
||||
|
||||
@@ -53,9 +53,6 @@ pub struct InitializeCapabilities {
|
||||
/// Allow downstream MCP servers to request OpenAI extended form elicitations.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub mcp_server_openai_form_elicitation: bool,
|
||||
/// Host features available to runtime plugins on this connection.
|
||||
#[ts(optional = nullable)]
|
||||
pub host_capabilities: Option<Vec<String>>,
|
||||
/// Exact notification method names that should be suppressed for this
|
||||
/// connection (for example `thread/started`).
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -1671,7 +1671,6 @@ impl CodexClient {
|
||||
.collect(),
|
||||
),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -93,20 +93,6 @@ App-server then advertises the downstream `openai/form` MCP extension for
|
||||
threads started, resumed, or forked by that connection. Clients that cannot
|
||||
handle the request envelope omit the field or set it to `false`.
|
||||
|
||||
Clients declare host features available to runtime plugins with
|
||||
`initialize.params.capabilities.hostCapabilities`. Capability names are opaque
|
||||
compatibility signals, not authorization grants; omitted or `null`
|
||||
capabilities are treated as an empty set. The capability set is fixed for the
|
||||
lifetime of a loaded thread, so clients must use the same set when resuming or
|
||||
performing model-visible runtime operations on that thread. Capability-independent
|
||||
settings updates remain available; the next `turn/start` enforces compatibility.
|
||||
|
||||
Plugin manifests declare all-of host requirements with
|
||||
`{"requires":{"hostCapabilities":["codex.inline_visualization"]}}`. A plugin
|
||||
whose requirements are not satisfied remains installed and manageable, but its
|
||||
skills, MCP servers, apps, hooks, and model-facing summaries are excluded from
|
||||
that runtime.
|
||||
|
||||
Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter.
|
||||
|
||||
**Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If
|
||||
@@ -1836,7 +1822,7 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat
|
||||
} }
|
||||
```
|
||||
|
||||
When `threadId` is provided, app feature gating (`Feature::Apps`) is evaluated using that thread's config snapshot, and the requesting connection must have the same host capability set as the loaded thread. When omitted, the latest global config is used.
|
||||
When `threadId` is provided, app feature gating (`Feature::Apps`) is evaluated using that thread's config snapshot. When omitted, the latest global config is used.
|
||||
|
||||
`app/list` returns after both accessible apps and directory apps are loaded. Set `forceRefetch: true` to bypass app caches and fetch fresh data from sources. Cache entries are only replaced when those refetches succeed.
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ where
|
||||
include_instructions: config.include_skill_instructions,
|
||||
bundled_skills_enabled: config.bundled_skills_enabled(),
|
||||
orchestrator_skills_enabled: config.orchestrator_skills_enabled,
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
},
|
||||
);
|
||||
Arc::new(builder.build())
|
||||
|
||||
@@ -501,16 +501,13 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
||||
created = thread_created_rx.recv(), if listen_for_threads => {
|
||||
match created {
|
||||
Ok(thread_id) => {
|
||||
let connections = if session.initialized() {
|
||||
vec![(
|
||||
IN_PROCESS_CONNECTION_ID,
|
||||
session.host_capabilities(),
|
||||
)]
|
||||
let connection_ids = if session.initialized() {
|
||||
vec![IN_PROCESS_CONNECTION_ID]
|
||||
} else {
|
||||
Vec::new()
|
||||
Vec::<ConnectionId>::new()
|
||||
};
|
||||
processor
|
||||
.try_attach_thread_listener(thread_id, connections)
|
||||
.try_attach_thread_listener(thread_id, connection_ids)
|
||||
.await;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
|
||||
@@ -1125,19 +1125,16 @@ pub async fn run_main_with_transport_options(
|
||||
created = thread_created_rx.recv(), if listen_for_threads => {
|
||||
match created {
|
||||
Ok(thread_id) => {
|
||||
let mut initialized_connections = Vec::new();
|
||||
let mut initialized_connection_ids = Vec::new();
|
||||
for (connection_id, connection_state) in &connections {
|
||||
if connection_state.session.initialized() {
|
||||
initialized_connections.push((
|
||||
*connection_id,
|
||||
connection_state.session.host_capabilities(),
|
||||
));
|
||||
initialized_connection_ids.push(*connection_id);
|
||||
}
|
||||
}
|
||||
processor
|
||||
.try_attach_thread_listener(
|
||||
thread_id,
|
||||
initialized_connections,
|
||||
initialized_connection_ids,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::outgoing_message::RequestContext;
|
||||
use crate::request_processors::AccountRequestProcessor;
|
||||
use crate::request_processors::AppServerClientInfo;
|
||||
use crate::request_processors::AppsRequestProcessor;
|
||||
use crate::request_processors::CatalogRequestProcessor;
|
||||
use crate::request_processors::CommandExecRequestProcessor;
|
||||
@@ -66,7 +65,6 @@ use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::HostCapabilities;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_goal_extension::GoalService;
|
||||
@@ -143,7 +141,6 @@ pub(crate) struct InitializedConnectionSessionState {
|
||||
pub(crate) client_version: String,
|
||||
pub(crate) request_attestation: bool,
|
||||
pub(crate) supports_openai_form_elicitation: bool,
|
||||
pub(crate) host_capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
impl Default for ConnectionSessionState {
|
||||
@@ -200,13 +197,6 @@ impl ConnectionSessionState {
|
||||
.get()
|
||||
.is_some_and(|session| session.supports_openai_form_elicitation)
|
||||
}
|
||||
|
||||
pub(crate) fn host_capabilities(&self) -> HostCapabilities {
|
||||
self.initialized
|
||||
.get()
|
||||
.map(|session| session.host_capabilities.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> {
|
||||
self.initialized.set(session).map_err(|_| ())
|
||||
}
|
||||
@@ -678,10 +668,10 @@ impl MessageProcessor {
|
||||
pub(crate) async fn try_attach_thread_listener(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
connections: Vec<(ConnectionId, HostCapabilities)>,
|
||||
connection_ids: Vec<ConnectionId>,
|
||||
) {
|
||||
self.thread_processor
|
||||
.try_attach_thread_listener(thread_id, connections)
|
||||
.try_attach_thread_listener(thread_id, connection_ids)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -818,12 +808,9 @@ impl MessageProcessor {
|
||||
);
|
||||
|
||||
let serialization_scope = codex_request.serialization_scope();
|
||||
let app_server_client_info = AppServerClientInfo {
|
||||
name: session.app_server_client_name().map(str::to_string),
|
||||
version: session.client_version().map(str::to_string),
|
||||
};
|
||||
let app_server_client_name = session.app_server_client_name().map(str::to_string);
|
||||
let client_version = session.client_version().map(str::to_string);
|
||||
let supports_openai_form_elicitation = session.supports_openai_form_elicitation();
|
||||
let host_capabilities = session.host_capabilities();
|
||||
let error_request_id = connection_request_id.clone();
|
||||
let rpc_gate = Arc::clone(&session.rpc_gate);
|
||||
let processor = Arc::clone(self);
|
||||
@@ -837,9 +824,9 @@ impl MessageProcessor {
|
||||
connection_request_id,
|
||||
codex_request,
|
||||
request_context,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
client_version,
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
@@ -867,9 +854,9 @@ impl MessageProcessor {
|
||||
connection_request_id: ConnectionRequestId,
|
||||
codex_request: ClientRequest,
|
||||
request_context: RequestContext,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
let request_id = ConnectionRequestId {
|
||||
@@ -921,7 +908,7 @@ impl MessageProcessor {
|
||||
.remote_control_processor
|
||||
.enable(
|
||||
params.is_some_and(|params| params.ephemeral),
|
||||
app_server_client_info.name.as_deref(),
|
||||
app_server_client_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
@@ -929,7 +916,7 @@ impl MessageProcessor {
|
||||
.remote_control_processor
|
||||
.disable(
|
||||
params.is_some_and(|params| params.ephemeral),
|
||||
app_server_client_info.name.as_deref(),
|
||||
app_server_client_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
@@ -939,7 +926,7 @@ impl MessageProcessor {
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::RemoteControlPairingStart { params, .. } => self
|
||||
.remote_control_processor
|
||||
.pairing_start(params, app_server_client_info.name.as_deref())
|
||||
.pairing_start(params, app_server_client_name.as_deref())
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::RemoteControlPairingStatus { params, .. } => self
|
||||
@@ -1023,9 +1010,9 @@ impl MessageProcessor {
|
||||
.thread_start(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_info.clone(),
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities.clone(),
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
@@ -1040,10 +1027,10 @@ impl MessageProcessor {
|
||||
.thread_resume(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_info.clone(),
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1052,10 +1039,10 @@ impl MessageProcessor {
|
||||
.thread_fork(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_info.clone(),
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1116,7 +1103,7 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::ThreadCompactStart { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_compact_start(&request_id, params, host_capabilities.clone())
|
||||
.thread_compact_start(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadBackgroundTerminalsClean { params, .. } => {
|
||||
@@ -1136,12 +1123,7 @@ impl MessageProcessor {
|
||||
}
|
||||
ClientRequest::ThreadRollback { params, .. } => {
|
||||
self.thread_processor
|
||||
.thread_rollback(
|
||||
&request_id,
|
||||
params,
|
||||
app_server_client_info.name.as_deref(),
|
||||
host_capabilities.clone(),
|
||||
)
|
||||
.thread_rollback(&request_id, params, app_server_client_name.as_deref())
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadList { params, .. } => {
|
||||
@@ -1176,17 +1158,13 @@ impl MessageProcessor {
|
||||
self.thread_processor.conversation_summary(params).await
|
||||
}
|
||||
ClientRequest::SkillsList { params, .. } => {
|
||||
self.catalog_processor
|
||||
.skills_list(params, host_capabilities.clone())
|
||||
.await
|
||||
self.catalog_processor.skills_list(params).await
|
||||
}
|
||||
ClientRequest::SkillsExtraRootsSet { params, .. } => {
|
||||
self.catalog_processor.skills_extra_roots_set(params).await
|
||||
}
|
||||
ClientRequest::HooksList { params, .. } => {
|
||||
self.catalog_processor
|
||||
.hooks_list(params, host_capabilities.clone())
|
||||
.await
|
||||
self.catalog_processor.hooks_list(params).await
|
||||
}
|
||||
ClientRequest::MarketplaceAdd { params, .. } => {
|
||||
self.marketplace_processor.marketplace_add(params).await
|
||||
@@ -1227,17 +1205,13 @@ impl MessageProcessor {
|
||||
self.plugin_processor.plugin_share_delete(params).await
|
||||
}
|
||||
ClientRequest::AppsList { params, .. } => {
|
||||
self.apps_processor
|
||||
.apps_list(&request_id, params, host_capabilities.clone())
|
||||
.await
|
||||
self.apps_processor.apps_list(&request_id, params).await
|
||||
}
|
||||
ClientRequest::SkillsConfigWrite { params, .. } => {
|
||||
self.catalog_processor.skills_config_write(params).await
|
||||
}
|
||||
ClientRequest::PluginInstall { params, .. } => {
|
||||
self.plugin_processor
|
||||
.plugin_install(&request_id, params, host_capabilities.clone())
|
||||
.await
|
||||
self.plugin_processor.plugin_install(params).await
|
||||
}
|
||||
ClientRequest::PluginUninstall { params, .. } => {
|
||||
self.plugin_processor.plugin_uninstall(params).await
|
||||
@@ -1266,84 +1240,74 @@ impl MessageProcessor {
|
||||
.turn_start(
|
||||
request_id.clone(),
|
||||
params,
|
||||
app_server_client_info,
|
||||
app_server_client_name.clone(),
|
||||
client_version.clone(),
|
||||
/*supports_openai_form_elicitation*/
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadInjectItems { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_inject_items(params, host_capabilities.clone())
|
||||
.await
|
||||
self.turn_processor.thread_inject_items(params).await
|
||||
}
|
||||
ClientRequest::TurnSteer { params, .. } => {
|
||||
self.turn_processor
|
||||
.turn_steer(&request_id, params, host_capabilities.clone())
|
||||
.await
|
||||
self.turn_processor.turn_steer(&request_id, params).await
|
||||
}
|
||||
ClientRequest::TurnInterrupt { params, .. } => {
|
||||
self.turn_processor
|
||||
.turn_interrupt(&request_id, params, host_capabilities.clone())
|
||||
.turn_interrupt(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeStart { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_start(&request_id, params, host_capabilities.clone())
|
||||
.thread_realtime_start(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeAppendAudio { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_append_audio(&request_id, params, host_capabilities.clone())
|
||||
.thread_realtime_append_audio(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeAppendText { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_append_text(&request_id, params, host_capabilities.clone())
|
||||
.thread_realtime_append_text(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeAppendSpeech { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_append_speech(&request_id, params, host_capabilities.clone())
|
||||
.thread_realtime_append_speech(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeStop { params, .. } => {
|
||||
self.turn_processor
|
||||
.thread_realtime_stop(&request_id, params, host_capabilities.clone())
|
||||
.thread_realtime_stop(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::ThreadRealtimeListVoices { params: _, .. } => {
|
||||
self.turn_processor.thread_realtime_list_voices().await
|
||||
}
|
||||
ClientRequest::ReviewStart { params, .. } => {
|
||||
self.turn_processor
|
||||
.review_start(&request_id, params, host_capabilities.clone())
|
||||
.await
|
||||
self.turn_processor.review_start(&request_id, params).await
|
||||
}
|
||||
ClientRequest::McpServerOauthLogin { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_oauth_login(&request_id, params, host_capabilities.clone())
|
||||
.await
|
||||
self.mcp_processor.mcp_server_oauth_login(params).await
|
||||
}
|
||||
ClientRequest::McpServerRefresh { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_refresh(params, host_capabilities.clone())
|
||||
.await
|
||||
self.mcp_processor.mcp_server_refresh(params).await
|
||||
}
|
||||
ClientRequest::McpServerStatusList { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_status_list(&request_id, params, host_capabilities.clone())
|
||||
.mcp_server_status_list(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpResourceRead { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_resource_read(&request_id, params, host_capabilities.clone())
|
||||
.mcp_resource_read(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::McpServerToolCall { params, .. } => {
|
||||
self.mcp_processor
|
||||
.mcp_server_tool_call(&request_id, params, host_capabilities)
|
||||
.mcp_server_tool_call(&request_id, params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::WindowsSandboxSetupStart { params, .. } => {
|
||||
|
||||
@@ -326,7 +326,6 @@ use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::HostCapabilities;
|
||||
use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
@@ -496,12 +495,6 @@ use uuid::Uuid;
|
||||
#[cfg(test)]
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct AppServerClientInfo {
|
||||
pub(crate) name: Option<String>,
|
||||
pub(crate) version: Option<String>,
|
||||
}
|
||||
|
||||
mod account_processor;
|
||||
mod apps_processor;
|
||||
mod catalog_processor;
|
||||
@@ -608,21 +601,6 @@ fn resolve_runtime_workspace_roots(workspace_roots: Vec<AbsolutePathBuf>) -> Vec
|
||||
resolved_roots
|
||||
}
|
||||
|
||||
async fn ensure_thread_host_capabilities(
|
||||
thread_id: ThreadId,
|
||||
thread: &CodexThread,
|
||||
requested: &HostCapabilities,
|
||||
operation: &str,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let active = thread.config_snapshot().await.host_capabilities;
|
||||
if &active != requested {
|
||||
return Err(invalid_request(format!(
|
||||
"cannot {operation} on thread {thread_id} with different host capabilities: requested={requested:?} active={active:?}"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod config_errors;
|
||||
mod request_errors;
|
||||
mod thread_delete;
|
||||
|
||||
@@ -36,9 +36,8 @@ impl AppsRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: AppsListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.apps_list_inner(request_id, params, host_capabilities)
|
||||
self.apps_list_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -47,19 +46,11 @@ impl AppsRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: AppsListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
|
||||
let installed_start = Instant::now();
|
||||
let reload = params.force_refetch;
|
||||
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
|
||||
let (thread_id, loaded_thread) = self.load_thread(thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
loaded_thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"list apps",
|
||||
)
|
||||
.await?;
|
||||
let (_, loaded_thread) = self.load_thread(thread_id).await?;
|
||||
Some(loaded_thread)
|
||||
} else {
|
||||
None
|
||||
@@ -69,7 +60,6 @@ impl AppsRequestProcessor {
|
||||
None => None,
|
||||
};
|
||||
let mut config = self.load_latest_config(fallback_cwd).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
|
||||
if let Some(thread) = thread {
|
||||
let _ = config
|
||||
@@ -141,7 +131,6 @@ impl AppsRequestProcessor {
|
||||
plugins_manager: Arc<PluginsManager>,
|
||||
installed_start: Instant,
|
||||
) {
|
||||
let connection_id = request_id.connection_id;
|
||||
let reload = params.force_refetch;
|
||||
let retry_params = params.clone();
|
||||
let retry_config = config.clone();
|
||||
@@ -150,7 +139,6 @@ impl AppsRequestProcessor {
|
||||
let retry_plugins_manager = Arc::clone(&plugins_manager);
|
||||
let result = Self::apps_list_response(
|
||||
&outgoing,
|
||||
connection_id,
|
||||
params,
|
||||
config,
|
||||
environment_manager,
|
||||
@@ -173,7 +161,6 @@ impl AppsRequestProcessor {
|
||||
retry_params.force_refetch = true;
|
||||
if let Err(err) = Self::apps_list_response(
|
||||
&outgoing,
|
||||
connection_id,
|
||||
retry_params,
|
||||
retry_config,
|
||||
retry_environment_manager,
|
||||
@@ -189,7 +176,6 @@ impl AppsRequestProcessor {
|
||||
|
||||
async fn apps_list_response(
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
connection_id: ConnectionId,
|
||||
params: AppsListParams,
|
||||
config: Config,
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
@@ -269,7 +255,7 @@ impl AppsRequestProcessor {
|
||||
accessible_loaded,
|
||||
all_loaded,
|
||||
) {
|
||||
send_app_list_updated_notification(outgoing, connection_id, merged.clone()).await;
|
||||
send_app_list_updated_notification(outgoing, merged.clone()).await;
|
||||
last_notified_apps = Some(merged);
|
||||
}
|
||||
}
|
||||
@@ -329,7 +315,7 @@ impl AppsRequestProcessor {
|
||||
all_loaded,
|
||||
) && last_notified_apps.as_ref() != Some(&merged)
|
||||
{
|
||||
send_app_list_updated_notification(outgoing, connection_id, merged.clone()).await;
|
||||
send_app_list_updated_notification(outgoing, merged.clone()).await;
|
||||
last_notified_apps = Some(merged.clone());
|
||||
}
|
||||
|
||||
@@ -458,47 +444,12 @@ fn paginate_apps(
|
||||
|
||||
async fn send_app_list_updated_notification(
|
||||
outgoing: &Arc<OutgoingMessageSender>,
|
||||
connection_id: ConnectionId,
|
||||
data: Vec<AppInfo>,
|
||||
) {
|
||||
let data = data.into_iter().map(app_info_to_api).collect();
|
||||
outgoing
|
||||
.send_server_notification_to_connections(
|
||||
&[connection_id],
|
||||
ServerNotification::AppListUpdated(AppListUpdatedNotification { data }),
|
||||
)
|
||||
.send_server_notification(ServerNotification::AppListUpdated(
|
||||
AppListUpdatedNotification { data },
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::outgoing_message::OutgoingEnvelope;
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_list_updated_notification_targets_requesting_connection() {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
|
||||
send_app_list_updated_notification(&outgoing, ConnectionId(42), Vec::new()).await;
|
||||
|
||||
let envelope = rx.recv().await.expect("notification should be sent");
|
||||
let OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message,
|
||||
..
|
||||
} = envelope
|
||||
else {
|
||||
panic!("app list update should not be broadcast");
|
||||
};
|
||||
assert_eq!(connection_id, ConnectionId(42));
|
||||
assert!(matches!(
|
||||
message,
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::AppListUpdated(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +120,8 @@ impl CatalogRequestProcessor {
|
||||
pub(crate) async fn skills_list(
|
||||
&self,
|
||||
params: SkillsListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.skills_list_response(params, host_capabilities)
|
||||
self.skills_list_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -130,9 +129,8 @@ impl CatalogRequestProcessor {
|
||||
pub(crate) async fn hooks_list(
|
||||
&self,
|
||||
params: HooksListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.hooks_list_response(params, host_capabilities)
|
||||
self.hooks_list_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -490,7 +488,6 @@ impl CatalogRequestProcessor {
|
||||
async fn skills_list_response(
|
||||
&self,
|
||||
params: SkillsListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<SkillsListResponse, JSONRPCErrorError> {
|
||||
let SkillsListParams { cwds, force_reload } = params;
|
||||
let cwds = if cwds.is_empty() {
|
||||
@@ -499,8 +496,7 @@ impl CatalogRequestProcessor {
|
||||
cwds
|
||||
};
|
||||
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let workspace_codex_plugins_enabled = self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
@@ -599,7 +595,6 @@ impl CatalogRequestProcessor {
|
||||
async fn hooks_list_response(
|
||||
&self,
|
||||
params: HooksListParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<HooksListResponse, JSONRPCErrorError> {
|
||||
let HooksListParams { cwds } = params;
|
||||
let cwds = if cwds.is_empty() {
|
||||
@@ -616,10 +611,7 @@ impl CatalogRequestProcessor {
|
||||
.config_manager
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
ConfigOverrides {
|
||||
host_capabilities: host_capabilities.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides::default(),
|
||||
Some(cwd.clone()),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -71,15 +71,6 @@ impl InitializeRequestProcessor {
|
||||
let experimental_api_enabled = capabilities.experimental_api;
|
||||
let request_attestation = capabilities.request_attestation;
|
||||
let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation;
|
||||
let host_capabilities = capabilities
|
||||
.host_capabilities
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|capability| {
|
||||
let capability = capability.trim();
|
||||
(!capability.is_empty()).then(|| capability.to_string())
|
||||
})
|
||||
.collect();
|
||||
let opt_out_notification_methods = capabilities
|
||||
.opt_out_notification_methods
|
||||
.unwrap_or_default();
|
||||
@@ -107,7 +98,6 @@ impl InitializeRequestProcessor {
|
||||
client_version: version,
|
||||
request_attestation,
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
|
||||
@@ -27,11 +27,9 @@ impl McpRequestProcessor {
|
||||
|
||||
pub(crate) async fn mcp_server_oauth_login(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerOauthLoginParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.mcp_server_oauth_login_response(request_id, params, host_capabilities)
|
||||
self.mcp_server_oauth_login_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -39,9 +37,8 @@ impl McpRequestProcessor {
|
||||
pub(crate) async fn mcp_server_refresh(
|
||||
&self,
|
||||
params: Option<()>,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.mcp_server_refresh_response(params, host_capabilities)
|
||||
self.mcp_server_refresh_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -50,9 +47,8 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.list_mcp_server_status(request_id, params, host_capabilities)
|
||||
self.list_mcp_server_status(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -61,9 +57,8 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpResourceReadParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.read_mcp_resource(request_id, params, host_capabilities)
|
||||
self.read_mcp_resource(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -72,9 +67,8 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerToolCallParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.call_mcp_server_tool(request_id, params, host_capabilities)
|
||||
self.call_mcp_server_tool(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -82,7 +76,6 @@ impl McpRequestProcessor {
|
||||
async fn mcp_server_refresh_response(
|
||||
&self,
|
||||
_params: Option<()>,
|
||||
_host_capabilities: HostCapabilities,
|
||||
) -> Result<McpServerRefreshResponse, JSONRPCErrorError> {
|
||||
crate::mcp_refresh::queue_strict_refresh(&self.thread_manager, &self.config_manager)
|
||||
.await
|
||||
@@ -118,9 +111,7 @@ impl McpRequestProcessor {
|
||||
|
||||
async fn mcp_server_oauth_login_response(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerOauthLoginParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<McpServerOauthLoginResponse, JSONRPCErrorError> {
|
||||
let McpServerOauthLoginParams {
|
||||
name,
|
||||
@@ -132,20 +123,12 @@ impl McpRequestProcessor {
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let (mcp_config, runtime_context) = match thread_id.as_deref() {
|
||||
Some(thread_id) => {
|
||||
let (thread_id, thread) = self.load_thread(thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"start MCP OAuth login",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self.load_thread(thread_id).await?;
|
||||
let runtime = thread.current_mcp_runtime().await;
|
||||
(runtime.config().clone(), runtime.runtime_context().clone())
|
||||
}
|
||||
None => {
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let mcp_config = self
|
||||
.thread_manager
|
||||
.mcp_manager()
|
||||
@@ -217,7 +200,6 @@ impl McpRequestProcessor {
|
||||
let authorization_url = handle.authorization_url().to_string();
|
||||
let notification_name = name.clone();
|
||||
let notification_thread_id = thread_id;
|
||||
let notification_connection_id = request_id.connection_id;
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -234,12 +216,7 @@ impl McpRequestProcessor {
|
||||
error,
|
||||
},
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification_to_connections(
|
||||
&[notification_connection_id],
|
||||
notification,
|
||||
)
|
||||
.await;
|
||||
outgoing.send_server_notification(notification).await;
|
||||
});
|
||||
|
||||
Ok(McpServerOauthLoginResponse { authorization_url })
|
||||
@@ -249,35 +226,22 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ListMcpServerStatusParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let request = request_id.clone();
|
||||
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let (config, thread) = match params.thread_id.as_deref() {
|
||||
Some(thread_id) => {
|
||||
let (thread_id, thread) = self.load_thread(thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"list MCP server status",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self.load_thread(thread_id).await?;
|
||||
let thread_config = thread.config().await;
|
||||
let mut config = self
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_latest_config_for_thread(thread_config.as_ref())
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to reload config: {err}")))?;
|
||||
config.host_capabilities = host_capabilities.clone();
|
||||
(config, Some(thread))
|
||||
}
|
||||
None => {
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
(config, None)
|
||||
}
|
||||
None => (self.load_latest_config(/*fallback_cwd*/ None).await?, None),
|
||||
};
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
|
||||
@@ -423,7 +387,6 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpResourceReadParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let McpResourceReadParams {
|
||||
@@ -433,14 +396,7 @@ impl McpRequestProcessor {
|
||||
} = params;
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
let (thread_id, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"read an MCP resource",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
let request_id = request_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -450,8 +406,7 @@ impl McpRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
let mcp_manager = self.thread_manager.mcp_manager();
|
||||
let mcp_config = mcp_manager.runtime_config(&config).await;
|
||||
let codex_apps_tools_cache = mcp_manager.codex_apps_tools_cache();
|
||||
@@ -501,19 +456,11 @@ impl McpRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: McpServerToolCallParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let thread_id = params.thread_id.clone();
|
||||
let (thread_id, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"call an MCP tool",
|
||||
)
|
||||
.await?;
|
||||
let meta = with_mcp_tool_call_thread_id_meta(params.meta, ¶ms.thread_id);
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
let meta = with_mcp_tool_call_thread_id_meta(params.meta, &thread_id);
|
||||
let request_id = request_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -23,7 +23,6 @@ use codex_core_plugins::remote_bundle::RemotePluginBundleInstallError;
|
||||
use codex_mcp::McpOAuthLoginSupport;
|
||||
use codex_mcp::oauth_login_support;
|
||||
use codex_mcp::should_retry_without_scopes;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::auth::AuthMode as DomainAuthMode;
|
||||
@@ -89,39 +88,6 @@ fn local_plugin_interface_to_info(interface: PluginManifestInterface) -> PluginI
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_plugin_auth_capabilities_for_host(
|
||||
config: &Config,
|
||||
plugin_root: &Path,
|
||||
auth_mode: Option<DomainAuthMode>,
|
||||
) -> (HashMap<String, McpServerConfig>, Vec<AppConnectorId>) {
|
||||
if !plugin_auth_side_effects_available_for_host(plugin_root, &config.host_capabilities) {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let (mcp_servers, app_declarations) = tokio::join!(
|
||||
load_plugin_mcp_servers(plugin_root, auth_mode),
|
||||
load_plugin_apps(plugin_root),
|
||||
);
|
||||
(
|
||||
mcp_servers,
|
||||
codex_plugin::app_connector_ids_from_declarations(&app_declarations),
|
||||
)
|
||||
}
|
||||
|
||||
fn plugin_auth_side_effects_available_for_host(
|
||||
plugin_root: &Path,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> bool {
|
||||
let Some(manifest) = codex_core_plugins::manifest::load_plugin_manifest(plugin_root) else {
|
||||
return false;
|
||||
};
|
||||
!manifest
|
||||
.requires
|
||||
.host_capabilities
|
||||
.iter()
|
||||
.any(|capability| !host_capabilities.contains(capability))
|
||||
}
|
||||
|
||||
fn marketplace_plugin_source_to_info(source: MarketplacePluginSource) -> PluginSource {
|
||||
match source {
|
||||
MarketplacePluginSource::Local { path } => PluginSource::Local { path },
|
||||
@@ -486,11 +452,9 @@ impl PluginRequestProcessor {
|
||||
|
||||
pub(crate) async fn plugin_install(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: PluginInstallParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.plugin_install_response(request_id, params, host_capabilities)
|
||||
self.plugin_install_response(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -1458,9 +1422,7 @@ impl PluginRequestProcessor {
|
||||
|
||||
async fn plugin_install_response(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: PluginInstallParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<PluginInstallResponse, JSONRPCErrorError> {
|
||||
let PluginInstallParams {
|
||||
marketplace_path,
|
||||
@@ -1471,12 +1433,7 @@ impl PluginRequestProcessor {
|
||||
(Some(marketplace_path), None) => marketplace_path,
|
||||
(None, Some(remote_marketplace_name)) => {
|
||||
return self
|
||||
.remote_plugin_install_response(
|
||||
request_id,
|
||||
remote_marketplace_name,
|
||||
plugin_name,
|
||||
host_capabilities,
|
||||
)
|
||||
.remote_plugin_install_response(remote_marketplace_name, plugin_name)
|
||||
.await;
|
||||
}
|
||||
(Some(_), Some(_)) | (None, None) => {
|
||||
@@ -1486,8 +1443,7 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
};
|
||||
let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf);
|
||||
let mut config = self.load_latest_config(config_cwd.clone()).await?;
|
||||
config.host_capabilities = host_capabilities.clone();
|
||||
let config = self.load_latest_config(config_cwd.clone()).await?;
|
||||
let auth = self.auth_manager.auth().await;
|
||||
|
||||
if !self
|
||||
@@ -1521,7 +1477,7 @@ impl PluginRequestProcessor {
|
||||
return Err(Self::plugin_install_error(err));
|
||||
}
|
||||
};
|
||||
let mut config = match self.load_latest_config(config_cwd).await {
|
||||
let config = match self.load_latest_config(config_cwd).await {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -1530,25 +1486,22 @@ impl PluginRequestProcessor {
|
||||
config
|
||||
}
|
||||
};
|
||||
config.host_capabilities = host_capabilities;
|
||||
|
||||
self.on_effective_plugins_changed();
|
||||
|
||||
let (plugin_mcp_servers, plugin_apps) = load_plugin_auth_capabilities_for_host(
|
||||
&config,
|
||||
let plugin_mcp_servers = load_plugin_mcp_servers(
|
||||
result.installed_path.as_path(),
|
||||
auth.as_ref().map(CodexAuth::auth_mode),
|
||||
)
|
||||
.await;
|
||||
if !plugin_mcp_servers.is_empty() {
|
||||
self.start_plugin_mcp_oauth_logins(
|
||||
request_id.connection_id,
|
||||
&config,
|
||||
plugin_mcp_servers,
|
||||
)
|
||||
.await;
|
||||
self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers)
|
||||
.await;
|
||||
}
|
||||
|
||||
let plugin_app_declarations = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
let plugin_apps =
|
||||
codex_plugin::app_connector_ids_from_declarations(&plugin_app_declarations);
|
||||
let apps_needing_auth = self
|
||||
.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
@@ -1566,13 +1519,10 @@ impl PluginRequestProcessor {
|
||||
|
||||
async fn remote_plugin_install_response(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
remote_marketplace_name: String,
|
||||
remote_plugin_id: String,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<PluginInstallResponse, JSONRPCErrorError> {
|
||||
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
config.host_capabilities = host_capabilities;
|
||||
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
if !config.features.enabled(Feature::Plugins) {
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin install is not enabled for marketplace {remote_marketplace_name}"
|
||||
@@ -1716,72 +1666,66 @@ impl PluginRequestProcessor {
|
||||
self.analytics_events_client
|
||||
.track_plugin_installed(plugin_metadata);
|
||||
|
||||
let auth_side_effects_available = plugin_auth_side_effects_available_for_host(
|
||||
result.installed_path.as_path(),
|
||||
&config.host_capabilities,
|
||||
);
|
||||
let (plugin_mcp_servers, plugin_apps) = load_plugin_auth_capabilities_for_host(
|
||||
&config,
|
||||
let plugin_mcp_servers = load_plugin_mcp_servers(
|
||||
result.installed_path.as_path(),
|
||||
auth.as_ref().map(CodexAuth::auth_mode),
|
||||
)
|
||||
.await;
|
||||
if !plugin_mcp_servers.is_empty() {
|
||||
self.start_plugin_mcp_oauth_logins(
|
||||
request_id.connection_id,
|
||||
&config,
|
||||
plugin_mcp_servers,
|
||||
)
|
||||
.await;
|
||||
self.start_plugin_mcp_oauth_logins(&config, plugin_mcp_servers)
|
||||
.await;
|
||||
}
|
||||
|
||||
let is_chatgpt_auth = auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth);
|
||||
let apps_needing_auth =
|
||||
if let Some(app_ids_needing_auth) = install_result.app_ids_needing_auth {
|
||||
if app_ids_needing_auth.is_empty()
|
||||
|| !config.features.apps_enabled_for_auth(is_chatgpt_auth)
|
||||
|| !auth_side_effects_available
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
let plugin_apps = app_ids_needing_auth
|
||||
.into_iter()
|
||||
.map(AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let app_category_by_id = remote_detail
|
||||
.app_manifest
|
||||
.as_ref()
|
||||
.map(plugin_app_category_by_id_from_value)
|
||||
.unwrap_or_default();
|
||||
let all_connectors = connectors::list_cached_all_connectors(&config, &[])
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
|
||||
.into_iter()
|
||||
.map(|connector| {
|
||||
let category = app_category_by_id
|
||||
.get(&connector.id)
|
||||
.cloned()
|
||||
.or_else(|| connector.category());
|
||||
AppSummary {
|
||||
category,
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let apps_needing_auth = if let Some(app_ids_needing_auth) =
|
||||
install_result.app_ids_needing_auth
|
||||
{
|
||||
if app_ids_needing_auth.is_empty()
|
||||
|| !config.features.apps_enabled_for_auth(is_chatgpt_auth)
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
self.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
is_chatgpt_auth,
|
||||
&result.plugin_id.as_key(),
|
||||
&plugin_apps,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let plugin_apps = app_ids_needing_auth
|
||||
.into_iter()
|
||||
.map(codex_plugin::AppConnectorId)
|
||||
.collect::<Vec<_>>();
|
||||
let app_category_by_id = remote_detail
|
||||
.app_manifest
|
||||
.as_ref()
|
||||
.map(plugin_app_category_by_id_from_value)
|
||||
.unwrap_or_default();
|
||||
let all_connectors = connectors::list_cached_all_connectors(&config, &[])
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
connectors::connectors_for_plugin_apps(all_connectors, &plugin_apps)
|
||||
.into_iter()
|
||||
.map(|connector| {
|
||||
let category = app_category_by_id
|
||||
.get(&connector.id)
|
||||
.cloned()
|
||||
.or_else(|| connector.category());
|
||||
AppSummary {
|
||||
category,
|
||||
id: connector.id,
|
||||
name: connector.name,
|
||||
description: connector.description,
|
||||
install_url: connector.install_url,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
let plugin_app_declarations = load_plugin_apps(result.installed_path.as_path()).await;
|
||||
let plugin_apps =
|
||||
codex_plugin::app_connector_ids_from_declarations(&plugin_app_declarations);
|
||||
self.plugin_apps_needing_auth_for_install(
|
||||
&config,
|
||||
is_chatgpt_auth,
|
||||
&result.plugin_id.as_key(),
|
||||
&plugin_apps,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(PluginInstallResponse {
|
||||
auth_policy: remote_detail.summary.auth_policy,
|
||||
@@ -1891,7 +1835,6 @@ impl PluginRequestProcessor {
|
||||
|
||||
async fn start_plugin_mcp_oauth_logins(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
config: &Config,
|
||||
plugin_mcp_servers: HashMap<String, McpServerConfig>,
|
||||
) {
|
||||
@@ -1970,9 +1913,7 @@ impl PluginRequestProcessor {
|
||||
error,
|
||||
},
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification_to_connections(&[connection_id], notification)
|
||||
.await;
|
||||
outgoing.send_server_notification(notification).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2457,37 +2398,3 @@ fn remote_plugin_bundle_install_error_to_jsonrpc(
|
||||
) -> JSONRPCErrorError {
|
||||
internal_error(format!("install remote plugin bundle: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod host_capability_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn post_install_auth_side_effects_require_host_capabilities() {
|
||||
let plugin_root = tempfile::tempdir().expect("temp plugin root");
|
||||
let manifest_dir = plugin_root.path().join(".codex-plugin");
|
||||
std::fs::create_dir_all(&manifest_dir).expect("create manifest directory");
|
||||
std::fs::write(
|
||||
manifest_dir.join("plugin.json"),
|
||||
r#"{
|
||||
"name": "gated-auth-plugin",
|
||||
"requires": {
|
||||
"hostCapabilities": ["codex.inline_visualization"]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write plugin manifest");
|
||||
|
||||
let mut host_capabilities = HostCapabilities::new();
|
||||
assert!(!plugin_auth_side_effects_available_for_host(
|
||||
plugin_root.path(),
|
||||
&host_capabilities,
|
||||
));
|
||||
|
||||
host_capabilities.insert("codex.inline_visualization".to_string());
|
||||
assert!(plugin_auth_side_effects_available_for_host(
|
||||
plugin_root.path(),
|
||||
&host_capabilities,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,17 +451,17 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadStartParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
request_context: RequestContext,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_start_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
request_context,
|
||||
)
|
||||
.await
|
||||
@@ -482,16 +482,16 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_resume_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
@@ -501,16 +501,16 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_fork_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
)
|
||||
.await
|
||||
.map(|()| None)
|
||||
@@ -629,9 +629,8 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadCompactStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_compact_start_inner(request_id, params, host_capabilities)
|
||||
self.thread_compact_start_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -669,13 +668,12 @@ impl ThreadRequestProcessor {
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRollbackParams,
|
||||
app_server_client_name: Option<&str>,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
if app_server_client_name != Some(CODEX_TUI_CLIENT_NAME) {
|
||||
self.send_thread_rollback_deprecation_notice(request_id.connection_id)
|
||||
.await;
|
||||
}
|
||||
self.thread_rollback_inner(request_id, params, host_capabilities)
|
||||
self.thread_rollback_inner(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -804,13 +802,19 @@ impl ThreadRequestProcessor {
|
||||
|
||||
async fn set_app_server_client_info(
|
||||
thread: &CodexThread,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let AppServerClientInfo { name, version } = app_server_client_info;
|
||||
let mcp_elicitations_auto_deny =
|
||||
xcode_26_4_mcp_elicitations_auto_deny(name.as_deref(), version.as_deref());
|
||||
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
|
||||
app_server_client_name.as_deref(),
|
||||
app_server_client_version.as_deref(),
|
||||
);
|
||||
thread
|
||||
.set_app_server_client_info(name, version, mcp_elicitations_auto_deny)
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
|
||||
}
|
||||
@@ -927,9 +931,9 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadStartParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
request_context: RequestContext,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadStartParams {
|
||||
@@ -981,7 +985,6 @@ impl ThreadRequestProcessor {
|
||||
developer_instructions,
|
||||
personality,
|
||||
);
|
||||
typesafe_overrides.host_capabilities = host_capabilities;
|
||||
typesafe_overrides.ephemeral = ephemeral;
|
||||
let listener_task_context = ListenerTaskContext {
|
||||
thread_manager: Arc::clone(&self.thread_manager),
|
||||
@@ -1004,7 +1007,8 @@ impl ThreadRequestProcessor {
|
||||
listener_task_context,
|
||||
config_manager,
|
||||
request_id,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
supports_openai_form_elicitation,
|
||||
config,
|
||||
typesafe_overrides,
|
||||
@@ -1080,7 +1084,8 @@ impl ThreadRequestProcessor {
|
||||
listener_task_context: ListenerTaskContext,
|
||||
config_manager: ConfigManager,
|
||||
request_id: ConnectionRequestId,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
config_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
@@ -1249,7 +1254,12 @@ impl ThreadRequestProcessor {
|
||||
Some("ready"),
|
||||
);
|
||||
|
||||
Self::set_app_server_client_info(thread.as_ref(), app_server_client_info).await?;
|
||||
Self::set_app_server_client_info(
|
||||
thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let instruction_sources = thread.legacy_instruction_sources().await;
|
||||
let config_snapshot = thread
|
||||
@@ -1751,17 +1761,14 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRollbackParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
self.thread_rollback_start(request_id, params, host_capabilities)
|
||||
.await
|
||||
self.thread_rollback_start(request_id, params).await
|
||||
}
|
||||
|
||||
async fn thread_rollback_start(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRollbackParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadRollbackParams {
|
||||
thread_id,
|
||||
@@ -1773,13 +1780,6 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
|
||||
let (thread_id, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"roll back",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let request = request_id.clone();
|
||||
|
||||
@@ -1821,18 +1821,10 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadCompactStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<ThreadCompactStartResponse, JSONRPCErrorError> {
|
||||
let ThreadCompactStartParams { thread_id } = params;
|
||||
|
||||
let (thread_id, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"start compaction",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self.load_thread(&thread_id).await?;
|
||||
self.submit_core_op(request_id, thread.as_ref(), Op::Compact)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to start compaction: {err}")))?;
|
||||
@@ -2635,35 +2627,30 @@ impl ThreadRequestProcessor {
|
||||
pub(crate) async fn try_attach_thread_listener(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
connections: Vec<(ConnectionId, HostCapabilities)>,
|
||||
connection_ids: Vec<ConnectionId>,
|
||||
) {
|
||||
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
|
||||
return;
|
||||
};
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let thread_host_capabilities = config_snapshot.host_capabilities.clone();
|
||||
let loaded_thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
thread.session_configured().session_id.to_string(),
|
||||
&config_snapshot,
|
||||
thread.rollout_path(),
|
||||
);
|
||||
self.thread_watch_manager.upsert_thread(loaded_thread).await;
|
||||
let raw_events_enabled = if let Some(parent_thread_id) = config_snapshot.parent_thread_id {
|
||||
self.thread_state_manager
|
||||
.thread_state(parent_thread_id)
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.experimental_raw_events
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
for (connection_id, host_capabilities) in connections {
|
||||
if host_capabilities != thread_host_capabilities {
|
||||
continue;
|
||||
let mut raw_events_enabled = false;
|
||||
if let Ok(thread) = self.thread_manager.get_thread(thread_id).await {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let loaded_thread = build_thread_from_snapshot(
|
||||
thread_id,
|
||||
thread.session_configured().session_id.to_string(),
|
||||
&config_snapshot,
|
||||
thread.rollout_path(),
|
||||
);
|
||||
self.thread_watch_manager.upsert_thread(loaded_thread).await;
|
||||
if let Some(parent_thread_id) = config_snapshot.parent_thread_id {
|
||||
raw_events_enabled = self
|
||||
.thread_state_manager
|
||||
.thread_state(parent_thread_id)
|
||||
.await
|
||||
.lock()
|
||||
.await
|
||||
.experimental_raw_events;
|
||||
}
|
||||
}
|
||||
|
||||
for connection_id in connection_ids {
|
||||
log_listener_attach_result(
|
||||
self.ensure_conversation_listener(thread_id, connection_id, raw_events_enabled)
|
||||
.await,
|
||||
@@ -2678,9 +2665,9 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadResumeParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if let Ok(thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2710,7 +2697,7 @@ impl ThreadRequestProcessor {
|
||||
return Ok(());
|
||||
}
|
||||
let redact_resume_payloads =
|
||||
should_redact_thread_resume_payloads(app_server_client_info.name.as_deref());
|
||||
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
|
||||
|
||||
let _thread_list_state_permit = match self.acquire_thread_list_state_permit().await {
|
||||
Ok(permit) => permit,
|
||||
@@ -2723,8 +2710,8 @@ impl ThreadRequestProcessor {
|
||||
.resume_running_thread(
|
||||
&request_id,
|
||||
¶ms,
|
||||
&app_server_client_info,
|
||||
&host_capabilities,
|
||||
app_server_client_name.clone(),
|
||||
app_server_client_version.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -2795,7 +2782,6 @@ impl ThreadRequestProcessor {
|
||||
developer_instructions,
|
||||
personality,
|
||||
);
|
||||
typesafe_overrides.host_capabilities = host_capabilities;
|
||||
self.load_and_apply_persisted_resume_metadata(
|
||||
&thread_history,
|
||||
&mut request_overrides,
|
||||
@@ -2836,9 +2822,12 @@ impl ThreadRequestProcessor {
|
||||
session_configured,
|
||||
..
|
||||
}) => {
|
||||
if let Err(err) =
|
||||
Self::set_app_server_client_info(codex_thread.as_ref(), app_server_client_info)
|
||||
.await
|
||||
if let Err(err) = Self::set_app_server_client_info(
|
||||
codex_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return Ok(());
|
||||
@@ -3019,8 +3008,8 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: &ThreadResumeParams,
|
||||
app_server_client_info: &AppServerClientInfo,
|
||||
host_capabilities: &HostCapabilities,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<RunningThreadResumeResult, JSONRPCErrorError> {
|
||||
let running_thread = if params.history.is_some() {
|
||||
if let Ok(existing_thread_id) = ThreadId::from_string(¶ms.thread_id)
|
||||
@@ -3080,12 +3069,6 @@ impl ThreadRequestProcessor {
|
||||
)));
|
||||
}
|
||||
let config_snapshot = existing_thread.config_snapshot().await;
|
||||
if &config_snapshot.host_capabilities != host_capabilities {
|
||||
return Err(invalid_request(format!(
|
||||
"cannot resume running thread {existing_thread_id} with different host capabilities: requested={host_capabilities:?} active={:?}",
|
||||
config_snapshot.host_capabilities
|
||||
)));
|
||||
}
|
||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||
if !mismatch_details.is_empty() {
|
||||
let has_subscribers = !self
|
||||
@@ -3130,7 +3113,7 @@ impl ThreadRequestProcessor {
|
||||
);
|
||||
}
|
||||
let redact_resume_payloads =
|
||||
should_redact_thread_resume_payloads(app_server_client_info.name.as_deref());
|
||||
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
|
||||
let history_items = source_thread
|
||||
.history
|
||||
.take()
|
||||
@@ -3153,7 +3136,8 @@ impl ThreadRequestProcessor {
|
||||
.await?;
|
||||
Self::set_app_server_client_info(
|
||||
existing_thread.as_ref(),
|
||||
app_server_client_info.clone(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3451,9 +3435,9 @@ impl ThreadRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadForkParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ThreadForkParams {
|
||||
thread_id,
|
||||
@@ -3545,7 +3529,6 @@ impl ThreadRequestProcessor {
|
||||
developer_instructions,
|
||||
/*personality*/ None,
|
||||
);
|
||||
typesafe_overrides.host_capabilities = host_capabilities;
|
||||
typesafe_overrides.ephemeral = ephemeral.then_some(true);
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = self
|
||||
@@ -3584,7 +3567,12 @@ impl ThreadRequestProcessor {
|
||||
err => internal_error(format!("error forking thread: {err}")),
|
||||
})?;
|
||||
|
||||
Self::set_app_server_client_info(forked_thread.as_ref(), app_server_client_info).await?;
|
||||
Self::set_app_server_client_info(
|
||||
forked_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await?;
|
||||
if session_configured.rollout_path.is_some()
|
||||
&& let Some(name) = source_thread_name.clone()
|
||||
{
|
||||
|
||||
@@ -783,7 +783,6 @@ mod thread_processor_behavior_tests {
|
||||
parent_thread_id: None,
|
||||
thread_source: None,
|
||||
originator: "test_originator".to_string(),
|
||||
host_capabilities: Default::default(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -156,17 +156,17 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: TurnStartParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
validate_user_input_image_urls(¶ms.input)?;
|
||||
self.turn_start_inner(
|
||||
request_id,
|
||||
params,
|
||||
app_server_client_info,
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
/*supports_openai_form_elicitation*/ supports_openai_form_elicitation,
|
||||
host_capabilities,
|
||||
)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
@@ -175,9 +175,8 @@ impl TurnRequestProcessor {
|
||||
pub(crate) async fn thread_inject_items(
|
||||
&self,
|
||||
params: ThreadInjectItemsParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_inject_items_response_inner(params, host_capabilities)
|
||||
self.thread_inject_items_response_inner(params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -196,10 +195,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: TurnSteerParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
validate_user_input_image_urls(¶ms.input)?;
|
||||
self.turn_steer_inner(request_id, params, host_capabilities)
|
||||
self.turn_steer_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
@@ -208,9 +206,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: TurnInterruptParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.turn_interrupt_inner(request_id, params, host_capabilities)
|
||||
self.turn_interrupt_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -219,9 +216,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_start_inner(request_id, params, host_capabilities)
|
||||
self.thread_realtime_start_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -230,9 +226,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendAudioParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_append_audio_inner(request_id, params, host_capabilities)
|
||||
self.thread_realtime_append_audio_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -241,9 +236,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendTextParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_append_text_inner(request_id, params, host_capabilities)
|
||||
self.thread_realtime_append_text_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -252,9 +246,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendSpeechParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_append_speech_inner(request_id, params, host_capabilities)
|
||||
self.thread_realtime_append_speech_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -263,9 +256,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStopParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_realtime_stop_inner(request_id, params, host_capabilities)
|
||||
self.thread_realtime_stop_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| response.map(Into::into))
|
||||
}
|
||||
@@ -285,9 +277,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.review_start_inner(request_id, params, host_capabilities)
|
||||
self.review_start_inner(request_id, params)
|
||||
.await
|
||||
.map(|()| None)
|
||||
}
|
||||
@@ -452,9 +443,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: TurnStartParams,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<TurnStartResponse, JSONRPCErrorError> {
|
||||
let (thread_id, thread) =
|
||||
self.load_thread(¶ms.thread_id)
|
||||
@@ -462,13 +453,6 @@ impl TurnRequestProcessor {
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"start a turn",
|
||||
)
|
||||
.await?;
|
||||
self.ensure_direct_input_allowed(&request_id, thread.as_ref())
|
||||
.await?;
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
@@ -479,11 +463,15 @@ impl TurnRequestProcessor {
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Self::set_app_server_client_info(thread.as_ref(), app_server_client_info)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
Self::set_app_server_client_info(
|
||||
thread.as_ref(),
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
thread
|
||||
.set_openai_form_elicitation_support(supports_openai_form_elicitation)
|
||||
.await
|
||||
@@ -814,16 +802,8 @@ impl TurnRequestProcessor {
|
||||
async fn thread_inject_items_response_inner(
|
||||
&self,
|
||||
params: ThreadInjectItemsParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<ThreadInjectItemsResponse, JSONRPCErrorError> {
|
||||
let (thread_id, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"inject items",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
|
||||
let items = params
|
||||
.items
|
||||
@@ -849,13 +829,19 @@ impl TurnRequestProcessor {
|
||||
|
||||
async fn set_app_server_client_info(
|
||||
thread: &CodexThread,
|
||||
app_server_client_info: AppServerClientInfo,
|
||||
app_server_client_name: Option<String>,
|
||||
app_server_client_version: Option<String>,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let AppServerClientInfo { name, version } = app_server_client_info;
|
||||
let mcp_elicitations_auto_deny =
|
||||
xcode_26_4_mcp_elicitations_auto_deny(name.as_deref(), version.as_deref());
|
||||
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
|
||||
app_server_client_name.as_deref(),
|
||||
app_server_client_version.as_deref(),
|
||||
);
|
||||
thread
|
||||
.set_app_server_client_info(name, version, mcp_elicitations_auto_deny)
|
||||
.set_app_server_client_info(
|
||||
app_server_client_name,
|
||||
app_server_client_version,
|
||||
mcp_elicitations_auto_deny,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
|
||||
}
|
||||
@@ -864,21 +850,13 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: TurnSteerParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<TurnSteerResponse, JSONRPCErrorError> {
|
||||
let (thread_id, thread) =
|
||||
self.load_thread(¶ms.thread_id)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"steer a turn",
|
||||
)
|
||||
.await?;
|
||||
let (_, thread) = self
|
||||
.load_thread(¶ms.thread_id)
|
||||
.await
|
||||
.inspect_err(|error| {
|
||||
self.track_error_response(request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
self.ensure_direct_input_allowed(request_id, thread.as_ref())
|
||||
.await?;
|
||||
|
||||
@@ -981,16 +959,8 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
thread_id: &str,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> Result<Option<(ThreadId, Arc<CodexThread>)>, JSONRPCErrorError> {
|
||||
let (thread_id, thread) = self.load_thread(thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_id,
|
||||
thread.as_ref(),
|
||||
host_capabilities,
|
||||
"use realtime",
|
||||
)
|
||||
.await?;
|
||||
|
||||
match self
|
||||
.ensure_conversation_listener(
|
||||
@@ -1020,10 +990,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ThreadRealtimeStartResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities)
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -1065,10 +1034,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendAudioParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ThreadRealtimeAppendAudioResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities)
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -1093,10 +1061,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendTextParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ThreadRealtimeAppendTextResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities)
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -1122,10 +1089,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeAppendSpeechParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ThreadRealtimeAppendSpeechResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities)
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -1148,10 +1114,9 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadRealtimeStopParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<ThreadRealtimeStopResponse>, JSONRPCErrorError> {
|
||||
let Some((_, thread)) = self
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id, &host_capabilities)
|
||||
.prepare_realtime_conversation_thread(request_id, ¶ms.thread_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
@@ -1251,7 +1216,7 @@ impl TurnRequestProcessor {
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut config = parent_thread.config().await.as_ref().clone();
|
||||
let mut config = self.config.as_ref().clone();
|
||||
if let Some(review_model) = &config.review_model {
|
||||
config.model = Some(review_model.clone());
|
||||
}
|
||||
@@ -1344,7 +1309,6 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ReviewStartParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let ReviewStartParams {
|
||||
thread_id,
|
||||
@@ -1353,13 +1317,6 @@ impl TurnRequestProcessor {
|
||||
} = params;
|
||||
|
||||
let (parent_thread_id, parent_thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
parent_thread_id,
|
||||
parent_thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"start a review",
|
||||
)
|
||||
.await?;
|
||||
let (review_request, display_text) = Self::review_request_from_target(target)?;
|
||||
match delivery.unwrap_or(ApiReviewDelivery::Inline).to_core() {
|
||||
CoreReviewDelivery::Inline => {
|
||||
@@ -1390,19 +1347,11 @@ impl TurnRequestProcessor {
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: TurnInterruptParams,
|
||||
host_capabilities: HostCapabilities,
|
||||
) -> Result<Option<TurnInterruptResponse>, JSONRPCErrorError> {
|
||||
let TurnInterruptParams { thread_id, turn_id } = params;
|
||||
let is_startup_interrupt = turn_id.is_empty();
|
||||
|
||||
let (thread_uuid, thread) = self.load_thread(&thread_id).await?;
|
||||
ensure_thread_host_capabilities(
|
||||
thread_uuid,
|
||||
thread.as_ref(),
|
||||
&host_capabilities,
|
||||
"interrupt a turn",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Record turn interrupts so we can reply when TurnAborted arrives. Startup
|
||||
// interrupts do not have a turn and are acknowledged after submission.
|
||||
|
||||
@@ -85,7 +85,6 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
request_attestation: true,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
@@ -18,14 +17,10 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use core_test_support::responses;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use hmac::Hmac;
|
||||
@@ -55,9 +50,6 @@ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
||||
use tokio_tungstenite::tungstenite::http::header::ORIGIN;
|
||||
use wiremock::Mock;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
// macOS and Windows CI can spend tens of seconds starting the app-server test
|
||||
// binary under Bazel before it accepts JSON-RPC or reports its websocket bind
|
||||
@@ -70,12 +62,6 @@ pub(super) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub(super) type WsClient = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
fn body_contains(req: &wiremock::Request, text: &str) -> bool {
|
||||
String::from_utf8(req.body.clone())
|
||||
.ok()
|
||||
.is_some_and(|body| body.contains(text))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
@@ -121,310 +107,6 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn host_capabilities_are_normalized_and_sticky_for_loaded_threads() -> Result<()> {
|
||||
const CHILD_PROMPT: &str = "child: verify capability-scoped auto-attach";
|
||||
const PARENT_PROMPT: &str = "spawn a child to test connection auto-attach";
|
||||
const SPAWN_CALL_ID: &str = "spawn-call-host-capabilities";
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let spawn_args = serde_json::to_string(&json!({
|
||||
"message": CHILD_PROMPT,
|
||||
"task_name": "capability_check",
|
||||
}))?;
|
||||
let _parent_turn = responses::mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, PARENT_PROMPT),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-parent-1"),
|
||||
responses::ev_function_call_with_namespace(
|
||||
SPAWN_CALL_ID,
|
||||
"collaboration",
|
||||
"spawn_agent",
|
||||
&spawn_args,
|
||||
),
|
||||
responses::ev_completed("resp-parent-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let child_response = responses::sse(vec![
|
||||
responses::ev_response_created("resp-child-1"),
|
||||
responses::ev_assistant_message("msg-child-1", "child done"),
|
||||
responses::ev_completed("resp-child-1"),
|
||||
]);
|
||||
Mock::given(method("POST"))
|
||||
.and(path_regex(".*/responses$"))
|
||||
.and(|req: &wiremock::Request| {
|
||||
body_contains(req, CHILD_PROMPT) && !body_contains(req, SPAWN_CALL_ID)
|
||||
})
|
||||
.respond_with(responses::sse_response(child_response).set_delay(Duration::from_millis(250)))
|
||||
.up_to_n_times(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let _parent_follow_up = responses::mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, SPAWN_CALL_ID),
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-parent-2"),
|
||||
responses::ev_assistant_message("msg-parent-2", "parent done"),
|
||||
responses::ev_completed("resp-parent-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let config_path = codex_home.path().join("config.toml");
|
||||
let config = std::fs::read_to_string(&config_path)?;
|
||||
std::fs::write(
|
||||
config_path,
|
||||
format!("{config}\n[features]\nmulti_agent_v2 = true\n"),
|
||||
)?;
|
||||
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let mut owner = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request_with_host_capabilities(
|
||||
&mut owner,
|
||||
/*id*/ 1,
|
||||
"ws_thread_owner",
|
||||
Some(vec![" capability-a ", "", "capability-a", "capability-b"]),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut owner, /*id*/ 1)
|
||||
.await
|
||||
.context("owner initialize response")?;
|
||||
|
||||
// A connection with a different capability set must not be auto-attached
|
||||
// when the owner creates the thread.
|
||||
let mut mismatched_client = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request_with_host_capabilities(
|
||||
&mut mismatched_client,
|
||||
/*id*/ 6,
|
||||
"ws_mismatched_client",
|
||||
Some(vec!["capability-c"]),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut mismatched_client, /*id*/ 6)
|
||||
.await
|
||||
.context("mismatched-client initialize response")?;
|
||||
|
||||
// Equivalent capabilities with different ordering and without duplicates
|
||||
// must be eligible for auto-attach after normalization.
|
||||
let mut equivalent_client = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request_with_host_capabilities(
|
||||
&mut equivalent_client,
|
||||
/*id*/ 4,
|
||||
"ws_equivalent_client",
|
||||
Some(vec!["capability-b", "capability-a"]),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut equivalent_client, /*id*/ 4)
|
||||
.await
|
||||
.context("equivalent-client initialize response")?;
|
||||
|
||||
let thread_id = start_thread(&mut owner, /*id*/ 2).await?;
|
||||
read_notification_for_method(&mut mismatched_client, "thread/started")
|
||||
.await
|
||||
.context("mismatched-client global thread/started notification")?;
|
||||
read_notification_for_method(&mut equivalent_client, "thread/started")
|
||||
.await
|
||||
.context("equivalent-client global thread/started notification")?;
|
||||
send_request(
|
||||
&mut owner,
|
||||
"turn/start",
|
||||
/*id*/ 3,
|
||||
Some(serde_json::to_value(TurnStartParams {
|
||||
thread_id: thread_id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: PARENT_PROMPT.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut owner, /*id*/ 3)
|
||||
.await
|
||||
.context("owner turn/start response")?;
|
||||
read_notification_for_method(&mut owner, "turn/completed")
|
||||
.await
|
||||
.context("owner turn/completed notification")?;
|
||||
read_notification_for_method(&mut equivalent_client, "turn/completed")
|
||||
.await
|
||||
.context("equivalent auto-attached client child turn/completed notification")?;
|
||||
|
||||
match timeout(Duration::from_millis(250), async {
|
||||
loop {
|
||||
if let JSONRPCMessage::Notification(notification) =
|
||||
read_jsonrpc_message(&mut mismatched_client).await?
|
||||
&& notification.method == "turn/completed"
|
||||
{
|
||||
return Ok::<_, anyhow::Error>(notification);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => bail!("mismatched client received thread-scoped turn/completed"),
|
||||
Ok(Err(err)) => return Err(err),
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
send_request(
|
||||
&mut equivalent_client,
|
||||
"thread/resume",
|
||||
/*id*/ 5,
|
||||
Some(serde_json::to_value(ThreadResumeParams {
|
||||
thread_id: thread_id.clone(),
|
||||
..Default::default()
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut equivalent_client, /*id*/ 5)
|
||||
.await
|
||||
.context("equivalent-client resume response")?;
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"thread/resume",
|
||||
/*id*/ 7,
|
||||
Some(serde_json::to_value(ThreadResumeParams {
|
||||
thread_id: thread_id.clone(),
|
||||
..Default::default()
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let resume_error = read_error_for_id(&mut mismatched_client, /*id*/ 7)
|
||||
.await
|
||||
.context("mismatched-client resume error")?;
|
||||
assert!(
|
||||
resume_error
|
||||
.error
|
||||
.message
|
||||
.contains("cannot resume running thread")
|
||||
);
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"turn/start",
|
||||
/*id*/ 8,
|
||||
Some(serde_json::to_value(TurnStartParams {
|
||||
thread_id: thread_id.clone(),
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})?),
|
||||
)
|
||||
.await?;
|
||||
let turn_error = read_error_for_id(&mut mismatched_client, /*id*/ 8)
|
||||
.await
|
||||
.context("mismatched-client turn/start error")?;
|
||||
assert!(turn_error.error.message.contains("cannot start a turn"));
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"turn/steer",
|
||||
/*id*/ 9,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
"expectedTurnId": "turn-under-test",
|
||||
"input": [],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let steer_error = read_error_for_id(&mut mismatched_client, /*id*/ 9)
|
||||
.await
|
||||
.context("mismatched-client turn/steer error")?;
|
||||
assert!(steer_error.error.message.contains("cannot steer a turn"));
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"thread/inject_items",
|
||||
/*id*/ 10,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
"items": [],
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let inject_error = read_error_for_id(&mut mismatched_client, /*id*/ 10)
|
||||
.await
|
||||
.context("mismatched-client thread/injectItems error")?;
|
||||
assert!(
|
||||
inject_error.error.message.contains("cannot inject items"),
|
||||
"unexpected inject error: {}",
|
||||
inject_error.error.message
|
||||
);
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"app/list",
|
||||
/*id*/ 11,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let apps_error = read_error_for_id(&mut mismatched_client, /*id*/ 11)
|
||||
.await
|
||||
.context("mismatched-client app/list error")?;
|
||||
assert!(apps_error.error.message.contains("cannot list apps"));
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"thread/rollback",
|
||||
/*id*/ 12,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
"numTurns": 1,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let rollback_error = read_error_for_id(&mut mismatched_client, /*id*/ 12)
|
||||
.await
|
||||
.context("mismatched-client thread/rollback error")?;
|
||||
assert!(rollback_error.error.message.contains("cannot roll back"));
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"turn/interrupt",
|
||||
/*id*/ 13,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
"turnId": "turn-under-test",
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let interrupt_error = read_error_for_id(&mut mismatched_client, /*id*/ 13)
|
||||
.await
|
||||
.context("mismatched-client turn/interrupt error")?;
|
||||
assert!(
|
||||
interrupt_error
|
||||
.error
|
||||
.message
|
||||
.contains("cannot interrupt a turn")
|
||||
);
|
||||
|
||||
send_request(
|
||||
&mut mismatched_client,
|
||||
"thread/settings/update",
|
||||
/*id*/ 14,
|
||||
Some(json!({
|
||||
"threadId": thread_id,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
read_response_for_id(&mut mismatched_client, /*id*/ 14)
|
||||
.await
|
||||
.context("mismatched-client thread/settings/update response")?;
|
||||
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_routes_project_exec_policy_warning_to_requester() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
@@ -1012,21 +694,6 @@ pub(super) async fn send_initialize_request(
|
||||
stream: &mut WsClient,
|
||||
id: i64,
|
||||
client_name: &str,
|
||||
) -> Result<()> {
|
||||
send_initialize_request_with_host_capabilities(
|
||||
stream,
|
||||
id,
|
||||
client_name,
|
||||
/*host_capabilities*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_initialize_request_with_host_capabilities(
|
||||
stream: &mut WsClient,
|
||||
id: i64,
|
||||
client_name: &str,
|
||||
host_capabilities: Option<Vec<&str>>,
|
||||
) -> Result<()> {
|
||||
let params = InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
@@ -1034,11 +701,7 @@ async fn send_initialize_request_with_host_capabilities(
|
||||
title: Some("WebSocket Test Client".to_string()),
|
||||
version: "0.1.0".to_string(),
|
||||
},
|
||||
capabilities: host_capabilities.map(|host_capabilities| InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
host_capabilities: Some(host_capabilities.into_iter().map(str::to_string).collect()),
|
||||
..Default::default()
|
||||
}),
|
||||
capabilities: None,
|
||||
};
|
||||
send_request(
|
||||
stream,
|
||||
|
||||
@@ -44,7 +44,6 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -81,7 +80,6 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -133,7 +131,6 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -173,7 +170,6 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -213,7 +209,6 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -268,7 +263,6 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -311,7 +305,6 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
@@ -353,7 +346,6 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -232,7 +232,6 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -155,7 +155,6 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> {
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]),
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: None,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -331,7 +331,7 @@ async fn reprojects_cached_configured_and_fallback_plugins_for_host_capabilities
|
||||
write_required_manifests(HOST_CAPABILITY_SESSION_OWNED_IAB);
|
||||
plugins
|
||||
.host_capabilities
|
||||
.insert(HOST_CAPABILITY_INLINE_VISUALIZATION.to_string());
|
||||
.insert(HOST_CAPABILITY_INLINE_VISUALIZATION);
|
||||
let supported = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(plugins, &[configured_plugin_id], &[], &[]),
|
||||
@@ -424,7 +424,7 @@ source = "/tmp/{marketplace_name}"
|
||||
|
||||
plugins
|
||||
.host_capabilities
|
||||
.insert(HOST_CAPABILITY_INLINE_VISUALIZATION.to_string());
|
||||
.insert(HOST_CAPABILITY_INLINE_VISUALIZATION);
|
||||
let supported = list_discoverable_plugins(
|
||||
&plugins_manager,
|
||||
discovery_input(
|
||||
|
||||
@@ -34,6 +34,7 @@ use codex_core_skills::loader::load_skills_from_roots;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_mcp::parse_plugin_mcp_config;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::LoadedPlugin;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginHookSource;
|
||||
@@ -178,7 +179,7 @@ pub async fn load_plugin_hooks_from_layer_stack(
|
||||
extra_plugins: HashMap<String, PluginConfig>,
|
||||
store: &PluginStore,
|
||||
remote_global_catalog_active: bool,
|
||||
host_capabilities: &HashSet<String>,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> PluginHookLoadOutcome {
|
||||
let mut plugins = load_plugins_from_layer_stack_with_scope(
|
||||
config_layer_stack,
|
||||
@@ -768,8 +769,8 @@ async fn load_plugin(
|
||||
manifest_description: None,
|
||||
root,
|
||||
enabled: plugin.enabled,
|
||||
required_host_capabilities: Vec::new(),
|
||||
missing_host_capabilities: Vec::new(),
|
||||
required_host_capabilities: HostCapabilities::default(),
|
||||
missing_host_capabilities: HostCapabilities::default(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
|
||||
@@ -72,6 +72,7 @@ use codex_hooks::plugin_hook_declarations;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginIdError;
|
||||
@@ -110,7 +111,7 @@ pub struct PluginsConfigInput {
|
||||
pub plugins_enabled: bool,
|
||||
pub remote_plugin_enabled: bool,
|
||||
pub chatgpt_base_url: String,
|
||||
pub host_capabilities: HashSet<String>,
|
||||
pub host_capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
impl PluginsConfigInput {
|
||||
@@ -125,7 +126,7 @@ impl PluginsConfigInput {
|
||||
plugins_enabled,
|
||||
remote_plugin_enabled,
|
||||
chatgpt_base_url,
|
||||
host_capabilities: HashSet::new(),
|
||||
host_capabilities: HostCapabilities::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,7 +592,7 @@ impl PluginsManager {
|
||||
fn resolve_loaded_plugins_for_runtime(
|
||||
&self,
|
||||
mut plugins: Vec<LoadedPlugin>,
|
||||
host_capabilities: &HashSet<String>,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> PluginLoadOutcome {
|
||||
let auth_mode = self.auth_mode();
|
||||
for plugin in &mut plugins {
|
||||
@@ -1257,6 +1258,9 @@ impl PluginsManager {
|
||||
!installed_plugin_ids.contains(plugin.config_id.as_str())
|
||||
&& !installed_remote_plugin_ids.contains(plugin.remote_plugin_id.as_str())
|
||||
&& !disabled_plugin_ids.contains(plugin.config_id.as_str())
|
||||
&& plugin
|
||||
.requirements
|
||||
.is_satisfied_by(&input.plugins_config.host_capabilities)
|
||||
})
|
||||
.map(|plugin| {
|
||||
DiscoverableTool::from(DiscoverablePluginInfo {
|
||||
@@ -1692,7 +1696,7 @@ impl PluginsManager {
|
||||
marketplace_name: &str,
|
||||
plugin: &ConfiguredMarketplacePlugin,
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
host_capabilities: &HashSet<String>,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> Result<Option<PluginCapabilitySummary>, MarketplaceError> {
|
||||
let fragment = self
|
||||
.tool_suggest_metadata_cache
|
||||
|
||||
@@ -45,6 +45,7 @@ use codex_core_skills::SkillsService;
|
||||
use codex_core_skills::config_rules::SkillConfigRules;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
@@ -808,10 +809,7 @@ fn write_host_capability_projection_plugin(codex_home: &Path) -> std::path::Path
|
||||
}
|
||||
|
||||
fn set_host_capabilities(config: &mut PluginsConfigInput, capabilities: &[&str]) {
|
||||
config.host_capabilities = capabilities
|
||||
.iter()
|
||||
.map(|capability| (*capability).to_string())
|
||||
.collect();
|
||||
config.host_capabilities = HostCapabilities::from_names(capabilities);
|
||||
}
|
||||
|
||||
async fn load_plugins_from_config(
|
||||
@@ -932,8 +930,8 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
),
|
||||
root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(),
|
||||
enabled: true,
|
||||
required_host_capabilities: Vec::new(),
|
||||
missing_host_capabilities: Vec::new(),
|
||||
required_host_capabilities: HostCapabilities::default(),
|
||||
missing_host_capabilities: HostCapabilities::default(),
|
||||
skill_roots: vec![plugin_root.join("skills").abs()],
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: true,
|
||||
@@ -1015,17 +1013,14 @@ async fn plugin_host_capability_projection_is_all_of_and_reprojects_cached_plugi
|
||||
assert_eq!(plugin.error, None);
|
||||
assert_eq!(
|
||||
plugin.missing_host_capabilities,
|
||||
missing_host_capabilities
|
||||
.iter()
|
||||
.map(|capability| (*capability).to_string())
|
||||
.collect::<Vec<_>>()
|
||||
HostCapabilities::from_names(missing_host_capabilities)
|
||||
);
|
||||
assert_eq!(
|
||||
plugin.required_host_capabilities,
|
||||
vec![
|
||||
HostCapabilities::from_names([
|
||||
HOST_CAPABILITY_INLINE_VISUALIZATION.to_string(),
|
||||
HOST_CAPABILITY_SESSION_OWNED_IAB.to_string(),
|
||||
]
|
||||
])
|
||||
);
|
||||
assert_eq!(plugin.skill_roots, vec![plugin_root.join("skills").abs()]);
|
||||
assert_eq!(
|
||||
@@ -2355,8 +2350,8 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
|
||||
manifest_description: None,
|
||||
root: AbsolutePathBuf::try_from(plugin_root).unwrap(),
|
||||
enabled: false,
|
||||
required_host_capabilities: Vec::new(),
|
||||
missing_host_capabilities: Vec::new(),
|
||||
required_host_capabilities: HostCapabilities::default(),
|
||||
missing_host_capabilities: HostCapabilities::default(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
@@ -2532,8 +2527,8 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
manifest_description: None,
|
||||
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
|
||||
enabled: true,
|
||||
required_host_capabilities: Vec::new(),
|
||||
missing_host_capabilities: Vec::new(),
|
||||
required_host_capabilities: HostCapabilities::default(),
|
||||
missing_host_capabilities: HostCapabilities::default(),
|
||||
skill_roots: Vec::new(),
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: false,
|
||||
@@ -5171,13 +5166,17 @@ plugins = true
|
||||
"name": "slack",
|
||||
"release": {
|
||||
"display_name": "Slack",
|
||||
"app_ids": ["connector_slack"]
|
||||
"app_ids": ["connector_slack"],
|
||||
"requires": {"hostCapabilities": []}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plugin_github",
|
||||
"name": "github",
|
||||
"release": {"display_name": "GitHub"}
|
||||
"release": {
|
||||
"display_name": "GitHub",
|
||||
"requires": {"hostCapabilities": []}
|
||||
}
|
||||
}
|
||||
]
|
||||
}))
|
||||
@@ -5198,12 +5197,14 @@ plugins = true
|
||||
remote_plugin_id: "plugin_github".to_string(),
|
||||
display_name: "GitHub".to_string(),
|
||||
app_connector_ids: Vec::new(),
|
||||
requirements: Default::default(),
|
||||
},
|
||||
RecommendedPlugin {
|
||||
config_id: "slack@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: "plugin_slack".to_string(),
|
||||
display_name: "Slack".to_string(),
|
||||
app_connector_ids: vec!["connector_slack".to_string()],
|
||||
requirements: Default::default(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -5222,7 +5223,7 @@ plugins = true
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugin_candidates_filter_installed_and_disabled_plugins() {
|
||||
async fn recommended_plugin_candidates_filter_installed_disabled_and_unsupported_plugins() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
@@ -5239,17 +5240,36 @@ plugins = true
|
||||
{
|
||||
"id": "plugin_linear",
|
||||
"name": "linear",
|
||||
"release": {"display_name": "Linear"}
|
||||
"release": {
|
||||
"display_name": "Linear",
|
||||
"requires": {"hostCapabilities": []}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plugin_github",
|
||||
"name": "github",
|
||||
"release": {"display_name": "GitHub"}
|
||||
"release": {
|
||||
"display_name": "GitHub",
|
||||
"requires": {"hostCapabilities": []}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plugin_slack",
|
||||
"name": "slack",
|
||||
"release": {"display_name": "Slack"}
|
||||
"release": {
|
||||
"display_name": "Slack",
|
||||
"requires": {"hostCapabilities": []}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plugin_visualize",
|
||||
"name": "visualize",
|
||||
"release": {
|
||||
"display_name": "Visualize",
|
||||
"requires": {
|
||||
"hostCapabilities": [HOST_CAPABILITY_INLINE_VISUALIZATION]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})))
|
||||
@@ -5291,6 +5311,43 @@ plugins = true
|
||||
app_connector_ids: Vec::new(),
|
||||
})])
|
||||
);
|
||||
|
||||
config
|
||||
.host_capabilities
|
||||
.insert(HOST_CAPABILITY_INLINE_VISUALIZATION);
|
||||
let candidates = manager
|
||||
.recommended_plugin_candidates_for_config(RecommendedPluginCandidatesInput {
|
||||
plugins_config: &config,
|
||||
loaded_plugins: &loaded_plugins,
|
||||
auth: Some(&auth),
|
||||
disabled_tools: &disabled_tools,
|
||||
app_server_client_name: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
candidates,
|
||||
Some(vec![
|
||||
DiscoverableTool::from(DiscoverablePluginInfo {
|
||||
id: "slack@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: Some("plugin_slack".to_string()),
|
||||
name: "Slack".to_string(),
|
||||
description: None,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
DiscoverableTool::from(DiscoverablePluginInfo {
|
||||
id: "visualize@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: Some("plugin_visualize".to_string()),
|
||||
name: "Visualize".to_string(),
|
||||
description: None,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_config::HooksFile;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -188,17 +189,7 @@ pub(crate) fn parse_plugin_manifest_uri(
|
||||
let version = version.trim();
|
||||
(!version.is_empty()).then(|| version.to_string())
|
||||
});
|
||||
let mut host_capabilities = Vec::new();
|
||||
for capability in requires.host_capabilities {
|
||||
let capability = capability.trim();
|
||||
if !capability.is_empty()
|
||||
&& !host_capabilities
|
||||
.iter()
|
||||
.any(|existing| existing == capability)
|
||||
{
|
||||
host_capabilities.push(capability.to_string());
|
||||
}
|
||||
}
|
||||
let host_capabilities = HostCapabilities::from_names(requires.host_capabilities);
|
||||
let interface = interface.and_then(|interface| {
|
||||
let RawPluginManifestInterface {
|
||||
display_name,
|
||||
@@ -535,6 +526,7 @@ mod tests {
|
||||
use super::load_plugin_manifest;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::PluginProvider;
|
||||
use codex_plugin::ResolvedPlugin;
|
||||
use codex_plugin::manifest::PluginManifest as GenericPluginManifest;
|
||||
@@ -757,10 +749,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
manifest.requires,
|
||||
PluginManifestRequirements {
|
||||
host_capabilities: vec![
|
||||
"codex.inline_visualization".to_string(),
|
||||
"codex.code_mode".to_string(),
|
||||
],
|
||||
host_capabilities: HostCapabilities::from_names([
|
||||
"codex.inline_visualization",
|
||||
"codex.code_mode",
|
||||
]),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -788,7 +780,7 @@ mod tests {
|
||||
assert_eq!(manifest.version, Some("2.0.0".to_string()));
|
||||
assert_eq!(
|
||||
manifest.requires.host_capabilities,
|
||||
vec!["codex.inline_visualization".to_string()]
|
||||
HostCapabilities::from_names(["codex.inline_visualization"])
|
||||
);
|
||||
assert_eq!(
|
||||
manifest
|
||||
@@ -897,7 +889,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
executor_plugin.manifest().requires,
|
||||
PluginManifestRequirements {
|
||||
host_capabilities: vec!["codex.inline_visualization".to_string()],
|
||||
host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]),
|
||||
}
|
||||
);
|
||||
assert_eq!(executor_plugin, expected_plugin);
|
||||
|
||||
@@ -13,9 +13,11 @@ use codex_login::CodexAuth;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::app_connector_ids_from_declarations;
|
||||
use codex_plugin::manifest::PluginManifestRequirements;
|
||||
use codex_plugin::prompt_safe_plugin_description;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use reqwest::RequestBuilder;
|
||||
@@ -263,6 +265,7 @@ pub struct RecommendedPlugin {
|
||||
pub remote_plugin_id: String,
|
||||
pub display_name: String,
|
||||
pub app_connector_ids: Vec<String>,
|
||||
pub requirements: PluginManifestRequirements,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -648,6 +651,15 @@ struct RecommendedPluginRelease {
|
||||
display_name: String,
|
||||
#[serde(default)]
|
||||
app_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
requires: Option<RecommendedPluginRequirements>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RecommendedPluginRequirements {
|
||||
#[serde(default)]
|
||||
host_capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
@@ -894,7 +906,11 @@ fn recommended_plugins_mode(response: RecommendedPluginsResponse) -> Recommended
|
||||
let RecommendedPluginRelease {
|
||||
display_name,
|
||||
app_ids,
|
||||
requires,
|
||||
} = plugin.release;
|
||||
let Some(requires) = requires else {
|
||||
return RecommendedPluginsMode::Legacy;
|
||||
};
|
||||
let display_name = non_empty_string(Some(&display_name))
|
||||
.unwrap_or_else(|| plugin.name.clone())
|
||||
.chars()
|
||||
@@ -913,6 +929,9 @@ fn recommended_plugins_mode(response: RecommendedPluginsResponse) -> Recommended
|
||||
remote_plugin_id: plugin.id,
|
||||
display_name,
|
||||
app_connector_ids,
|
||||
requirements: PluginManifestRequirements {
|
||||
host_capabilities: HostCapabilities::from_names(requires.host_capabilities),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ fn item(name: &str, display_name: &str) -> RecommendedPluginItem {
|
||||
release: RecommendedPluginRelease {
|
||||
display_name: display_name.to_string(),
|
||||
app_ids: Vec::new(),
|
||||
requires: Some(RecommendedPluginRequirements::default()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -218,6 +219,20 @@ fn recommended_plugins_require_remote_install_identity() {
|
||||
assert!(response.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recommended_plugins_without_requirements_use_legacy_discovery() {
|
||||
let mut plugin = item("github", "GitHub");
|
||||
plugin.release.requires = None;
|
||||
|
||||
assert_eq!(
|
||||
recommended_plugins_mode(RecommendedPluginsResponse {
|
||||
enabled: Some(true),
|
||||
plugins: vec![plugin],
|
||||
}),
|
||||
RecommendedPluginsMode::Legacy
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
||||
let mut plugins = (0..=52)
|
||||
@@ -234,6 +249,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
||||
release: RecommendedPluginRelease {
|
||||
display_name: "Disabled".to_string(),
|
||||
app_ids: Vec::new(),
|
||||
requires: Some(RecommendedPluginRequirements::default()),
|
||||
},
|
||||
});
|
||||
plugins.push(RecommendedPluginItem {
|
||||
@@ -244,6 +260,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
||||
release: RecommendedPluginRelease {
|
||||
display_name: "Not Available".to_string(),
|
||||
app_ids: Vec::new(),
|
||||
requires: Some(RecommendedPluginRequirements::default()),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -263,6 +280,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
||||
remote_plugin_id: "plugin_plugin-00".to_string(),
|
||||
display_name: "Plugin 00".to_string(),
|
||||
app_connector_ids: Vec::new(),
|
||||
requirements: PluginManifestRequirements::default(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -272,6 +290,7 @@ fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
||||
remote_plugin_id: "plugin_plugin-49".to_string(),
|
||||
display_name: "Plugin 49".to_string(),
|
||||
app_connector_ids: Vec::new(),
|
||||
requirements: PluginManifestRequirements::default(),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -296,6 +315,7 @@ fn recommended_plugins_bound_model_visible_fields() {
|
||||
remote_plugin_id: "plugin_bounded".to_string(),
|
||||
display_name: "D".repeat(MAX_RECOMMENDED_PLUGIN_DISPLAY_NAME_LEN),
|
||||
app_connector_ids: Vec::new(),
|
||||
requirements: PluginManifestRequirements::default(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -318,6 +338,7 @@ fn recommended_plugins_preserve_install_identity_and_normalize_app_ids() {
|
||||
"connector_two".to_string(),
|
||||
"connector_one".to_string(),
|
||||
],
|
||||
requires: Some(RecommendedPluginRequirements::default()),
|
||||
},
|
||||
}],
|
||||
});
|
||||
@@ -330,6 +351,7 @@ fn recommended_plugins_preserve_install_identity_and_normalize_app_ids() {
|
||||
remote_plugin_id: "plugin_connector_sample".to_string(),
|
||||
display_name: "Sample".to_string(),
|
||||
app_connector_ids: vec!["connector_one".to_string(), "connector_two".to_string(),],
|
||||
requirements: PluginManifestRequirements::default(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -347,6 +369,7 @@ fn recommended_plugins_ignore_invalid_remote_plugin_ids() {
|
||||
release: RecommendedPluginRelease {
|
||||
display_name: "Sample".to_string(),
|
||||
app_ids: Vec::new(),
|
||||
requires: Some(RecommendedPluginRequirements::default()),
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_core_skills::config_rules::SkillConfigRules;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::HostCapabilities;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginIdError;
|
||||
use codex_plugin::app_connector_ids_from_declarations;
|
||||
use codex_plugin::manifest::PluginManifestRequirements;
|
||||
use codex_plugin::prompt_safe_plugin_description;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use codex_protocol::protocol::Product;
|
||||
@@ -55,7 +56,7 @@ pub(crate) struct ToolSuggestMetadataFragment {
|
||||
config_name: String,
|
||||
display_name: String,
|
||||
description: Option<String>,
|
||||
required_host_capabilities: Vec<String>,
|
||||
requirements: PluginManifestRequirements,
|
||||
mcp_server_names: Vec<String>,
|
||||
app_declarations: Vec<AppDeclaration>,
|
||||
skill_inventory: Option<PluginSkillInventory>,
|
||||
@@ -66,13 +67,9 @@ impl ToolSuggestMetadataFragment {
|
||||
&self,
|
||||
skill_config_rules: &SkillConfigRules,
|
||||
auth_mode: Option<AuthMode>,
|
||||
host_capabilities: &HashSet<String>,
|
||||
host_capabilities: &HostCapabilities,
|
||||
) -> Option<PluginCapabilitySummary> {
|
||||
if self
|
||||
.required_host_capabilities
|
||||
.iter()
|
||||
.any(|capability| !host_capabilities.contains(capability))
|
||||
{
|
||||
if !self.requirements.is_satisfied_by(host_capabilities) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -206,15 +203,14 @@ async fn load_plugin_metadata(
|
||||
)?;
|
||||
|
||||
let MarketplacePluginSource::Local { path: plugin_root } = &plugin.source else {
|
||||
let required_host_capabilities = match plugin.manifest_fallback.as_ref() {
|
||||
let requirements = match plugin.manifest_fallback.as_ref() {
|
||||
Some(fallback) => {
|
||||
fallback
|
||||
.parse_for_listing()
|
||||
.ok_or_else(|| "invalid marketplace plugin manifest fallback".to_string())?
|
||||
.requires
|
||||
.host_capabilities
|
||||
}
|
||||
None => Vec::new(),
|
||||
None => PluginManifestRequirements::default(),
|
||||
};
|
||||
return Ok(Arc::new(ToolSuggestMetadataFragment {
|
||||
config_name: plugin.id.clone(),
|
||||
@@ -222,7 +218,7 @@ async fn load_plugin_metadata(
|
||||
description: prompt_safe_plugin_description(Some(
|
||||
&remote_plugin_install_required_description(&plugin.source),
|
||||
)),
|
||||
required_host_capabilities,
|
||||
requirements,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_declarations: Vec::new(),
|
||||
skill_inventory: None,
|
||||
@@ -254,7 +250,7 @@ async fn load_plugin_metadata(
|
||||
config_name: plugin.id.clone(),
|
||||
display_name: plugin.name.clone(),
|
||||
description: prompt_safe_plugin_description(manifest.description.as_deref()),
|
||||
required_host_capabilities: manifest.requires.host_capabilities,
|
||||
requirements: manifest.requires,
|
||||
mcp_server_names,
|
||||
app_declarations,
|
||||
skill_inventory: Some(skill_inventory),
|
||||
|
||||
@@ -69,7 +69,7 @@ pub struct SkillsService {
|
||||
codex_home: AbsolutePathBuf,
|
||||
restriction_product: Option<Product>,
|
||||
extra_roots: RwLock<Vec<AbsolutePathBuf>>,
|
||||
cache_by_cwd: RwLock<HashMap<CwdSkillsCacheKey, HostSkillsSnapshot>>,
|
||||
cache_by_cwd: RwLock<HashMap<AbsolutePathBuf, HostSkillsSnapshot>>,
|
||||
cache_by_config: RwLock<HashMap<ConfigSkillsCacheKey, HostSkillsSnapshot>>,
|
||||
}
|
||||
|
||||
@@ -173,17 +173,9 @@ impl SkillsService {
|
||||
fs: Option<Arc<dyn ExecutorFileSystem>>,
|
||||
) -> HostSkillsSnapshot {
|
||||
let use_cwd_cache = fs.is_some();
|
||||
let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack);
|
||||
let bundled_skills_enabled = bundled_skills_enabled_from_stack(&input.config_layer_stack);
|
||||
let cache_key = CwdSkillsCacheKey {
|
||||
cwd: input.cwd.clone(),
|
||||
effective_skill_roots: input.effective_skill_roots.clone(),
|
||||
bundled_skills_enabled,
|
||||
skill_config_rules: skill_config_rules.clone(),
|
||||
};
|
||||
if use_cwd_cache
|
||||
&& !force_reload
|
||||
&& let Some(snapshot) = self.cached_snapshot_for_cwd(&cache_key)
|
||||
&& let Some(snapshot) = self.cached_snapshot_for_cwd(&input.cwd)
|
||||
{
|
||||
return snapshot;
|
||||
}
|
||||
@@ -196,9 +188,10 @@ impl SkillsService {
|
||||
self.extra_roots(),
|
||||
)
|
||||
.await;
|
||||
if !bundled_skills_enabled {
|
||||
if !bundled_skills_enabled_from_stack(&input.config_layer_stack) {
|
||||
roots.retain(|root| root.scope != SkillScope::System);
|
||||
}
|
||||
let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack);
|
||||
let snapshot = HostSkillsSnapshot::new(Arc::new(
|
||||
self.build_skill_outcome(input, roots, &skill_config_rules)
|
||||
.await,
|
||||
@@ -208,7 +201,7 @@ impl SkillsService {
|
||||
.cache_by_cwd
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
cache.insert(cache_key, snapshot.clone());
|
||||
cache.insert(input.cwd.clone(), snapshot.clone());
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
@@ -250,10 +243,10 @@ impl SkillsService {
|
||||
info!("skills cache cleared ({cleared} entries)");
|
||||
}
|
||||
|
||||
fn cached_snapshot_for_cwd(&self, cache_key: &CwdSkillsCacheKey) -> Option<HostSkillsSnapshot> {
|
||||
fn cached_snapshot_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option<HostSkillsSnapshot> {
|
||||
match self.cache_by_cwd.read() {
|
||||
Ok(cache) => cache.get(cache_key).cloned(),
|
||||
Err(err) => err.into_inner().get(cache_key).cloned(),
|
||||
Ok(cache) => cache.get(cwd).cloned(),
|
||||
Err(err) => err.into_inner().get(cwd).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,14 +268,6 @@ impl SkillsService {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct CwdSkillsCacheKey {
|
||||
cwd: AbsolutePathBuf,
|
||||
effective_skill_roots: Vec<PluginSkillRoot>,
|
||||
bundled_skills_enabled: bool,
|
||||
skill_config_rules: SkillConfigRules,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct ConfigSkillsCacheKey {
|
||||
roots: Vec<(AbsolutePathBuf, u8, Option<String>, Option<String>)>,
|
||||
|
||||
@@ -228,68 +228,6 @@ async fn skills_for_config_reuses_cache_for_same_effective_config() {
|
||||
assert_eq!(outcome2.skills, outcome1.skills);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cwd_cache_separates_effective_plugin_skill_roots() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let cwd = tempfile::tempdir().expect("tempdir");
|
||||
let config_layer_stack = config_stack(&codex_home, "");
|
||||
let skills_service = SkillsService::new(
|
||||
codex_home.path().abs(),
|
||||
/*bundled_skills_enabled*/ true,
|
||||
);
|
||||
let skill_path = write_plugin_skill(
|
||||
&codex_home,
|
||||
"test",
|
||||
"visualize",
|
||||
"visualize",
|
||||
"visualize",
|
||||
"render inline visualizations",
|
||||
);
|
||||
let plugin_root = plugin_skill_root_for_skill_path(&skill_path, "visualize@test", "visualize");
|
||||
|
||||
let supported_input = SkillsLoadInput::new(
|
||||
cwd.path().abs(),
|
||||
vec![plugin_root],
|
||||
config_layer_stack.clone(),
|
||||
bundled_skills_enabled_from_stack(&config_layer_stack),
|
||||
);
|
||||
let supported = skills_service
|
||||
.snapshot_for_cwd(
|
||||
&supported_input,
|
||||
/*force_reload*/ false,
|
||||
Some(Arc::clone(&LOCAL_FS)),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
supported
|
||||
.outcome()
|
||||
.skills
|
||||
.iter()
|
||||
.any(|skill| skill.name == "visualize:visualize")
|
||||
);
|
||||
|
||||
let unsupported_input = SkillsLoadInput::new(
|
||||
cwd.path().abs(),
|
||||
Vec::new(),
|
||||
config_layer_stack.clone(),
|
||||
bundled_skills_enabled_from_stack(&config_layer_stack),
|
||||
);
|
||||
let unsupported = skills_service
|
||||
.snapshot_for_cwd(
|
||||
&unsupported_input,
|
||||
/*force_reload*/ false,
|
||||
Some(Arc::clone(&LOCAL_FS)),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
unsupported
|
||||
.outcome()
|
||||
.skills
|
||||
.iter()
|
||||
.all(|skill| skill.name != "visualize:visualize")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -205,7 +205,6 @@ mod reload {
|
||||
) -> ConfigOverrides {
|
||||
ConfigOverrides {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
|
||||
service_tier: preserve_current_service_tier.then(|| config.service_tier.clone()),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
|
||||
@@ -181,7 +181,6 @@ async fn apply_role_preserves_unspecified_keys() {
|
||||
.await;
|
||||
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
|
||||
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
|
||||
config.host_capabilities = ["codex.inline_visualization".to_string()].into();
|
||||
let role_path = write_role_config(
|
||||
&home,
|
||||
"effort-only.toml",
|
||||
@@ -203,10 +202,6 @@ async fn apply_role_preserves_unspecified_keys() {
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("base-model"));
|
||||
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
|
||||
assert_eq!(
|
||||
config.host_capabilities,
|
||||
["codex.inline_visualization".to_string()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
config.codex_linux_sandbox_exe,
|
||||
Some(PathBuf::from("/tmp/codex-linux-sandbox"))
|
||||
|
||||
@@ -55,7 +55,6 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::config::HostCapabilities;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -81,7 +80,6 @@ pub struct ThreadConfigSnapshot {
|
||||
pub parent_thread_id: Option<ThreadId>,
|
||||
pub thread_source: Option<ThreadSource>,
|
||||
pub originator: String,
|
||||
pub host_capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
/// Explains why `CodexThread::try_start_turn_if_idle` rejected an automatic
|
||||
|
||||
@@ -4741,12 +4741,7 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config()
|
||||
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "sample",
|
||||
"requires": {
|
||||
"hostCapabilities": ["codex.inline_visualization"]
|
||||
}
|
||||
}"#,
|
||||
r#"{"name":"sample"}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.join(".mcp.json"),
|
||||
@@ -4813,7 +4808,6 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config()
|
||||
thread_layer_stack.effective_config().try_into()?,
|
||||
ConfigOverrides {
|
||||
cwd: Some(codex_home.path().to_path_buf()),
|
||||
host_capabilities: ["codex.inline_visualization".to_string()].into(),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
@@ -4827,10 +4821,6 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config()
|
||||
let mcp_config = config.to_mcp_config(&plugins_manager).await;
|
||||
let configured_servers = mcp_config.mcp_server_catalog.configured_servers();
|
||||
|
||||
assert_eq!(
|
||||
config.host_capabilities,
|
||||
["codex.inline_visualization".to_string()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
configured_servers.get("sample"),
|
||||
Some(&http_mcp("https://sample.example/mcp"))
|
||||
|
||||
@@ -120,7 +120,6 @@ use rmcp::model::UrlElicitationCapability;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io::ErrorKind;
|
||||
@@ -611,9 +610,6 @@ pub enum ThreadStoreConfig {
|
||||
InMemory { id: String },
|
||||
}
|
||||
|
||||
/// Opaque host features available to runtime plugins for one thread.
|
||||
pub type HostCapabilities = BTreeSet<String>;
|
||||
|
||||
/// Application configuration loaded from disk and merged with overrides.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
@@ -624,12 +620,6 @@ pub struct Config {
|
||||
/// Warnings collected during config load that should be shown on startup.
|
||||
pub startup_warnings: Vec<String>,
|
||||
|
||||
/// Host features available to runtime plugins for this thread.
|
||||
///
|
||||
/// This is supplied by the embedding client rather than config.toml and is
|
||||
/// intentionally not persisted with the thread.
|
||||
pub host_capabilities: HostCapabilities,
|
||||
|
||||
/// Optional override of model selection.
|
||||
pub model: Option<String>,
|
||||
|
||||
@@ -1508,14 +1498,12 @@ impl Config {
|
||||
|
||||
/// Build the plugin-manager input from the effective config.
|
||||
pub fn plugins_config_input(&self) -> PluginsConfigInput {
|
||||
let mut input = PluginsConfigInput::new(
|
||||
PluginsConfigInput::new(
|
||||
self.config_layer_stack.clone(),
|
||||
self.features.enabled(Feature::Plugins),
|
||||
self.features.enabled(Feature::RemotePlugin),
|
||||
self.chatgpt_base_url.clone(),
|
||||
);
|
||||
input.host_capabilities = self.host_capabilities.iter().cloned().collect();
|
||||
input
|
||||
)
|
||||
}
|
||||
|
||||
/// Applies managed MCP requirements to servers supplied by one plugin.
|
||||
@@ -1689,7 +1677,6 @@ impl Config {
|
||||
ConfigOverrides {
|
||||
cwd: Some(self.cwd.to_path_buf()),
|
||||
default_zsh_path,
|
||||
host_capabilities: self.host_capabilities.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
refreshed_config.codex_home.clone(),
|
||||
@@ -2439,8 +2426,6 @@ pub struct ConfigOverrides {
|
||||
/// Explicit absolute runtime workspace roots for this session. When set,
|
||||
/// this is the full runtime root list rather than an additive override.
|
||||
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
|
||||
/// Opaque host features supplied by the embedding client for this thread.
|
||||
pub host_capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
|
||||
@@ -3031,7 +3016,6 @@ impl Config {
|
||||
bypass_hook_trust,
|
||||
additional_writable_roots,
|
||||
workspace_roots: workspace_roots_override,
|
||||
host_capabilities,
|
||||
} = overrides;
|
||||
let bypass_hook_trust = bypass_hook_trust.unwrap_or_default();
|
||||
|
||||
@@ -3815,7 +3799,6 @@ impl Config {
|
||||
workspace_roots: workspace_roots.clone(),
|
||||
workspace_roots_explicit,
|
||||
startup_warnings,
|
||||
host_capabilities,
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
permission_profile_state,
|
||||
|
||||
@@ -50,7 +50,6 @@ struct AccessibleConnectorsCacheKey {
|
||||
account_id: Option<String>,
|
||||
chatgpt_user_id: Option<String>,
|
||||
is_workspace_account: bool,
|
||||
host_capabilities: crate::config::HostCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -360,7 +359,6 @@ fn accessible_connectors_cache_key(
|
||||
account_id,
|
||||
chatgpt_user_id,
|
||||
is_workspace_account,
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,12 +224,6 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
.expect("config should load");
|
||||
let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);
|
||||
let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
|
||||
let mut other_host_config = config.clone();
|
||||
other_host_config
|
||||
.host_capabilities
|
||||
.insert("codex.inline_visualization".to_string());
|
||||
let other_host_cache_key =
|
||||
accessible_connectors_cache_key(&other_host_config, /*auth*/ None);
|
||||
let tools = vec![
|
||||
codex_app_tool(
|
||||
"calendar_list_events",
|
||||
@@ -247,10 +241,6 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
|
||||
|
||||
let cached = with_accessible_connectors_cache_cleared(|| {
|
||||
refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools);
|
||||
assert_eq!(
|
||||
read_cached_accessible_connectors(&other_host_cache_key),
|
||||
None
|
||||
);
|
||||
read_cached_accessible_connectors(&cache_key).expect("cache should be populated")
|
||||
});
|
||||
|
||||
|
||||
@@ -199,7 +199,6 @@ impl SessionConfiguration {
|
||||
parent_thread_id: self.parent_thread_id,
|
||||
thread_source: self.thread_source.clone(),
|
||||
originator: self.originator.clone(),
|
||||
host_capabilities: self.original_config_do_not_use.host_capabilities.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_mcp_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
@@ -275,109 +274,6 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn host_capabilities_gate_plugin_guidance_and_tools_all_of() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
let rmcp_test_server_bin = match stdio_server_bin() {
|
||||
Ok(bin) => bin,
|
||||
Err(err) => {
|
||||
eprintln!("test_stdio_server binary not available, skipping test: {err}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
for (host_capabilities, expected_supported) in [
|
||||
(Vec::new(), false),
|
||||
(vec!["capability-a"], false),
|
||||
(vec!["capability-b"], false),
|
||||
(vec!["capability-a", "capability-b"], true),
|
||||
(
|
||||
vec!["capability-a", "capability-b", "capability-extra"],
|
||||
true,
|
||||
),
|
||||
] {
|
||||
let server = start_mock_server().await;
|
||||
let mock = mount_plugin_tool_search_turn(&server).await;
|
||||
let codex_home = Arc::new(TempDir::new()?);
|
||||
write_plugin_skill_plugin(codex_home.as_ref());
|
||||
write_plugin_mcp_plugin(codex_home.as_ref(), &rmcp_test_server_bin);
|
||||
std::fs::write(
|
||||
sample_plugin_root(codex_home.as_ref()).join(".codex-plugin/plugin.json"),
|
||||
format!(
|
||||
r#"{{
|
||||
"name": "{SAMPLE_PLUGIN_DISPLAY_NAME}",
|
||||
"description": "{SAMPLE_PLUGIN_DESCRIPTION}",
|
||||
"requires": {{
|
||||
"hostCapabilities": ["capability-a", "capability-b"]
|
||||
}}
|
||||
}}"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(codex_home)
|
||||
.with_auth(CodexAuth::from_api_key("Test API Key"))
|
||||
.with_config(move |config| {
|
||||
config.host_capabilities = host_capabilities
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
});
|
||||
let test_codex = builder
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("create new conversation");
|
||||
let codex = Arc::clone(&test_codex.codex);
|
||||
let startup = loop {
|
||||
let event = codex
|
||||
.next_event()
|
||||
.await
|
||||
.expect("stream ended unexpectedly while waiting for MCP startup");
|
||||
if let EventMsg::McpStartupComplete(startup) = event.msg {
|
||||
break startup;
|
||||
}
|
||||
};
|
||||
let sample_ready = startup.ready.iter().any(|server| server == "sample");
|
||||
let sample_reported = sample_ready
|
||||
|| startup
|
||||
.failed
|
||||
.iter()
|
||||
.any(|failure| failure.server == "sample")
|
||||
|| startup.cancelled.iter().any(|server| server == "sample");
|
||||
assert_eq!(sample_reported, expected_supported);
|
||||
assert_eq!(sample_ready, expected_supported);
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![codex_protocol::user_input::UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: Default::default(),
|
||||
thread_settings: Default::default(),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let requests = mock.requests();
|
||||
let developer_text = requests[0].message_input_texts("developer").join("\n\n");
|
||||
assert_eq!(
|
||||
developer_text.contains("sample:sample-search: inspect sample data"),
|
||||
expected_supported
|
||||
);
|
||||
assert_eq!(
|
||||
developer_text.contains(SAMPLE_PLUGIN_DESCRIPTION),
|
||||
expected_supported
|
||||
);
|
||||
let (_, echo_tool) = searched_plugin_tools(&requests[1]);
|
||||
assert_eq!(echo_tool.is_some(), expected_supported);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -446,7 +446,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
ephemeral: ephemeral.then_some(true),
|
||||
bypass_hook_trust: bypass_hook_trust.then_some(true),
|
||||
additional_writable_roots: add_dir,
|
||||
host_capabilities: Default::default(),
|
||||
};
|
||||
|
||||
let build_config = |overrides| {
|
||||
@@ -574,7 +573,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ mod provider;
|
||||
struct SelectedPluginMetadata {
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
required_host_capabilities: Vec<String>,
|
||||
servers: Vec<(String, codex_config::McpServerConfig)>,
|
||||
connector_ids: Vec<String>,
|
||||
}
|
||||
@@ -113,12 +112,6 @@ impl SelectedExecutorPluginMcpContributor {
|
||||
Some(SelectedPluginMetadata {
|
||||
plugin_id: plugin.plugin().selected_root_id().to_string(),
|
||||
plugin_display_name: plugin.plugin().manifest().display_name().to_string(),
|
||||
required_host_capabilities: plugin
|
||||
.plugin()
|
||||
.manifest()
|
||||
.requires
|
||||
.host_capabilities
|
||||
.clone(),
|
||||
servers,
|
||||
connector_ids,
|
||||
})
|
||||
@@ -178,13 +171,6 @@ impl McpServerContributor<Config> for SelectedExecutorPluginMcpContributor {
|
||||
let Some(plugin) = self.metadata_for_root(&state, selected_root).await else {
|
||||
continue;
|
||||
};
|
||||
if !plugin
|
||||
.required_host_capabilities
|
||||
.iter()
|
||||
.all(|capability| context.config().host_capabilities.contains(capability))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut servers = plugin.servers.iter().cloned().collect::<HashMap<_, _>>();
|
||||
context
|
||||
.config()
|
||||
|
||||
@@ -17,7 +17,6 @@ use codex_plugin::ResolvedPlugin;
|
||||
use codex_plugin::manifest::PluginManifest;
|
||||
use codex_plugin::manifest::PluginManifestMcpServers;
|
||||
use codex_plugin::manifest::PluginManifestPaths;
|
||||
use codex_plugin::manifest::PluginManifestRequirements;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
@@ -401,7 +400,6 @@ fn resolved_plugin(
|
||||
version: None,
|
||||
description: None,
|
||||
keywords: Vec::new(),
|
||||
requires: PluginManifestRequirements::default(),
|
||||
paths: PluginManifestPaths {
|
||||
skills: Vec::new(),
|
||||
mcp_servers,
|
||||
|
||||
@@ -147,104 +147,6 @@ async fn selected_plugin_package_is_contributed_without_servers_or_connectors()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_plugin_contributions_require_all_host_capabilities_after_caching() -> TestResult {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let plugin_root = tempfile::tempdir()?;
|
||||
std::fs::create_dir_all(plugin_root.path().join(".codex-plugin"))?;
|
||||
std::fs::write(
|
||||
plugin_root.path().join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "gated-plugin",
|
||||
"requires": {
|
||||
"hostCapabilities": ["codex.inline_visualization", "codex.code_mode"]
|
||||
},
|
||||
"apps": "./apps.json",
|
||||
"interface": {"displayName": "Gated Plugin"}
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.path().join(".mcp.json"),
|
||||
r#"{"mcpServers":{"gated":{"command":"gated-command"}}}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
plugin_root.path().join("apps.json"),
|
||||
r#"{"apps":{"demo":{"id":"connector_demo"}}}"#,
|
||||
)?;
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await?;
|
||||
let mut builder = ExtensionRegistryBuilder::new();
|
||||
codex_mcp_extension::install_executor_plugins(
|
||||
&mut builder,
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
);
|
||||
let registry = builder.build();
|
||||
let mut thread_init = ExtensionDataInit::new();
|
||||
thread_init.insert(vec![SelectedCapabilityRoot {
|
||||
id: "selected-root".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
path: PathUri::from_host_native_path(plugin_root.path())?,
|
||||
},
|
||||
}]);
|
||||
let thread_store = ExtensionData::new_with_init("test-thread", thread_init.clone());
|
||||
let available_environment_ids = vec![LOCAL_ENVIRONMENT_ID.to_string()];
|
||||
let contributor = ®istry.mcp_server_contributors()[0];
|
||||
|
||||
let unsupported = contributor
|
||||
.contribute(McpServerContributionContext::for_step(
|
||||
&config,
|
||||
&thread_init,
|
||||
&thread_store,
|
||||
"test_originator",
|
||||
&available_environment_ids,
|
||||
))
|
||||
.await;
|
||||
assert!(unsupported.is_empty());
|
||||
|
||||
config.host_capabilities = ["codex.inline_visualization".to_string()].into();
|
||||
let partially_supported = contributor
|
||||
.contribute(McpServerContributionContext::for_step(
|
||||
&config,
|
||||
&thread_init,
|
||||
&thread_store,
|
||||
"test_originator",
|
||||
&available_environment_ids,
|
||||
))
|
||||
.await;
|
||||
assert!(partially_supported.is_empty());
|
||||
|
||||
config.host_capabilities = [
|
||||
"codex.code_mode".to_string(),
|
||||
"codex.inline_visualization".to_string(),
|
||||
]
|
||||
.into();
|
||||
let supported = contributor
|
||||
.contribute(McpServerContributionContext::for_step(
|
||||
&config,
|
||||
&thread_init,
|
||||
&thread_store,
|
||||
"test_originator",
|
||||
&available_environment_ids,
|
||||
))
|
||||
.await;
|
||||
assert_eq!(supported.len(), 2);
|
||||
assert!(matches!(
|
||||
&supported[0],
|
||||
McpServerContribution::SelectedPlugin { name, .. } if name == "gated"
|
||||
));
|
||||
assert!(matches!(
|
||||
&supported[1],
|
||||
McpServerContribution::SelectedPluginPackage { connector_ids, .. }
|
||||
if connector_ids.as_slice() == ["connector_demo"]
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn selected_plugin_contributions(
|
||||
config: &Config,
|
||||
plugin_root: &std::path::Path,
|
||||
|
||||
@@ -15,7 +15,6 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-core-skills = { workspace = true }
|
||||
codex-core-plugins = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-mcp = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use codex_core_skills::model::SkillDependencies;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Source authority that owns a skill package and must be used to read it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
@@ -118,7 +117,6 @@ pub struct SkillCatalogEntry {
|
||||
pub dependencies: Option<SkillDependencies>,
|
||||
pub enabled: bool,
|
||||
pub prompt_visible: bool,
|
||||
pub(crate) required_host_capabilities: Vec<String>,
|
||||
}
|
||||
|
||||
impl SkillCatalogEntry {
|
||||
@@ -140,7 +138,6 @@ impl SkillCatalogEntry {
|
||||
dependencies: None,
|
||||
enabled: true,
|
||||
prompt_visible: true,
|
||||
required_host_capabilities: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,21 +166,6 @@ impl SkillCatalogEntry {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_required_host_capabilities(
|
||||
mut self,
|
||||
required_host_capabilities: Vec<String>,
|
||||
) -> Self {
|
||||
self.required_host_capabilities = required_host_capabilities;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether every host capability required by this package is available.
|
||||
pub fn host_requirements_satisfied_by(&self, host_capabilities: &BTreeSet<String>) -> bool {
|
||||
self.required_host_capabilities
|
||||
.iter()
|
||||
.all(|capability| host_capabilities.contains(capability))
|
||||
}
|
||||
|
||||
pub(crate) fn rendered_path(&self) -> &str {
|
||||
self.display_path
|
||||
.as_deref()
|
||||
@@ -196,9 +178,6 @@ impl SkillCatalogEntry {
|
||||
pub struct SkillCatalog {
|
||||
pub entries: Vec<SkillCatalogEntry>,
|
||||
pub warnings: Vec<String>,
|
||||
/// Warnings whose visibility depends on the host capabilities available at projection time.
|
||||
#[doc(hidden)]
|
||||
pub host_capability_gated_warnings: Vec<HostCapabilityGatedWarnings>,
|
||||
}
|
||||
|
||||
impl SkillCatalog {
|
||||
@@ -207,8 +186,6 @@ impl SkillCatalog {
|
||||
self.push_entry(entry);
|
||||
}
|
||||
self.warnings.extend(other.warnings);
|
||||
self.host_capability_gated_warnings
|
||||
.extend(other.host_capability_gated_warnings);
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: SkillCatalogEntry) {
|
||||
@@ -222,33 +199,6 @@ impl SkillCatalog {
|
||||
|
||||
self.entries.push(entry);
|
||||
}
|
||||
|
||||
pub(crate) fn extend_warnings_with_host_requirements(
|
||||
&mut self,
|
||||
warnings: Vec<String>,
|
||||
required_host_capabilities: &[String],
|
||||
) {
|
||||
if warnings.is_empty() {
|
||||
return;
|
||||
}
|
||||
if required_host_capabilities.is_empty() {
|
||||
self.warnings.extend(warnings);
|
||||
} else {
|
||||
self.host_capability_gated_warnings
|
||||
.push(HostCapabilityGatedWarnings {
|
||||
required_host_capabilities: required_host_capabilities.to_vec(),
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warning group carried in raw catalogs until host capabilities are projected.
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HostCapabilityGatedWarnings {
|
||||
pub required_host_capabilities: Vec<String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Contents returned after resolving a skill resource through its owner.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Host-supplied configuration used by the skills extension.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SkillsExtensionConfig {
|
||||
@@ -9,6 +7,4 @@ pub struct SkillsExtensionConfig {
|
||||
pub bundled_skills_enabled: bool,
|
||||
/// Whether orchestrator-owned skills are eligible for discovery.
|
||||
pub orchestrator_skills_enabled: bool,
|
||||
/// Opaque host features available to runtime plugins for this thread.
|
||||
pub host_capabilities: BTreeSet<String>,
|
||||
}
|
||||
|
||||
@@ -123,7 +123,6 @@ where
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
|
||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
},
|
||||
&thread_state,
|
||||
)
|
||||
@@ -161,7 +160,6 @@ where
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_orchestrator_skills: false,
|
||||
mcp_resources: input.session_store.get::<McpResourceClient>(),
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -233,7 +231,6 @@ where
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
|
||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
};
|
||||
let mut catalog = self.list_skills(query, &thread_state).await;
|
||||
if let Some(executor_skills) = turn_store.get::<ExecutorSkillsStepState>() {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
@@ -32,7 +31,6 @@ pub struct SkillListQuery {
|
||||
pub include_bundled_skills: bool,
|
||||
pub include_orchestrator_skills: bool,
|
||||
pub mcp_resources: Option<Arc<McpResourceClient>>,
|
||||
pub host_capabilities: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core_plugins::ExecutorPluginProvider;
|
||||
use codex_core_skills::loader::EnvironmentSkillMetadata;
|
||||
use codex_core_skills::loader::load_environment_skills_from_root;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
@@ -27,7 +26,6 @@ use crate::provider::SkillSearchRequest;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExecutorSkillProvider {
|
||||
environment_manager: Arc<EnvironmentManager>,
|
||||
plugin_provider: ExecutorPluginProvider,
|
||||
restriction_product: Option<Product>,
|
||||
}
|
||||
|
||||
@@ -37,7 +35,6 @@ impl ExecutorSkillProvider {
|
||||
restriction_product: Option<Product>,
|
||||
) -> Self {
|
||||
Self {
|
||||
plugin_provider: ExecutorPluginProvider::new(Arc::clone(&environment_manager)),
|
||||
environment_manager,
|
||||
restriction_product,
|
||||
}
|
||||
@@ -49,26 +46,7 @@ impl SkillProvider for ExecutorSkillProvider {
|
||||
Box::pin(async move {
|
||||
let mut catalog = SkillCatalog::default();
|
||||
for selected_root in query.executor_roots {
|
||||
let selected_root_id = selected_root.id.clone();
|
||||
let required_host_capabilities = match self
|
||||
.plugin_provider
|
||||
.resolve_bound(&selected_root)
|
||||
.await
|
||||
{
|
||||
Ok(Some(plugin)) => plugin
|
||||
.plugin()
|
||||
.manifest()
|
||||
.requires
|
||||
.host_capabilities
|
||||
.clone(),
|
||||
Ok(None) => Vec::new(),
|
||||
Err(err) => {
|
||||
catalog.warnings.push(format!(
|
||||
"Selected capability root `{selected_root_id}` could not be resolved as a plugin: {err}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let selected_root_id = selected_root.id;
|
||||
let CapabilityRootLocation::Environment {
|
||||
environment_id,
|
||||
path,
|
||||
@@ -77,12 +55,9 @@ impl SkillProvider for ExecutorSkillProvider {
|
||||
SkillAuthority::new(SkillSourceKind::Executor, selected_root_id.clone());
|
||||
let Some(environment) = self.environment_manager.get_environment(&environment_id)
|
||||
else {
|
||||
catalog.extend_warnings_with_host_requirements(
|
||||
vec![format!(
|
||||
"Selected capability root `{selected_root_id}` references unavailable environment `{environment_id}`."
|
||||
)],
|
||||
&required_host_capabilities,
|
||||
);
|
||||
catalog.warnings.push(format!(
|
||||
"Selected capability root `{selected_root_id}` references unavailable environment `{environment_id}`."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let file_system = environment.get_filesystem();
|
||||
@@ -92,17 +67,13 @@ impl SkillProvider for ExecutorSkillProvider {
|
||||
self.restriction_product,
|
||||
)
|
||||
.await;
|
||||
catalog.extend_warnings_with_host_requirements(
|
||||
outcome.warnings,
|
||||
&required_host_capabilities,
|
||||
);
|
||||
catalog.warnings.extend(outcome.warnings);
|
||||
for skill in outcome.skills {
|
||||
catalog.push_entry(catalog_entry_from_skill(
|
||||
&skill,
|
||||
authority.clone(),
|
||||
&selected_root_id,
|
||||
&environment_id,
|
||||
&required_host_capabilities,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -162,7 +133,6 @@ fn catalog_entry_from_skill(
|
||||
authority: SkillAuthority,
|
||||
selected_root_id: &str,
|
||||
environment_id: &str,
|
||||
required_host_capabilities: &[String],
|
||||
) -> SkillCatalogEntry {
|
||||
let skill_path = skill.path_to_skills_md.inferred_native_path_string();
|
||||
let normalized_path = match skill.path_to_skills_md.infer_path_convention() {
|
||||
@@ -186,8 +156,7 @@ fn catalog_entry_from_skill(
|
||||
)
|
||||
.with_short_description(skill.short_description.clone())
|
||||
.with_display_path(display_path)
|
||||
.with_dependencies(skill.dependencies.clone())
|
||||
.with_required_host_capabilities(required_host_capabilities.to_vec());
|
||||
.with_dependencies(skill.dependencies.clone());
|
||||
|
||||
if skill.allows_implicit_invocation() {
|
||||
entry
|
||||
|
||||
@@ -95,7 +95,6 @@ fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog {
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (skill, enabled) in outcome.skills_with_enabled() {
|
||||
|
||||
@@ -78,26 +78,13 @@ impl SkillsThreadState {
|
||||
mut query: SkillListQuery,
|
||||
) -> SkillCatalog {
|
||||
let roots = std::mem::take(&mut query.executor_roots);
|
||||
let host_capabilities = query.host_capabilities.clone();
|
||||
let mut catalog = SkillCatalog::default();
|
||||
for root in roots {
|
||||
query.executor_roots = vec![root.clone()];
|
||||
let mut root_catalog = self
|
||||
.executor_root_catalog(providers, root, query.clone())
|
||||
.await;
|
||||
root_catalog
|
||||
.entries
|
||||
.retain(|entry| entry.host_requirements_satisfied_by(&host_capabilities));
|
||||
for gated_warnings in std::mem::take(&mut root_catalog.host_capability_gated_warnings) {
|
||||
if gated_warnings
|
||||
.required_host_capabilities
|
||||
.iter()
|
||||
.all(|capability| host_capabilities.contains(capability))
|
||||
{
|
||||
root_catalog.warnings.extend(gated_warnings.warnings);
|
||||
}
|
||||
}
|
||||
catalog.extend(root_catalog);
|
||||
catalog.extend(
|
||||
self.executor_root_catalog(providers, root, query.clone())
|
||||
.await,
|
||||
);
|
||||
}
|
||||
catalog
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ impl SkillToolContext {
|
||||
include_bundled_skills: false,
|
||||
include_orchestrator_skills: true,
|
||||
mcp_resources: self.mcp_resources.clone(),
|
||||
host_capabilities: self.thread_state.config().host_capabilities,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -265,7 +265,6 @@ async fn selected_root_id_distinguishes_identical_executor_paths() {
|
||||
include_bundled_skills: true,
|
||||
include_orchestrator_skills: false,
|
||||
mcp_resources: None,
|
||||
host_capabilities: Default::default(),
|
||||
})
|
||||
.await
|
||||
.expect("list executor skills");
|
||||
@@ -300,79 +299,6 @@ async fn selected_root_id_distinguishes_identical_executor_paths() {
|
||||
std::fs::remove_dir_all(test_root).expect("remove skill directory");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executor_plugin_skills_preserve_all_host_capability_requirements() {
|
||||
let test_root = create_local_skill_root("host-requirements").expect("create local skill root");
|
||||
std::fs::create_dir_all(test_root.join(".codex-plugin")).expect("create manifest directory");
|
||||
std::fs::write(
|
||||
test_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{
|
||||
"name": "gated-skill-plugin",
|
||||
"requires": {
|
||||
"hostCapabilities": ["codex.inline_visualization", "codex.code_mode"]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write plugin manifest");
|
||||
std::fs::create_dir_all(test_root.join("broken")).expect("create broken skill directory");
|
||||
std::fs::write(test_root.join("broken/SKILL.md"), "missing frontmatter")
|
||||
.expect("write broken skill");
|
||||
let provider = ExecutorSkillProvider::new_with_restriction_product(
|
||||
Arc::new(EnvironmentManager::default_for_tests()),
|
||||
/*restriction_product*/ None,
|
||||
);
|
||||
let catalog = provider
|
||||
.list(SkillListQuery {
|
||||
turn_id: "turn-1".to_string(),
|
||||
executor_roots: vec![SelectedCapabilityRoot {
|
||||
id: "selected-root".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: "local".to_string(),
|
||||
path: PathUri::from_host_native_path(&test_root).expect("skill root URI"),
|
||||
},
|
||||
}],
|
||||
host_snapshot: None,
|
||||
include_host_skills: false,
|
||||
include_bundled_skills: true,
|
||||
include_orchestrator_skills: false,
|
||||
mcp_resources: None,
|
||||
host_capabilities: Default::default(),
|
||||
})
|
||||
.await
|
||||
.expect("list executor skills");
|
||||
let [entry] = catalog.entries.as_slice() else {
|
||||
panic!("expected one executor skill");
|
||||
};
|
||||
|
||||
assert!(catalog.warnings.is_empty());
|
||||
let [gated_warnings] = catalog.host_capability_gated_warnings.as_slice() else {
|
||||
panic!("expected one host-gated warning group");
|
||||
};
|
||||
assert_eq!(
|
||||
gated_warnings.required_host_capabilities,
|
||||
vec![
|
||||
"codex.inline_visualization".to_string(),
|
||||
"codex.code_mode".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(gated_warnings.warnings.len(), 1);
|
||||
assert!(!entry.host_requirements_satisfied_by(&Default::default()));
|
||||
assert!(
|
||||
!entry.host_requirements_satisfied_by(&["codex.inline_visualization".to_string()].into())
|
||||
);
|
||||
assert!(
|
||||
entry.host_requirements_satisfied_by(
|
||||
&[
|
||||
"codex.code_mode".to_string(),
|
||||
"codex.inline_visualization".to_string(),
|
||||
]
|
||||
.into()
|
||||
)
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(test_root).expect("remove skill directory");
|
||||
}
|
||||
|
||||
fn create_local_skill_root(label: &str) -> io::Result<std::path::PathBuf> {
|
||||
let id = NEXT_TEST_ROOT_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let test_root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -158,7 +158,6 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
|
||||
"lint-fix/SKILL.md",
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::clone(&read_requests),
|
||||
list_calls: Some(Arc::clone(&list_calls)),
|
||||
@@ -336,196 +335,6 @@ async fn selected_executor_catalog_follows_step_availability_and_reuses_its_cach
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_executor_skills_require_all_host_capabilities_after_caching() -> TestResult {
|
||||
let list_calls = Arc::new(AtomicUsize::new(0));
|
||||
let executor_provider = Arc::new(StaticSkillProvider {
|
||||
catalog: SkillCatalog {
|
||||
entries: vec![
|
||||
test_entry(
|
||||
SkillSourceKind::Executor,
|
||||
"env-1",
|
||||
"executor/visualize",
|
||||
"visualize/SKILL.md",
|
||||
)
|
||||
.with_required_host_capabilities(vec![
|
||||
"codex.inline_visualization".to_string(),
|
||||
"codex.code_mode".to_string(),
|
||||
]),
|
||||
],
|
||||
warnings: Vec::new(),
|
||||
host_capability_gated_warnings: vec![
|
||||
codex_skills_extension::catalog::HostCapabilityGatedWarnings {
|
||||
required_host_capabilities: vec![
|
||||
"codex.inline_visualization".to_string(),
|
||||
"codex.code_mode".to_string(),
|
||||
],
|
||||
warnings: vec!["gated plugin skill warning".to_string()],
|
||||
},
|
||||
],
|
||||
},
|
||||
read_requests: Arc::new(Mutex::new(Vec::new())),
|
||||
list_calls: Some(Arc::clone(&list_calls)),
|
||||
fail_first_list: false,
|
||||
});
|
||||
let providers = SkillProviders::new().with_executor_provider(executor_provider);
|
||||
let (event_tx, event_rx) = std::sync::mpsc::channel();
|
||||
let mut builder =
|
||||
ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx)));
|
||||
install_with_providers(&mut builder, providers, skills_extension_config);
|
||||
let registry = builder.build();
|
||||
let session_store = ExtensionData::new("session");
|
||||
let thread_store = ExtensionData::new("thread");
|
||||
let selected_roots = vec![SelectedCapabilityRoot {
|
||||
id: "visualize".to_string(),
|
||||
location: CapabilityRootLocation::Environment {
|
||||
environment_id: "env-1".to_string(),
|
||||
path: PathUri::parse("file:///skills/visualize").expect("skill root URI"),
|
||||
},
|
||||
}];
|
||||
let session_source = SessionSource::Cli;
|
||||
let config = default_config();
|
||||
registry.thread_lifecycle_contributors()[0]
|
||||
.on_thread_start(ThreadStartInput {
|
||||
config: &config,
|
||||
session_source: &session_source,
|
||||
persistent_thread_state_available: true,
|
||||
environments: &[],
|
||||
session_store: &session_store,
|
||||
thread_store: &thread_store,
|
||||
})
|
||||
.await;
|
||||
let turn_environment = TurnEnvironmentSelection {
|
||||
environment_id: "env-1".to_string(),
|
||||
cwd: PathUri::parse("file:///workspace").expect("cwd URI"),
|
||||
};
|
||||
|
||||
let unsupported_turn_store = ExtensionData::new("turn-1");
|
||||
let unsupported = registry.context_contributors()[0]
|
||||
.contribute_world_state(WorldStateContributionInput {
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
turn_id: "turn-1",
|
||||
environments: std::slice::from_ref(&turn_environment),
|
||||
ready_selected_capability_roots: &selected_roots,
|
||||
session_store: &session_store,
|
||||
thread_store: &thread_store,
|
||||
turn_store: &unsupported_turn_store,
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
unsupported[0]
|
||||
.render_diff(PreviousWorldStateSection::Absent)
|
||||
.is_none()
|
||||
);
|
||||
assert!(unsupported[0].snapshot()["body"].is_null());
|
||||
let unsupported_fragments = registry.turn_input_contributors()[0]
|
||||
.contribute(
|
||||
TurnInputContext {
|
||||
turn_id: "turn-1".to_string(),
|
||||
user_input: Vec::new(),
|
||||
environments: Vec::new(),
|
||||
},
|
||||
&session_store,
|
||||
&thread_store,
|
||||
&unsupported_turn_store,
|
||||
)
|
||||
.await;
|
||||
assert!(unsupported_fragments.is_empty());
|
||||
assert!(matches!(
|
||||
event_rx.try_recv(),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty)
|
||||
));
|
||||
|
||||
let mut partial_config = config.clone();
|
||||
partial_config.host_capabilities = ["codex.inline_visualization".to_string()].into();
|
||||
registry.config_contributors()[0].on_config_changed(
|
||||
&session_store,
|
||||
&thread_store,
|
||||
&config,
|
||||
&partial_config,
|
||||
);
|
||||
let partial_turn_store = ExtensionData::new("turn-2");
|
||||
let partially_supported = registry.context_contributors()[0]
|
||||
.contribute_world_state(WorldStateContributionInput {
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
turn_id: "turn-2",
|
||||
environments: std::slice::from_ref(&turn_environment),
|
||||
ready_selected_capability_roots: &selected_roots,
|
||||
session_store: &session_store,
|
||||
thread_store: &thread_store,
|
||||
turn_store: &partial_turn_store,
|
||||
})
|
||||
.await;
|
||||
assert!(partially_supported[0].snapshot()["body"].is_null());
|
||||
let partially_supported_fragments = registry.turn_input_contributors()[0]
|
||||
.contribute(
|
||||
TurnInputContext {
|
||||
turn_id: "turn-2".to_string(),
|
||||
user_input: Vec::new(),
|
||||
environments: Vec::new(),
|
||||
},
|
||||
&session_store,
|
||||
&thread_store,
|
||||
&partial_turn_store,
|
||||
)
|
||||
.await;
|
||||
assert!(partially_supported_fragments.is_empty());
|
||||
assert!(matches!(
|
||||
event_rx.try_recv(),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty)
|
||||
));
|
||||
|
||||
let mut supported_config = partial_config.clone();
|
||||
supported_config.host_capabilities = [
|
||||
"codex.code_mode".to_string(),
|
||||
"codex.inline_visualization".to_string(),
|
||||
]
|
||||
.into();
|
||||
registry.config_contributors()[0].on_config_changed(
|
||||
&session_store,
|
||||
&thread_store,
|
||||
&partial_config,
|
||||
&supported_config,
|
||||
);
|
||||
let supported_turn_store = ExtensionData::new("turn-3");
|
||||
let supported = registry.context_contributors()[0]
|
||||
.contribute_world_state(WorldStateContributionInput {
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
turn_id: "turn-3",
|
||||
environments: &[turn_environment],
|
||||
ready_selected_capability_roots: &selected_roots,
|
||||
session_store: &session_store,
|
||||
thread_store: &thread_store,
|
||||
turn_store: &supported_turn_store,
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
supported[0].snapshot()["body"]
|
||||
.as_str()
|
||||
.is_some_and(|body| body.contains("visualize"))
|
||||
);
|
||||
let supported_fragments = registry.turn_input_contributors()[0]
|
||||
.contribute(
|
||||
TurnInputContext {
|
||||
turn_id: "turn-3".to_string(),
|
||||
user_input: Vec::new(),
|
||||
environments: Vec::new(),
|
||||
},
|
||||
&session_store,
|
||||
&thread_store,
|
||||
&supported_turn_store,
|
||||
)
|
||||
.await;
|
||||
assert!(supported_fragments.is_empty());
|
||||
let EventMsg::Warning(warning) = event_rx.try_recv()?.msg else {
|
||||
panic!("expected gated warning for supported plugin");
|
||||
};
|
||||
assert_eq!(warning.message, "gated plugin skill warning");
|
||||
assert_eq!(1, list_calls.load(Ordering::Relaxed));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_context_truncates_catalog_descriptions() -> TestResult {
|
||||
let description = "x".repeat(1_025);
|
||||
@@ -541,7 +350,6 @@ async fn default_context_truncates_catalog_descriptions() -> TestResult {
|
||||
catalog: SkillCatalog {
|
||||
entries: vec![entry],
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::new(Mutex::new(Vec::new())),
|
||||
list_calls: None,
|
||||
@@ -592,7 +400,6 @@ async fn skills_list_truncates_catalog_descriptions_in_tool_output() -> TestResu
|
||||
catalog: SkillCatalog {
|
||||
entries: vec![entry],
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::new(Mutex::new(Vec::new())),
|
||||
list_calls: None,
|
||||
@@ -663,7 +470,6 @@ async fn orchestrator_catalog_snapshot_caches_failure() -> TestResult {
|
||||
"skill://orchestrator/first/SKILL.md",
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::new(Mutex::new(Vec::new())),
|
||||
list_calls: Some(Arc::clone(&list_calls)),
|
||||
@@ -745,7 +551,6 @@ async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> Te
|
||||
})
|
||||
.collect(),
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::clone(&read_requests),
|
||||
list_calls: None,
|
||||
@@ -846,7 +651,6 @@ async fn prompt_hidden_skill_can_still_be_invoked() -> TestResult {
|
||||
.hidden_from_prompt(),
|
||||
],
|
||||
warnings: Vec::new(),
|
||||
..Default::default()
|
||||
},
|
||||
read_requests: Arc::clone(&read_requests),
|
||||
list_calls: None,
|
||||
@@ -977,7 +781,6 @@ struct TestConfig {
|
||||
include_instructions: bool,
|
||||
bundled_skills_enabled: bool,
|
||||
orchestrator_skills_enabled: bool,
|
||||
host_capabilities: std::collections::BTreeSet<String>,
|
||||
}
|
||||
|
||||
fn default_config() -> TestConfig {
|
||||
@@ -985,7 +788,6 @@ fn default_config() -> TestConfig {
|
||||
include_instructions: true,
|
||||
bundled_skills_enabled: true,
|
||||
orchestrator_skills_enabled: true,
|
||||
host_capabilities: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -994,7 +796,6 @@ fn skills_extension_config(config: &TestConfig) -> SkillsExtensionConfig {
|
||||
include_instructions: config.include_instructions,
|
||||
bundled_skills_enabled: config.bundled_skills_enabled,
|
||||
orchestrator_skills_enabled: config.orchestrator_skills_enabled,
|
||||
host_capabilities: config.host_capabilities.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Opaque host features available to runtime plugin contributions.
|
||||
///
|
||||
/// Capability names are case-sensitive. Construction trims surrounding
|
||||
/// whitespace, drops empty names, and deduplicates values deterministically.
|
||||
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub struct HostCapabilities(BTreeSet<String>);
|
||||
|
||||
impl HostCapabilities {
|
||||
pub fn from_names<I, S>(names: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
Self(
|
||||
names
|
||||
.into_iter()
|
||||
.filter_map(|name| {
|
||||
let name = name.as_ref().trim();
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn contains(&self, capability: &str) -> bool {
|
||||
self.0.contains(capability)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, capability: impl AsRef<str>) -> bool {
|
||||
let capability = capability.as_ref().trim();
|
||||
!capability.is_empty() && self.0.insert(capability.to_string())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.0.iter().map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "host_capabilities_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,14 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::HostCapabilities;
|
||||
|
||||
#[test]
|
||||
fn capability_names_are_normalized_deterministically() {
|
||||
let capabilities =
|
||||
HostCapabilities::from_names([" capability-b ", "", "capability-a", "capability-b"]);
|
||||
|
||||
assert_eq!(
|
||||
capabilities.iter().collect::<Vec<_>>(),
|
||||
vec!["capability-a", "capability-b"]
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::collections::HashSet;
|
||||
pub use codex_utils_plugins::mention_syntax;
|
||||
pub use codex_utils_plugins::plugin_namespace_for_skill_path;
|
||||
|
||||
mod host_capabilities;
|
||||
mod load_outcome;
|
||||
pub mod manifest;
|
||||
mod plugin_id;
|
||||
@@ -12,6 +13,7 @@ mod provider;
|
||||
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
pub use host_capabilities::HostCapabilities;
|
||||
pub use load_outcome::EffectiveSkillRoots;
|
||||
pub use load_outcome::LoadedPlugin;
|
||||
pub use load_outcome::PluginLoadOutcome;
|
||||
|
||||
@@ -6,9 +6,11 @@ use codex_utils_plugins::PluginSkillRoot;
|
||||
|
||||
use crate::AppConnectorId;
|
||||
use crate::AppDeclaration;
|
||||
use crate::HostCapabilities;
|
||||
use crate::PluginCapabilitySummary;
|
||||
use crate::PluginHookSource;
|
||||
use crate::app_connector_ids_from_declarations;
|
||||
use crate::manifest::PluginManifestRequirements;
|
||||
|
||||
const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024;
|
||||
|
||||
@@ -21,8 +23,8 @@ pub struct LoadedPlugin<M> {
|
||||
pub manifest_description: Option<String>,
|
||||
pub root: AbsolutePathBuf,
|
||||
pub enabled: bool,
|
||||
pub required_host_capabilities: Vec<String>,
|
||||
pub missing_host_capabilities: Vec<String>,
|
||||
pub required_host_capabilities: HostCapabilities,
|
||||
pub missing_host_capabilities: HostCapabilities,
|
||||
pub skill_roots: Vec<AbsolutePathBuf>,
|
||||
pub disabled_skill_paths: HashSet<AbsolutePathBuf>,
|
||||
pub has_enabled_skills: bool,
|
||||
@@ -42,13 +44,11 @@ impl<M> LoadedPlugin<M> {
|
||||
self.manifest_name.as_deref().unwrap_or(&self.config_name)
|
||||
}
|
||||
|
||||
pub fn apply_host_capabilities(&mut self, host_capabilities: &HashSet<String>) {
|
||||
self.missing_host_capabilities = self
|
||||
.required_host_capabilities
|
||||
.iter()
|
||||
.filter(|capability| !host_capabilities.contains(*capability))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
pub fn apply_host_capabilities(&mut self, host_capabilities: &HostCapabilities) {
|
||||
self.missing_host_capabilities = PluginManifestRequirements {
|
||||
host_capabilities: self.required_host_capabilities.clone(),
|
||||
}
|
||||
.missing_from(host_capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,8 +243,8 @@ mod tests {
|
||||
manifest_description: None,
|
||||
root: test_path(config_name),
|
||||
enabled: true,
|
||||
required_host_capabilities: Vec::new(),
|
||||
missing_host_capabilities: Vec::new(),
|
||||
required_host_capabilities: HostCapabilities::default(),
|
||||
missing_host_capabilities: HostCapabilities::default(),
|
||||
skill_roots,
|
||||
disabled_skill_paths: HashSet::new(),
|
||||
has_enabled_skills: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use codex_config::HooksFile;
|
||||
|
||||
use crate::HostCapabilities;
|
||||
|
||||
/// Parsed plugin metadata parameterized by its resource locator representation.
|
||||
///
|
||||
/// Host loading uses absolute paths, while resolved packages replace them with
|
||||
@@ -19,7 +21,23 @@ pub struct PluginManifest<Resource> {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PluginManifestRequirements {
|
||||
/// Every listed capability must be advertised by the host.
|
||||
pub host_capabilities: Vec<String>,
|
||||
pub host_capabilities: HostCapabilities,
|
||||
}
|
||||
|
||||
impl PluginManifestRequirements {
|
||||
pub fn is_satisfied_by(&self, host_capabilities: &HostCapabilities) -> bool {
|
||||
self.host_capabilities
|
||||
.iter()
|
||||
.all(|capability| host_capabilities.contains(capability))
|
||||
}
|
||||
|
||||
pub fn missing_from(&self, host_capabilities: &HostCapabilities) -> HostCapabilities {
|
||||
HostCapabilities::from_names(
|
||||
self.host_capabilities
|
||||
.iter()
|
||||
.filter(|capability| !host_capabilities.contains(capability)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Component resources declared by a plugin manifest.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::PluginResourceLocator;
|
||||
use super::ResolvedPlugin;
|
||||
use super::ResolvedPluginError;
|
||||
use crate::HostCapabilities;
|
||||
use crate::manifest::PluginManifest;
|
||||
use crate::manifest::PluginManifestHooks;
|
||||
use crate::manifest::PluginManifestInterface;
|
||||
@@ -44,7 +45,7 @@ fn environment_descriptor_binds_every_manifest_resource() {
|
||||
description: None,
|
||||
keywords: Vec::new(),
|
||||
requires: PluginManifestRequirements {
|
||||
host_capabilities: vec!["codex.inline_visualization".to_string()],
|
||||
host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]),
|
||||
},
|
||||
paths: PluginManifestPaths {
|
||||
skills: vec![path_uri(&skills)],
|
||||
@@ -81,7 +82,7 @@ fn environment_descriptor_binds_every_manifest_resource() {
|
||||
description: None,
|
||||
keywords: Vec::new(),
|
||||
requires: PluginManifestRequirements {
|
||||
host_capabilities: vec!["codex.inline_visualization".to_string()],
|
||||
host_capabilities: HostCapabilities::from_names(["codex.inline_visualization"]),
|
||||
},
|
||||
paths: PluginManifestPaths {
|
||||
skills: vec![resource("executor-1", &skills)],
|
||||
|
||||
@@ -176,7 +176,6 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
let mut config = Config {
|
||||
config_layer_stack: ConfigLayerStack::default(),
|
||||
startup_warnings: Vec::new(),
|
||||
host_capabilities: Default::default(),
|
||||
bypass_hook_trust: false,
|
||||
model,
|
||||
service_tier: None,
|
||||
|
||||
@@ -400,7 +400,6 @@ async fn connect_remote_app_server(
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
@@ -565,7 +564,6 @@ where
|
||||
client_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
|
||||
@@ -1069,7 +1069,6 @@ mod tests {
|
||||
client_version: "test".to_string(),
|
||||
experimental_api: true,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
host_capabilities: Vec::new(),
|
||||
opt_out_notification_methods: Vec::new(),
|
||||
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user