Delete tool suggestion marketplace organ
This commit is contained in:
parent
4d25e96b25
commit
c88f5620c0
41 changed files with 29 additions and 2293 deletions
|
|
@ -810,14 +810,13 @@ plugin/app capability summaries, inject available-skill/available-plugin/plugin
|
|||
instructions, parse plugin/app mentions, record app/plugin invocation telemetry,
|
||||
or filter MCP tools through Codex app connectors.
|
||||
|
||||
The tool-suggestion marketplace layer is now cold as well. `tool_suggest` is
|
||||
never exposed in model-turn tool config, its core handler and discoverable
|
||||
plugin scan were deleted, MCP startup/refresh passes empty plugin provenance,
|
||||
session/thread state no longer carries `PluginsManager`, and the old
|
||||
session-level explicit connector selection cache was removed because it only
|
||||
served app/plugin suggestion bookkeeping. The remaining plugin code is now a
|
||||
public Codex product library/CLI surface, not a live session organ; judge it as
|
||||
the next removable husk, not as keeper spine.
|
||||
The tool-suggestion marketplace layer is now deleted, not merely cold.
|
||||
`tool_suggest` no longer exists as a feature/config/schema key, shared tool
|
||||
spec, handler kind, discoverable-tool side channel, connector filter, TUI
|
||||
suggestion elicitation flow, prompt template, or implementation-shape test
|
||||
surface. If a future subsystem wants missing-tool installation, it needs an
|
||||
Epiphany-owned typed MCP/CultNet adapter contract; do not resurrect this Codex
|
||||
marketplace product path.
|
||||
|
||||
That husk has now been cut from `codex-core`. The root core crate no longer
|
||||
exports `plugins`, no longer depends on `codex-core-plugins` or `codex-plugin`,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -336,6 +336,10 @@ current_status:
|
|||
import product is gone too: no `externalAgentConfig/detect`,
|
||||
`externalAgentConfig/import`, import-completed notification, TUI startup
|
||||
prompt, migration modules, snapshots, disabled app-server handlers, or
|
||||
generated schemas remain. MCP remains JSON-RPC at the edge; user-declared
|
||||
MCP servers are the only runtime MCP authority.
|
||||
generated schemas remain. The tool-suggestion marketplace organ is now
|
||||
deleted, not merely disabled: no `tool_suggest` feature/config/schema, tool
|
||||
spec, handler kind, discoverable-tool side channel, connector filter, TUI
|
||||
suggestion elicitation flow, prompt template, or implementation-shape tests
|
||||
remain. MCP remains JSON-RPC at the edge; user-declared MCP servers are the
|
||||
only runtime MCP authority.
|
||||
next_action: Continue starving coordinator/runtime execution and operator surfaces off app-server JSON-RPC and out of `codex_message_processor.rs`; keep the vestigial Codex organ narrowed to subscription auth/model transport.
|
||||
|
|
|
|||
|
|
@ -32,12 +32,8 @@ use std::path::PathBuf;
|
|||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[
|
||||
"memories",
|
||||
"tool_search",
|
||||
"tool_suggest",
|
||||
"tool_call_mcp_elicitation",
|
||||
];
|
||||
const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] =
|
||||
&["memories", "tool_search", "tool_call_mcp_elicitation"];
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait UserConfigReloader: Send + Sync {
|
||||
|
|
|
|||
|
|
@ -176,7 +176,6 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
|
|||
&mut mcp,
|
||||
BTreeMap::from([
|
||||
("memories".to_string(), true),
|
||||
("tool_suggest".to_string(), true),
|
||||
("tool_call_mcp_elicitation".to_string(), false),
|
||||
]),
|
||||
)
|
||||
|
|
@ -187,7 +186,6 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
|
|||
ExperimentalFeatureEnablementSetResponse {
|
||||
enablement: BTreeMap::from([
|
||||
("memories".to_string(), true),
|
||||
("tool_suggest".to_string(), true),
|
||||
("tool_call_mcp_elicitation".to_string(), false),
|
||||
]),
|
||||
}
|
||||
|
|
@ -209,13 +207,6 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
|
|||
.and_then(|features| features.get("memories")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.additional
|
||||
.get("features")
|
||||
.and_then(|features| features.get("tool_suggest")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.additional
|
||||
|
|
@ -288,7 +279,7 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() -
|
|||
assert!(
|
||||
error
|
||||
.message
|
||||
.contains("memories, tool_search, tool_suggest, tool_call_mcp_elicitation"),
|
||||
.contains("memories, tool_search, tool_call_mcp_elicitation"),
|
||||
"{}",
|
||||
error.message
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ use crate::types::PluginConfig;
|
|||
use crate::types::SandboxWorkspaceWrite;
|
||||
use crate::types::ShellEnvironmentPolicyToml;
|
||||
use crate::types::SkillsConfig;
|
||||
use crate::types::ToolSuggestConfig;
|
||||
use crate::types::Tui;
|
||||
use crate::types::UriBasedFileOpener;
|
||||
use crate::types::WindowsToml;
|
||||
|
|
@ -321,7 +320,6 @@ pub struct ConfigToml {
|
|||
pub tools: Option<ToolsToml>,
|
||||
|
||||
/// Additional discoverable tools that can be suggested for installation.
|
||||
pub tool_suggest: Option<ToolSuggestConfig>,
|
||||
|
||||
/// Agent-related settings (thread limits, etc.).
|
||||
pub agents: Option<AgentsToml>,
|
||||
|
|
|
|||
22
vendor/codex/codex-rs/config/src/types.rs
vendored
22
vendor/codex/codex-rs/config/src/types.rs
vendored
|
|
@ -157,28 +157,6 @@ pub struct FeedbackConfigToml {
|
|||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolSuggestDiscoverableType {
|
||||
Connector,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestDiscoverable {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: ToolSuggestDiscoverableType,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestConfig {
|
||||
#[serde(default)]
|
||||
pub discoverables: Vec<ToolSuggestDiscoverable>,
|
||||
}
|
||||
|
||||
/// Memories settings loaded from config.toml.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
|
|
|
|||
27
vendor/codex/codex-rs/connectors/src/filter.rs
vendored
27
vendor/codex/codex-rs/connectors/src/filter.rs
vendored
|
|
@ -1,32 +1,5 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
|
||||
pub fn filter_tool_suggest_discoverable_connectors(
|
||||
directory_connectors: Vec<AppInfo>,
|
||||
accessible_connectors: &[AppInfo],
|
||||
discoverable_connector_ids: &HashSet<String>,
|
||||
originator_value: &str,
|
||||
) -> Vec<AppInfo> {
|
||||
let accessible_connector_ids: HashSet<&str> = accessible_connectors
|
||||
.iter()
|
||||
.filter(|connector| connector.is_accessible)
|
||||
.map(|connector| connector.id.as_str())
|
||||
.collect();
|
||||
|
||||
let mut connectors = filter_disallowed_connectors(directory_connectors, originator_value)
|
||||
.into_iter()
|
||||
.filter(|connector| !accessible_connector_ids.contains(connector.id.as_str()))
|
||||
.filter(|connector| discoverable_connector_ids.contains(connector.id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
connectors.sort_by(|left, right| {
|
||||
left.name
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
connectors
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
|
|
|
|||
64
vendor/codex/codex-rs/core/config.schema.json
vendored
64
vendor/codex/codex-rs/core/config.schema.json
vendored
|
|
@ -36,12 +36,6 @@
|
|||
"$ref": "#/definitions/AgentRoleToml"
|
||||
},
|
||||
"properties": {
|
||||
"job_max_runtime_seconds": {
|
||||
"description": "Default maximum runtime in seconds for agent job workers.",
|
||||
"format": "uint64",
|
||||
"minimum": 1.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"max_depth": {
|
||||
"description": "Maximum nesting depth allowed for spawned agent threads. Root sessions start at depth 0.",
|
||||
"format": "int32",
|
||||
|
|
@ -390,9 +384,6 @@
|
|||
"enable_experimental_windows_sandbox": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_fanout": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_request_compression": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -525,9 +516,6 @@
|
|||
"tool_search_always_defer_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_suggest": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tui_app_server": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -1953,42 +1941,6 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"discoverables": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolSuggestDiscoverable"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDiscoverable": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/ToolSuggestDiscoverableType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDiscoverableType": {
|
||||
"enum": [
|
||||
"connector",
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ToolsToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
|
@ -2221,7 +2173,7 @@
|
|||
"$ref": "#/definitions/AgentsToml"
|
||||
}
|
||||
],
|
||||
"description": "Agent-related settings (thread limits, etc.)."
|
||||
"description": "Additional discoverable tools that can be suggested for installation. Agent-related settings (thread limits, etc.)."
|
||||
},
|
||||
"allow_login_shell": {
|
||||
"description": "Whether the model may request a login shell for shell-based tools. Default to `true`\n\nIf `true`, the model may request a login shell (`login = true`), and omitting `login` defaults to using a login shell. If `false`, the model can never use a login shell: `login = true` requests are rejected, and omitting `login` defaults to a non-login shell.",
|
||||
|
|
@ -2408,9 +2360,6 @@
|
|||
"enable_experimental_windows_sandbox": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_fanout": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_request_compression": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -2543,9 +2492,6 @@
|
|||
"tool_search_always_defer_mcp_tools": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tool_suggest": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tui_app_server": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -2957,14 +2903,6 @@
|
|||
"minimum": 0.0,
|
||||
"type": "integer"
|
||||
},
|
||||
"tool_suggest": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolSuggestConfig"
|
||||
}
|
||||
],
|
||||
"description": "Additional discoverable tools that can be suggested for installation."
|
||||
},
|
||||
"tools": {
|
||||
"allOf": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ use codex_config::types::NotificationMethod;
|
|||
use codex_config::types::Notifications;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_config::types::Tui;
|
||||
use codex_config::types::TuiNotificationSettings;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
|
|
@ -5258,7 +5257,6 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_terminal_title: None,
|
||||
|
|
@ -5409,7 +5407,6 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_terminal_title: None,
|
||||
|
|
@ -5558,7 +5555,6 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(false),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_terminal_title: None,
|
||||
|
|
@ -5692,7 +5688,6 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
|||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
analytics_enabled: Some(true),
|
||||
feedback_enabled: true,
|
||||
tool_suggest: ToolSuggestConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
tui_terminal_title: None,
|
||||
|
|
@ -7035,66 +7030,6 @@ async fn feature_requirements_reject_collab_legacy_alias() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_discoverables_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "connector", id = "connector_alpha" },
|
||||
{ type = "plugin", id = "plugin_alpha@openai-curated" },
|
||||
{ type = "connector", id = " " }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: vec![
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: "connector_alpha".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: "plugin_alpha@openai-curated".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: " ".to_string(),
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.tool_suggest,
|
||||
ToolSuggestConfig {
|
||||
discoverables: vec![
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: "connector_alpha".to_string(),
|
||||
},
|
||||
ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: "plugin_alpha@openai-curated".to_string(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
|
|
|
|||
29
vendor/codex/codex-rs/core/src/config/mod.rs
vendored
29
vendor/codex/codex-rs/core/src/config/mod.rs
vendored
|
|
@ -44,8 +44,6 @@ use codex_config::types::OtelConfig;
|
|||
use codex_config::types::OtelConfigToml;
|
||||
use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::ShellEnvironmentPolicy;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
use codex_config::types::TuiNotificationSettings;
|
||||
use codex_config::types::UriBasedFileOpener;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
|
|
@ -606,9 +604,6 @@ pub struct Config {
|
|||
/// Defaults to `true`.
|
||||
pub feedback_enabled: bool,
|
||||
|
||||
/// Configured discoverable tools for tool suggestions.
|
||||
pub tool_suggest: ToolSuggestConfig,
|
||||
|
||||
/// OTEL configuration (exporter type, endpoint, headers, etc.).
|
||||
pub otel: codex_config::types::OtelConfig,
|
||||
}
|
||||
|
|
@ -1249,28 +1244,6 @@ pub struct AgentRoleConfig {
|
|||
pub nickname_candidates: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig {
|
||||
let discoverables = config_toml
|
||||
.tool_suggest
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|tool_suggest| tool_suggest.discoverables.iter())
|
||||
.filter_map(|discoverable| {
|
||||
let trimmed = discoverable.id.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ToolSuggestDiscoverable {
|
||||
kind: discoverable.kind,
|
||||
id: trimmed.to_string(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
ToolSuggestConfig { discoverables }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PermissionConfigSyntax {
|
||||
Legacy,
|
||||
|
|
@ -1625,7 +1598,6 @@ impl Config {
|
|||
.clone(),
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg);
|
||||
let feature_overrides = FeatureOverrides {
|
||||
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||
web_search_request: override_tools_web_search_request,
|
||||
|
|
@ -2389,7 +2361,6 @@ impl Config {
|
|||
.as_ref()
|
||||
.and_then(|feedback| feedback.enabled)
|
||||
.unwrap_or(true),
|
||||
tool_suggest,
|
||||
tui_notifications: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ use codex_config::types::AppToolConfig;
|
|||
use codex_config::types::AppToolsConfig;
|
||||
use codex_config::types::AppsDefaultConfig;
|
||||
use codex_connectors::filter::filter_disallowed_connectors;
|
||||
use codex_connectors::filter::filter_tool_suggest_discoverable_connectors;
|
||||
use codex_connectors::merge::merge_connectors;
|
||||
use codex_connectors::merge::plugin_connector_to_app_info;
|
||||
use codex_connectors::metadata::connector_install_url;
|
||||
|
|
@ -1084,98 +1083,3 @@ fn first_party_chat_originator_filters_target_and_openai_prefixed_connectors() {
|
|||
vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c")]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "connector", id = "connector_2128aebfecb84f64a069897515042a44" },
|
||||
{ type = "plugin", id = "slack@openai-curated" },
|
||||
{ type = "connector", id = " " }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
|
||||
assert_eq!(
|
||||
tool_suggest_connector_ids(&config).await,
|
||||
HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() {
|
||||
let filtered = filter_tool_suggest_discoverable_connectors(
|
||||
vec![
|
||||
named_app(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
),
|
||||
named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"),
|
||||
named_app("connector_other", "Other"),
|
||||
],
|
||||
&[AppInfo {
|
||||
is_accessible: true,
|
||||
..named_app(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
)
|
||||
}],
|
||||
&HashSet::from([
|
||||
"connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
"connector_68df038e0ba48191908c8434991bbac2".to_string(),
|
||||
]),
|
||||
"codex_cli",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
filtered,
|
||||
vec![named_app(
|
||||
"connector_68df038e0ba48191908c8434991bbac2",
|
||||
"Gmail",
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tool_suggest_discoverable_connectors_excludes_accessible_apps_even_when_disabled() {
|
||||
let filtered = filter_tool_suggest_discoverable_connectors(
|
||||
vec![
|
||||
named_app(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
),
|
||||
named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail"),
|
||||
],
|
||||
&[
|
||||
AppInfo {
|
||||
is_accessible: true,
|
||||
..named_app(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
)
|
||||
},
|
||||
AppInfo {
|
||||
is_accessible: true,
|
||||
is_enabled: false,
|
||||
..named_app("connector_68df038e0ba48191908c8434991bbac2", "Gmail")
|
||||
},
|
||||
],
|
||||
&HashSet::from([
|
||||
"connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
"connector_68df038e0ba48191908c8434991bbac2".to_string(),
|
||||
]),
|
||||
"codex_cli",
|
||||
);
|
||||
|
||||
assert_eq!(filtered, Vec::<AppInfo>::new());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ use crate::context::TurnAborted;
|
|||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::skills::build_available_skills;
|
||||
use crate::skills::SkillRenderSideEffects;
|
||||
use crate::skills::build_available_skills;
|
||||
use crate::skills::render::SkillMetadataBudget;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_core_skills::SkillMetadata;
|
||||
use codex_core_skills::SkillsLoadInput;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
|
|
@ -751,7 +751,6 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
|
|||
deferred_mcp_tools: None,
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
));
|
||||
|
|
@ -6906,7 +6905,6 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
|
|||
mcp_tools: Some(tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -907,7 +907,6 @@ pub(crate) async fn built_tools(
|
|||
deferred_mcp_tools,
|
||||
unavailable_called_tools,
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn_context.dynamic_tools.as_slice(),
|
||||
},
|
||||
)))
|
||||
|
|
|
|||
|
|
@ -293,7 +293,6 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
|||
mcp_tools: Some(listed_mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1603,7 +1603,6 @@ impl JsReplManager {
|
|||
// JS REPL dispatches nested tool calls directly, not through
|
||||
// `ToolCallRuntime`'s parallel scheduling lock.
|
||||
parallel_mcp_server_names: std::collections::HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::tools::context::ToolPayload;
|
|||
use crate::tools::registry::AnyToolResult;
|
||||
use crate::tools::registry::ToolArgumentDiffConsumer;
|
||||
use crate::tools::registry::ToolRegistry;
|
||||
use crate::tools::spec::build_specs_with_discoverable_tools;
|
||||
use crate::tools::spec::build_specs;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::LocalShellAction;
|
||||
|
|
@ -16,7 +16,6 @@ use codex_protocol::models::ResponseItem;
|
|||
use codex_protocol::models::SearchToolCallParams;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
|
@ -48,7 +47,6 @@ pub(crate) struct ToolRouterParams<'a> {
|
|||
pub(crate) deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
pub(crate) unavailable_called_tools: Vec<ToolName>,
|
||||
pub(crate) parallel_mcp_server_names: HashSet<String>,
|
||||
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
|
||||
}
|
||||
|
||||
|
|
@ -59,15 +57,13 @@ impl ToolRouter {
|
|||
deferred_mcp_tools,
|
||||
unavailable_called_tools,
|
||||
parallel_mcp_server_names,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
} = params;
|
||||
let builder = build_specs_with_discoverable_tools(
|
||||
let builder = build_specs(
|
||||
config,
|
||||
mcp_tools,
|
||||
deferred_mcp_tools,
|
||||
unavailable_called_tools,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
);
|
||||
let (specs, registry) = builder.build();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
|
|||
mcp_tools: Some(mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
@ -99,7 +98,6 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()>
|
|||
mcp_tools: Some(mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
@ -147,7 +145,6 @@ async fn js_repl_tools_only_blocks_namespaced_js_repl_tool() -> anyhow::Result<(
|
|||
mcp_tools: None,
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
@ -203,7 +200,6 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
|
|||
mcp_tools: Some(mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
@ -276,7 +272,6 @@ async fn mcp_parallel_support_uses_exact_payload_server() -> anyhow::Result<()>
|
|||
mcp_tools: None,
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::from(["echo".to_string()]),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
6
vendor/codex/codex-rs/core/src/tools/spec.rs
vendored
6
vendor/codex/codex-rs/core/src/tools/spec.rs
vendored
|
|
@ -7,7 +7,6 @@ use crate::tools::registry::ToolRegistryBuilder;
|
|||
use codex_mcp::ToolInfo;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_tools::AdditionalProperties;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ToolHandlerKind;
|
||||
|
|
@ -67,12 +66,11 @@ fn map_mcp_tools_for_plan(mcp_tools: &HashMap<String, ToolInfo>) -> McpToolPlanI
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_specs_with_discoverable_tools(
|
||||
pub(crate) fn build_specs(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
deferred_mcp_tools: Option<HashMap<String, ToolInfo>>,
|
||||
unavailable_called_tools: Vec<ToolName>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
use crate::tools::handlers::ApplyPatchHandler;
|
||||
|
|
@ -133,7 +131,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
|||
tool_namespaces: mcp_tool_plan_inputs
|
||||
.as_ref()
|
||||
.map(|inputs| &inputs.tool_namespaces),
|
||||
discoverable_tools: discoverable_tools.as_deref(),
|
||||
dynamic_tools,
|
||||
default_agent_type_description: &default_agent_type_description,
|
||||
wait_agent_timeouts: WaitAgentTimeoutOptions {
|
||||
|
|
@ -269,7 +266,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
|||
builder.register_handler(handler.name, tool_search_handler.clone());
|
||||
}
|
||||
}
|
||||
ToolHandlerKind::ToolSuggest => {}
|
||||
ToolHandlerKind::UnifiedExec => {
|
||||
builder.register_handler(handler.name, unified_exec_handler.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use crate::shell::ShellType;
|
|||
use crate::test_support::construct_model_info_offline;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::router::ToolRouterParams;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
|
|
@ -17,13 +16,11 @@ use codex_protocol::protocol::SandboxPolicy;
|
|||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_tools::AdditionalProperties;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ShellCommandBackendConfig;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolsConfig;
|
||||
|
|
@ -87,25 +84,6 @@ fn mcp_tool_info_with_display_name(display_name: &str, tool: rmcp::model::Tool)
|
|||
}
|
||||
}
|
||||
|
||||
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
||||
let slug = name.replace(' ', "-").to_lowercase();
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: Some(description.to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn search_capable_model_info() -> ModelInfo {
|
||||
let config = test_config().await;
|
||||
let mut model_info = construct_model_info_offline("gpt-5.4", &config);
|
||||
|
|
@ -284,12 +262,11 @@ fn build_specs_with_unavailable_tools(
|
|||
unavailable_called_tools: Vec<ToolName>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
build_specs_with_discoverable_tools(
|
||||
build_specs(
|
||||
config,
|
||||
mcp_tools,
|
||||
deferred_mcp_tools,
|
||||
unavailable_called_tools,
|
||||
/*discoverable_tools*/ None,
|
||||
dynamic_tools,
|
||||
)
|
||||
}
|
||||
|
|
@ -375,7 +352,6 @@ async fn assert_model_tools(
|
|||
deferred_mcp_tools: None,
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: std::collections::HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: &[],
|
||||
},
|
||||
);
|
||||
|
|
@ -780,53 +756,6 @@ async fn spawn_agent_description_uses_configured_usage_hint_text() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_requires_apps_and_plugins_features() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
let discoverable_tools = Some(vec![discoverable_connector(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
"Plan events and schedules.",
|
||||
)]);
|
||||
let available_models = Vec::new();
|
||||
|
||||
for disabled_feature in [Feature::Apps, Feature::Plugins] {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ToolSearch);
|
||||
features.enable(Feature::ToolSuggest);
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::Plugins);
|
||||
features.disable(disabled_feature);
|
||||
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs_with_discoverable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
Vec::new(),
|
||||
discoverable_tools.clone(),
|
||||
&[],
|
||||
)
|
||||
.build();
|
||||
|
||||
assert!(
|
||||
!tools
|
||||
.iter()
|
||||
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME),
|
||||
"tool_suggest should be absent when {disabled_feature:?} is disabled"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_tool_description_handles_no_enabled_mcp_tools() {
|
||||
let model_info = search_capable_model_info().await;
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# Tool suggestion discovery
|
||||
|
||||
Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.
|
||||
|
||||
Use this ONLY when:
|
||||
- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.
|
||||
- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.
|
||||
- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.
|
||||
|
||||
Tool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.
|
||||
|
||||
Discoverable tools:
|
||||
{{discoverable_tools}}
|
||||
|
||||
Workflow:
|
||||
|
||||
1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.
|
||||
2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.
|
||||
3. If one tool clearly fits, call `tool_suggest` with:
|
||||
- `tool_type`: `connector` or `plugin`
|
||||
- `action_type`: `install` or `enable`
|
||||
- `tool_id`: exact id from the discoverable tools list above
|
||||
- `suggest_reason`: concise one-line user-facing reason this tool can help with the current request
|
||||
4. After the suggestion flow completes:
|
||||
- if the user finished the install or enable flow, continue by searching again or using the newly available tool
|
||||
- if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks you to.
|
||||
|
|
@ -96,7 +96,6 @@ mod stream_no_completed;
|
|||
mod subagent_notifications;
|
||||
mod tool_harness;
|
||||
mod tool_parallelism;
|
||||
mod tool_suggest;
|
||||
mod tools;
|
||||
mod truncation;
|
||||
mod turn_state;
|
||||
|
|
|
|||
|
|
@ -1,143 +0,0 @@
|
|||
#![cfg(not(target_os = "windows"))]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_core::config::Config;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use serde_json::Value;
|
||||
|
||||
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
|
||||
const DISCOVERABLE_GMAIL_ID: &str = "connector_68df038e0ba48191908c8434991bbac2";
|
||||
|
||||
fn tool_names(body: &Value) -> Vec<String> {
|
||||
body.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.map(|tools| {
|
||||
tools
|
||||
.iter()
|
||||
.filter_map(|tool| {
|
||||
tool.get("name")
|
||||
.or_else(|| tool.get("type"))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn function_tool_description(body: &Value, name: &str) -> Option<String> {
|
||||
body.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|tools| {
|
||||
tools.iter().find_map(|tool| {
|
||||
if tool.get("name").and_then(Value::as_str) == Some(name) {
|
||||
tool.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_apps_without_search_tool(config: &mut Config, apps_base_url: &str) {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Plugins)
|
||||
.expect("test config should allow feature update");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::ToolSuggest)
|
||||
.expect("test config should allow feature update");
|
||||
let mut model_catalog = bundled_models_response()
|
||||
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
|
||||
let model = model_catalog
|
||||
.models
|
||||
.iter_mut()
|
||||
.find(|model| model.slug == "gpt-5.4")
|
||||
.expect("gpt-5.4 exists in bundled models.json");
|
||||
config.chatgpt_base_url = apps_base_url.to_string();
|
||||
config.model = Some("gpt-5.4".to_string());
|
||||
config.tool_suggest.discoverables = vec![ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: DISCOVERABLE_GMAIL_ID.to_string(),
|
||||
}];
|
||||
model.supports_search_tool = false;
|
||||
config.model_catalog = Some(model_catalog);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn tool_suggest_is_available_without_search_tool_after_discovery_attempts() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let apps_server = AppsTestServer::mount(&server).await?;
|
||||
let mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
configure_apps_without_search_tool(config, apps_server.chatgpt_base_url.as_str())
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn_with_policies(
|
||||
"list tools",
|
||||
AskForApproval::Never,
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let body = mock.single_request().body_json();
|
||||
let tools = tool_names(&body);
|
||||
assert!(
|
||||
!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
|
||||
"tools list should not include {TOOL_SEARCH_TOOL_NAME}: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
tools.iter().any(|name| name == TOOL_SUGGEST_TOOL_NAME),
|
||||
"tools list should include {TOOL_SUGGEST_TOOL_NAME}: {tools:?}"
|
||||
);
|
||||
|
||||
let description =
|
||||
function_tool_description(&body, TOOL_SUGGEST_TOOL_NAME).expect("description");
|
||||
assert!(
|
||||
description.contains(
|
||||
"You've already tried to find a matching available tool for the user's request"
|
||||
)
|
||||
);
|
||||
assert!(description.contains("This includes `tool_search` (if available) and other means."));
|
||||
assert!(!description.contains("tool_search fails to find a good match"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
8
vendor/codex/codex-rs/features/src/lib.rs
vendored
8
vendor/codex/codex-rs/features/src/lib.rs
vendored
|
|
@ -150,8 +150,6 @@ pub enum Feature {
|
|||
ToolSearchAlwaysDeferMcpTools,
|
||||
/// Expose placeholder tools for unavailable historical tool calls.
|
||||
UnavailableDummyTools,
|
||||
/// Enable discoverable tool suggestions for apps.
|
||||
ToolSuggest,
|
||||
/// Enable plugins.
|
||||
Plugins,
|
||||
/// Allow the in-app browser pane in desktop apps.
|
||||
|
|
@ -833,12 +831,6 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ToolSuggest,
|
||||
key: "tool_suggest",
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Plugins,
|
||||
key: "plugins",
|
||||
|
|
|
|||
6
vendor/codex/codex-rs/features/src/tests.rs
vendored
6
vendor/codex/codex-rs/features/src/tests.rs
vendored
|
|
@ -129,12 +129,6 @@ fn request_permissions_tool_is_under_development() {
|
|||
assert_eq!(Feature::RequestPermissionsTool.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolSuggest.stage(), Stage::Stable);
|
||||
assert_eq!(Feature::ToolSuggest.default_enabled(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolSearch.stage(), Stage::Stable);
|
||||
|
|
|
|||
4
vendor/codex/codex-rs/tools/README.md
vendored
4
vendor/codex/codex-rs/tools/README.md
vendored
|
|
@ -27,8 +27,8 @@ schema and Responses API tool primitives that no longer need to live in
|
|||
- local host tool spec builders for shell/exec/request-permissions/view-image
|
||||
- collaboration `ToolSpec` builders for spawn/send/wait/close,
|
||||
`request_user_input`, and CSV fanout/reporting
|
||||
- discoverable-tool models, client filtering, and `ToolSpec` builders for
|
||||
`tool_search` and `tool_suggest`
|
||||
- deferred MCP/dynamic discovery models and `ToolSpec` builders for
|
||||
`tool_search`
|
||||
- `parse_tool_input_schema()`
|
||||
- `parse_dynamic_tool()`
|
||||
- `parse_mcp_tool()`
|
||||
|
|
|
|||
17
vendor/codex/codex-rs/tools/src/lib.rs
vendored
17
vendor/codex/codex-rs/tools/src/lib.rs
vendored
|
|
@ -20,7 +20,6 @@ mod tool_discovery;
|
|||
mod tool_registry_plan;
|
||||
mod tool_registry_plan_types;
|
||||
mod tool_spec;
|
||||
mod tool_suggest;
|
||||
mod utility_tool;
|
||||
mod view_image;
|
||||
|
||||
|
|
@ -98,22 +97,13 @@ pub use tool_config::ToolsConfigParams;
|
|||
pub use tool_config::UnifiedExecShellMode;
|
||||
pub use tool_config::ZshForkConfig;
|
||||
pub use tool_definition::ToolDefinition;
|
||||
pub use tool_discovery::DiscoverablePluginInfo;
|
||||
pub use tool_discovery::DiscoverableTool;
|
||||
pub use tool_discovery::DiscoverableToolAction;
|
||||
pub use tool_discovery::DiscoverableToolType;
|
||||
pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
|
||||
pub use tool_discovery::TOOL_SUGGEST_TOOL_NAME;
|
||||
pub use tool_discovery::ToolSearchResultSource;
|
||||
pub use tool_discovery::ToolSearchSource;
|
||||
pub use tool_discovery::ToolSearchSourceInfo;
|
||||
pub use tool_discovery::ToolSuggestEntry;
|
||||
pub use tool_discovery::collect_tool_search_source_infos;
|
||||
pub use tool_discovery::collect_tool_suggest_entries;
|
||||
pub use tool_discovery::create_tool_search_tool;
|
||||
pub use tool_discovery::create_tool_suggest_tool;
|
||||
pub use tool_discovery::filter_tool_suggest_discoverable_tools_for_client;
|
||||
pub use tool_discovery::tool_search_result_source_to_loadable_tool_spec;
|
||||
pub use tool_registry_plan::build_tool_registry_plan;
|
||||
pub use tool_registry_plan_types::ToolHandlerKind;
|
||||
|
|
@ -132,13 +122,6 @@ pub use tool_spec::create_image_generation_tool;
|
|||
pub use tool_spec::create_local_shell_tool;
|
||||
pub use tool_spec::create_tools_json_for_responses_api;
|
||||
pub use tool_spec::create_web_search_tool;
|
||||
pub use tool_suggest::TOOL_SUGGEST_APPROVAL_KIND_VALUE;
|
||||
pub use tool_suggest::ToolSuggestArgs;
|
||||
pub use tool_suggest::ToolSuggestMeta;
|
||||
pub use tool_suggest::ToolSuggestResult;
|
||||
pub use tool_suggest::all_suggested_connectors_picked_up;
|
||||
pub use tool_suggest::build_tool_suggestion_elicitation_request;
|
||||
pub use tool_suggest::verified_connector_suggestion_completed;
|
||||
pub use utility_tool::create_list_dir_tool;
|
||||
pub use utility_tool::create_test_sync_tool;
|
||||
pub use view_image::ViewImageToolOptions;
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ pub struct ToolsConfig {
|
|||
pub web_search_tool_type: WebSearchToolType,
|
||||
pub image_gen_tool: bool,
|
||||
pub search_tool: bool,
|
||||
pub tool_suggest: bool,
|
||||
pub exec_permission_approvals_enabled: bool,
|
||||
pub request_permissions_tool_enabled: bool,
|
||||
pub code_mode_enabled: bool,
|
||||
|
|
@ -146,9 +145,6 @@ impl ToolsConfig {
|
|||
features.enabled(Feature::DefaultModeRequestUserInput);
|
||||
let include_search_tool =
|
||||
model_info.supports_search_tool && features.enabled(Feature::ToolSearch);
|
||||
// Epiphany keeps Codex for subscription auth/model routing, not for
|
||||
// app/plugin marketplace discovery inside model turns.
|
||||
let include_tool_suggest = false;
|
||||
let include_original_image_detail = can_request_original_image_detail(model_info);
|
||||
// API-key auth bypasses Codex backend entitlement/tool normalization, so
|
||||
// callers must confirm ChatGPT auth before exposing the built-in tool.
|
||||
|
|
@ -204,7 +200,6 @@ impl ToolsConfig {
|
|||
web_search_tool_type: model_info.web_search_tool_type,
|
||||
image_gen_tool: include_image_gen_tool,
|
||||
search_tool: include_search_tool,
|
||||
tool_suggest: include_tool_suggest,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
code_mode_enabled: include_code_mode,
|
||||
|
|
|
|||
261
vendor/codex/codex-rs/tools/src/tool_discovery.rs
vendored
261
vendor/codex/codex-rs/tools/src/tool_discovery.rs
vendored
|
|
@ -2,20 +2,14 @@ use crate::JsonSchema;
|
|||
use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolName;
|
||||
use crate::ToolSpec;
|
||||
use crate::default_namespace_description;
|
||||
use crate::mcp_tool_to_deferred_responses_api_tool;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const TUI_CLIENT_NAME: &str = "codex-tui";
|
||||
pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8;
|
||||
pub const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ToolSearchSourceInfo {
|
||||
|
|
@ -40,112 +34,6 @@ pub struct ToolSearchResultSource<'a> {
|
|||
pub connector_description: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiscoverableToolType {
|
||||
Connector,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
impl DiscoverableToolType {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Connector => "connector",
|
||||
Self::Plugin => "plugin",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiscoverableToolAction {
|
||||
Install,
|
||||
Enable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DiscoverableTool {
|
||||
Connector(Box<AppInfo>),
|
||||
Plugin(Box<DiscoverablePluginInfo>),
|
||||
}
|
||||
|
||||
impl DiscoverableTool {
|
||||
pub fn tool_type(&self) -> DiscoverableToolType {
|
||||
match self {
|
||||
Self::Connector(_) => DiscoverableToolType::Connector,
|
||||
Self::Plugin(_) => DiscoverableToolType::Plugin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::Connector(connector) => connector.id.as_str(),
|
||||
Self::Plugin(plugin) => plugin.id.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Connector(connector) => connector.name.as_str(),
|
||||
Self::Plugin(plugin) => plugin.name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_url(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Connector(connector) => connector.install_url.as_deref(),
|
||||
Self::Plugin(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AppInfo> for DiscoverableTool {
|
||||
fn from(value: AppInfo) -> Self {
|
||||
Self::Connector(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiscoverablePluginInfo> for DiscoverableTool {
|
||||
fn from(value: DiscoverablePluginInfo) -> Self {
|
||||
Self::Plugin(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_tool_suggest_discoverable_tools_for_client(
|
||||
discoverable_tools: Vec<DiscoverableTool>,
|
||||
app_server_client_name: Option<&str>,
|
||||
) -> Vec<DiscoverableTool> {
|
||||
if app_server_client_name != Some(TUI_CLIENT_NAME) {
|
||||
return discoverable_tools;
|
||||
}
|
||||
|
||||
discoverable_tools
|
||||
.into_iter()
|
||||
.filter(|tool| !matches!(tool, DiscoverableTool::Plugin(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DiscoverablePluginInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub has_skills: bool,
|
||||
pub mcp_server_names: Vec<String>,
|
||||
pub app_connector_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ToolSuggestEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub tool_type: DiscoverableToolType,
|
||||
pub has_skills: bool,
|
||||
pub mcp_server_names: Vec<String>,
|
||||
pub app_connector_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn create_tool_search_tool(
|
||||
searchable_sources: &[ToolSearchSourceInfo],
|
||||
default_limit: usize,
|
||||
|
|
@ -271,155 +159,6 @@ pub fn collect_tool_search_source_infos<'a>(
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> ToolSpec {
|
||||
let discoverable_tool_ids = discoverable_tools
|
||||
.iter()
|
||||
.map(|tool| tool.id.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"tool_type".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"action_type".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Suggested action for the tool. Use \"install\" or \"enable\".".to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"tool_id".to_string(),
|
||||
JsonSchema::string(Some(format!(
|
||||
"Connector or plugin id to suggest. Must be one of: {discoverable_tool_ids}."
|
||||
))),
|
||||
),
|
||||
(
|
||||
"suggest_reason".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Concise one-line user-facing reason why this tool can help with the current request."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
]);
|
||||
|
||||
let discoverable_tools = format_discoverable_tools(discoverable_tools);
|
||||
let description = format!(
|
||||
"# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `{TOOL_SEARCH_TOOL_NAME}` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n{discoverable_tools}\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it."
|
||||
);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: TOOL_SUGGEST_TOOL_NAME.to_string(),
|
||||
description,
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
properties,
|
||||
Some(vec![
|
||||
"tool_type".to_string(),
|
||||
"action_type".to_string(),
|
||||
"tool_id".to_string(),
|
||||
"suggest_reason".to_string(),
|
||||
]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_tool_suggest_entries(
|
||||
discoverable_tools: &[DiscoverableTool],
|
||||
) -> Vec<ToolSuggestEntry> {
|
||||
discoverable_tools
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
DiscoverableTool::Connector(connector) => ToolSuggestEntry {
|
||||
id: connector.id.clone(),
|
||||
name: connector.name.clone(),
|
||||
description: connector.description.clone(),
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
},
|
||||
DiscoverableTool::Plugin(plugin) => ToolSuggestEntry {
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
description: plugin.description.clone(),
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
has_skills: plugin.has_skills,
|
||||
mcp_server_names: plugin.mcp_server_names.clone(),
|
||||
app_connector_ids: plugin.app_connector_ids.clone(),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn format_discoverable_tools(discoverable_tools: &[ToolSuggestEntry]) -> String {
|
||||
let mut discoverable_tools = discoverable_tools.to_vec();
|
||||
discoverable_tools.sort_by(|left, right| {
|
||||
left.name
|
||||
.cmp(&right.name)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
|
||||
discoverable_tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let description = tool_description_or_fallback(&tool);
|
||||
format!(
|
||||
"- {} (id: `{}`, type: {}, action: install): {}",
|
||||
tool.name,
|
||||
tool.id,
|
||||
tool.tool_type.as_str(),
|
||||
description
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn tool_description_or_fallback(tool: &ToolSuggestEntry) -> String {
|
||||
if let Some(description) = tool
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
{
|
||||
return description.to_string();
|
||||
}
|
||||
|
||||
match tool.tool_type {
|
||||
DiscoverableToolType::Connector => "No description provided.".to_string(),
|
||||
DiscoverableToolType::Plugin => plugin_summary(tool),
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_summary(tool: &ToolSuggestEntry) -> String {
|
||||
let mut details = Vec::new();
|
||||
if tool.has_skills {
|
||||
details.push("skills".to_string());
|
||||
}
|
||||
if !tool.mcp_server_names.is_empty() {
|
||||
details.push(format!("MCP servers: {}", tool.mcp_server_names.join(", ")));
|
||||
}
|
||||
if !tool.app_connector_ids.is_empty() {
|
||||
details.push(format!(
|
||||
"app connectors: {}",
|
||||
tool.app_connector_ids.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
if details.is_empty() {
|
||||
"No description provided.".to_string()
|
||||
} else {
|
||||
details.join("; ")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tool_discovery_tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
use super::*;
|
||||
use crate::JsonSchema;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
|
|
@ -47,133 +45,3 @@ fn create_tool_search_tool_deduplicates_and_renders_enabled_sources() {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_tool_suggest_tool_uses_plugin_summary_fallback() {
|
||||
assert_eq!(
|
||||
create_tool_suggest_tool(&[
|
||||
ToolSuggestEntry {
|
||||
id: "slack@openai-curated".to_string(),
|
||||
name: "Slack".to_string(),
|
||||
description: None,
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
},
|
||||
ToolSuggestEntry {
|
||||
id: "github".to_string(),
|
||||
name: "GitHub".to_string(),
|
||||
description: None,
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["github-mcp".to_string()],
|
||||
app_connector_ids: vec!["github-app".to_string()],
|
||||
},
|
||||
]),
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "tool_suggest".to_string(),
|
||||
description: "# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n- GitHub (id: `github`, type: plugin, action: install): skills; MCP servers: github-mcp; app connectors: github-app\n- Slack (id: `slack@openai-curated`, type: connector, action: install): No description provided.\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `tool_suggest` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(BTreeMap::from([
|
||||
(
|
||||
"action_type".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Suggested action for the tool. Use \"install\" or \"enable\"."
|
||||
.to_string(),
|
||||
),),
|
||||
),
|
||||
(
|
||||
"suggest_reason".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Concise one-line user-facing reason why this tool can help with the current request."
|
||||
.to_string(),
|
||||
),),
|
||||
),
|
||||
(
|
||||
"tool_id".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Connector or plugin id to suggest. Must be one of: slack@openai-curated, github."
|
||||
.to_string(),
|
||||
),),
|
||||
),
|
||||
(
|
||||
"tool_type".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"."
|
||||
.to_string(),
|
||||
),),
|
||||
),
|
||||
]), Some(vec![
|
||||
"tool_type".to_string(),
|
||||
"action_type".to_string(),
|
||||
"tool_id".to_string(),
|
||||
"suggest_reason".to_string(),
|
||||
]), Some(false.into())),
|
||||
output_schema: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discoverable_tool_enums_use_expected_wire_names() {
|
||||
assert_eq!(
|
||||
json!({
|
||||
"tool_type": DiscoverableToolType::Connector,
|
||||
"action_type": DiscoverableToolAction::Install,
|
||||
}),
|
||||
json!({
|
||||
"tool_type": "connector",
|
||||
"action_type": "install",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tool_suggest_discoverable_tools_for_codex_tui_omits_plugins() {
|
||||
let discoverable_tools = vec![
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: "connector_google_calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://example.test/google-calendar".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
})),
|
||||
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
||||
id: "slack@openai-curated".to_string(),
|
||||
name: "Slack".to_string(),
|
||||
description: Some("Search Slack messages".to_string()),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["slack".to_string()],
|
||||
app_connector_ids: vec!["connector_slack".to_string()],
|
||||
})),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
filter_tool_suggest_discoverable_tools_for_client(discoverable_tools, Some("codex-tui"),),
|
||||
vec![DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: "connector_google_calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://example.test/google-calendar".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}))]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use crate::ShellToolOptions;
|
|||
use crate::SpawnAgentToolOptions;
|
||||
use crate::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use crate::TOOL_SEARCH_TOOL_NAME;
|
||||
use crate::TOOL_SUGGEST_TOOL_NAME;
|
||||
use crate::ToolHandlerKind;
|
||||
use crate::ToolName;
|
||||
use crate::ToolRegistryPlan;
|
||||
|
|
@ -20,7 +19,6 @@ use crate::WebSearchToolOptions;
|
|||
use crate::coalesce_loadable_tool_specs;
|
||||
use crate::collect_code_mode_exec_prompt_tool_definitions;
|
||||
use crate::collect_tool_search_source_infos;
|
||||
use crate::collect_tool_suggest_entries;
|
||||
use crate::create_apply_patch_freeform_tool;
|
||||
use crate::create_apply_patch_json_tool;
|
||||
use crate::create_close_agent_tool_v1;
|
||||
|
|
@ -48,7 +46,6 @@ use crate::create_spawn_agent_tool_v1;
|
|||
use crate::create_spawn_agent_tool_v2;
|
||||
use crate::create_test_sync_tool;
|
||||
use crate::create_tool_search_tool;
|
||||
use crate::create_tool_suggest_tool;
|
||||
use crate::create_update_plan_tool;
|
||||
use crate::create_view_image_tool;
|
||||
use crate::create_wait_agent_tool_v1;
|
||||
|
|
@ -89,13 +86,8 @@ pub fn build_tool_registry_plan(
|
|||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let nested_config = config.for_code_mode_nested_tools();
|
||||
let nested_plan = build_tool_registry_plan(
|
||||
&nested_config,
|
||||
ToolRegistryPlanParams {
|
||||
discoverable_tools: None,
|
||||
..params
|
||||
},
|
||||
);
|
||||
let nested_plan =
|
||||
build_tool_registry_plan(&nested_config, ToolRegistryPlanParams { ..params });
|
||||
let mut enabled_tools = collect_code_mode_exec_prompt_tool_definitions(
|
||||
nested_plan
|
||||
.specs
|
||||
|
|
@ -295,18 +287,6 @@ pub fn build_tool_registry_plan(
|
|||
}
|
||||
}
|
||||
|
||||
if config.tool_suggest
|
||||
&& let Some(discoverable_tools) =
|
||||
params.discoverable_tools.filter(|tools| !tools.is_empty())
|
||||
{
|
||||
plan.push_spec(
|
||||
create_tool_suggest_tool(&collect_tool_suggest_entries(discoverable_tools)),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
/*code_mode_enabled*/ false,
|
||||
);
|
||||
plan.register_handler(TOOL_SUGGEST_TOOL_NAME, ToolHandlerKind::ToolSuggest);
|
||||
}
|
||||
|
||||
if config.has_environment
|
||||
&& let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
use super::*;
|
||||
use crate::AdditionalProperties;
|
||||
use crate::ConfiguredToolSpec;
|
||||
use crate::DiscoverablePluginInfo;
|
||||
use crate::DiscoverableTool;
|
||||
use crate::FreeformTool;
|
||||
use crate::JsonSchema;
|
||||
use crate::JsonSchemaPrimitiveType;
|
||||
|
|
@ -19,7 +17,6 @@ use crate::ToolRegistryPlanMcpTool;
|
|||
use crate::ToolsConfigParams;
|
||||
use crate::WaitAgentTimeoutOptions;
|
||||
use crate::mcp_call_tool_result_output_schema;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::config_types::WebSearchConfig;
|
||||
|
|
@ -1442,195 +1439,6 @@ fn search_tool_registers_for_deferred_dynamic_tools() {
|
|||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_is_not_registered_without_feature_flag() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::ToolSearch);
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::Plugins);
|
||||
features.disable(Feature::ToolSuggest);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs_with_discoverable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
Some(vec![discoverable_connector(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
"Plan events and schedules.",
|
||||
)]),
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(
|
||||
!tools
|
||||
.iter()
|
||||
.any(|tool| tool.name() == TOOL_SUGGEST_TOOL_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_can_be_registered_without_search_tool() {
|
||||
let model_info = ModelInfo {
|
||||
supports_search_tool: false,
|
||||
..search_capable_model_info()
|
||||
};
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::Plugins);
|
||||
features.enable(Feature::ToolSuggest);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs_with_discoverable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
Some(vec![discoverable_connector(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
"Plan events and schedules.",
|
||||
)]),
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_contains_tool_names(&tools, &[TOOL_SUGGEST_TOOL_NAME]);
|
||||
assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME);
|
||||
|
||||
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
||||
let ToolSpec::Function(ResponsesApiTool { description, .. }) = &tool_suggest.spec else {
|
||||
panic!("expected function tool");
|
||||
};
|
||||
assert!(description.contains(
|
||||
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
||||
));
|
||||
assert!(description.contains(
|
||||
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_description_lists_discoverable_tools() {
|
||||
let model_info = search_capable_model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Apps);
|
||||
features.enable(Feature::Plugins);
|
||||
features.enable(Feature::ToolSearch);
|
||||
features.enable(Feature::ToolSuggest);
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
|
||||
let discoverable_tools = vec![
|
||||
discoverable_connector(
|
||||
"connector_2128aebfecb84f64a069897515042a44",
|
||||
"Google Calendar",
|
||||
"Plan events and schedules.",
|
||||
),
|
||||
discoverable_connector(
|
||||
"connector_68df038e0ba48191908c8434991bbac2",
|
||||
"Gmail",
|
||||
"Find and summarize email threads.",
|
||||
),
|
||||
DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
||||
id: "sample@test".to_string(),
|
||||
name: "Sample Plugin".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["sample-docs".to_string()],
|
||||
app_connector_ids: vec!["connector_sample".to_string()],
|
||||
})),
|
||||
];
|
||||
|
||||
let (tools, _) = build_specs_with_discoverable_tools(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
Some(discoverable_tools),
|
||||
&[],
|
||||
);
|
||||
|
||||
let tool_suggest = find_tool(&tools, TOOL_SUGGEST_TOOL_NAME);
|
||||
let ToolSpec::Function(ResponsesApiTool {
|
||||
description,
|
||||
parameters,
|
||||
..
|
||||
}) = &tool_suggest.spec
|
||||
else {
|
||||
panic!("expected function tool");
|
||||
};
|
||||
assert!(description.contains(
|
||||
"Suggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin"
|
||||
));
|
||||
assert!(description.contains("Google Calendar"));
|
||||
assert!(description.contains("Gmail"));
|
||||
assert!(description.contains("Sample Plugin"));
|
||||
assert!(description.contains("Plan events and schedules."));
|
||||
assert!(description.contains("Find and summarize email threads."));
|
||||
assert!(description.contains("id: `sample@test`, type: plugin, action: install"));
|
||||
assert!(description.contains("`action_type`: `install` or `enable`"));
|
||||
assert!(
|
||||
description.contains("skills; MCP servers: sample-docs; app connectors: connector_sample")
|
||||
);
|
||||
assert!(
|
||||
description.contains(
|
||||
"You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means."
|
||||
)
|
||||
);
|
||||
assert!(description.contains(
|
||||
"For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely."
|
||||
));
|
||||
assert!(description.contains(
|
||||
"For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself."
|
||||
));
|
||||
assert!(description.contains(
|
||||
"Do not suggest a plugin just because one of its connectors or capabilities seems relevant."
|
||||
));
|
||||
assert!(description.contains(
|
||||
"Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard."
|
||||
));
|
||||
assert!(description.contains("DO NOT explore or recommend tools that are not on this list."));
|
||||
assert!(!description.contains("{{discoverable_tools}}"));
|
||||
assert!(!description.contains("tool_search fails to find a good match"));
|
||||
let (_, required) = expect_object_schema(parameters);
|
||||
assert_eq!(
|
||||
required,
|
||||
Some(&vec![
|
||||
"tool_type".to_string(),
|
||||
"action_type".to_string(),
|
||||
"tool_id".to_string(),
|
||||
"suggest_reason".to_string(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
|
||||
let model_info = model_info();
|
||||
|
|
@ -1924,29 +1732,12 @@ fn build_specs<'a>(
|
|||
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
||||
build_specs_with_discoverable_tools(
|
||||
config,
|
||||
mcp_tools,
|
||||
deferred_mcp_tools,
|
||||
/*discoverable_tools*/ None,
|
||||
dynamic_tools,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_specs_with_discoverable_tools<'a>(
|
||||
config: &ToolsConfig,
|
||||
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
||||
build_specs_with_optional_tool_namespaces(
|
||||
config,
|
||||
mcp_tools,
|
||||
deferred_mcp_tools,
|
||||
/*tool_namespaces*/ None,
|
||||
discoverable_tools,
|
||||
dynamic_tools,
|
||||
)
|
||||
}
|
||||
|
|
@ -1956,7 +1747,6 @@ fn build_specs_with_optional_tool_namespaces<'a>(
|
|||
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
|
||||
deferred_mcp_tools: Option<Vec<ToolRegistryPlanDeferredTool<'a>>>,
|
||||
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
|
||||
discoverable_tools: Option<Vec<DiscoverableTool>>,
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> (Vec<ConfiguredToolSpec>, Vec<ToolHandlerSpec>) {
|
||||
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
|
||||
|
|
@ -1974,7 +1764,6 @@ fn build_specs_with_optional_tool_namespaces<'a>(
|
|||
mcp_tools: mcp_tool_inputs.as_deref(),
|
||||
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
|
||||
tool_namespaces: tool_namespaces.as_ref(),
|
||||
discoverable_tools: discoverable_tools.as_deref(),
|
||||
dynamic_tools,
|
||||
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
|
||||
wait_agent_timeouts: wait_agent_timeout_options(),
|
||||
|
|
@ -2069,25 +1858,6 @@ declare const tools: { mcp__sample__echo(args: { message: string; }): Promise<Ca
|
|||
);
|
||||
}
|
||||
|
||||
fn discoverable_connector(id: &str, name: &str, description: &str) -> DiscoverableTool {
|
||||
let slug = name.replace(' ', "-").to_lowercase();
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: Some(description.to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(format!("https://chatgpt.com/apps/{slug}/{id}")),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn deferred_mcp_tool<'a>(
|
||||
tool_name: &'a str,
|
||||
tool_namespace: &'a str,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::ConfiguredToolSpec;
|
||||
use crate::DiscoverableTool;
|
||||
use crate::ToolName;
|
||||
use crate::ToolSpec;
|
||||
use crate::ToolsConfig;
|
||||
|
|
@ -35,7 +34,6 @@ pub enum ToolHandlerKind {
|
|||
SpawnAgentV2,
|
||||
TestSync,
|
||||
ToolSearch,
|
||||
ToolSuggest,
|
||||
UnifiedExec,
|
||||
ViewImage,
|
||||
WaitAgentV1,
|
||||
|
|
@ -59,7 +57,6 @@ pub struct ToolRegistryPlanParams<'a> {
|
|||
pub mcp_tools: Option<&'a [ToolRegistryPlanMcpTool<'a>]>,
|
||||
pub deferred_mcp_tools: Option<&'a [ToolRegistryPlanDeferredTool<'a>]>,
|
||||
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
|
||||
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
|
||||
pub dynamic_tools: &'a [DynamicToolSpec],
|
||||
pub default_agent_type_description: &'a str,
|
||||
pub wait_agent_timeouts: WaitAgentTimeoutOptions,
|
||||
|
|
|
|||
125
vendor/codex/codex-rs/tools/src/tool_suggest.rs
vendored
125
vendor/codex/codex-rs/tools/src/tool_suggest.rs
vendored
|
|
@ -1,125 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::McpElicitationObjectType;
|
||||
use codex_app_server_protocol::McpElicitationSchema;
|
||||
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::DiscoverableTool;
|
||||
use crate::DiscoverableToolAction;
|
||||
use crate::DiscoverableToolType;
|
||||
|
||||
pub const TOOL_SUGGEST_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ToolSuggestArgs {
|
||||
pub tool_type: DiscoverableToolType,
|
||||
pub action_type: DiscoverableToolAction,
|
||||
pub tool_id: String,
|
||||
pub suggest_reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ToolSuggestResult {
|
||||
pub completed: bool,
|
||||
pub user_confirmed: bool,
|
||||
pub tool_type: DiscoverableToolType,
|
||||
pub action_type: DiscoverableToolAction,
|
||||
pub tool_id: String,
|
||||
pub tool_name: String,
|
||||
pub suggest_reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ToolSuggestMeta<'a> {
|
||||
pub codex_approval_kind: &'static str,
|
||||
pub tool_type: DiscoverableToolType,
|
||||
pub suggest_type: DiscoverableToolAction,
|
||||
pub suggest_reason: &'a str,
|
||||
pub tool_id: &'a str,
|
||||
pub tool_name: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub install_url: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub fn build_tool_suggestion_elicitation_request(
|
||||
server_name: &str,
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
args: &ToolSuggestArgs,
|
||||
suggest_reason: &str,
|
||||
tool: &DiscoverableTool,
|
||||
) -> McpServerElicitationRequestParams {
|
||||
let tool_name = tool.name().to_string();
|
||||
let install_url = tool.install_url().map(ToString::to_string);
|
||||
let message = suggest_reason.to_string();
|
||||
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id,
|
||||
turn_id: Some(turn_id),
|
||||
server_name: server_name.to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(json!(build_tool_suggestion_meta(
|
||||
args.tool_type,
|
||||
args.action_type,
|
||||
suggest_reason,
|
||||
tool.id(),
|
||||
tool_name.as_str(),
|
||||
install_url.as_deref(),
|
||||
))),
|
||||
message,
|
||||
requested_schema: McpElicitationSchema {
|
||||
schema_uri: None,
|
||||
type_: McpElicitationObjectType::Object,
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_suggested_connectors_picked_up(
|
||||
expected_connector_ids: &[String],
|
||||
accessible_connectors: &[AppInfo],
|
||||
) -> bool {
|
||||
expected_connector_ids.iter().all(|connector_id| {
|
||||
verified_connector_suggestion_completed(connector_id, accessible_connectors)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verified_connector_suggestion_completed(
|
||||
tool_id: &str,
|
||||
accessible_connectors: &[AppInfo],
|
||||
) -> bool {
|
||||
accessible_connectors
|
||||
.iter()
|
||||
.find(|connector| connector.id == tool_id)
|
||||
.is_some_and(|connector| connector.is_accessible)
|
||||
}
|
||||
|
||||
fn build_tool_suggestion_meta<'a>(
|
||||
tool_type: DiscoverableToolType,
|
||||
action_type: DiscoverableToolAction,
|
||||
suggest_reason: &'a str,
|
||||
tool_id: &'a str,
|
||||
tool_name: &'a str,
|
||||
install_url: Option<&'a str>,
|
||||
) -> ToolSuggestMeta<'a> {
|
||||
ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
tool_type,
|
||||
suggest_type: action_type,
|
||||
suggest_reason,
|
||||
tool_id,
|
||||
tool_name,
|
||||
install_url,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tool_suggest_tests.rs"]
|
||||
mod tests;
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
use super::*;
|
||||
use crate::DiscoverablePluginInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn build_tool_suggestion_elicitation_request_uses_expected_shape() {
|
||||
let args = ToolSuggestArgs {
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
action_type: DiscoverableToolAction::Install,
|
||||
tool_id: "connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
suggest_reason: "Plan and reference events from your calendar".to_string(),
|
||||
};
|
||||
let connector = DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: "connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some(
|
||||
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
|
||||
.to_string(),
|
||||
),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}));
|
||||
|
||||
let request = build_tool_suggestion_elicitation_request(
|
||||
"codex-apps",
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
&args,
|
||||
"Plan and reference events from your calendar",
|
||||
&connector,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
server_name: "codex-apps".to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(json!(ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Plan and reference events from your calendar",
|
||||
tool_id: "connector_2128aebfecb84f64a069897515042a44",
|
||||
tool_name: "Google Calendar",
|
||||
install_url: Some(
|
||||
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
|
||||
),
|
||||
})),
|
||||
message: "Plan and reference events from your calendar".to_string(),
|
||||
requested_schema: McpElicitationSchema {
|
||||
schema_uri: None,
|
||||
type_: McpElicitationObjectType::Object,
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tool_suggestion_elicitation_request_for_plugin_omits_install_url() {
|
||||
let args = ToolSuggestArgs {
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
action_type: DiscoverableToolAction::Install,
|
||||
tool_id: "sample@openai-curated".to_string(),
|
||||
suggest_reason: "Use the sample plugin's skills and MCP server".to_string(),
|
||||
};
|
||||
let plugin = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
||||
id: "sample@openai-curated".to_string(),
|
||||
name: "Sample Plugin".to_string(),
|
||||
description: Some("Includes skills, MCP servers, and apps.".to_string()),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["sample-docs".to_string()],
|
||||
app_connector_ids: vec!["connector_calendar".to_string()],
|
||||
}));
|
||||
|
||||
let request = build_tool_suggestion_elicitation_request(
|
||||
"codex-apps",
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
&args,
|
||||
"Use the sample plugin's skills and MCP server",
|
||||
&plugin,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
McpServerElicitationRequestParams {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
server_name: "codex-apps".to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(json!(ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Use the sample plugin's skills and MCP server",
|
||||
tool_id: "sample@openai-curated",
|
||||
tool_name: "Sample Plugin",
|
||||
install_url: None,
|
||||
})),
|
||||
message: "Use the sample plugin's skills and MCP server".to_string(),
|
||||
requested_schema: McpElicitationSchema {
|
||||
schema_uri: None,
|
||||
type_: McpElicitationObjectType::Object,
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tool_suggestion_meta_uses_expected_shape() {
|
||||
let meta = build_tool_suggestion_meta(
|
||||
DiscoverableToolType::Connector,
|
||||
DiscoverableToolAction::Install,
|
||||
"Find and reference emails from your inbox",
|
||||
"connector_68df038e0ba48191908c8434991bbac2",
|
||||
"Gmail",
|
||||
Some("https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
meta,
|
||||
ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Find and reference emails from your inbox",
|
||||
tool_id: "connector_68df038e0ba48191908c8434991bbac2",
|
||||
tool_name: "Gmail",
|
||||
install_url: Some(
|
||||
"https://chatgpt.com/apps/gmail/connector_68df038e0ba48191908c8434991bbac2"
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_connector_suggestion_completed_requires_accessible_connector() {
|
||||
let accessible_connectors = vec![AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: false,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert!(verified_connector_suggestion_completed(
|
||||
"calendar",
|
||||
&accessible_connectors,
|
||||
));
|
||||
assert!(!verified_connector_suggestion_completed(
|
||||
"gmail",
|
||||
&accessible_connectors,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_suggested_connectors_picked_up_requires_every_expected_connector() {
|
||||
let accessible_connectors = vec![AppInfo {
|
||||
id: "calendar".to_string(),
|
||||
name: "Google Calendar".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: true,
|
||||
is_enabled: false,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
|
||||
assert!(all_suggested_connectors_picked_up(
|
||||
&["calendar".to_string()],
|
||||
&accessible_connectors,
|
||||
));
|
||||
assert!(!all_suggested_connectors_picked_up(
|
||||
&["calendar".to_string(), "gmail".to_string()],
|
||||
&accessible_connectors,
|
||||
));
|
||||
}
|
||||
|
|
@ -292,9 +292,6 @@ impl App {
|
|||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
suggest_reason: None,
|
||||
suggestion_type: None,
|
||||
elicitation_target: None,
|
||||
});
|
||||
}
|
||||
AppEvent::OpenUrlInBrowser { url } => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::ElicitationAction;
|
||||
use codex_protocol::mcp::RequestId as McpRequestId;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::Op;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
|
@ -24,7 +19,6 @@ use super::scroll_state::ScrollState;
|
|||
use super::selection_popup_common::GenericDisplayRow;
|
||||
use super::selection_popup_common::measure_rows_height;
|
||||
use super::selection_popup_common::render_rows;
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::key_hint;
|
||||
|
|
@ -40,19 +34,6 @@ enum AppLinkScreen {
|
|||
InstallConfirmation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AppLinkSuggestionType {
|
||||
Install,
|
||||
Enable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct AppLinkElicitationTarget {
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) server_name: String,
|
||||
pub(crate) request_id: McpRequestId,
|
||||
}
|
||||
|
||||
pub(crate) struct AppLinkViewParams {
|
||||
pub(crate) app_id: String,
|
||||
pub(crate) title: String,
|
||||
|
|
@ -61,9 +42,6 @@ pub(crate) struct AppLinkViewParams {
|
|||
pub(crate) url: String,
|
||||
pub(crate) is_installed: bool,
|
||||
pub(crate) is_enabled: bool,
|
||||
pub(crate) suggest_reason: Option<String>,
|
||||
pub(crate) suggestion_type: Option<AppLinkSuggestionType>,
|
||||
pub(crate) elicitation_target: Option<AppLinkElicitationTarget>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppLinkView {
|
||||
|
|
@ -74,9 +52,6 @@ pub(crate) struct AppLinkView {
|
|||
url: String,
|
||||
is_installed: bool,
|
||||
is_enabled: bool,
|
||||
suggest_reason: Option<String>,
|
||||
suggestion_type: Option<AppLinkSuggestionType>,
|
||||
elicitation_target: Option<AppLinkElicitationTarget>,
|
||||
app_event_tx: AppEventSender,
|
||||
screen: AppLinkScreen,
|
||||
selected_action: usize,
|
||||
|
|
@ -93,9 +68,6 @@ impl AppLinkView {
|
|||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
suggest_reason,
|
||||
suggestion_type,
|
||||
elicitation_target,
|
||||
} = params;
|
||||
Self {
|
||||
app_id,
|
||||
|
|
@ -105,9 +77,6 @@ impl AppLinkView {
|
|||
url,
|
||||
is_installed,
|
||||
is_enabled,
|
||||
suggest_reason,
|
||||
suggestion_type,
|
||||
elicitation_target,
|
||||
app_event_tx,
|
||||
screen: AppLinkScreen::Link,
|
||||
selected_action: 0,
|
||||
|
|
@ -144,29 +113,6 @@ impl AppLinkView {
|
|||
self.selected_action = (self.selected_action + 1).min(self.action_labels().len() - 1);
|
||||
}
|
||||
|
||||
fn is_tool_suggestion(&self) -> bool {
|
||||
self.elicitation_target.is_some()
|
||||
}
|
||||
|
||||
fn resolve_elicitation(&self, decision: ElicitationAction) {
|
||||
let Some(target) = self.elicitation_target.as_ref() else {
|
||||
return;
|
||||
};
|
||||
self.app_event_tx.resolve_elicitation(
|
||||
target.thread_id,
|
||||
target.server_name.clone(),
|
||||
target.request_id.clone(),
|
||||
decision,
|
||||
/*content*/ None,
|
||||
/*meta*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
fn decline_tool_suggestion(&mut self) {
|
||||
self.resolve_elicitation(ElicitationAction::Decline);
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
fn open_chatgpt_link(&mut self) {
|
||||
self.app_event_tx.send(AppEvent::OpenUrlInBrowser {
|
||||
url: self.url.clone(),
|
||||
|
|
@ -181,9 +127,6 @@ impl AppLinkView {
|
|||
self.app_event_tx.send(AppEvent::RefreshConnectors {
|
||||
force_refetch: true,
|
||||
});
|
||||
if self.is_tool_suggestion() {
|
||||
self.resolve_elicitation(ElicitationAction::Accept);
|
||||
}
|
||||
self.complete = true;
|
||||
}
|
||||
|
||||
|
|
@ -198,40 +141,9 @@ impl AppLinkView {
|
|||
id: self.app_id.clone(),
|
||||
enabled: self.is_enabled,
|
||||
});
|
||||
if self.is_tool_suggestion() {
|
||||
self.resolve_elicitation(ElicitationAction::Accept);
|
||||
self.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_selected_action(&mut self) {
|
||||
if self.is_tool_suggestion() {
|
||||
match self.suggestion_type {
|
||||
Some(AppLinkSuggestionType::Enable) => match self.screen {
|
||||
AppLinkScreen::Link => match self.selected_action {
|
||||
0 => self.open_chatgpt_link(),
|
||||
1 if self.is_installed => self.toggle_enabled(),
|
||||
_ => self.decline_tool_suggestion(),
|
||||
},
|
||||
AppLinkScreen::InstallConfirmation => match self.selected_action {
|
||||
0 => self.refresh_connectors_and_close(),
|
||||
_ => self.decline_tool_suggestion(),
|
||||
},
|
||||
},
|
||||
Some(AppLinkSuggestionType::Install) | None => match self.screen {
|
||||
AppLinkScreen::Link => match self.selected_action {
|
||||
0 => self.open_chatgpt_link(),
|
||||
_ => self.decline_tool_suggestion(),
|
||||
},
|
||||
AppLinkScreen::InstallConfirmation => match self.selected_action {
|
||||
0 => self.refresh_connectors_and_close(),
|
||||
_ => self.decline_tool_suggestion(),
|
||||
},
|
||||
},
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match self.screen {
|
||||
AppLinkScreen::Link => match self.selected_action {
|
||||
0 => self.open_chatgpt_link(),
|
||||
|
|
@ -269,17 +181,6 @@ impl AppLinkView {
|
|||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
if let Some(suggest_reason) = self
|
||||
.suggest_reason
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|suggest_reason| !suggest_reason.is_empty())
|
||||
{
|
||||
for line in wrap(suggest_reason, usable_width) {
|
||||
lines.push(Line::from(line.into_owned().italic()));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
if self.is_installed {
|
||||
for line in wrap("Use $ to insert this app into the prompt.", usable_width) {
|
||||
lines.push(Line::from(line.into_owned()));
|
||||
|
|
@ -465,9 +366,6 @@ impl BottomPaneView for AppLinkView {
|
|||
}
|
||||
|
||||
fn on_ctrl_c(&mut self) -> CancellationEvent {
|
||||
if self.is_tool_suggestion() {
|
||||
self.resolve_elicitation(ElicitationAction::Decline);
|
||||
}
|
||||
self.complete = true;
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
|
|
@ -475,25 +373,6 @@ impl BottomPaneView for AppLinkView {
|
|||
fn is_complete(&self) -> bool {
|
||||
self.complete
|
||||
}
|
||||
|
||||
fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool {
|
||||
let ResolvedAppServerRequest::McpElicitation {
|
||||
server_name,
|
||||
request_id,
|
||||
} = request
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(target) = self.elicitation_target.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
if target.server_name != *server_name || target.request_id != *request_id {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.complete = true;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::render::renderable::Renderable for AppLinkView {
|
||||
|
|
@ -566,44 +445,11 @@ impl crate::render::renderable::Renderable for AppLinkView {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::render::renderable::Renderable;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
fn suggestion_target() -> AppLinkElicitationTarget {
|
||||
AppLinkElicitationTarget {
|
||||
thread_id: ThreadId::try_from("00000000-0000-0000-0000-000000000001")
|
||||
.expect("valid thread id"),
|
||||
server_name: "codex_apps".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_snapshot(view: &AppLinkView, area: Rect) -> String {
|
||||
let mut buf = Buffer::empty(area);
|
||||
view.render(area, &mut buf);
|
||||
(0..area.height)
|
||||
.map(|y| {
|
||||
(0..area.width)
|
||||
.map(|x| {
|
||||
let symbol = buf[(x, y)].symbol();
|
||||
if symbol.is_empty() {
|
||||
' '
|
||||
} else {
|
||||
symbol.chars().next().unwrap_or(' ')
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_app_has_toggle_action() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
|
@ -617,9 +463,6 @@ mod tests {
|
|||
url: "https://example.test/notion".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: true,
|
||||
suggest_reason: None,
|
||||
suggestion_type: None,
|
||||
elicitation_target: None,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
|
@ -643,9 +486,6 @@ mod tests {
|
|||
url: "https://example.test/notion".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: true,
|
||||
suggest_reason: None,
|
||||
suggestion_type: None,
|
||||
elicitation_target: None,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
|
@ -682,9 +522,6 @@ mod tests {
|
|||
url: url_like.to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: true,
|
||||
suggest_reason: None,
|
||||
suggestion_type: None,
|
||||
elicitation_target: None,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
|
@ -725,9 +562,6 @@ mod tests {
|
|||
url: url.to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: true,
|
||||
suggest_reason: None,
|
||||
suggestion_type: None,
|
||||
elicitation_target: None,
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
|
@ -760,270 +594,4 @@ mod tests {
|
|||
"expected wrapped setup URL tail to remain visible in narrow pane, got:\n{rendered_blob}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_tool_suggestion_resolves_elicitation_after_confirmation() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Install this app in your browser, then return here.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: false,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Install),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::OpenUrlInBrowser { url }) => {
|
||||
assert_eq!(url, "https://example.test/google-calendar".to_string());
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
assert_eq!(view.screen, AppLinkScreen::InstallConfirmation);
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::RefreshConnectors { force_refetch }) => {
|
||||
assert!(force_refetch);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SubmitThreadOp { thread_id, op }) => {
|
||||
assert_eq!(thread_id, suggestion_target().thread_id);
|
||||
assert_eq!(
|
||||
op,
|
||||
Op::ResolveElicitation {
|
||||
server_name: "codex_apps".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
decision: ElicitationAction::Accept,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
assert!(view.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declined_tool_suggestion_resolves_elicitation_decline() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: None,
|
||||
instructions: "Install this app in your browser, then return here.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: false,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Install),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SubmitThreadOp { thread_id, op }) => {
|
||||
assert_eq!(thread_id, suggestion_target().thread_id);
|
||||
assert_eq!(
|
||||
op,
|
||||
Op::ResolveElicitation {
|
||||
server_name: "codex_apps".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
decision: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
assert!(view.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enable_tool_suggestion_resolves_elicitation_after_enable() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Enable this app to use it for the current request.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Enable),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
view.handle_key_event(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SetAppEnabled { id, enabled }) => {
|
||||
assert_eq!(id, "connector_google_calendar");
|
||||
assert!(enabled);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(AppEvent::SubmitThreadOp { thread_id, op }) => {
|
||||
assert_eq!(thread_id, suggestion_target().thread_id);
|
||||
assert_eq!(
|
||||
op,
|
||||
Op::ResolveElicitation {
|
||||
server_name: "codex_apps".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
decision: ElicitationAction::Accept,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(other) => panic!("unexpected app event: {other:?}"),
|
||||
Err(err) => panic!("missing app event: {err}"),
|
||||
}
|
||||
assert!(view.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_tool_suggestion_dismisses_matching_view() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Enable this app to use it for the current request.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Enable),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
assert!(
|
||||
view.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
|
||||
server_name: "codex_apps".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
})
|
||||
);
|
||||
assert!(view.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_tool_suggestion_ignores_non_matching_request() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Enable this app to use it for the current request.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Enable),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
assert!(
|
||||
!view.dismiss_app_server_request(&ResolvedAppServerRequest::McpElicitation {
|
||||
server_name: "other_server".to_string(),
|
||||
request_id: McpRequestId::String("request-1".to_string()),
|
||||
})
|
||||
);
|
||||
assert!(!view.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_suggestion_with_reason_snapshot() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Install this app in your browser, then return here.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: false,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Install),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
"app_link_view_install_suggestion_with_reason",
|
||||
render_snapshot(
|
||||
&view,
|
||||
Rect::new(0, 0, 72, view.desired_height(/*width*/ 72))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enable_suggestion_with_reason_snapshot() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: "connector_google_calendar".to_string(),
|
||||
title: "Google Calendar".to_string(),
|
||||
description: Some("Plan events and schedules.".to_string()),
|
||||
instructions: "Enable this app to use it for the current request.".to_string(),
|
||||
url: "https://example.test/google-calendar".to_string(),
|
||||
is_installed: true,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
|
||||
suggestion_type: Some(AppLinkSuggestionType::Enable),
|
||||
elicitation_target: Some(suggestion_target()),
|
||||
},
|
||||
tx,
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
"app_link_view_enable_suggestion_with_reason",
|
||||
render_snapshot(
|
||||
&view,
|
||||
Rect::new(0, 0, 72, view.desired_height(/*width*/ 72))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ const APPROVAL_DECLINE_VALUE: &str = "decline";
|
|||
const APPROVAL_CANCEL_VALUE: &str = "cancel";
|
||||
const APPROVAL_META_KIND_KEY: &str = "codex_approval_kind";
|
||||
const APPROVAL_META_KIND_MCP_TOOL_CALL: &str = "mcp_tool_call";
|
||||
const APPROVAL_META_KIND_TOOL_SUGGESTION: &str = "tool_suggestion";
|
||||
const APPROVAL_PERSIST_KEY: &str = "persist";
|
||||
const APPROVAL_PERSIST_SESSION_VALUE: &str = "session";
|
||||
const APPROVAL_PERSIST_ALWAYS_VALUE: &str = "always";
|
||||
|
|
@ -67,12 +66,6 @@ const APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
|
|||
const APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
|
||||
const APPROVAL_TOOL_PARAM_DISPLAY_LIMIT: usize = 3;
|
||||
const APPROVAL_TOOL_PARAM_VALUE_TRUNCATE_GRAPHEMES: usize = 60;
|
||||
const TOOL_TYPE_KEY: &str = "tool_type";
|
||||
const TOOL_ID_KEY: &str = "tool_id";
|
||||
const TOOL_NAME_KEY: &str = "tool_name";
|
||||
const TOOL_SUGGEST_SUGGEST_TYPE_KEY: &str = "suggest_type";
|
||||
const TOOL_SUGGEST_REASON_KEY: &str = "suggest_reason";
|
||||
const TOOL_SUGGEST_INSTALL_URL_KEY: &str = "install_url";
|
||||
|
||||
#[derive(Clone, PartialEq, Default)]
|
||||
struct ComposerDraft {
|
||||
|
|
@ -133,28 +126,6 @@ enum McpServerElicitationResponseMode {
|
|||
ApprovalAction,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ToolSuggestionToolType {
|
||||
Connector,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ToolSuggestionType {
|
||||
Install,
|
||||
Enable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ToolSuggestionRequest {
|
||||
pub(crate) tool_type: ToolSuggestionToolType,
|
||||
pub(crate) suggest_type: ToolSuggestionType,
|
||||
pub(crate) suggest_reason: String,
|
||||
pub(crate) tool_id: String,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) install_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct McpToolApprovalDisplayParam {
|
||||
name: String,
|
||||
|
|
@ -171,7 +142,6 @@ pub(crate) struct McpServerElicitationFormRequest {
|
|||
approval_display_params: Vec<McpToolApprovalDisplayParam>,
|
||||
response_mode: McpServerElicitationResponseMode,
|
||||
fields: Vec<McpServerElicitationField>,
|
||||
tool_suggestion: Option<ToolSuggestionRequest>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -265,7 +235,6 @@ impl McpServerElicitationFormRequest {
|
|||
message: String,
|
||||
requested_schema: Value,
|
||||
) -> Option<Self> {
|
||||
let tool_suggestion = parse_tool_suggestion_request(meta.as_ref());
|
||||
let is_tool_approval = meta
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
|
|
@ -287,9 +256,7 @@ impl McpServerElicitationFormRequest {
|
|||
Vec::new()
|
||||
};
|
||||
|
||||
let (response_mode, fields) = if tool_suggestion.is_some() && is_message_only_schema {
|
||||
(McpServerElicitationResponseMode::FormContent, Vec::new())
|
||||
} else if is_message_only_schema {
|
||||
let (response_mode, fields) = if is_message_only_schema {
|
||||
let allow_description = if is_tool_approval_action {
|
||||
"Run the tool and continue."
|
||||
} else {
|
||||
|
|
@ -372,63 +339,9 @@ impl McpServerElicitationFormRequest {
|
|||
approval_display_params,
|
||||
response_mode,
|
||||
fields,
|
||||
tool_suggestion,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn tool_suggestion(&self) -> Option<&ToolSuggestionRequest> {
|
||||
self.tool_suggestion.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn thread_id(&self) -> ThreadId {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
pub(crate) fn server_name(&self) -> &str {
|
||||
self.server_name.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn request_id(&self) -> &McpRequestId {
|
||||
&self.request_id
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tool_suggestion_request(meta: Option<&Value>) -> Option<ToolSuggestionRequest> {
|
||||
let meta = meta?.as_object()?;
|
||||
if meta.get(APPROVAL_META_KIND_KEY).and_then(Value::as_str)
|
||||
!= Some(APPROVAL_META_KIND_TOOL_SUGGESTION)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let tool_type = match meta.get(TOOL_TYPE_KEY).and_then(Value::as_str) {
|
||||
Some("connector") => ToolSuggestionToolType::Connector,
|
||||
Some("plugin") => ToolSuggestionToolType::Plugin,
|
||||
_ => return None,
|
||||
};
|
||||
let suggest_type = match meta
|
||||
.get(TOOL_SUGGEST_SUGGEST_TYPE_KEY)
|
||||
.and_then(Value::as_str)
|
||||
{
|
||||
Some("install") => ToolSuggestionType::Install,
|
||||
Some("enable") => ToolSuggestionType::Enable,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(ToolSuggestionRequest {
|
||||
tool_type,
|
||||
suggest_type,
|
||||
suggest_reason: meta
|
||||
.get(TOOL_SUGGEST_REASON_KEY)
|
||||
.and_then(Value::as_str)?
|
||||
.to_string(),
|
||||
tool_id: meta.get(TOOL_ID_KEY).and_then(Value::as_str)?.to_string(),
|
||||
tool_name: meta.get(TOOL_NAME_KEY).and_then(Value::as_str)?.to_string(),
|
||||
install_url: meta
|
||||
.get(TOOL_SUGGEST_INSTALL_URL_KEY)
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string),
|
||||
})
|
||||
}
|
||||
|
||||
fn approval_supports_persist_mode(meta: Option<&Value>, expected_mode: &str) -> bool {
|
||||
|
|
@ -1862,7 +1775,6 @@ mod tests {
|
|||
default_idx: None,
|
||||
},
|
||||
}],
|
||||
tool_suggestion: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1933,7 +1845,6 @@ mod tests {
|
|||
default_idx: Some(0),
|
||||
},
|
||||
}],
|
||||
tool_suggestion: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1985,76 +1896,10 @@ mod tests {
|
|||
default_idx: Some(0),
|
||||
},
|
||||
}],
|
||||
tool_suggestion: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggestion_meta_is_parsed_into_request_payload() {
|
||||
let request = McpServerElicitationFormRequest::from_event(
|
||||
ThreadId::default(),
|
||||
form_request(
|
||||
"Suggest Google Calendar",
|
||||
empty_object_schema(),
|
||||
Some(serde_json::json!({
|
||||
"codex_approval_kind": "tool_suggestion",
|
||||
"tool_type": "connector",
|
||||
"suggest_type": "install",
|
||||
"suggest_reason": "Plan and reference events from your calendar",
|
||||
"tool_id": "connector_2128aebfecb84f64a069897515042a44",
|
||||
"tool_name": "Google Calendar",
|
||||
"install_url": "https://example.test/google-calendar",
|
||||
})),
|
||||
),
|
||||
)
|
||||
.expect("expected tool suggestion form");
|
||||
|
||||
assert_eq!(
|
||||
request.tool_suggestion(),
|
||||
Some(&ToolSuggestionRequest {
|
||||
tool_type: ToolSuggestionToolType::Connector,
|
||||
suggest_type: ToolSuggestionType::Install,
|
||||
suggest_reason: "Plan and reference events from your calendar".to_string(),
|
||||
tool_id: "connector_2128aebfecb84f64a069897515042a44".to_string(),
|
||||
tool_name: "Google Calendar".to_string(),
|
||||
install_url: Some("https://example.test/google-calendar".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_tool_suggestion_meta_without_install_url_is_parsed_into_request_payload() {
|
||||
let request = McpServerElicitationFormRequest::from_event(
|
||||
ThreadId::default(),
|
||||
form_request(
|
||||
"Suggest Slack",
|
||||
empty_object_schema(),
|
||||
Some(serde_json::json!({
|
||||
"codex_approval_kind": "tool_suggestion",
|
||||
"tool_type": "plugin",
|
||||
"suggest_type": "install",
|
||||
"suggest_reason": "Install the Slack plugin to search messages",
|
||||
"tool_id": "slack@openai-curated",
|
||||
"tool_name": "Slack",
|
||||
})),
|
||||
),
|
||||
)
|
||||
.expect("expected tool suggestion form");
|
||||
|
||||
assert_eq!(
|
||||
request.tool_suggestion(),
|
||||
Some(&ToolSuggestionRequest {
|
||||
tool_type: ToolSuggestionToolType::Plugin,
|
||||
suggest_type: ToolSuggestionType::Install,
|
||||
suggest_reason: "Install the Slack plugin to search messages".to_string(),
|
||||
tool_id: "slack@openai-curated".to_string(),
|
||||
tool_name: "Slack".to_string(),
|
||||
install_url: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_approval_display_params_prefer_explicit_display_order() {
|
||||
let request = McpServerElicitationFormRequest::from_event(
|
||||
|
|
|
|||
50
vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs
vendored
50
vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs
vendored
|
|
@ -50,8 +50,6 @@ mod request_user_input;
|
|||
mod status_line_setup;
|
||||
mod status_surface_preview;
|
||||
mod title_setup;
|
||||
pub(crate) use app_link_view::AppLinkElicitationTarget;
|
||||
pub(crate) use app_link_view::AppLinkSuggestionType;
|
||||
pub(crate) use app_link_view::AppLinkView;
|
||||
pub(crate) use app_link_view::AppLinkViewParams;
|
||||
pub(crate) use approval_overlay::ApprovalOverlay;
|
||||
|
|
@ -1002,54 +1000,6 @@ impl BottomPane {
|
|||
request
|
||||
};
|
||||
|
||||
if let Some(tool_suggestion) = request.tool_suggestion()
|
||||
&& let Some(install_url) = tool_suggestion.install_url.clone()
|
||||
{
|
||||
let suggestion_type = match tool_suggestion.suggest_type {
|
||||
mcp_server_elicitation::ToolSuggestionType::Install => {
|
||||
AppLinkSuggestionType::Install
|
||||
}
|
||||
mcp_server_elicitation::ToolSuggestionType::Enable => AppLinkSuggestionType::Enable,
|
||||
};
|
||||
let is_installed = matches!(
|
||||
tool_suggestion.suggest_type,
|
||||
mcp_server_elicitation::ToolSuggestionType::Enable
|
||||
);
|
||||
let view = AppLinkView::new(
|
||||
AppLinkViewParams {
|
||||
app_id: tool_suggestion.tool_id.clone(),
|
||||
title: tool_suggestion.tool_name.clone(),
|
||||
description: None,
|
||||
instructions: match suggestion_type {
|
||||
AppLinkSuggestionType::Install => {
|
||||
"Install this app in your browser, then return here.".to_string()
|
||||
}
|
||||
AppLinkSuggestionType::Enable => {
|
||||
"Enable this app to use it for the current request.".to_string()
|
||||
}
|
||||
},
|
||||
url: install_url,
|
||||
is_installed,
|
||||
is_enabled: false,
|
||||
suggest_reason: Some(tool_suggestion.suggest_reason.clone()),
|
||||
suggestion_type: Some(suggestion_type),
|
||||
elicitation_target: Some(AppLinkElicitationTarget {
|
||||
thread_id: request.thread_id(),
|
||||
server_name: request.server_name().to_string(),
|
||||
request_id: request.request_id().clone(),
|
||||
}),
|
||||
},
|
||||
self.app_event_tx.clone(),
|
||||
);
|
||||
self.pause_status_timer_for_modal();
|
||||
self.set_composer_input_enabled(
|
||||
/*enabled*/ false,
|
||||
Some("Respond to the tool suggestion to continue.".to_string()),
|
||||
);
|
||||
self.push_view(Box::new(view));
|
||||
return;
|
||||
}
|
||||
|
||||
let modal = McpServerElicitationOverlay::new(
|
||||
request,
|
||||
self.app_event_tx.clone(),
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
source: tui/src/bottom_pane/app_link_view.rs
|
||||
expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)))"
|
||||
---
|
||||
|
||||
Google Calendar
|
||||
Plan events and schedules.
|
||||
|
||||
Plan and reference events from your calendar
|
||||
|
||||
Use $ to insert this app into the prompt.
|
||||
|
||||
Enable this app to use it for the current request.
|
||||
Newly installed apps can take a few minutes to appear in /apps.
|
||||
|
||||
|
||||
› 1. Manage on ChatGPT
|
||||
2. Enable app
|
||||
3. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
source: tui/src/bottom_pane/app_link_view.rs
|
||||
expression: "render_snapshot(&view, Rect::new(0, 0, 72, view.desired_height(72)))"
|
||||
---
|
||||
|
||||
Google Calendar
|
||||
Plan events and schedules.
|
||||
|
||||
Plan and reference events from your calendar
|
||||
|
||||
Install this app in your browser, then return here.
|
||||
Newly installed apps can take a few minutes to appear in /apps.
|
||||
After installed, use $ to insert this app into the prompt.
|
||||
|
||||
|
||||
› 1. Install on ChatGPT
|
||||
2. Back
|
||||
Use tab / ↑ ↓ to move, enter to select, esc to close
|
||||
Reference in a new issue