Compare commits

...

1 Commits

Author SHA1 Message Date
kylepatel-oai
5e86a50f84 Add prompt-for-writes MCP tool approval mode 2026-05-20 16:59:23 -07:00
20 changed files with 599 additions and 19 deletions

View File

@@ -6218,6 +6218,7 @@
"enum": [
"auto",
"prompt",
"prompt_for_writes",
"approve"
],
"type": "string"

View File

@@ -614,6 +614,7 @@
"enum": [
"auto",
"prompt",
"prompt_for_writes",
"approve"
],
"type": "string"

View File

@@ -68,6 +68,7 @@
"enum": [
"auto",
"prompt",
"prompt_for_writes",
"approve"
],
"type": "string"

View File

@@ -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";

View File

@@ -172,6 +172,7 @@ pub struct AnalyticsConfig {
pub enum AppToolApproval {
Auto,
Prompt,
PromptForWrites,
Approve,
}

View File

@@ -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");

View File

@@ -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}");

View File

@@ -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;

View File

@@ -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());

View File

@@ -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",
});
}

View File

@@ -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"
"#
);

View File

@@ -19,6 +19,7 @@ pub enum AppToolApproval {
#[default]
Auto,
Prompt,
PromptForWrites,
Approve,
}

View File

@@ -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(

View File

@@ -142,6 +142,7 @@
"enum": [
"auto",
"prompt",
"prompt_for_writes",
"approve"
],
"type": "string"
@@ -4883,4 +4884,4 @@
},
"title": "ConfigToml",
"type": "object"
}
}

View File

@@ -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>(

View File

@@ -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",
});
}

View File

@@ -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");

View File

@@ -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,

View File

@@ -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

View File

@@ -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();