mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
Compare commits
1 Commits
btraut/app
...
dev/kylepa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e86a50f84 |
@@ -6218,6 +6218,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"prompt_for_writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -614,6 +614,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"prompt_for_writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"prompt_for_writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -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 AppToolApproval = "auto" | "prompt" | "approve";
|
||||
export type AppToolApproval = "auto" | "prompt" | "prompt_for_writes" | "approve";
|
||||
|
||||
@@ -172,6 +172,7 @@ pub struct AnalyticsConfig {
|
||||
pub enum AppToolApproval {
|
||||
Auto,
|
||||
Prompt,
|
||||
PromptForWrites,
|
||||
Approve,
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,43 @@ async fn write_value_supports_nested_app_paths() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_supports_prompt_for_writes_app_default_tool_approval_mode() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
|
||||
|
||||
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
key_path: "apps.app1.default_tools_approval_mode".to_string(),
|
||||
value: serde_json::json!("prompt_for_writes"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect("write apps.app1.default_tools_approval_mode succeeds");
|
||||
|
||||
let read = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("config read succeeds");
|
||||
|
||||
assert_eq!(
|
||||
read.config.apps.and_then(|apps| {
|
||||
apps.apps
|
||||
.get("app1")
|
||||
.and_then(|app| app.default_tools_approval_mode)
|
||||
}),
|
||||
Some(AppToolApproval::PromptForWrites)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
@@ -234,6 +271,49 @@ async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_value_supports_prompt_for_writes_mcp_default_tool_approval_mode() -> Result<()> {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(
|
||||
tmp.path().join(CONFIG_TOML_FILE),
|
||||
"[mcp_servers.docs]\ncommand = \"docs-server\"\n",
|
||||
)?;
|
||||
|
||||
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
|
||||
service
|
||||
.write_value(ConfigValueWriteParams {
|
||||
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
|
||||
key_path: "mcp_servers.docs.default_tools_approval_mode".to_string(),
|
||||
value: serde_json::json!("prompt_for_writes"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: None,
|
||||
})
|
||||
.await
|
||||
.expect("write mcp server default_tools_approval_mode succeeds");
|
||||
|
||||
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE))?;
|
||||
assert!(contents.contains("default_tools_approval_mode = \"prompt_for_writes\""));
|
||||
|
||||
let read = service
|
||||
.read(ConfigReadParams {
|
||||
include_layers: false,
|
||||
cwd: None,
|
||||
})
|
||||
.await
|
||||
.expect("config read succeeds");
|
||||
|
||||
assert_eq!(
|
||||
read.config
|
||||
.additional
|
||||
.get("mcp_servers")
|
||||
.and_then(|servers| servers.get("docs"))
|
||||
.and_then(|docs| docs.get("default_tools_approval_mode")),
|
||||
Some(&serde_json::json!("prompt_for_writes"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_includes_origins_and_layers() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -896,6 +896,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re
|
||||
let approval_mode = match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
};
|
||||
println!(" default_tools_approval_mode: {approval_mode}");
|
||||
|
||||
@@ -75,6 +75,9 @@ pub fn mcp_permission_prompt_is_auto_approved(
|
||||
if context.tool_approval_mode == Some(AppToolApproval::Approve) {
|
||||
return true;
|
||||
}
|
||||
if context.tool_approval_mode == Some(AppToolApproval::PromptForWrites) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if approval_policy != AskForApproval::Never {
|
||||
return false;
|
||||
|
||||
@@ -123,6 +123,18 @@ fn mcp_prompt_auto_approval_rejects_auto_mode_in_default_permission_mode() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_prompt_auto_approval_rejects_prompt_for_writes_even_with_full_access() {
|
||||
assert!(!mcp_permission_prompt_is_auto_approved(
|
||||
AskForApproval::Never,
|
||||
&PermissionProfile::Disabled,
|
||||
McpPermissionPromptAutoApproveContext {
|
||||
approvals_reviewer: Some(ApprovalsReviewer::User),
|
||||
tool_approval_mode: Some(AppToolApproval::PromptForWrites),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
||||
let mut config = test_mcp_config(PathBuf::new());
|
||||
|
||||
@@ -194,6 +194,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
||||
entry["default_tools_approval_mode"] = value(match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
});
|
||||
}
|
||||
@@ -238,6 +239,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem {
|
||||
tool_entry["approval_mode"] = value(match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ async fn replace_mcp_servers_serializes_per_tool_approval_overrides() -> anyhow:
|
||||
approval_mode: Some(AppToolApproval::Prompt),
|
||||
},
|
||||
),
|
||||
(
|
||||
"write".to_string(),
|
||||
McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::PromptForWrites),
|
||||
},
|
||||
),
|
||||
]),
|
||||
},
|
||||
)]);
|
||||
@@ -74,6 +80,9 @@ approval_mode = "prompt"
|
||||
|
||||
[mcp_servers.docs.tools.search]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.docs.tools.write]
|
||||
approval_mode = "prompt_for_writes"
|
||||
"#
|
||||
);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub enum AppToolApproval {
|
||||
#[default]
|
||||
Auto,
|
||||
Prompt,
|
||||
PromptForWrites,
|
||||
Approve,
|
||||
}
|
||||
|
||||
|
||||
@@ -363,6 +363,39 @@ fn deserialize_server_config_with_default_tool_approval_mode() {
|
||||
assert_eq!(round_tripped, cfg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_server_config_with_prompt_for_writes_tool_approval_mode() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
r#"
|
||||
command = "echo"
|
||||
default_tools_approval_mode = "prompt_for_writes"
|
||||
|
||||
[tools.write]
|
||||
approval_mode = "prompt_for_writes"
|
||||
"#,
|
||||
)
|
||||
.expect("should deserialize prompt_for_writes approval mode");
|
||||
|
||||
assert_eq!(
|
||||
cfg.default_tools_approval_mode,
|
||||
Some(AppToolApproval::PromptForWrites)
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.tools.get("write"),
|
||||
Some(&McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::PromptForWrites),
|
||||
})
|
||||
);
|
||||
|
||||
let serialized = toml::to_string(&cfg).expect("should serialize MCP config");
|
||||
assert!(serialized.contains("default_tools_approval_mode = \"prompt_for_writes\""));
|
||||
assert!(serialized.contains("approval_mode = \"prompt_for_writes\""));
|
||||
|
||||
let round_tripped: McpServerConfig =
|
||||
toml::from_str(&serialized).expect("should deserialize serialized MCP config");
|
||||
assert_eq!(round_tripped, cfg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_round_trips_server_config_with_parallel_tool_calls() {
|
||||
let cfg: McpServerConfig = toml::from_str(
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"prompt",
|
||||
"prompt_for_writes",
|
||||
"approve"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -4883,4 +4884,4 @@
|
||||
},
|
||||
"title": "ConfigToml",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -5126,6 +5126,38 @@ approval_mode = "approve"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_servers_toml_parses_prompt_for_writes_approval_overrides() {
|
||||
let config = toml::from_str::<ConfigToml>(
|
||||
r#"
|
||||
[mcp_servers.docs]
|
||||
command = "docs-server"
|
||||
name = "Docs"
|
||||
default_tools_approval_mode = "prompt_for_writes"
|
||||
|
||||
[mcp_servers.docs.tools.write]
|
||||
approval_mode = "prompt_for_writes"
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let server = config
|
||||
.mcp_servers
|
||||
.get("docs")
|
||||
.expect("docs server config exists");
|
||||
|
||||
assert_eq!(
|
||||
server.default_tools_approval_mode,
|
||||
Some(AppToolApproval::PromptForWrites)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server.tools.get("write"),
|
||||
Some(&McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::PromptForWrites),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_servers_toml_ignores_unknown_server_fields() {
|
||||
let config = toml::from_str::<ConfigToml>(
|
||||
|
||||
@@ -321,6 +321,7 @@ mod document_helpers {
|
||||
entry["default_tools_approval_mode"] = value(match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
});
|
||||
}
|
||||
@@ -373,6 +374,7 @@ mod document_helpers {
|
||||
entry["approval_mode"] = value(match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1073,6 +1073,55 @@ approval_mode = \"approve\"
|
||||
assert_eq!(raw, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_replace_mcp_servers_serializes_prompt_for_writes_default() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
let mut servers = BTreeMap::new();
|
||||
servers.insert(
|
||||
"docs".to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: "docs-server".to_string(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: Some(AppToolApproval::PromptForWrites),
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
);
|
||||
|
||||
apply_blocking(
|
||||
codex_home,
|
||||
/*profile*/ None,
|
||||
&[ConfigEdit::ReplaceMcpServers(servers)],
|
||||
)
|
||||
.expect("persist");
|
||||
|
||||
let raw = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = "\
|
||||
[mcp_servers.docs]
|
||||
command = \"docs-server\"
|
||||
default_tools_approval_mode = \"prompt_for_writes\"
|
||||
";
|
||||
assert_eq!(raw, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_replace_mcp_servers_preserves_inline_comments() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -1149,21 +1149,27 @@ async fn maybe_request_mcp_tool_approval(
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalDecision> {
|
||||
if mcp_permission_prompt_is_auto_approved(
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
McpPermissionPromptAutoApproveContext {
|
||||
approvals_reviewer: Some(turn_context.config.approvals_reviewer),
|
||||
tool_approval_mode: Some(approval_mode),
|
||||
},
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let annotations = metadata.and_then(|metadata| metadata.annotations.as_ref());
|
||||
let approval_required = requires_mcp_tool_approval(annotations);
|
||||
if !approval_required && approval_mode != AppToolApproval::Prompt {
|
||||
return None;
|
||||
if approval_mode == AppToolApproval::PromptForWrites {
|
||||
if mcp_tool_is_read_only(annotations) {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
if mcp_permission_prompt_is_auto_approved(
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
McpPermissionPromptAutoApproveContext {
|
||||
approvals_reviewer: Some(turn_context.config.approvals_reviewer),
|
||||
tool_approval_mode: Some(approval_mode),
|
||||
},
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let approval_required = requires_mcp_tool_approval(annotations);
|
||||
if !approval_required && approval_mode != AppToolApproval::Prompt {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let session_approval_key = session_mcp_tool_approval_key(invocation, metadata, approval_mode);
|
||||
@@ -1205,7 +1211,9 @@ async fn maybe_request_mcp_tool_approval(
|
||||
.features
|
||||
.enabled(Feature::ToolCallMcpElicitation);
|
||||
|
||||
if routes_approval_to_guardian(turn_context) {
|
||||
if approval_mode != AppToolApproval::PromptForWrites
|
||||
&& routes_approval_to_guardian(turn_context)
|
||||
{
|
||||
let review_id = new_guardian_review_id();
|
||||
let decision = review_approval_request(
|
||||
sess,
|
||||
@@ -1320,7 +1328,10 @@ fn session_mcp_tool_approval_key(
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
approval_mode: AppToolApproval,
|
||||
) -> Option<McpToolApprovalKey> {
|
||||
if approval_mode != AppToolApproval::Auto {
|
||||
if !matches!(
|
||||
approval_mode,
|
||||
AppToolApproval::Auto | AppToolApproval::PromptForWrites
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2087,6 +2098,10 @@ fn requires_mcp_tool_approval(annotations: Option<&ToolAnnotations>) -> bool {
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn mcp_tool_is_read_only(annotations: Option<&ToolAnnotations>) -> bool {
|
||||
annotations.and_then(|annotations| annotations.read_only_hint) == Some(true)
|
||||
}
|
||||
|
||||
async fn notify_mcp_tool_call_skip(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
|
||||
@@ -405,6 +405,24 @@ fn prompt_mode_does_not_allow_persistent_remember() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_for_writes_allows_persistent_remember() {
|
||||
assert_eq!(
|
||||
normalize_approval_decision_for_mode(
|
||||
McpToolApprovalDecision::AcceptForSession,
|
||||
AppToolApproval::PromptForWrites,
|
||||
),
|
||||
McpToolApprovalDecision::AcceptForSession
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_approval_decision_for_mode(
|
||||
McpToolApprovalDecision::AcceptAndRemember,
|
||||
AppToolApproval::PromptForWrites,
|
||||
),
|
||||
McpToolApprovalDecision::AcceptAndRemember
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_call_span_records_expected_fields() {
|
||||
let buffer: &'static std::sync::Mutex<Vec<u8>> =
|
||||
@@ -861,6 +879,22 @@ fn custom_servers_support_session_and_persistent_approval() {
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::Auto
|
||||
),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::PromptForWrites
|
||||
),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::PromptForWrites
|
||||
),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
@@ -891,6 +925,22 @@ fn codex_apps_connectors_support_persistent_approval() {
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(&invocation, Some(&metadata), AppToolApproval::Auto),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
session_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
AppToolApproval::PromptForWrites
|
||||
),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
persistent_mcp_tool_approval_key(
|
||||
&invocation,
|
||||
Some(&metadata),
|
||||
AppToolApproval::PromptForWrites
|
||||
),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
@@ -1914,6 +1964,78 @@ async fn persist_custom_mcp_tool_approval_writes_tool_override() {
|
||||
assert!(contents.contains("[mcp_servers.docs.tools.search]"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_for_writes_session_approval_is_remembered() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let invocation = McpInvocation {
|
||||
server: "docs".to_string(),
|
||||
tool: "write".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let key = session_mcp_tool_approval_key(&invocation, None, AppToolApproval::PromptForWrites)
|
||||
.expect("prompt_for_writes should support session approval");
|
||||
|
||||
apply_mcp_tool_approval_decision(
|
||||
&session,
|
||||
&turn_context,
|
||||
&McpToolApprovalDecision::AcceptForSession,
|
||||
Some(key.clone()),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(mcp_tool_approval_is_remembered(&session, &key).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_for_writes_always_allow_persists_approve_override() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
std::fs::write(
|
||||
tmp.path().join(CONFIG_TOML_FILE),
|
||||
"[mcp_servers.docs]\ncommand = \"docs-server\"\n",
|
||||
)
|
||||
.expect("seed config");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(tmp.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("load config");
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
turn_context.config = Arc::new(config);
|
||||
let invocation = McpInvocation {
|
||||
server: "docs".to_string(),
|
||||
tool: "write".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let key = persistent_mcp_tool_approval_key(&invocation, None, AppToolApproval::PromptForWrites)
|
||||
.expect("prompt_for_writes should support persistent approval");
|
||||
|
||||
apply_mcp_tool_approval_decision(
|
||||
&session,
|
||||
&turn_context,
|
||||
&McpToolApprovalDecision::AcceptAndRemember,
|
||||
None,
|
||||
Some(key.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let parsed: ConfigToml = toml::from_str(&contents).expect("parse config");
|
||||
let tool = parsed
|
||||
.mcp_servers
|
||||
.get("docs")
|
||||
.and_then(|server| server.tools.get("write"))
|
||||
.expect("docs/write tool config exists");
|
||||
|
||||
assert_eq!(
|
||||
tool,
|
||||
&McpServerToolConfig {
|
||||
approval_mode: Some(AppToolApproval::Approve),
|
||||
}
|
||||
);
|
||||
assert!(mcp_tool_approval_is_remembered(&session, &key).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn custom_mcp_tool_approval_mode_uses_server_default_with_tool_override() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
@@ -2698,7 +2820,220 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
|
||||
async fn prompt_for_writes_skips_explicit_read_only_tool() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let invocation = McpInvocation {
|
||||
server: "custom_server".to_string(),
|
||||
tool: "read_only_tool".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let metadata = McpToolApprovalMetadata {
|
||||
annotations: Some(annotations(Some(true), Some(true), Some(true))),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_id: None,
|
||||
tool_title: Some("Read Only Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
let decision = maybe_request_mcp_tool_approval(
|
||||
&session,
|
||||
&turn_context,
|
||||
"call-prompt-for-writes-read",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::PromptForWrites,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(decision, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_for_writes_waits_for_approval_when_read_only_is_false() {
|
||||
let (session, turn_context, _rx_event) = make_session_and_context_with_rx().await;
|
||||
{
|
||||
let mut active_turn = session.active_turn.lock().await;
|
||||
*active_turn = Some(ActiveTurn::default());
|
||||
}
|
||||
let invocation = McpInvocation {
|
||||
server: "custom_server".to_string(),
|
||||
tool: "write_tool".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let metadata = McpToolApprovalMetadata {
|
||||
annotations: Some(annotations(
|
||||
Some(false),
|
||||
/*destructive*/ None,
|
||||
/*open_world*/ None,
|
||||
)),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_id: None,
|
||||
tool_title: Some("Write Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
let mut approval_task = {
|
||||
let session = Arc::clone(&session);
|
||||
let turn_context = Arc::clone(&turn_context);
|
||||
tokio::spawn(async move {
|
||||
maybe_request_mcp_tool_approval(
|
||||
&session,
|
||||
&turn_context,
|
||||
"call-prompt-for-writes-false",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::PromptForWrites,
|
||||
)
|
||||
.await
|
||||
})
|
||||
};
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(200), &mut approval_task)
|
||||
.await
|
||||
.is_err(),
|
||||
"prompt_for_writes should wait for approval when read_only_hint is false"
|
||||
);
|
||||
approval_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_for_writes_waits_for_approval_when_annotations_are_absent() {
|
||||
let (session, turn_context, _rx_event) = make_session_and_context_with_rx().await;
|
||||
{
|
||||
let mut active_turn = session.active_turn.lock().await;
|
||||
*active_turn = Some(ActiveTurn::default());
|
||||
}
|
||||
let invocation = McpInvocation {
|
||||
server: "custom_server".to_string(),
|
||||
tool: "unknown_tool".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
|
||||
let mut approval_task = {
|
||||
let session = Arc::clone(&session);
|
||||
let turn_context = Arc::clone(&turn_context);
|
||||
tokio::spawn(async move {
|
||||
maybe_request_mcp_tool_approval(
|
||||
&session,
|
||||
&turn_context,
|
||||
"call-prompt-for-writes-absent",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
/*metadata*/ None,
|
||||
AppToolApproval::PromptForWrites,
|
||||
)
|
||||
.await
|
||||
})
|
||||
};
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(200), &mut approval_task)
|
||||
.await
|
||||
.is_err(),
|
||||
"prompt_for_writes should wait for approval when read_only_hint is absent"
|
||||
);
|
||||
approval_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_for_writes_bypasses_guardian_for_write_prompts() {
|
||||
use wiremock::Mock;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
let server = start_mock_server().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.expect(0)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let (mut session, mut turn_context) = make_session_and_context().await;
|
||||
{
|
||||
let mut active_turn = session.active_turn.lock().await;
|
||||
*active_turn = Some(ActiveTurn::default());
|
||||
}
|
||||
turn_context
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
let mut config = (*turn_context.config).clone();
|
||||
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
let config = Arc::new(config);
|
||||
let models_manager = models_manager_with_provider(
|
||||
config.codex_home.to_path_buf(),
|
||||
Arc::clone(&session.services.auth_manager),
|
||||
config.model_provider.clone(),
|
||||
);
|
||||
session.services.models_manager = models_manager;
|
||||
turn_context.config = Arc::clone(&config);
|
||||
turn_context.provider = create_model_provider(
|
||||
config.model_provider.clone(),
|
||||
turn_context.auth_manager.clone(),
|
||||
);
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let invocation = McpInvocation {
|
||||
server: "custom_server".to_string(),
|
||||
tool: "write_tool".to_string(),
|
||||
arguments: None,
|
||||
};
|
||||
let metadata = McpToolApprovalMetadata {
|
||||
annotations: Some(annotations(Some(false), Some(true), Some(true))),
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
connector_description: None,
|
||||
plugin_id: None,
|
||||
tool_title: Some("Write Tool".to_string()),
|
||||
tool_description: None,
|
||||
mcp_app_resource_uri: None,
|
||||
codex_apps_meta: None,
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
let mut approval_task = tokio::spawn(async move {
|
||||
maybe_request_mcp_tool_approval(
|
||||
&session,
|
||||
&turn_context,
|
||||
"call-prompt-for-writes-guardian",
|
||||
&invocation,
|
||||
"mcp__test__tool",
|
||||
Some(&metadata),
|
||||
AppToolApproval::PromptForWrites,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
assert!(
|
||||
tokio::time::timeout(std::time::Duration::from_millis(200), &mut approval_task)
|
||||
.await
|
||||
.is_err(),
|
||||
"prompt_for_writes should show the user prompt instead of routing to guardian"
|
||||
);
|
||||
approval_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_access_mode_skips_mcp_tool_approval_for_existing_approval_modes() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
turn_context
|
||||
.approval_policy
|
||||
|
||||
@@ -43,6 +43,7 @@ fn set_calendar_approval_mode(config: &mut Config, approval_mode: AppToolApprova
|
||||
let approval_mode = match approval_mode {
|
||||
AppToolApproval::Auto => "auto",
|
||||
AppToolApproval::Prompt => "prompt",
|
||||
AppToolApproval::PromptForWrites => "prompt_for_writes",
|
||||
AppToolApproval::Approve => "approve",
|
||||
};
|
||||
let user_config_path = config.codex_home.join("config.toml").abs();
|
||||
|
||||
Reference in New Issue
Block a user