mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
@@ -10665,30 +10665,51 @@
|
||||
},
|
||||
"AppConfig": {
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppDisabledReason"
|
||||
"$ref": "#/definitions/v2/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppToolsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppInfo": {
|
||||
"description": "EXPERIMENTAL - app metadata returned by app-list APIs.",
|
||||
"properties": {
|
||||
@@ -10914,7 +10935,69 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolsConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfig": {
|
||||
"properties": {
|
||||
"_default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/AppsDefaultConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsListParams": {
|
||||
|
||||
@@ -19,10 +19,65 @@
|
||||
},
|
||||
"AppConfig": {
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppDisabledReason"
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolsConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -30,20 +85,48 @@
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
],
|
||||
"type": "string"
|
||||
"AppToolsConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfig": {
|
||||
"properties": {
|
||||
"_default": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppsDefaultConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AskForApproval": {
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AppDisabledReason = "unknown" | "user";
|
||||
export type AppToolApproval = "auto" | "prompt" | "approve";
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AppToolApproval } from "./AppToolApproval";
|
||||
|
||||
export type AppToolsConfig = { [key in string]?: { enabled: boolean | null, approval_mode: AppToolApproval | null, } };
|
||||
@@ -1,6 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AppDisabledReason } from "./AppDisabledReason";
|
||||
import type { AppToolApproval } from "./AppToolApproval";
|
||||
import type { AppToolsConfig } from "./AppToolsConfig";
|
||||
import type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
||||
|
||||
export type AppsConfig = { [key in string]?: { enabled: boolean, disabled_reason: AppDisabledReason | null, } };
|
||||
export type AppsConfig = { _default: AppsDefaultConfig | null, } & ({ [key in string]?: { enabled: boolean, destructive_enabled: boolean | null, open_world_enabled: boolean | null, default_tools_approval_mode: AppToolApproval | null, default_tools_enabled: boolean | null, tools: AppToolsConfig | null, } });
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AppsDefaultConfig = { enabled: boolean, destructive_enabled: boolean, open_world_enabled: boolean, };
|
||||
@@ -7,13 +7,15 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
|
||||
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
|
||||
export type { AnalyticsConfig } from "./AnalyticsConfig";
|
||||
export type { AppBranding } from "./AppBranding";
|
||||
export type { AppDisabledReason } from "./AppDisabledReason";
|
||||
export type { AppInfo } from "./AppInfo";
|
||||
export type { AppListUpdatedNotification } from "./AppListUpdatedNotification";
|
||||
export type { AppMetadata } from "./AppMetadata";
|
||||
export type { AppReview } from "./AppReview";
|
||||
export type { AppScreenshot } from "./AppScreenshot";
|
||||
export type { AppToolApproval } from "./AppToolApproval";
|
||||
export type { AppToolsConfig } from "./AppToolsConfig";
|
||||
export type { AppsConfig } from "./AppsConfig";
|
||||
export type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
||||
export type { AppsListParams } from "./AppsListParams";
|
||||
export type { AppsListResponse } from "./AppsListResponse";
|
||||
export type { AskForApproval } from "./AskForApproval";
|
||||
|
||||
@@ -398,12 +398,41 @@ pub struct AnalyticsConfig {
|
||||
pub additional: HashMap<String, JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AppToolApproval {
|
||||
Auto,
|
||||
Prompt,
|
||||
Approve,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum AppDisabledReason {
|
||||
Unknown,
|
||||
User,
|
||||
pub struct AppsDefaultConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub destructive_enabled: bool,
|
||||
#[serde(default = "default_enabled")]
|
||||
pub open_world_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppToolConfig {
|
||||
pub enabled: Option<bool>,
|
||||
pub approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppToolsConfig {
|
||||
#[serde(default, flatten)]
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -412,15 +441,20 @@ pub enum AppDisabledReason {
|
||||
pub struct AppConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub disabled_reason: Option<AppDisabledReason>,
|
||||
pub destructive_enabled: Option<bool>,
|
||||
pub open_world_enabled: Option<bool>,
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
pub default_tools_enabled: Option<bool>,
|
||||
pub tools: Option<AppToolsConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AppsConfig {
|
||||
#[serde(default, rename = "_default")]
|
||||
pub default: Option<AppsDefaultConfig>,
|
||||
#[serde(default, flatten)]
|
||||
#[schemars(with = "HashMap<String, AppConfig>")]
|
||||
pub apps: HashMap<String, AppConfig>,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use app_test_support::test_path_buf_with_windows;
|
||||
use app_test_support::test_tmp_path_buf;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppDisabledReason;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
@@ -156,7 +156,8 @@ async fn config_read_includes_apps() -> Result<()> {
|
||||
r#"
|
||||
[apps.app1]
|
||||
enabled = false
|
||||
disabled_reason = "user"
|
||||
destructive_enabled = false
|
||||
default_tools_approval_mode = "prompt"
|
||||
"#,
|
||||
)?;
|
||||
let codex_home_path = codex_home.path().canonicalize()?;
|
||||
@@ -185,11 +186,16 @@ disabled_reason = "user"
|
||||
assert_eq!(
|
||||
config.apps,
|
||||
Some(AppsConfig {
|
||||
default: None,
|
||||
apps: std::collections::HashMap::from([(
|
||||
"app1".to_string(),
|
||||
AppConfig {
|
||||
enabled: false,
|
||||
disabled_reason: Some(AppDisabledReason::User),
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
@@ -202,7 +208,16 @@ disabled_reason = "user"
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("apps.app1.disabled_reason")
|
||||
.get("apps.app1.destructive_enabled")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
file: user_file.clone(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("apps.app1.default_tools_approval_mode")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerSource::User {
|
||||
|
||||
@@ -85,34 +85,111 @@
|
||||
"additionalProperties": false,
|
||||
"description": "Config values for a single app/connector.",
|
||||
"properties": {
|
||||
"disabled_reason": {
|
||||
"default_tools_approval_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppDisabledReason"
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
}
|
||||
],
|
||||
"description": "Reason this app was disabled."
|
||||
"description": "Approval mode for tools in this app unless a tool override exists."
|
||||
},
|
||||
"default_tools_enabled": {
|
||||
"description": "Whether tools are enabled by default for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"destructive_enabled": {
|
||||
"description": "Whether tools with `destructive_hint = true` are allowed for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"description": "When `false`, Codex does not surface this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"description": "Whether tools with `open_world_hint = true` are allowed for this app.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"tools": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolsConfig"
|
||||
}
|
||||
],
|
||||
"description": "Per-tool settings for this app."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppDisabledReason": {
|
||||
"AppToolApproval": {
|
||||
"enum": [
|
||||
"unknown",
|
||||
"user"
|
||||
"auto",
|
||||
"prompt",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"AppToolConfig": {
|
||||
"additionalProperties": false,
|
||||
"description": "Per-tool settings for a single app tool.",
|
||||
"properties": {
|
||||
"approval_mode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppToolApproval"
|
||||
}
|
||||
],
|
||||
"description": "Approval mode for this tool."
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Whether this tool is enabled. `Some(true)` explicitly allows this tool.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppToolsConfig": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AppToolConfig"
|
||||
},
|
||||
"description": "Tool settings for a single app.",
|
||||
"type": "object"
|
||||
},
|
||||
"AppsConfigToml": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/AppConfig"
|
||||
},
|
||||
"description": "App/connector settings loaded from `config.toml`.",
|
||||
"properties": {
|
||||
"_default": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AppsDefaultConfig"
|
||||
}
|
||||
],
|
||||
"description": "Default settings for all apps."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AppsDefaultConfig": {
|
||||
"additionalProperties": false,
|
||||
"description": "Default settings that apply to all apps.",
|
||||
"properties": {
|
||||
"destructive_enabled": {
|
||||
"description": "Whether tools with `destructive_hint = true` are allowed by default.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"description": "When `false`, apps are disabled unless overridden by per-app settings.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"open_world_enabled": {
|
||||
"description": "Whether tools with `open_world_hint = true` are allowed by default.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AskForApproval": {
|
||||
|
||||
@@ -4834,6 +4834,7 @@ fn connector_inserted_in_messages(
|
||||
fn filter_codex_apps_mcp_tools(
|
||||
mcp_tools: &HashMap<String, crate::mcp_connection_manager::ToolInfo>,
|
||||
connectors: &[connectors::AppInfo],
|
||||
config: &Config,
|
||||
) -> HashMap<String, crate::mcp_connection_manager::ToolInfo> {
|
||||
let allowed: HashSet<&str> = connectors
|
||||
.iter()
|
||||
@@ -4849,7 +4850,7 @@ fn filter_codex_apps_mcp_tools(
|
||||
let Some(connector_id) = codex_apps_connector_id(tool) else {
|
||||
return false;
|
||||
};
|
||||
allowed.contains(connector_id)
|
||||
allowed.contains(connector_id) && connectors::codex_app_tool_is_enabled(config, tool)
|
||||
})
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
@@ -5023,9 +5024,9 @@ async fn built_tools(
|
||||
None
|
||||
};
|
||||
|
||||
let app_tools = connectors
|
||||
.as_ref()
|
||||
.map(|connectors| filter_codex_apps_mcp_tools(&mcp_tools, connectors));
|
||||
let app_tools = connectors.as_ref().map(|connectors| {
|
||||
filter_codex_apps_mcp_tools(&mcp_tools, connectors, &turn_context.config)
|
||||
});
|
||||
|
||||
if let Some(connectors) = connectors.as_ref() {
|
||||
let skill_name_counts_lower = skills_outcome.map_or_else(HashMap::new, |outcome| {
|
||||
@@ -5050,7 +5051,8 @@ async fn built_tools(
|
||||
explicitly_enabled.as_ref(),
|
||||
));
|
||||
|
||||
mcp_tools = selected_mcp_tools;
|
||||
mcp_tools =
|
||||
connectors::filter_codex_apps_tools_by_policy(selected_mcp_tools, &turn_context.config);
|
||||
}
|
||||
|
||||
Ok(Arc::new(ToolRouter::from_config(
|
||||
@@ -6001,7 +6003,7 @@ mod tests {
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
std::fs::write(
|
||||
&config_toml_path,
|
||||
"[apps.calendar]\nenabled = false\ndisabled_reason = \"user\"\n",
|
||||
"[apps.calendar]\nenabled = false\ndestructive_enabled = false\n",
|
||||
)
|
||||
.expect("write user config");
|
||||
|
||||
@@ -6023,10 +6025,7 @@ mod tests {
|
||||
.expect("calendar app config exists");
|
||||
|
||||
assert!(!app.enabled);
|
||||
assert_eq!(
|
||||
app.disabled_reason,
|
||||
Some(crate::config::types::AppDisabledReason::User)
|
||||
);
|
||||
assert_eq!(app.destructive_enabled, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -701,7 +701,7 @@ mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppDisabledReason;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -827,13 +827,13 @@ personality = true
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
key_path: "apps.app1.disabled_reason".to_string(),
|
||||
value: serde_json::json!("user"),
|
||||
key_path: "apps.app1.default_tools_approval_mode".to_string(),
|
||||
value: serde_json::json!("prompt"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect("write apps.app1.disabled_reason succeeds");
|
||||
.expect("write apps.app1.default_tools_approval_mode succeeds");
|
||||
|
||||
let read = service
|
||||
.read(ConfigReadParams {
|
||||
@@ -846,11 +846,16 @@ personality = true
|
||||
assert_eq!(
|
||||
read.config.apps,
|
||||
Some(AppsConfig {
|
||||
default: None,
|
||||
apps: std::collections::HashMap::from([(
|
||||
"app1".to_string(),
|
||||
AppConfig {
|
||||
enabled: false,
|
||||
disabled_reason: Some(AppDisabledReason::User),
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
})
|
||||
|
||||
@@ -425,20 +425,58 @@ impl From<MemoriesToml> for MemoriesConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AppDisabledReason {
|
||||
Unknown,
|
||||
User,
|
||||
pub enum AppToolApproval {
|
||||
#[default]
|
||||
Auto,
|
||||
Prompt,
|
||||
Approve,
|
||||
}
|
||||
|
||||
impl fmt::Display for AppDisabledReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AppDisabledReason::Unknown => write!(f, "unknown"),
|
||||
AppDisabledReason::User => write!(f, "user"),
|
||||
}
|
||||
}
|
||||
/// Default settings that apply to all apps.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppsDefaultConfig {
|
||||
/// When `false`, apps are disabled unless overridden by per-app settings.
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Whether tools with `destructive_hint = true` are allowed by default.
|
||||
#[serde(
|
||||
default = "default_enabled",
|
||||
skip_serializing_if = "std::clone::Clone::clone"
|
||||
)]
|
||||
pub destructive_enabled: bool,
|
||||
|
||||
/// Whether tools with `open_world_hint = true` are allowed by default.
|
||||
#[serde(
|
||||
default = "default_enabled",
|
||||
skip_serializing_if = "std::clone::Clone::clone"
|
||||
)]
|
||||
pub open_world_enabled: bool,
|
||||
}
|
||||
|
||||
/// Per-tool settings for a single app tool.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppToolConfig {
|
||||
/// Whether this tool is enabled. `Some(true)` explicitly allows this tool.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// Approval mode for this tool.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub approval_mode: Option<AppToolApproval>,
|
||||
}
|
||||
|
||||
/// Tool settings for a single app.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppToolsConfig {
|
||||
/// Per-tool overrides keyed by tool name (for example `repos/list`).
|
||||
#[serde(default, flatten)]
|
||||
pub tools: HashMap<String, AppToolConfig>,
|
||||
}
|
||||
|
||||
/// Config values for a single app/connector.
|
||||
@@ -449,15 +487,35 @@ pub struct AppConfig {
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Reason this app was disabled.
|
||||
/// Whether tools with `destructive_hint = true` are allowed for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disabled_reason: Option<AppDisabledReason>,
|
||||
pub destructive_enabled: Option<bool>,
|
||||
|
||||
/// Whether tools with `open_world_hint = true` are allowed for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub open_world_enabled: Option<bool>,
|
||||
|
||||
/// Approval mode for tools in this app unless a tool override exists.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_tools_approval_mode: Option<AppToolApproval>,
|
||||
|
||||
/// Whether tools are enabled by default for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_tools_enabled: Option<bool>,
|
||||
|
||||
/// Per-tool settings for this app.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<AppToolsConfig>,
|
||||
}
|
||||
|
||||
/// App/connector settings loaded from `config.toml`.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AppsConfigToml {
|
||||
/// Default settings for all apps.
|
||||
#[serde(default, rename = "_default", skip_serializing_if = "Option::is_none")]
|
||||
pub default: Option<AppsDefaultConfig>,
|
||||
|
||||
/// Per-app settings keyed by app ID (for example `[apps.google_drive]`).
|
||||
#[serde(default, flatten)]
|
||||
pub apps: HashMap<String, AppConfig>,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub use codex_app_server_protocol::AppBranding;
|
||||
pub use codex_app_server_protocol::AppInfo;
|
||||
pub use codex_app_server_protocol::AppMetadata;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -18,6 +19,7 @@ use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::SandboxState;
|
||||
use crate::config::Config;
|
||||
use crate::config::types::AppToolApproval;
|
||||
use crate::config::types::AppsConfigToml;
|
||||
use crate::default_client::is_first_party_chat_originator;
|
||||
use crate::default_client::originator;
|
||||
@@ -32,6 +34,21 @@ use crate::token_data::TokenData;
|
||||
pub const CONNECTORS_CACHE_TTL: Duration = Duration::from_secs(3600);
|
||||
const CONNECTORS_READY_TIMEOUT_ON_EMPTY_TOOLS: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct AppToolPolicy {
|
||||
pub enabled: bool,
|
||||
pub approval: AppToolApproval,
|
||||
}
|
||||
|
||||
impl Default for AppToolPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct AccessibleConnectorsCacheKey {
|
||||
chatgpt_base_url: String,
|
||||
@@ -337,15 +354,58 @@ pub fn merge_connectors(
|
||||
}
|
||||
|
||||
pub fn with_app_enabled_state(mut connectors: Vec<AppInfo>, config: &Config) -> Vec<AppInfo> {
|
||||
let apps = read_apps_config(config).map(|apps_config| apps_config.apps);
|
||||
for connector in &mut connectors {
|
||||
if let Some(app) = apps.as_ref().and_then(|apps| apps.get(&connector.id)) {
|
||||
connector.is_enabled = app.enabled;
|
||||
let apps_config = read_apps_config(config);
|
||||
if let Some(apps_config) = apps_config.as_ref() {
|
||||
for connector in &mut connectors {
|
||||
connector.is_enabled = app_is_enabled(apps_config, Some(connector.id.as_str()));
|
||||
}
|
||||
}
|
||||
connectors
|
||||
}
|
||||
|
||||
pub(crate) fn app_tool_policy(
|
||||
config: &Config,
|
||||
connector_id: Option<&str>,
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
) -> AppToolPolicy {
|
||||
let apps_config = read_apps_config(config);
|
||||
app_tool_policy_from_apps_config(
|
||||
apps_config.as_ref(),
|
||||
connector_id,
|
||||
tool_name,
|
||||
tool_title,
|
||||
annotations,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn codex_app_tool_is_enabled(
|
||||
config: &Config,
|
||||
tool_info: &crate::mcp_connection_manager::ToolInfo,
|
||||
) -> bool {
|
||||
if tool_info.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return true;
|
||||
}
|
||||
|
||||
app_tool_policy(
|
||||
config,
|
||||
tool_info.connector_id.as_deref(),
|
||||
&tool_info.tool_name,
|
||||
tool_info.tool.title.as_deref(),
|
||||
tool_info.tool.annotations.as_ref(),
|
||||
)
|
||||
.enabled
|
||||
}
|
||||
|
||||
pub(crate) fn filter_codex_apps_tools_by_policy(
|
||||
mut mcp_tools: HashMap<String, crate::mcp_connection_manager::ToolInfo>,
|
||||
config: &Config,
|
||||
) -> HashMap<String, crate::mcp_connection_manager::ToolInfo> {
|
||||
mcp_tools.retain(|_, tool_info| codex_app_tool_is_enabled(config, tool_info));
|
||||
mcp_tools
|
||||
}
|
||||
|
||||
const DISALLOWED_CONNECTOR_IDS: &[&str] = &[
|
||||
"asdk_app_6938a94a61d881918ef32cb999ff937c",
|
||||
"connector_2b0a9009c9c64bf9933a3dae3f2b1254",
|
||||
@@ -394,6 +454,85 @@ fn read_apps_config(config: &Config) -> Option<AppsConfigToml> {
|
||||
AppsConfigToml::deserialize(apps_config).ok()
|
||||
}
|
||||
|
||||
fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool {
|
||||
let default_enabled = apps_config
|
||||
.default
|
||||
.as_ref()
|
||||
.map(|defaults| defaults.enabled)
|
||||
.unwrap_or(true);
|
||||
|
||||
connector_id
|
||||
.and_then(|connector_id| apps_config.apps.get(connector_id))
|
||||
.map(|app| app.enabled)
|
||||
.unwrap_or(default_enabled)
|
||||
}
|
||||
|
||||
fn app_tool_policy_from_apps_config(
|
||||
apps_config: Option<&AppsConfigToml>,
|
||||
connector_id: Option<&str>,
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
) -> AppToolPolicy {
|
||||
let Some(apps_config) = apps_config else {
|
||||
return AppToolPolicy::default();
|
||||
};
|
||||
|
||||
let app = connector_id.and_then(|connector_id| apps_config.apps.get(connector_id));
|
||||
let tools = app.and_then(|app| app.tools.as_ref());
|
||||
let tool_config = tools.and_then(|tools| {
|
||||
tools
|
||||
.tools
|
||||
.get(tool_name)
|
||||
.or_else(|| tool_title.and_then(|title| tools.tools.get(title)))
|
||||
});
|
||||
let approval = tool_config
|
||||
.and_then(|tool| tool.approval_mode)
|
||||
.or_else(|| app.and_then(|app| app.default_tools_approval_mode))
|
||||
.unwrap_or(AppToolApproval::Auto);
|
||||
|
||||
if !app_is_enabled(apps_config, connector_id) {
|
||||
return AppToolPolicy {
|
||||
enabled: false,
|
||||
approval,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) {
|
||||
return AppToolPolicy { enabled, approval };
|
||||
}
|
||||
|
||||
if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) {
|
||||
return AppToolPolicy { enabled, approval };
|
||||
}
|
||||
|
||||
let app_defaults = apps_config.default.as_ref();
|
||||
let destructive_enabled = app
|
||||
.and_then(|app| app.destructive_enabled)
|
||||
.unwrap_or_else(|| {
|
||||
app_defaults
|
||||
.map(|defaults| defaults.destructive_enabled)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let open_world_enabled = app
|
||||
.and_then(|app| app.open_world_enabled)
|
||||
.unwrap_or_else(|| {
|
||||
app_defaults
|
||||
.map(|defaults| defaults.open_world_enabled)
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let destructive_hint = annotations
|
||||
.and_then(|annotations| annotations.destructive_hint)
|
||||
.unwrap_or(false);
|
||||
let open_world_hint = annotations
|
||||
.and_then(|annotations| annotations.open_world_hint)
|
||||
.unwrap_or(false);
|
||||
let enabled =
|
||||
(destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint);
|
||||
|
||||
AppToolPolicy { enabled, approval }
|
||||
}
|
||||
|
||||
fn collect_accessible_connectors<I>(tools: I) -> Vec<AppInfo>
|
||||
where
|
||||
I: IntoIterator<Item = (String, Option<String>)>,
|
||||
@@ -472,8 +611,25 @@ fn format_connector_label(name: &str, _id: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::types::AppConfig;
|
||||
use crate::config::types::AppToolConfig;
|
||||
use crate::config::types::AppToolsConfig;
|
||||
use crate::config::types::AppsDefaultConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn annotations(
|
||||
destructive_hint: Option<bool>,
|
||||
open_world_hint: Option<bool>,
|
||||
) -> ToolAnnotations {
|
||||
ToolAnnotations {
|
||||
destructive_hint,
|
||||
idempotent_hint: None,
|
||||
open_world_hint,
|
||||
read_only_hint: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn app(id: &str) -> AppInfo {
|
||||
AppInfo {
|
||||
id: id.to_string(),
|
||||
@@ -491,6 +647,328 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_global_defaults_for_destructive_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: false,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_is_enabled_uses_default_for_unconfigured_apps() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
assert!(!app_is_enabled(&apps_config, Some("calendar")));
|
||||
assert!(!app_is_enabled(&apps_config, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_is_enabled_prefers_per_app_override_over_default() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
assert!(app_is_enabled(&apps_config, Some("calendar")));
|
||||
assert!(!app_is_enabled(&apps_config, Some("drive")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_honors_default_app_enabled_false() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::new(),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: Some(AppsDefaultConfig {
|
||||
enabled: false,
|
||||
destructive_enabled: true,
|
||||
open_world_enabled: true,
|
||||
}),
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: None,
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::from([(
|
||||
"events/create".to_string(),
|
||||
AppToolConfig {
|
||||
enabled: Some(true),
|
||||
approval_mode: None,
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: None,
|
||||
default_tools_enabled: Some(true),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/create",
|
||||
None,
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Auto,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(true),
|
||||
open_world_enabled: Some(true),
|
||||
default_tools_approval_mode: Some(AppToolApproval::Approve),
|
||||
default_tools_enabled: Some(false),
|
||||
tools: None,
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: false,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_uses_default_tools_approval_mode() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: None,
|
||||
open_world_enabled: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::Prompt),
|
||||
default_tools_enabled: None,
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::new(),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"events/list",
|
||||
None,
|
||||
Some(&annotations(None, None)),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Prompt,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() {
|
||||
let apps_config = AppsConfigToml {
|
||||
default: None,
|
||||
apps: HashMap::from([(
|
||||
"calendar".to_string(),
|
||||
AppConfig {
|
||||
enabled: true,
|
||||
destructive_enabled: Some(false),
|
||||
open_world_enabled: Some(false),
|
||||
default_tools_approval_mode: Some(AppToolApproval::Auto),
|
||||
default_tools_enabled: Some(false),
|
||||
tools: Some(AppToolsConfig {
|
||||
tools: HashMap::from([(
|
||||
"events/create".to_string(),
|
||||
AppToolConfig {
|
||||
enabled: Some(true),
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
};
|
||||
|
||||
let policy = app_tool_policy_from_apps_config(
|
||||
Some(&apps_config),
|
||||
Some("calendar"),
|
||||
"calendar_events/create",
|
||||
Some("events/create"),
|
||||
Some(&annotations(Some(true), Some(true))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
policy,
|
||||
AppToolPolicy {
|
||||
enabled: true,
|
||||
approval: AppToolApproval::Approve,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_disallowed_connectors_allows_non_disallowed_connectors() {
|
||||
let filtered = filter_disallowed_connectors(vec![app("asdk_app_hidden"), app("alpha")]);
|
||||
@@ -528,7 +1006,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
filtered,
|
||||
vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c"),]
|
||||
vec![app("asdk_app_6938a94a61d881918ef32cb999ff937c")]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use crate::analytics_client::AppInvocation;
|
||||
use crate::analytics_client::build_track_events_context;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::config::types::AppToolApproval;
|
||||
use crate::connectors;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::protocol::EventMsg;
|
||||
use crate::protocol::McpInvocation;
|
||||
@@ -64,9 +66,51 @@ pub(crate) async fn handle_mcp_tool_call(
|
||||
arguments: arguments_value.clone(),
|
||||
};
|
||||
|
||||
if let Some(decision) =
|
||||
maybe_request_mcp_tool_approval(sess.as_ref(), turn_context, &call_id, &server, &tool_name)
|
||||
.await
|
||||
let metadata = lookup_mcp_tool_metadata(sess.as_ref(), &server, &tool_name).await;
|
||||
let app_tool_policy = if server == CODEX_APPS_MCP_SERVER_NAME {
|
||||
connectors::app_tool_policy(
|
||||
&turn_context.config,
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
&tool_name,
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.annotations.as_ref()),
|
||||
)
|
||||
} else {
|
||||
connectors::AppToolPolicy::default()
|
||||
};
|
||||
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && !app_tool_policy.enabled {
|
||||
let result = notify_mcp_tool_call_skip(
|
||||
sess.as_ref(),
|
||||
turn_context,
|
||||
&call_id,
|
||||
invocation,
|
||||
"MCP tool call blocked by app configuration".to_string(),
|
||||
)
|
||||
.await;
|
||||
let status = if result.is_ok() { "ok" } else { "error" };
|
||||
turn_context
|
||||
.otel_manager
|
||||
.counter("codex.mcp.call", 1, &[("status", status)]);
|
||||
return ResponseInputItem::McpToolCallOutput { call_id, result };
|
||||
}
|
||||
|
||||
if let Some(decision) = maybe_request_mcp_tool_approval(
|
||||
sess.as_ref(),
|
||||
turn_context,
|
||||
&call_id,
|
||||
&server,
|
||||
&tool_name,
|
||||
metadata.as_ref(),
|
||||
app_tool_policy.approval,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let result = match decision {
|
||||
McpToolApprovalDecision::Accept | McpToolApprovalDecision::AcceptAndRemember => {
|
||||
@@ -267,7 +311,7 @@ enum McpToolApprovalDecision {
|
||||
}
|
||||
|
||||
struct McpToolApprovalMetadata {
|
||||
annotations: ToolAnnotations,
|
||||
annotations: Option<ToolAnnotations>,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
@@ -292,21 +336,39 @@ async fn maybe_request_mcp_tool_approval(
|
||||
call_id: &str,
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
if is_full_access_mode(turn_context) {
|
||||
if approval_mode == AppToolApproval::Approve {
|
||||
return None;
|
||||
}
|
||||
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
|
||||
if approval_mode == AppToolApproval::Auto {
|
||||
if is_full_access_mode(turn_context) {
|
||||
return None;
|
||||
}
|
||||
if !annotations.is_some_and(requires_mcp_tool_approval) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = lookup_mcp_tool_metadata(sess, server, tool_name).await?;
|
||||
if !requires_mcp_tool_approval(&metadata.annotations) {
|
||||
return None;
|
||||
}
|
||||
let approval_key = McpToolApprovalKey {
|
||||
server: server.to_string(),
|
||||
connector_id: metadata.connector_id.clone(),
|
||||
tool_name: tool_name.to_string(),
|
||||
let approval_key = if approval_mode == AppToolApproval::Auto {
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.clone());
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME && connector_id.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(McpToolApprovalKey {
|
||||
server: server.to_string(),
|
||||
connector_id,
|
||||
tool_name: tool_name.to_string(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if mcp_tool_approval_is_remembered(sess, &approval_key).await {
|
||||
if let Some(key) = approval_key.as_ref()
|
||||
&& mcp_tool_approval_is_remembered(sess, key).await
|
||||
{
|
||||
return Some(McpToolApprovalDecision::Accept);
|
||||
}
|
||||
|
||||
@@ -315,10 +377,10 @@ async fn maybe_request_mcp_tool_approval(
|
||||
question_id.clone(),
|
||||
server,
|
||||
tool_name,
|
||||
metadata.tool_title.as_deref(),
|
||||
metadata.connector_name.as_deref(),
|
||||
&metadata.annotations,
|
||||
true,
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
annotations,
|
||||
approval_key.is_some(),
|
||||
);
|
||||
let args = RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
@@ -326,9 +388,14 @@ async fn maybe_request_mcp_tool_approval(
|
||||
let response = sess
|
||||
.request_user_input(turn_context, call_id.to_string(), args)
|
||||
.await;
|
||||
let decision = parse_mcp_tool_approval_response(response, &question_id);
|
||||
if matches!(decision, McpToolApprovalDecision::AcceptAndRemember) {
|
||||
remember_mcp_tool_approval(sess, approval_key).await;
|
||||
let decision = normalize_approval_decision_for_mode(
|
||||
parse_mcp_tool_approval_response(response, &question_id),
|
||||
approval_mode,
|
||||
);
|
||||
if matches!(decision, McpToolApprovalDecision::AcceptAndRemember)
|
||||
&& let Some(key) = approval_key
|
||||
{
|
||||
remember_mcp_tool_approval(sess, key).await;
|
||||
}
|
||||
Some(decision)
|
||||
}
|
||||
@@ -356,15 +423,12 @@ async fn lookup_mcp_tool_metadata(
|
||||
|
||||
tools.into_values().find_map(|tool_info| {
|
||||
if tool_info.server_name == server && tool_info.tool_name == tool_name {
|
||||
tool_info
|
||||
.tool
|
||||
.annotations
|
||||
.map(|annotations| McpToolApprovalMetadata {
|
||||
annotations,
|
||||
connector_id: tool_info.connector_id,
|
||||
connector_name: tool_info.connector_name,
|
||||
tool_title: tool_info.tool.title,
|
||||
})
|
||||
Some(McpToolApprovalMetadata {
|
||||
annotations: tool_info.tool.annotations,
|
||||
connector_id: tool_info.connector_id,
|
||||
connector_name: tool_info.connector_name,
|
||||
tool_title: tool_info.tool.title,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -402,11 +466,12 @@ fn build_mcp_tool_approval_question(
|
||||
tool_name: &str,
|
||||
tool_title: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
annotations: &ToolAnnotations,
|
||||
annotations: Option<&ToolAnnotations>,
|
||||
allow_remember_option: bool,
|
||||
) -> RequestUserInputQuestion {
|
||||
let destructive = annotations.destructive_hint == Some(true);
|
||||
let open_world = annotations.open_world_hint == Some(true);
|
||||
let destructive =
|
||||
annotations.and_then(|annotations| annotations.destructive_hint) == Some(true);
|
||||
let open_world = annotations.and_then(|annotations| annotations.open_world_hint) == Some(true);
|
||||
let reason = match (destructive, open_world) {
|
||||
(true, true) => "may modify data and access external systems",
|
||||
(true, false) => "may modify or delete data",
|
||||
@@ -493,6 +558,19 @@ fn parse_mcp_tool_approval_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_approval_decision_for_mode(
|
||||
decision: McpToolApprovalDecision,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> McpToolApprovalDecision {
|
||||
if approval_mode == AppToolApproval::Prompt
|
||||
&& decision == McpToolApprovalDecision::AcceptAndRemember
|
||||
{
|
||||
McpToolApprovalDecision::Accept
|
||||
} else {
|
||||
decision
|
||||
}
|
||||
}
|
||||
|
||||
async fn mcp_tool_approval_is_remembered(sess: &Session, key: &McpToolApprovalKey) -> bool {
|
||||
let store = sess.services.tool_approvals.lock().await;
|
||||
matches!(store.get(key), Some(ReviewDecision::ApprovedForSession))
|
||||
@@ -568,6 +646,17 @@ mod tests {
|
||||
assert_eq!(requires_mcp_tool_approval(&annotations), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_mode_does_not_allow_session_remember() {
|
||||
assert_eq!(
|
||||
normalize_approval_decision_for_mode(
|
||||
McpToolApprovalDecision::AcceptAndRemember,
|
||||
AppToolApproval::Prompt,
|
||||
),
|
||||
McpToolApprovalDecision::Accept
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_mcp_tool_question_mentions_server_name() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
@@ -576,7 +665,7 @@ mod tests {
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
&annotations(Some(false), Some(true), None),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -603,7 +692,7 @@ mod tests {
|
||||
"run_action",
|
||||
Some("Run Action"),
|
||||
None,
|
||||
&annotations(Some(false), Some(true), None),
|
||||
Some(&annotations(Some(false), Some(true), None)),
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ impl ToolHandler for SearchToolBm25Handler {
|
||||
&turn.config,
|
||||
);
|
||||
let mcp_tools = filter_codex_apps_mcp_tools(mcp_tools, &connectors);
|
||||
let mcp_tools = connectors::filter_codex_apps_tools_by_policy(mcp_tools, &turn.config);
|
||||
|
||||
let mut entries: Vec<ToolEntry> = mcp_tools
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user