mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Split plugin install discovery into list and request tools (#23372)
## Summary - Add `list_available_plugins_to_install` as the inventory step for plugin and connector install suggestions. - Slim `request_plugin_install` so it only handles the actual elicitation, instead of carrying the full discoverable list in its prompt. - Emit send-time telemetry when an install elicitation is dispatched, including requested tool identity in the event payload. - Emit install-result telemetry through `SessionTelemetry`, including tool type, user response action, and completion status. - Update registration and tests to cover the new two-step flow while keeping the existing `tool_suggest` feature gate unchanged. ## Testing - `just fmt` - `cargo test -p codex-tools` - `cargo test -p codex-core request_plugin_install` - `cargo test -p codex-core list_available_plugins_to_install` - `cargo test -p codex-core install_suggestion_tools_can_be_registered_without_search_tool` - `cargo test -p codex-otel manager_records_plugin_install_suggestion_metric` - `cargo test -p codex-otel manager_records_plugin_install_elicitation_sent_metric` - `just fix -p codex-core` - `just fix -p codex-tools` - `just fix -p codex-otel` - `cargo check -p codex-core`
This commit is contained in:
@@ -5,6 +5,7 @@ use codex_mcp::ElicitationReviewerHandle;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_ELICITATION_APPROVAL_KIND_KEY;
|
||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_ELICITATION_APPROVAL_KIND_MCP_TOOL_CALL;
|
||||
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_TOOL_SUGGESTION as MCP_ELICITATION_APPROVAL_KIND_TOOL_SUGGESTION;
|
||||
use codex_protocol::mcp_approval_meta::APPROVALS_REVIEWER_KEY as MCP_ELICITATION_APPROVALS_REVIEWER_KEY;
|
||||
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_ELICITATION_CONNECTOR_DESCRIPTION_KEY;
|
||||
use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_ELICITATION_CONNECTOR_ID_KEY;
|
||||
@@ -21,6 +22,10 @@ use rmcp::model::Meta;
|
||||
use serde_json::Map;
|
||||
|
||||
const MCP_ELICITATION_DECLINE_MESSAGE_KEY: &str = "message";
|
||||
const TOOL_SUGGESTION_ACTION_INSTALL: &str = "install";
|
||||
const TOOL_SUGGESTION_ACTION_KEY: &str = "suggest_type";
|
||||
const TOOL_SUGGESTION_TOOL_ID_KEY: &str = "tool_id";
|
||||
const TOOL_SUGGESTION_TOOL_TYPE_KEY: &str = "tool_type";
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum GuardianElicitationReview {
|
||||
@@ -33,6 +38,18 @@ struct GuardianMcpElicitationReviewer {
|
||||
session: std::sync::Weak<Session>,
|
||||
}
|
||||
|
||||
pub(crate) struct McpServerElicitationOutcome {
|
||||
pub(crate) response: Option<ElicitationResponse>,
|
||||
pub(crate) sent: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PluginInstallElicitationTelemetryMetadata {
|
||||
tool_type: String,
|
||||
tool_id: String,
|
||||
tool_name: String,
|
||||
}
|
||||
|
||||
impl GuardianMcpElicitationReviewer {
|
||||
fn new(session: &Arc<Session>) -> Self {
|
||||
Self {
|
||||
@@ -70,7 +87,7 @@ impl Session {
|
||||
turn_context: &TurnContext,
|
||||
request_id: RequestId,
|
||||
params: McpServerElicitationRequestParams,
|
||||
) -> Option<ElicitationResponse> {
|
||||
) -> McpServerElicitationOutcome {
|
||||
if self
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
@@ -78,11 +95,14 @@ impl Session {
|
||||
.await
|
||||
.elicitations_auto_deny()
|
||||
{
|
||||
return Some(ElicitationResponse {
|
||||
action: codex_rmcp_client::ElicitationAction::Accept,
|
||||
content: Some(serde_json::json!({})),
|
||||
meta: None,
|
||||
});
|
||||
return McpServerElicitationOutcome {
|
||||
response: Some(ElicitationResponse {
|
||||
action: codex_rmcp_client::ElicitationAction::Accept,
|
||||
content: Some(serde_json::json!({})),
|
||||
meta: None,
|
||||
}),
|
||||
sent: false,
|
||||
};
|
||||
}
|
||||
|
||||
let server_name = params.server_name.clone();
|
||||
@@ -98,7 +118,10 @@ impl Session {
|
||||
warn!(
|
||||
"failed to serialize MCP elicitation schema for server_name: {server_name}, request_id: {request_id}: {err:#}"
|
||||
);
|
||||
return None;
|
||||
return McpServerElicitationOutcome {
|
||||
response: None,
|
||||
sent: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
codex_protocol::approvals::ElicitationRequest::Form {
|
||||
@@ -154,11 +177,24 @@ impl Session {
|
||||
id,
|
||||
request,
|
||||
});
|
||||
let plugin_install_telemetry = plugin_install_elicitation_telemetry_metadata(&event);
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.mark_user_input_requested_during_turn();
|
||||
self.send_event(turn_context, event).await;
|
||||
rx_response.await.ok()
|
||||
if let Some(plugin_install_telemetry) = plugin_install_telemetry {
|
||||
turn_context
|
||||
.session_telemetry
|
||||
.record_plugin_install_elicitation_sent(
|
||||
plugin_install_telemetry.tool_type.as_str(),
|
||||
plugin_install_telemetry.tool_id.as_str(),
|
||||
plugin_install_telemetry.tool_name.as_str(),
|
||||
);
|
||||
}
|
||||
McpServerElicitationOutcome {
|
||||
response: rx_response.await.ok(),
|
||||
sent: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
@@ -551,6 +587,33 @@ fn metadata_owned_string(meta: &Map<String, Value>, key: &str) -> Option<String>
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn plugin_install_elicitation_telemetry_metadata(
|
||||
event: &EventMsg,
|
||||
) -> Option<PluginInstallElicitationTelemetryMetadata> {
|
||||
let EventMsg::ElicitationRequest(ElicitationRequestEvent { request, .. }) = event else {
|
||||
return None;
|
||||
};
|
||||
let codex_protocol::approvals::ElicitationRequest::Form {
|
||||
meta: Some(Value::Object(meta)),
|
||||
..
|
||||
} = request
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if metadata_str(meta, MCP_ELICITATION_APPROVAL_KIND_KEY)
|
||||
!= Some(MCP_ELICITATION_APPROVAL_KIND_TOOL_SUGGESTION)
|
||||
|| metadata_str(meta, TOOL_SUGGESTION_ACTION_KEY) != Some(TOOL_SUGGESTION_ACTION_INSTALL)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(PluginInstallElicitationTelemetryMetadata {
|
||||
tool_type: metadata_owned_string(meta, TOOL_SUGGESTION_TOOL_TYPE_KEY)?,
|
||||
tool_id: metadata_owned_string(meta, TOOL_SUGGESTION_TOOL_ID_KEY)?,
|
||||
tool_name: metadata_owned_string(meta, MCP_ELICITATION_TOOL_NAME_KEY)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn mcp_elicitation_request_id(id: &RequestId) -> String {
|
||||
match id {
|
||||
rmcp::model::NumberOrString::String(value) => value.to_string(),
|
||||
|
||||
@@ -96,6 +96,63 @@ fn guardian_elicitation_review_request_defaults_missing_tool_params() {
|
||||
assert_eq!(arguments, Some(json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_install_elicitation_telemetry_metadata_requires_install_tool_suggestion() {
|
||||
let event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
server_name: "codex_apps".to_string(),
|
||||
id: codex_protocol::mcp::RequestId::String("request-1".to_string()),
|
||||
request: codex_protocol::approvals::ElicitationRequest::Form {
|
||||
meta: Some(json!({
|
||||
"codex_approval_kind": "tool_suggestion",
|
||||
"suggest_type": "install",
|
||||
"tool_type": "plugin",
|
||||
"tool_id": "slack@openai-curated",
|
||||
"tool_name": "Slack",
|
||||
})),
|
||||
message: "Install Slack?".to_string(),
|
||||
requested_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
plugin_install_elicitation_telemetry_metadata(&event),
|
||||
Some(PluginInstallElicitationTelemetryMetadata {
|
||||
tool_type: "plugin".to_string(),
|
||||
tool_id: "slack@openai-curated".to_string(),
|
||||
tool_name: "Slack".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
let enable_event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
server_name: "codex_apps".to_string(),
|
||||
id: codex_protocol::mcp::RequestId::String("request-2".to_string()),
|
||||
request: codex_protocol::approvals::ElicitationRequest::Form {
|
||||
meta: Some(json!({
|
||||
"codex_approval_kind": "tool_suggestion",
|
||||
"suggest_type": "enable",
|
||||
"tool_type": "plugin",
|
||||
"tool_id": "slack@openai-curated",
|
||||
"tool_name": "Slack",
|
||||
})),
|
||||
message: "Enable Slack?".to_string(),
|
||||
requested_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
plugin_install_elicitation_telemetry_metadata(&enable_event),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_elicitation_review_request_requires_opt_in() {
|
||||
let request = form_request(meta(json!({
|
||||
|
||||
@@ -328,13 +328,14 @@ async fn request_mcp_server_elicitation_auto_accepts_when_auto_deny_is_enabled()
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
response.response,
|
||||
Some(ElicitationResponse {
|
||||
action: ElicitationAction::Accept,
|
||||
content: Some(json!({})),
|
||||
meta: None,
|
||||
})
|
||||
);
|
||||
assert!(!response.sent);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user