Skip to main content

codex_tools/
request_plugin_install.rs

1use codex_connectors::AppInfo;
2use codex_protocol::approvals::ElicitationRequest;
3use serde::Deserialize;
4use serde::Serialize;
5use serde_json::json;
6
7use crate::DiscoverableTool;
8use crate::DiscoverableToolAction;
9use crate::DiscoverableToolType;
10
11pub const REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
12pub const REQUEST_PLUGIN_INSTALL_PERSIST_KEY: &str = "persist";
13pub const REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE: &str = "always";
14
15#[derive(Debug, Deserialize)]
16pub struct RequestPluginInstallArgs {
17    pub tool_type: DiscoverableToolType,
18    pub action_type: DiscoverableToolAction,
19    pub tool_id: String,
20    pub suggest_reason: String,
21}
22
23#[derive(Debug, Serialize, PartialEq, Eq)]
24pub struct RequestPluginInstallResult {
25    pub completed: bool,
26    pub user_confirmed: bool,
27    pub tool_type: DiscoverableToolType,
28    pub action_type: DiscoverableToolAction,
29    pub tool_id: String,
30    pub tool_name: String,
31    pub suggest_reason: String,
32}
33
34#[derive(Debug, Serialize, PartialEq, Eq)]
35pub struct RequestPluginInstallMeta<'a> {
36    pub codex_approval_kind: &'static str,
37    pub persist: &'static str,
38    pub tool_type: DiscoverableToolType,
39    pub suggest_type: DiscoverableToolAction,
40    pub suggest_reason: &'a str,
41    pub tool_id: &'a str,
42    pub tool_name: &'a str,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub install_url: Option<&'a str>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub remote_plugin_id: Option<&'a str>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub app_connector_ids: Option<&'a [String]>,
49}
50
51pub fn build_request_plugin_install_elicitation_request(
52    suggest_reason: &str,
53    tool: &DiscoverableTool,
54) -> ElicitationRequest {
55    let message = suggest_reason.to_string();
56
57    ElicitationRequest::Form {
58        meta: Some(json!(build_request_plugin_install_meta(
59            suggest_reason,
60            tool,
61        ))),
62        message,
63        requested_schema: json!({
64            "type": "object",
65            "properties": {},
66        }),
67    }
68}
69
70pub fn all_requested_connectors_picked_up(
71    expected_connector_ids: &[String],
72    accessible_connectors: &[AppInfo],
73) -> bool {
74    expected_connector_ids.iter().all(|connector_id| {
75        verified_connector_install_completed(connector_id, accessible_connectors)
76    })
77}
78
79pub fn verified_connector_install_completed(
80    tool_id: &str,
81    accessible_connectors: &[AppInfo],
82) -> bool {
83    accessible_connectors
84        .iter()
85        .find(|connector| connector.id == tool_id)
86        .is_some_and(|connector| connector.is_accessible)
87}
88
89fn build_request_plugin_install_meta<'a>(
90    suggest_reason: &'a str,
91    tool: &'a DiscoverableTool,
92) -> RequestPluginInstallMeta<'a> {
93    let (tool_type, remote_plugin_id, app_connector_ids) = match tool {
94        DiscoverableTool::Connector(_) => (DiscoverableToolType::Connector, None, None),
95        DiscoverableTool::Plugin(plugin) => (
96            DiscoverableToolType::Plugin,
97            plugin.remote_plugin_id.as_deref(),
98            Some(plugin.app_connector_ids.as_slice()),
99        ),
100    };
101    RequestPluginInstallMeta {
102        codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE,
103        persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE,
104        tool_type,
105        suggest_type: DiscoverableToolAction::Install,
106        suggest_reason,
107        tool_id: tool.id(),
108        tool_name: tool.name(),
109        install_url: tool.install_url(),
110        remote_plugin_id,
111        app_connector_ids,
112    }
113}
114
115#[cfg(test)]
116#[path = "request_plugin_install_tests.rs"]
117mod tests;