diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 7950007e0..87c28d0b7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -52,7 +52,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -81,9 +81,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 343393cd1..3eb6da033 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -5192,7 +5192,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -5221,9 +5221,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index edf308082..3512fd5c7 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -638,7 +638,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -667,9 +667,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index fb832b424..d0e5fb453 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -143,7 +143,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -172,9 +172,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 19d328f75..e0c8304c1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -15,7 +15,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -44,9 +44,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 9d765cc86..328dca36b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -15,7 +15,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -44,9 +44,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 82532ca0c..049739320 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -19,7 +19,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -48,9 +48,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 1b54a95a0..37f5f1df6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -15,7 +15,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -44,9 +44,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 6a1ec1ccc..be1c33d52 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -19,7 +19,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -48,9 +48,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 630176f8c..e614ccf15 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -15,7 +15,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -44,9 +44,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 3382e76b1..85c13afe4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -19,7 +19,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -48,9 +48,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index 2ea5881a2..0d26176f9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -19,7 +19,7 @@ { "additionalProperties": false, "properties": { - "reject": { + "granular": { "properties": { "mcp_elicitations": { "type": "boolean" @@ -48,9 +48,9 @@ } }, "required": [ - "reject" + "granular" ], - "title": "RejectAskForApproval", + "title": "GranularAskForApproval", "type": "object" } ] diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts index 55415eaea..8d41214e0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AskForApproval.ts @@ -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 AskForApproval = "untrusted" | "on-failure" | "on-request" | { "reject": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never"; +export type AskForApproval = "untrusted" | "on-failure" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e5daf8568..49c2d321e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -50,6 +50,7 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; +use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; use codex_protocol::protocol::HookEventName as CoreHookEventName; use codex_protocol::protocol::HookExecutionMode as CoreHookExecutionMode; use codex_protocol::protocol::HookHandlerType as CoreHookHandlerType; @@ -65,7 +66,6 @@ use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; -use codex_protocol::protocol::RejectConfig as CoreRejectConfig; use codex_protocol::protocol::ReviewDecision as CoreReviewDecision; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; @@ -201,8 +201,8 @@ pub enum AskForApproval { UnlessTrusted, OnFailure, OnRequest, - #[experimental("askForApproval.reject")] - Reject { + #[experimental("askForApproval.granular")] + Granular { sandbox_approval: bool, rules: bool, #[serde(default)] @@ -220,13 +220,13 @@ impl AskForApproval { AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted, AskForApproval::OnFailure => CoreAskForApproval::OnFailure, AskForApproval::OnRequest => CoreAskForApproval::OnRequest, - AskForApproval::Reject { + AskForApproval::Granular { sandbox_approval, rules, skill_approval, request_permissions, mcp_elicitations, - } => CoreAskForApproval::Reject(CoreRejectConfig { + } => CoreAskForApproval::Granular(CoreGranularApprovalConfig { sandbox_approval, rules, skill_approval, @@ -244,12 +244,12 @@ impl From for AskForApproval { CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted, CoreAskForApproval::OnFailure => AskForApproval::OnFailure, CoreAskForApproval::OnRequest => AskForApproval::OnRequest, - CoreAskForApproval::Reject(reject_config) => AskForApproval::Reject { - sandbox_approval: reject_config.sandbox_approval, - rules: reject_config.rules, - skill_approval: reject_config.skill_approval, - request_permissions: reject_config.request_permissions, - mcp_elicitations: reject_config.mcp_elicitations, + CoreAskForApproval::Granular(granular_config) => AskForApproval::Granular { + sandbox_approval: granular_config.sandbox_approval, + rules: granular_config.rules, + skill_approval: granular_config.skill_approval, + request_permissions: granular_config.request_permissions, + mcp_elicitations: granular_config.mcp_elicitations, }, CoreAskForApproval::Never => AskForApproval::Never, } @@ -6192,8 +6192,8 @@ mod tests { } #[test] - fn ask_for_approval_reject_round_trips_request_permissions_flag() { - let v2_policy = AskForApproval::Reject { + fn ask_for_approval_granular_round_trips_request_permissions_flag() { + let v2_policy = AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6204,7 +6204,7 @@ mod tests { let core_policy = v2_policy.to_core(); assert_eq!( core_policy, - CoreAskForApproval::Reject(CoreRejectConfig { + CoreAskForApproval::Granular(CoreGranularApprovalConfig { sandbox_approval: true, rules: false, skill_approval: false, @@ -6218,19 +6218,19 @@ mod tests { } #[test] - fn ask_for_approval_reject_defaults_missing_optional_flags_to_false() { + fn ask_for_approval_granular_defaults_missing_optional_flags_to_false() { let decoded = serde_json::from_value::(serde_json::json!({ - "reject": { + "granular": { "sandbox_approval": true, "rules": false, "mcp_elicitations": true, } })) - .expect("legacy reject approval policy should deserialize"); + .expect("granular approval policy should deserialize"); assert_eq!( decoded, - AskForApproval::Reject { + AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6241,9 +6241,9 @@ mod tests { } #[test] - fn ask_for_approval_reject_is_marked_experimental() { + fn ask_for_approval_granular_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( - &AskForApproval::Reject { + &AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6252,7 +6252,7 @@ mod tests { }, ); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); assert_eq!( crate::experimental_api::ExperimentalApi::experimental_reason( &AskForApproval::OnRequest, @@ -6262,11 +6262,11 @@ mod tests { } #[test] - fn profile_v2_reject_approval_policy_is_marked_experimental() { + fn profile_v2_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ProfileV2 { model: None, model_provider: None, - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6283,18 +6283,18 @@ mod tests { additional: HashMap::new(), }); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn config_reject_approval_policy_is_marked_experimental() { + fn config_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { model: None, review_model: None, model_context_window: None, model_auto_compact_token_limit: None, model_provider: None, - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: false, rules: true, skill_approval: false, @@ -6321,11 +6321,11 @@ mod tests { additional: HashMap::new(), }); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn config_nested_profile_reject_approval_policy_is_marked_experimental() { + fn config_nested_profile_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&Config { model: None, review_model: None, @@ -6345,7 +6345,7 @@ mod tests { ProfileV2 { model: None, model_provider: None, - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6374,14 +6374,14 @@ mod tests { additional: HashMap::new(), }); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn config_requirements_reject_allowed_approval_policy_is_marked_experimental() { + fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements { - allowed_approval_policies: Some(vec![AskForApproval::Reject { + allowed_approval_policies: Some(vec![AskForApproval::Granular { sandbox_approval: true, rules: true, skill_approval: false, @@ -6395,16 +6395,16 @@ mod tests { network: None, }); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn client_request_thread_start_reject_approval_policy_is_marked_experimental() { + fn client_request_thread_start_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( &crate::ClientRequest::ThreadStart { request_id: crate::RequestId::Integer(1), params: ThreadStartParams { - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6416,17 +6416,17 @@ mod tests { }, ); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn client_request_thread_resume_reject_approval_policy_is_marked_experimental() { + fn client_request_thread_resume_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( &crate::ClientRequest::ThreadResume { request_id: crate::RequestId::Integer(2), params: ThreadResumeParams { thread_id: "thr_123".to_string(), - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: false, rules: true, skill_approval: false, @@ -6438,17 +6438,17 @@ mod tests { }, ); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn client_request_thread_fork_reject_approval_policy_is_marked_experimental() { + fn client_request_thread_fork_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( &crate::ClientRequest::ThreadFork { request_id: crate::RequestId::Integer(3), params: ThreadForkParams { thread_id: "thr_456".to_string(), - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -6460,18 +6460,18 @@ mod tests { }, ); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] - fn client_request_turn_start_reject_approval_policy_is_marked_experimental() { + fn client_request_turn_start_granular_approval_policy_is_marked_experimental() { let reason = crate::experimental_api::ExperimentalApi::experimental_reason( &crate::ClientRequest::TurnStart { request_id: crate::RequestId::Integer(4), params: TurnStartParams { thread_id: "thr_123".to_string(), input: Vec::new(), - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: false, rules: true, skill_approval: false, @@ -6483,7 +6483,7 @@ mod tests { }, ); - assert_eq!(reason, Some("askForApproval.reject")); + assert_eq!(reason, Some("askForApproval.granular")); } #[test] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 8b2399a2d..881dff52a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -928,7 +928,7 @@ Only the granted subset matters on the wire. Any permissions omitted from `resul Within the same turn, granted permissions are sticky: later shell-like tool calls can automatically reuse the granted subset without reissuing a separate permission request. -If the session approval policy uses `Reject` with `request_permissions: true`, standalone `request_permissions` tool calls are auto-denied and no `item/permissions/requestApproval` prompt is sent. Inline `with_additional_permissions` command requests remain controlled by `sandbox_approval`, and any previously granted permissions remain sticky for later shell-like calls in the same turn. +If the session approval policy uses `Granular` with `request_permissions: false`, standalone `request_permissions` tool calls are auto-denied and no `item/permissions/requestApproval` prompt is sent. Inline `with_additional_permissions` command requests remain controlled by `sandbox_approval`, and any previously granted permissions remain sticky for later shell-like calls in the same turn. ### Dynamic tool calls (experimental) @@ -1319,7 +1319,7 @@ Examples of descriptor strings: - `mock/experimentalMethod` (method-level gate) - `thread/start.mockExperimentalField` (field-level gate) -- `askForApproval.reject` (enum-variant gate, for `approvalPolicy: { "reject": ... }`) +- `askForApproval.granular` (enum-variant gate, for `approvalPolicy: { "granular": ... }`) ### For maintainers: Adding experimental fields and methods @@ -1341,8 +1341,8 @@ Enum variants can be gated too: ```rust #[derive(ExperimentalApi)] enum AskForApproval { - #[experimental("askForApproval.reject")] - Reject { /* ... */ }, + #[experimental("askForApproval.granular")] + Granular { /* ... */ }, } ``` diff --git a/codex-rs/app-server/tests/suite/v2/experimental_api.rs b/codex-rs/app-server/tests/suite/v2/experimental_api.rs index aeb23814a..29ee7f1ac 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_api.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_api.rs @@ -159,7 +159,8 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa } #[tokio::test] -async fn thread_start_reject_approval_policy_requires_experimental_api_capability() -> Result<()> { +async fn thread_start_granular_approval_policy_requires_experimental_api_capability() -> Result<()> +{ let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let codex_home = TempDir::new()?; create_config_toml(codex_home.path(), &server.uri())?; @@ -180,7 +181,7 @@ async fn thread_start_reject_approval_policy_requires_experimental_api_capabilit let request_id = mcp .send_thread_start_request(ThreadStartParams { - approval_policy: Some(AskForApproval::Reject { + approval_policy: Some(AskForApproval::Granular { sandbox_approval: true, rules: false, skill_approval: false, @@ -196,7 +197,7 @@ async fn thread_start_reject_approval_policy_requires_experimental_api_capabilit mcp.read_stream_until_error_message(RequestId::Integer(request_id)), ) .await??; - assert_experimental_capability_error(error, "askForApproval.reject"); + assert_experimental_capability_error(error, "askForApproval.granular"); Ok(()) } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 84509cda0..5a0353b5d 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -231,14 +231,14 @@ }, { "additionalProperties": false, - "description": "Fine-grained rejection controls for approval prompts.\n\nWhen a field is `true`, prompts of that category are automatically rejected instead of shown to the user.", + "description": "Fine-grained controls for individual approval flows.\n\nWhen a field is `true`, commands in that category are allowed. When it is `false`, those requests are automatically rejected instead of shown to the user.", "properties": { - "reject": { - "$ref": "#/definitions/RejectConfig" + "granular": { + "$ref": "#/definitions/GranularApprovalConfig" } }, "required": [ - "reject" + "granular" ], "type": "object" }, @@ -369,6 +369,9 @@ "enable_request_compression": { "type": "boolean" }, + "exec_permission_approvals": { + "type": "boolean" + }, "experimental_use_freeform_apply_patch": { "type": "boolean" }, @@ -651,6 +654,38 @@ }, "type": "object" }, + "GranularApprovalConfig": { + "properties": { + "mcp_elicitations": { + "description": "Whether to allow MCP elicitation prompts.", + "type": "boolean" + }, + "request_permissions": { + "default": false, + "description": "Whether to allow prompts triggered by the `request_permissions` tool.", + "type": "boolean" + }, + "rules": { + "description": "Whether to allow prompts triggered by execpolicy `prompt` rules.", + "type": "boolean" + }, + "sandbox_approval": { + "description": "Whether to allow shell command approval requests, including inline `with_additional_permissions` and `require_escalated` requests.", + "type": "boolean" + }, + "skill_approval": { + "default": false, + "description": "Whether to allow approval prompts triggered by skill script execution.", + "type": "boolean" + } + }, + "required": [ + "mcp_elicitations", + "rules", + "sandbox_approval" + ], + "type": "object" + }, "History": { "additionalProperties": false, "description": "Settings that govern if and what will be written to `~/.codex/history.jsonl`.", @@ -1336,38 +1371,6 @@ } ] }, - "RejectConfig": { - "properties": { - "mcp_elicitations": { - "description": "Reject MCP elicitation prompts.", - "type": "boolean" - }, - "request_permissions": { - "default": false, - "description": "Reject `request_permissions` tool requests.", - "type": "boolean" - }, - "rules": { - "description": "Reject prompts triggered by execpolicy `prompt` rules.", - "type": "boolean" - }, - "sandbox_approval": { - "description": "Reject shell command approval requests, including inline `with_additional_permissions` and `require_escalated` requests.", - "type": "boolean" - }, - "skill_approval": { - "default": false, - "description": "Reject approval prompts triggered by skill script execution.", - "type": "boolean" - } - }, - "required": [ - "mcp_elicitations", - "rules", - "sandbox_approval" - ], - "type": "object" - }, "SandboxMode": { "enum": [ "read-only", @@ -1877,6 +1880,9 @@ "enable_request_compression": { "type": "boolean" }, + "exec_permission_approvals": { + "type": "boolean" + }, "experimental_use_freeform_apply_patch": { "type": "boolean" }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4dbe86da1..0e5d6bc2a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2912,8 +2912,8 @@ impl Session { scope: PermissionGrantScope::Turn, }); } - AskForApproval::Reject(reject_config) - if reject_config.rejects_request_permissions() => + AskForApproval::Granular(granular_config) + if !granular_config.allows_request_permissions() => { return Some(RequestPermissionsResponse { permissions: PermissionProfile::default(), @@ -2923,7 +2923,7 @@ impl Session { AskForApproval::OnFailure | AskForApproval::OnRequest | AskForApproval::UnlessTrusted - | AskForApproval::Reject(_) => {} + | AskForApproval::Granular(_) => {} } let (tx_response, rx_response) = oneshot::channel(); @@ -3381,7 +3381,9 @@ impl Session { turn_context.approval_policy.value(), self.services.exec_policy.current().as_ref(), &turn_context.cwd, - turn_context.features.enabled(Feature::RequestPermissions), + turn_context + .features + .enabled(Feature::ExecPermissionApprovals), ) .into_text(), ); @@ -5565,7 +5567,7 @@ pub(crate) async fn run_turn( AskForApproval::UnlessTrusted | AskForApproval::OnFailure | AskForApproval::OnRequest - | AskForApproval::Reject(_) => "default", + | AskForApproval::Granular(_) => "default", } .to_string(); let session_start_request = codex_hooks::SessionStartRequest { @@ -5714,7 +5716,7 @@ pub(crate) async fn run_turn( AskForApproval::UnlessTrusted | AskForApproval::OnFailure | AskForApproval::OnRequest - | AskForApproval::Reject(_) => "default", + | AskForApproval::Granular(_) => "default", } .to_string(); let stop_request = codex_hooks::StopRequest { diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 81ac04d22..5e397d69a 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2231,18 +2231,18 @@ async fn notify_request_permissions_response_ignores_unmatched_call_id() { } #[tokio::test] -async fn request_permissions_emits_event_when_reject_policy_allows_requests() { +async fn request_permissions_emits_event_when_granular_policy_allows_requests() { let (session, mut turn_context, rx) = make_session_and_context_with_rx().await; *session.active_turn.lock().await = Some(ActiveTurn::default()); Arc::get_mut(&mut turn_context) .expect("single turn context ref") .approval_policy - .set(crate::protocol::AskForApproval::Reject( - crate::protocol::RejectConfig { + .set(crate::protocol::AskForApproval::Granular( + crate::protocol::GranularApprovalConfig { sandbox_approval: true, rules: true, - skill_approval: false, - request_permissions: false, + skill_approval: true, + request_permissions: true, mcp_elicitations: true, }, )) @@ -2306,19 +2306,19 @@ async fn request_permissions_emits_event_when_reject_policy_allows_requests() { } #[tokio::test] -async fn request_permissions_is_auto_denied_when_reject_policy_blocks_tool_requests() { +async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_requests() { let (session, mut turn_context, rx) = make_session_and_context_with_rx().await; *session.active_turn.lock().await = Some(ActiveTurn::default()); Arc::get_mut(&mut turn_context) .expect("single turn context ref") .approval_policy - .set(crate::protocol::AskForApproval::Reject( - crate::protocol::RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, + .set(crate::protocol::AskForApproval::Granular( + crate::protocol::GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: false, + mcp_elicitations: true, }, )) .expect("test setup should allow updating approval policy"); @@ -2355,7 +2355,7 @@ async fn request_permissions_is_auto_denied_when_reject_policy_blocks_tool_reque tokio::time::timeout(StdDuration::from_millis(100), rx.recv()) .await .is_err(), - "request_permissions should not emit an event when reject.request_permissions is set" + "request_permissions should not emit an event when granular.request_permissions is false" ); } diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index 00b3adf43..b0a87e186 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -75,7 +75,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid .expect("test setup should allow enabling guardian approvals"); session .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test setup should allow enabling request permissions"); turn_context_raw .sandbox_policy @@ -191,7 +191,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic .expect("test setup should allow enabling guardian approvals"); session .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test setup should allow enabling request permissions"); let session = Arc::new(session); let turn_context = Arc::new(turn_context_raw); diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 0d26c551e..a8689265b 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -45,7 +45,7 @@ fn build_permissions_update_item( next.approval_policy.value(), exec_policy, &next.cwd, - next.features.enabled(Feature::RequestPermissions), + next.features.enabled(Feature::ExecPermissionApprovals), )) } diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 2c9ba28b1..aa0691f88 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -38,9 +38,9 @@ use shlex::try_join as shlex_try_join; const PROMPT_CONFLICT_REASON: &str = "approval required by policy, but AskForApproval is set to Never"; const REJECT_SANDBOX_APPROVAL_REASON: &str = - "approval required by policy, but AskForApproval::Reject.sandbox_approval is set"; + "approval required by policy, but AskForApproval::Granular.sandbox_approval is false"; const REJECT_RULES_APPROVAL_REASON: &str = - "approval required by policy rule, but AskForApproval::Reject.rules is set"; + "approval required by policy rule, but AskForApproval::Granular.rules is false"; const RULES_DIR_NAME: &str = "rules"; const RULE_EXTENSION: &str = "rules"; const DEFAULT_POLICY_FILE: &str = "default.rules"; @@ -104,7 +104,7 @@ fn is_policy_match(rule_match: &RuleMatch) -> bool { /// current prompt to the user. /// /// `prompt_is_rule` distinguishes policy-rule prompts from sandbox/escalation -/// prompts so `Reject.rules` and `Reject.sandbox_approval` are honored +/// prompts so granular `rules` and `sandbox_approval` settings are honored /// independently. When both are present, policy-rule prompts take precedence. pub(crate) fn prompt_is_rejected_by_policy( approval_policy: AskForApproval, @@ -115,14 +115,14 @@ pub(crate) fn prompt_is_rejected_by_policy( AskForApproval::OnFailure => None, AskForApproval::OnRequest => None, AskForApproval::UnlessTrusted => None, - AskForApproval::Reject(reject_config) => { + AskForApproval::Granular(granular_config) => { if prompt_is_rule { - if reject_config.rejects_rules_approval() { + if !granular_config.allows_rules_approval() { Some(REJECT_RULES_APPROVAL_REASON) } else { None } - } else if reject_config.rejects_sandbox_approval() { + } else if !granular_config.allows_sandbox_approval() { Some(REJECT_SANDBOX_APPROVAL_REASON) } else { None @@ -519,7 +519,7 @@ pub fn render_decision_for_unmatched_command( AskForApproval::OnFailure | AskForApproval::OnRequest | AskForApproval::UnlessTrusted - | AskForApproval::Reject(_) => Decision::Prompt, + | AskForApproval::Granular(_) => Decision::Prompt, }; } @@ -554,7 +554,7 @@ pub fn render_decision_for_unmatched_command( } } } - AskForApproval::Reject(_) => match file_system_sandbox_policy.kind { + AskForApproval::Granular(_) => match file_system_sandbox_policy.kind { FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { // Mirror on-request behavior for unmatched commands; prompt-vs-reject is handled // by `prompt_is_rejected_by_policy`. diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index 8c7286635..aaf098951 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -9,7 +9,7 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::RejectConfig; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -766,18 +766,18 @@ async fn exec_approval_requirement_respects_approval_policy() { } #[test] -fn unmatched_reject_policy_still_prompts_for_restricted_sandbox_escalation() { +fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() { let command = vec!["madeup-cmd".to_string()]; assert_eq!( Decision::Prompt, render_decision_for_unmatched_command( - AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &SandboxPolicy::new_read_only_policy(), &read_only_file_system_sandbox_policy(), @@ -807,19 +807,19 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() { } #[tokio::test] -async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_sandbox_rejection_enabled() +async fn exec_approval_requirement_rejects_unmatched_sandbox_escalation_when_granular_sandbox_is_disabled() { let command = vec!["madeup-cmd".to_string()]; let requirement = ExecPolicyManager::default() .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, - approval_policy: AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + approval_policy: AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), sandbox_policy: &SandboxPolicy::new_read_only_policy(), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), @@ -853,12 +853,12 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision() let requirement = manager .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, - approval_policy: AskForApproval::Reject(RejectConfig { + approval_policy: AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), sandbox_policy: &SandboxPolicy::new_read_only_policy(), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), @@ -874,7 +874,7 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision() } #[tokio::test] -async fn mixed_rule_and_sandbox_prompt_rejects_when_rules_rejection_enabled() { +async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled() { let policy_src = r#"prefix_rule(pattern=["git"], decision="prompt")"#; let mut parser = PolicyParser::new(); parser @@ -890,12 +890,12 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_rules_rejection_enabled() { let requirement = manager .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, - approval_policy: AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + approval_policy: AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: false, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), sandbox_policy: &SandboxPolicy::new_read_only_policy(), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 702365bdd..d0e4e290b 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -95,8 +95,8 @@ pub enum Feature { ShellZshFork, /// Include the freeform apply_patch tool. ApplyPatchFreeform, - /// Allow requesting additional filesystem permissions while staying sandboxed. - RequestPermissions, + /// Allow exec tools to request additional permissions while staying sandboxed. + ExecPermissionApprovals, /// Enable Claude-style lifecycle hooks loaded from hooks.json files. CodexHooks, /// Expose the built-in request_permissions tool. @@ -626,8 +626,8 @@ pub const FEATURES: &[FeatureSpec] = &[ default_enabled: false, }, FeatureSpec { - id: Feature::RequestPermissions, - key: "request_permissions", + id: Feature::ExecPermissionApprovals, + key: "exec_permission_approvals", stage: Stage::UnderDevelopment, default_enabled: false, }, diff --git a/codex-rs/core/src/features/legacy.rs b/codex-rs/core/src/features/legacy.rs index b7aa30482..48e19c0df 100644 --- a/codex-rs/core/src/features/legacy.rs +++ b/codex-rs/core/src/features/legacy.rs @@ -29,6 +29,10 @@ const ALIASES: &[Alias] = &[ legacy_key: "include_apply_patch_tool", feature: Feature::ApplyPatchFreeform, }, + Alias { + legacy_key: "request_permissions", + feature: Feature::ExecPermissionApprovals, + }, Alias { legacy_key: "web_search", feature: Feature::WebSearchRequest, diff --git a/codex-rs/core/src/features_tests.rs b/codex-rs/core/src/features_tests.rs index 620200c30..895cee1b8 100644 --- a/codex-rs/core/src/features_tests.rs +++ b/codex-rs/core/src/features_tests.rs @@ -80,8 +80,11 @@ fn guardian_approval_is_experimental_and_user_toggleable() { #[test] fn request_permissions_is_under_development() { - assert_eq!(Feature::RequestPermissions.stage(), Stage::UnderDevelopment); - assert_eq!(Feature::RequestPermissions.default_enabled(), false); + assert_eq!( + Feature::ExecPermissionApprovals.stage(), + Stage::UnderDevelopment + ); + assert_eq!(Feature::ExecPermissionApprovals.default_enabled(), false); } #[test] diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index c93bb13d7..19997f039 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -254,7 +254,7 @@ fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool { AskForApproval::OnFailure => false, AskForApproval::OnRequest => false, AskForApproval::UnlessTrusted => false, - AskForApproval::Reject(reject_config) => reject_config.rejects_mcp_elicitations(), + AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(), } } diff --git a/codex-rs/core/src/mcp_connection_manager_tests.rs b/codex-rs/core/src/mcp_connection_manager_tests.rs index 51eaa67b1..f584e5947 100644 --- a/codex-rs/core/src/mcp_connection_manager_tests.rs +++ b/codex-rs/core/src/mcp_connection_manager_tests.rs @@ -1,6 +1,6 @@ use super::*; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::McpAuthStatus; -use codex_protocol::protocol::RejectConfig; use rmcp::model::JsonObject; use std::collections::HashSet; use std::sync::Arc; @@ -61,7 +61,7 @@ fn create_codex_apps_tools_cache_context( } #[test] -fn elicitation_reject_policy_defaults_to_prompting() { +fn elicitation_granular_policy_defaults_to_prompting() { assert!(!elicitation_is_rejected_by_policy( AskForApproval::OnFailure )); @@ -71,27 +71,27 @@ fn elicitation_reject_policy_defaults_to_prompting() { assert!(!elicitation_is_rejected_by_policy( AskForApproval::UnlessTrusted )); - assert!(!elicitation_is_rejected_by_policy(AskForApproval::Reject( - RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: false, + assert!(elicitation_is_rejected_by_policy(AskForApproval::Granular( + GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, mcp_elicitations: false, } ))); } #[test] -fn elicitation_reject_policy_respects_never_and_reject_config() { +fn elicitation_granular_policy_respects_never_and_config() { assert!(elicitation_is_rejected_by_policy(AskForApproval::Never)); - assert!(elicitation_is_rejected_by_policy(AskForApproval::Reject( - RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, + assert!(elicitation_is_rejected_by_policy(AskForApproval::Granular( + GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: false, } ))); } diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 1fdd51a1b..37bc9065d 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -43,7 +43,7 @@ pub fn assess_patch_safety( AskForApproval::OnFailure | AskForApproval::Never | AskForApproval::OnRequest - | AskForApproval::Reject(_) => { + | AskForApproval::Granular(_) => { // Continue to see if this can be auto-approved. } // TODO(ragona): I'm not sure this is actually correct? I believe in this case @@ -56,7 +56,7 @@ pub fn assess_patch_safety( let rejects_sandbox_approval = matches!(policy, AskForApproval::Never) || matches!( policy, - AskForApproval::Reject(reject_config) if reject_config.sandbox_approval + AskForApproval::Granular(granular_config) if !granular_config.sandbox_approval ); // Even though the patch appears to be constrained to writable paths, it is diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index 555d557d3..3d05664ba 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -3,7 +3,7 @@ use codex_protocol::protocol::FileSystemAccessMode; use codex_protocol::protocol::FileSystemPath; use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSpecialPath; -use codex_protocol::protocol::RejectConfig; +use codex_protocol::protocol::GranularApprovalConfig; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -87,7 +87,7 @@ fn external_sandbox_auto_approves_in_on_request() { } #[test] -fn reject_with_all_flags_false_matches_on_request_for_out_of_root_patch() { +fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() { let tmp = TempDir::new().unwrap(); let cwd = tmp.path().to_path_buf(); let parent = cwd.parent().unwrap().to_path_buf(); @@ -115,12 +115,12 @@ fn reject_with_all_flags_false_matches_on_request_for_out_of_root_patch() { assert_eq!( assess_patch_safety( &add_outside, - AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &policy_workspace_only, &FileSystemSandboxPolicy::from(&policy_workspace_only), @@ -132,7 +132,7 @@ fn reject_with_all_flags_false_matches_on_request_for_out_of_root_patch() { } #[test] -fn reject_sandbox_approval_rejects_out_of_root_patch() { +fn granular_sandbox_approval_false_rejects_out_of_root_patch() { let tmp = TempDir::new().unwrap(); let cwd = tmp.path().to_path_buf(); let parent = cwd.parent().unwrap().to_path_buf(); @@ -149,12 +149,12 @@ fn reject_sandbox_approval_rejects_out_of_root_patch() { assert_eq!( assess_patch_safety( &add_outside, - AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &policy_workspace_only, &FileSystemSandboxPolicy::from(&policy_workspace_only), diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index 030571ebe..aa6923fd1 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -118,7 +118,7 @@ pub(crate) fn normalize_and_validate_additional_permissions( && (uses_additional_permissions || additional_permissions.is_some()) { return Err( - "additional permissions are disabled; enable `features.request_permissions` before using `with_additional_permissions`" + "additional permissions are disabled; enable `features.exec_permission_approvals` before using `with_additional_permissions`" .to_string(), ); } @@ -239,7 +239,7 @@ mod tests { use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::RejectConfig; + use codex_protocol::protocol::GranularApprovalConfig; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; @@ -266,18 +266,18 @@ mod tests { } #[test] - fn preapproved_permissions_work_when_request_permissions_tool_is_enabled_without_inline_feature() + fn preapproved_permissions_work_when_request_permissions_tool_is_enabled_without_exec_permission_approvals_feature() { let cwd = tempdir().expect("tempdir"); let normalized = normalize_and_validate_additional_permissions( false, - AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: false, + mcp_elicitations: true, }), SandboxPermissions::WithAdditionalPermissions, Some(network_permissions()), @@ -290,7 +290,7 @@ mod tests { } #[test] - fn fresh_additional_permissions_still_require_request_permissions_feature() { + fn fresh_additional_permissions_still_require_exec_permission_approvals_feature() { let cwd = tempdir().expect("tempdir"); let err = normalize_and_validate_additional_permissions( @@ -305,7 +305,7 @@ mod tests { assert_eq!( err, - "additional permissions are disabled; enable `features.request_permissions` before using `with_additional_permissions`" + "additional permissions are disabled; enable `features.exec_permission_approvals` before using `with_additional_permissions`" ); } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 0318fe90f..d8a564b17 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -336,7 +336,8 @@ impl ShellHandler { } } - let request_permission_enabled = session.features().enabled(Feature::RequestPermissions); + let exec_permission_approvals_enabled = + session.features().enabled(Feature::ExecPermissionApprovals); let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( session.as_ref(), @@ -344,7 +345,7 @@ impl ShellHandler { additional_permissions, ) .await; - let additional_permissions_allowed = request_permission_enabled + let additional_permissions_allowed = exec_permission_approvals_enabled || (session.features().enabled(Feature::RequestPermissionsTool) && effective_additional_permissions.permissions_preapproved); let normalized_additional_permissions = implicit_granted_permissions( diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 80c132008..123225065 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -170,8 +170,8 @@ impl ToolHandler for UnifiedExecHandler { .. } = args; - let request_permission_enabled = - session.features().enabled(Feature::RequestPermissions); + let exec_permission_approvals_enabled = + session.features().enabled(Feature::ExecPermissionApprovals); let requested_additional_permissions = additional_permissions.clone(); let effective_additional_permissions = apply_granted_turn_permissions( context.session.as_ref(), @@ -179,7 +179,7 @@ impl ToolHandler for UnifiedExecHandler { additional_permissions, ) .await; - let additional_permissions_allowed = request_permission_enabled + let additional_permissions_allowed = exec_permission_approvals_enabled || (session.features().enabled(Feature::RequestPermissionsTool) && effective_additional_permissions.permissions_preapproved); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 6cf1a4fa0..48b30fcec 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -166,7 +166,7 @@ impl Approvable for ApplyPatchRuntime { fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { match policy { AskForApproval::Never => false, - AskForApproval::Reject(reject_config) => !reject_config.rejects_sandbox_approval(), + AskForApproval::Granular(granular_config) => granular_config.allows_sandbox_approval(), AskForApproval::OnFailure => true, AskForApproval::OnRequest => true, AskForApproval::UnlessTrusted => true, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index c59816276..e30898855 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -1,28 +1,28 @@ use super::*; -use codex_protocol::protocol::RejectConfig; +use codex_protocol::protocol::GranularApprovalConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; #[test] -fn wants_no_sandbox_approval_reject_respects_sandbox_flag() { +fn wants_no_sandbox_approval_granular_respects_sandbox_flag() { let runtime = ApplyPatchRuntime::new(); assert!(runtime.wants_no_sandbox_approval(AskForApproval::OnRequest)); assert!( - !runtime.wants_no_sandbox_approval(AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + !runtime.wants_no_sandbox_approval(AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, })) ); assert!( - runtime.wants_no_sandbox_approval(AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + runtime.wants_no_sandbox_approval(AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, })) ); } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 4f3240c12..987fdc2dc 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -65,11 +65,11 @@ pub(crate) struct PreparedUnifiedExecZshFork { const PROMPT_CONFLICT_REASON: &str = "approval required by policy, but AskForApproval is set to Never"; const REJECT_SANDBOX_APPROVAL_REASON: &str = - "approval required by policy, but AskForApproval::Reject.sandbox_approval is set"; + "approval required by policy, but AskForApproval::Granular.sandbox_approval is false"; const REJECT_RULES_APPROVAL_REASON: &str = - "approval required by policy rule, but AskForApproval::Reject.rules is set"; + "approval required by policy rule, but AskForApproval::Granular.rules is false"; const REJECT_SKILL_APPROVAL_REASON: &str = - "approval required by skill, but AskForApproval::Reject.skill_approval is set"; + "approval required by skill, but AskForApproval::Granular.skill_approval is false"; fn approval_sandbox_permissions( sandbox_permissions: SandboxPermissions, @@ -358,18 +358,18 @@ fn execve_prompt_is_rejected_by_policy( ) -> Option<&'static str> { match (approval_policy, decision_source) { (AskForApproval::Never, _) => Some(PROMPT_CONFLICT_REASON), - (AskForApproval::Reject(reject_config), DecisionSource::SkillScript { .. }) - if reject_config.rejects_skill_approval() => + (AskForApproval::Granular(granular_config), DecisionSource::SkillScript { .. }) + if !granular_config.allows_skill_approval() => { Some(REJECT_SKILL_APPROVAL_REASON) } - (AskForApproval::Reject(reject_config), DecisionSource::PrefixRule) - if reject_config.rejects_rules_approval() => + (AskForApproval::Granular(granular_config), DecisionSource::PrefixRule) + if !granular_config.allows_rules_approval() => { Some(REJECT_RULES_APPROVAL_REASON) } - (AskForApproval::Reject(reject_config), DecisionSource::UnmatchedCommandFallback) - if reject_config.rejects_sandbox_approval() => + (AskForApproval::Granular(granular_config), DecisionSource::UnmatchedCommandFallback) + if !granular_config.allows_sandbox_approval() => { Some(REJECT_SANDBOX_APPROVAL_REASON) } diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 23f813670..a8020c0fb 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -16,8 +16,8 @@ use crate::config::Permissions; use crate::config::types::ShellEnvironmentPolicy; use crate::exec::SandboxType; use crate::protocol::AskForApproval; +use crate::protocol::GranularApprovalConfig; use crate::protocol::ReadOnlyAccess; -use crate::protocol::RejectConfig; use crate::protocol::SandboxPolicy; use crate::sandboxing::SandboxPermissions; #[cfg(target_os = "macos")] @@ -105,12 +105,12 @@ fn execve_prompt_rejection_uses_skill_approval_for_skill_scripts() { assert_eq!( super::execve_prompt_is_rejected_by_policy( - AskForApproval::Reject(RejectConfig { + AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &decision_source, ), @@ -118,16 +118,16 @@ fn execve_prompt_rejection_uses_skill_approval_for_skill_scripts() { ); assert_eq!( super::execve_prompt_is_rejected_by_policy( - AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: true, - request_permissions: false, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: false, + request_permissions: true, + mcp_elicitations: true, }), &decision_source, ), - Some("approval required by skill, but AskForApproval::Reject.skill_approval is set"), + Some("approval required by skill, but AskForApproval::Granular.skill_approval is false"), ); } @@ -135,16 +135,16 @@ fn execve_prompt_rejection_uses_skill_approval_for_skill_scripts() { fn execve_prompt_rejection_keeps_prefix_rules_on_rules_flag() { assert_eq!( super::execve_prompt_is_rejected_by_policy( - AskForApproval::Reject(RejectConfig { + AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + rules: false, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &super::DecisionSource::PrefixRule, ), - Some("approval required by policy rule, but AskForApproval::Reject.rules is set"), + Some("approval required by policy rule, but AskForApproval::Granular.rules is false"), ); } @@ -152,16 +152,16 @@ fn execve_prompt_rejection_keeps_prefix_rules_on_rules_flag() { fn execve_prompt_rejection_keeps_unmatched_commands_on_sandbox_flag() { assert_eq!( super::execve_prompt_is_rejected_by_policy( - AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }), &super::DecisionSource::UnmatchedCommandFallback, ), - Some("approval required by policy, but AskForApproval::Reject.sandbox_approval is set"), + Some("approval required by policy, but AskForApproval::Granular.sandbox_approval is false"), ); } diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 16fd5b1a7..29a950598 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -161,8 +161,8 @@ impl ExecApprovalRequirement { /// - Never, OnFailure: do not ask /// - OnRequest: ask unless filesystem access is unrestricted -/// - Reject: ask unless filesystem access is unrestricted, but auto-reject -/// when `sandbox_approval` rejection is enabled. +/// - Granular: ask unless filesystem access is unrestricted, but auto-reject +/// when granular sandbox approval is disabled. /// - UnlessTrusted: always ask pub(crate) fn default_exec_approval_requirement( policy: AskForApproval, @@ -170,7 +170,7 @@ pub(crate) fn default_exec_approval_requirement( ) -> ExecApprovalRequirement { let needs_approval = match policy { AskForApproval::Never | AskForApproval::OnFailure => false, - AskForApproval::OnRequest | AskForApproval::Reject(_) => { + AskForApproval::OnRequest | AskForApproval::Granular(_) => { matches!( file_system_sandbox_policy.kind, FileSystemSandboxKind::Restricted @@ -182,11 +182,12 @@ pub(crate) fn default_exec_approval_requirement( if needs_approval && matches!( policy, - AskForApproval::Reject(reject_config) if reject_config.rejects_sandbox_approval() + AskForApproval::Granular(granular_config) + if !granular_config.allows_sandbox_approval() ) { ExecApprovalRequirement::Forbidden { - reason: "approval policy rejected sandbox approval prompt".to_string(), + reason: "approval policy disallowed sandbox approval prompt".to_string(), } } else if needs_approval { ExecApprovalRequirement::NeedsApproval { @@ -268,7 +269,7 @@ pub(crate) trait Approvable { AskForApproval::UnlessTrusted => true, AskForApproval::Never => false, AskForApproval::OnRequest => false, - AskForApproval::Reject(reject_config) => !reject_config.sandbox_approval, + AskForApproval::Granular(granular_config) => granular_config.sandbox_approval, } } diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index cf68307ad..4a4dac3e8 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::sandboxing::SandboxPermissions; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::RejectConfig; use pretty_assertions::assert_eq; #[test] @@ -37,13 +37,13 @@ fn restricted_sandbox_requires_exec_approval_on_request() { } #[test] -fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() { - let policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, +fn default_exec_approval_requirement_rejects_sandbox_prompt_when_granular_disables_it() { + let policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }); let sandbox_policy = SandboxPolicy::new_read_only_policy(); @@ -53,19 +53,19 @@ fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() { assert_eq!( requirement, ExecApprovalRequirement::Forbidden { - reason: "approval policy rejected sandbox approval prompt".to_string(), + reason: "approval policy disallowed sandbox approval prompt".to_string(), } ); } #[test] -fn default_exec_approval_requirement_keeps_prompt_when_sandbox_rejection_is_disabled() { - let policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: true, +fn default_exec_approval_requirement_keeps_prompt_when_granular_allows_sandbox_approval() { + let policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: false, + skill_approval: true, + request_permissions: true, + mcp_elicitations: false, }); let sandbox_policy = SandboxPolicy::new_read_only_policy(); diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index a7ddb398a..b61856275 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -118,7 +118,7 @@ pub(crate) struct ToolsConfig { pub agent_roles: BTreeMap, pub search_tool: bool, pub tool_suggest: bool, - pub request_permission_enabled: bool, + pub exec_permission_approvals_enabled: bool, pub request_permissions_tool_enabled: bool, pub code_mode_enabled: bool, pub js_repl_enabled: bool, @@ -184,7 +184,7 @@ impl ToolsConfig { features.enabled(Feature::Artifact) && codex_artifacts::can_manage_artifact_runtime(); let include_image_gen_tool = features.enabled(Feature::ImageGeneration) && supports_image_generation(model_info); - let request_permission_enabled = features.enabled(Feature::RequestPermissions); + let exec_permission_approvals_enabled = features.enabled(Feature::ExecPermissionApprovals); let request_permissions_tool_enabled = features.enabled(Feature::RequestPermissionsTool); let shell_command_backend = if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) { @@ -255,7 +255,7 @@ impl ToolsConfig { agent_roles: BTreeMap::new(), search_tool: include_search_tool, tool_suggest: include_tool_suggest, - request_permission_enabled, + exec_permission_approvals_enabled, request_permissions_tool_enabled, code_mode_enabled: include_code_mode, js_repl_enabled: include_js_repl, @@ -441,13 +441,15 @@ fn create_permissions_schema() -> JsonSchema { } } -fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap { +fn create_approval_parameters( + exec_permission_approvals_enabled: bool, +) -> BTreeMap { let mut properties = BTreeMap::from([ ( "sandbox_permissions".to_string(), JsonSchema::String { description: Some( - if request_permission_enabled { + if exec_permission_approvals_enabled { "Sandbox permissions for the command. Use \"with_additional_permissions\" to request additional sandboxed filesystem, network, or macOS permissions (preferred), or \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." } else { "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"." @@ -482,7 +484,7 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap BTreeMap ToolSpec { +fn create_exec_command_tool( + allow_login_shell: bool, + exec_permission_approvals_enabled: bool, +) -> ToolSpec { let mut properties = BTreeMap::from([ ( "cmd".to_string(), @@ -552,7 +557,9 @@ fn create_exec_command_tool(allow_login_shell: bool, request_permission_enabled: }, ); } - properties.extend(create_approval_parameters(request_permission_enabled)); + properties.extend(create_approval_parameters( + exec_permission_approvals_enabled, + )); ToolSpec::Function(ResponsesApiTool { name: "exec_command".to_string(), @@ -669,7 +676,7 @@ fn create_exec_wait_tool() -> ToolSpec { }) } -fn create_shell_tool(request_permission_enabled: bool) -> ToolSpec { +fn create_shell_tool(exec_permission_approvals_enabled: bool) -> ToolSpec { let mut properties = BTreeMap::from([ ( "command".to_string(), @@ -691,7 +698,9 @@ fn create_shell_tool(request_permission_enabled: bool) -> ToolSpec { }, ), ]); - properties.extend(create_approval_parameters(request_permission_enabled)); + properties.extend(create_approval_parameters( + exec_permission_approvals_enabled, + )); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"]. @@ -726,7 +735,7 @@ Examples of valid command strings: fn create_shell_command_tool( allow_login_shell: bool, - request_permission_enabled: bool, + exec_permission_approvals_enabled: bool, ) -> ToolSpec { let mut properties = BTreeMap::from([ ( @@ -761,7 +770,9 @@ fn create_shell_command_tool( }, ); } - properties.extend(create_approval_parameters(request_permission_enabled)); + properties.extend(create_approval_parameters( + exec_permission_approvals_enabled, + )); let description = if cfg!(windows) { r#"Runs a Powershell command (Windows) and returns its output. @@ -2359,7 +2370,7 @@ pub(crate) fn build_specs_with_discoverable_tools( let js_repl_handler = Arc::new(JsReplHandler); let js_repl_reset_handler = Arc::new(JsReplResetHandler); let artifacts_handler = Arc::new(ArtifactsHandler); - let request_permission_enabled = config.request_permission_enabled; + let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled; if config.code_mode_enabled { let nested_config = config.for_code_mode_nested_tools(); @@ -2399,7 +2410,7 @@ pub(crate) fn build_specs_with_discoverable_tools( ConfigShellToolType::Default => { push_tool_spec( &mut builder, - create_shell_tool(request_permission_enabled), + create_shell_tool(exec_permission_approvals_enabled), true, config.code_mode_enabled, ); @@ -2415,7 +2426,10 @@ pub(crate) fn build_specs_with_discoverable_tools( ConfigShellToolType::UnifiedExec => { push_tool_spec( &mut builder, - create_exec_command_tool(config.allow_login_shell, request_permission_enabled), + create_exec_command_tool( + config.allow_login_shell, + exec_permission_approvals_enabled, + ), true, config.code_mode_enabled, ); @@ -2434,7 +2448,10 @@ pub(crate) fn build_specs_with_discoverable_tools( ConfigShellToolType::ShellCommand => { push_tool_spec( &mut builder, - create_shell_command_tool(config.allow_login_shell, request_permission_enabled), + create_shell_command_tool( + config.allow_login_shell, + exec_permission_approvals_enabled, + ), true, config.code_mode_enabled, ); diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 292902f9c..30147ffc9 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -461,7 +461,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { expected.insert(tool_name(&spec).to_string(), spec); } - if config.request_permission_enabled { + if config.exec_permission_approvals_enabled { let spec = create_request_permissions_tool(); expected.insert(tool_name(&spec).to_string(), spec); } @@ -744,7 +744,7 @@ fn request_permissions_tool_is_independent_from_additional_permissions() { let config = test_config(); let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); let mut features = Features::with_defaults(); - features.enable(Feature::RequestPermissions); + features.enable(Feature::ExecPermissionApprovals); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index fd2044738..2e5c6c264 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -9,8 +9,8 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::Op; -use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_permissions::PermissionGrantScope; @@ -323,7 +323,7 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -397,18 +397,18 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res } #[tokio::test(flavor = "current_thread")] -async fn request_permissions_tool_is_auto_denied_when_reject_request_permissions_is_enabled() +async fn request_permissions_tool_is_auto_denied_when_granular_request_permissions_is_disabled() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_sandbox!(Ok(())); let server = start_mock_server().await; - let approval_policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: false, - request_permissions: true, - mcp_elicitations: false, + let approval_policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: true, + request_permissions: false, + mcp_elicitations: true, }); let sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_policy_for_config = sandbox_policy.clone(); @@ -453,7 +453,7 @@ async fn request_permissions_tool_is_auto_denied_when_reject_request_permissions submit_turn( &test, - "request permissions under reject.request_permissions", + "request permissions under granular.request_permissions = false", approval_policy, sandbox_policy, ) @@ -468,7 +468,7 @@ async fn request_permissions_tool_is_auto_denied_when_reject_request_permissions .await; assert!( matches!(event, EventMsg::TurnComplete(_)), - "request_permissions should not emit a prompt when reject.request_permissions is set: {event:?}" + "request_permissions should not emit a prompt when granular.request_permissions is false: {event:?}" ); let call_output = results.single_request().function_call_output(call_id); @@ -500,7 +500,7 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -601,7 +601,7 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -701,7 +701,7 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -800,7 +800,7 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() -> config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -904,7 +904,7 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1009,7 +1009,7 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1132,7 +1132,7 @@ async fn request_permissions_preapprove_explicit_exec_permissions_outside_on_req config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1249,7 +1249,7 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1471,7 +1471,7 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions() config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1631,7 +1631,7 @@ async fn request_permissions_grants_do_not_carry_across_turns() -> Result<()> { config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -1743,7 +1743,7 @@ async fn request_permissions_session_grants_carry_across_turns() -> Result<()> { config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 8f99a9a0f..9aff62d94 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -196,7 +196,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features @@ -318,7 +318,7 @@ async fn approved_folder_write_request_permissions_unblocks_later_apply_patch_wi config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); config .features - .enable(Feature::RequestPermissions) + .enable(Feature::ExecPermissionApprovals) .expect("test config should allow feature update"); config .features diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index 5abe2e8e9..b5fda12ae 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -8,8 +8,8 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; use codex_protocol::protocol::ExecApprovalRequestSkillMetadata; +use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::Op; -use codex_protocol::protocol::RejectConfig; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; @@ -285,12 +285,12 @@ async fn shell_zsh_fork_skill_script_reject_policy_with_sandbox_approval_false_s return Ok(()); }; - let approval_policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: false, + let approval_policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, rules: true, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }); let server = start_mock_server().await; let tool_call_id = "zsh-fork-skill-reject-false"; @@ -381,12 +381,12 @@ async fn shell_zsh_fork_skill_script_reject_policy_with_sandbox_approval_true_st return Ok(()); }; - let approval_policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: true, - rules: false, - skill_approval: false, - request_permissions: false, - mcp_elicitations: false, + let approval_policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: false, + rules: true, + skill_approval: true, + request_permissions: true, + mcp_elicitations: true, }); let server = start_mock_server().await; let tool_call_id = "zsh-fork-skill-reject-true"; @@ -475,12 +475,12 @@ async fn shell_zsh_fork_skill_script_reject_policy_with_skill_approval_true_skip return Ok(()); }; - let approval_policy = AskForApproval::Reject(RejectConfig { - sandbox_approval: false, - rules: false, - skill_approval: true, - request_permissions: false, - mcp_elicitations: false, + let approval_policy = AskForApproval::Granular(GranularApprovalConfig { + sandbox_approval: true, + rules: true, + skill_approval: false, + request_permissions: true, + mcp_elicitations: true, }); let server = start_mock_server().await; let tool_call_id = "zsh-fork-skill-reject-skill-approval-true"; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 857ec14d4..7f055dd0e 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -483,10 +483,10 @@ impl DeveloperInstructions { pub fn from( approval_policy: AskForApproval, exec_policy: &Policy, - request_permission_enabled: bool, + exec_permission_approvals_enabled: bool, ) -> DeveloperInstructions { let on_request_instructions = || { - let on_request_rule = if request_permission_enabled { + let on_request_rule = if exec_permission_approvals_enabled { APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION } else { APPROVAL_POLICY_ON_REQUEST_RULE @@ -506,22 +506,22 @@ impl DeveloperInstructions { AskForApproval::UnlessTrusted => APPROVAL_POLICY_UNLESS_TRUSTED.to_string(), AskForApproval::OnFailure => APPROVAL_POLICY_ON_FAILURE.to_string(), AskForApproval::OnRequest => on_request_instructions(), - AskForApproval::Reject(reject_config) => { + AskForApproval::Granular(granular_config) => { let on_request_instructions = on_request_instructions(); - let sandbox_approval = reject_config.sandbox_approval; - let rules = reject_config.rules; - let skill_approval = reject_config.skill_approval; - let request_permissions = reject_config.request_permissions; - let mcp_elicitations = reject_config.mcp_elicitations; + let sandbox_approval = granular_config.sandbox_approval; + let rules = granular_config.rules; + let skill_approval = granular_config.skill_approval; + let request_permissions = granular_config.request_permissions; + let mcp_elicitations = granular_config.mcp_elicitations; format!( "{on_request_instructions}\n\n\ - Approval policy is `reject`.\n\ + Approval policy is `granular`.\n\ - `sandbox_approval`: {sandbox_approval}\n\ - `rules`: {rules}\n\ - `skill_approval`: {skill_approval}\n\ - `request_permissions`: {request_permissions}\n\ - `mcp_elicitations`: {mcp_elicitations}\n\ - When a category is `true`, requests in that category are auto-rejected instead of prompting the user." + When a category is `true`, requests in that category are allowed. When it is `false`, they are auto-rejected instead of prompting the user." ) } }; @@ -577,7 +577,7 @@ impl DeveloperInstructions { approval_policy: AskForApproval, exec_policy: &Policy, cwd: &Path, - request_permission_enabled: bool, + exec_permission_approvals_enabled: bool, ) -> Self { let network_access = if sandbox_policy.has_full_network_access() { NetworkAccess::Enabled @@ -601,7 +601,7 @@ impl DeveloperInstructions { approval_policy, exec_policy, writable_roots, - request_permission_enabled, + exec_permission_approvals_enabled, ) } @@ -625,7 +625,7 @@ impl DeveloperInstructions { approval_policy: AskForApproval, exec_policy: &Policy, writable_roots: Option>, - request_permission_enabled: bool, + exec_permission_approvals_enabled: bool, ) -> Self { let start_tag = DeveloperInstructions::new(""); let end_tag = DeveloperInstructions::new(""); @@ -637,7 +637,7 @@ impl DeveloperInstructions { .concat(DeveloperInstructions::from( approval_policy, exec_policy, - request_permission_enabled, + exec_permission_approvals_enabled, )) .concat(DeveloperInstructions::from_writable_roots(writable_roots)) .concat(end_tag) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index fb90a5ccf..b450db37f 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -516,11 +516,13 @@ pub enum AskForApproval { #[default] OnRequest, - /// Fine-grained rejection controls for approval prompts. + /// Fine-grained controls for individual approval flows. /// - /// When a field is `true`, prompts of that category are automatically - /// rejected instead of shown to the user. - Reject(RejectConfig), + /// When a field is `true`, commands in that category are allowed. When it + /// is `false`, those requests are automatically rejected instead of shown + /// to the user. + #[strum(serialize = "granular")] + Granular(GranularApprovalConfig), /// Never ask the user to approve commands. Failures are immediately returned /// to the model, and never escalated to the user for approval. @@ -528,40 +530,40 @@ pub enum AskForApproval { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] -pub struct RejectConfig { - /// Reject shell command approval requests, including inline +pub struct GranularApprovalConfig { + /// Whether to allow shell command approval requests, including inline /// `with_additional_permissions` and `require_escalated` requests. pub sandbox_approval: bool, - /// Reject prompts triggered by execpolicy `prompt` rules. + /// Whether to allow prompts triggered by execpolicy `prompt` rules. pub rules: bool, - /// Reject approval prompts triggered by skill script execution. + /// Whether to allow approval prompts triggered by skill script execution. #[serde(default)] pub skill_approval: bool, - /// Reject `request_permissions` tool requests. + /// Whether to allow prompts triggered by the `request_permissions` tool. #[serde(default)] pub request_permissions: bool, - /// Reject MCP elicitation prompts. + /// Whether to allow MCP elicitation prompts. pub mcp_elicitations: bool, } -impl RejectConfig { - pub const fn rejects_sandbox_approval(self) -> bool { +impl GranularApprovalConfig { + pub const fn allows_sandbox_approval(self) -> bool { self.sandbox_approval } - pub const fn rejects_rules_approval(self) -> bool { + pub const fn allows_rules_approval(self) -> bool { self.rules } - pub const fn rejects_skill_approval(self) -> bool { + pub const fn allows_skill_approval(self) -> bool { self.skill_approval } - pub const fn rejects_request_permissions(self) -> bool { + pub const fn allows_request_permissions(self) -> bool { self.request_permissions } - pub const fn rejects_mcp_elicitations(self) -> bool { + pub const fn allows_mcp_elicitations(self) -> bool { self.mcp_elicitations } } @@ -3466,89 +3468,89 @@ mod tests { } #[test] - fn reject_config_mcp_elicitation_flag_is_field_driven() { + fn granular_approval_config_mcp_elicitation_flag_is_field_driven() { assert!( - RejectConfig { + GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: false, mcp_elicitations: true, } - .rejects_mcp_elicitations() + .allows_mcp_elicitations() ); assert!( - !RejectConfig { + !GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: false, mcp_elicitations: false, } - .rejects_mcp_elicitations() + .allows_mcp_elicitations() ); } #[test] - fn reject_config_skill_approval_flag_is_field_driven() { + fn granular_approval_config_skill_approval_flag_is_field_driven() { assert!( - RejectConfig { + GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: true, request_permissions: false, mcp_elicitations: false, } - .rejects_skill_approval() + .allows_skill_approval() ); assert!( - !RejectConfig { + !GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: false, mcp_elicitations: false, } - .rejects_skill_approval() + .allows_skill_approval() ); } #[test] - fn reject_config_request_permissions_flag_is_field_driven() { + fn granular_approval_config_request_permissions_flag_is_field_driven() { assert!( - RejectConfig { + GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: true, mcp_elicitations: false, } - .rejects_request_permissions() + .allows_request_permissions() ); assert!( - !RejectConfig { + !GranularApprovalConfig { sandbox_approval: false, rules: false, skill_approval: false, request_permissions: false, mcp_elicitations: false, } - .rejects_request_permissions() + .allows_request_permissions() ); } #[test] - fn reject_config_defaults_missing_optional_flags_to_false() { - let decoded = serde_json::from_value::(serde_json::json!({ + fn granular_approval_config_defaults_missing_optional_flags_to_false() { + let decoded = serde_json::from_value::(serde_json::json!({ "sandbox_approval": true, "rules": false, "mcp_elicitations": true, })) - .expect("legacy reject config should deserialize"); + .expect("granular approval config should deserialize"); assert_eq!( decoded, - RejectConfig { + GranularApprovalConfig { sandbox_approval: true, rules: false, skill_approval: false,