Rename reject approval policy to granular (#14516)

This commit is contained in:
Jack Mousseau
2026-03-12 16:38:04 -07:00
committed by GitHub
Unverified
parent d32820ab07
commit b7dba72dbd
46 changed files with 456 additions and 419 deletions
+11 -11
View File
@@ -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`"
);
}
+3 -2
View File
@@ -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(
@@ -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);
@@ -166,7 +166,7 @@ impl Approvable<ApplyPatchRequest> 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,
@@ -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,
}))
);
}
@@ -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)
}
@@ -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"),
);
}
+7 -6
View File
@@ -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<Req> {
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,
}
}
+16 -16
View File
@@ -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();
+33 -16
View File
@@ -118,7 +118,7 @@ pub(crate) struct ToolsConfig {
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
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<String, JsonSchema> {
fn create_approval_parameters(
exec_permission_approvals_enabled: bool,
) -> BTreeMap<String, JsonSchema> {
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<Stri
)
]);
if request_permission_enabled {
if exec_permission_approvals_enabled {
properties.insert(
"additional_permissions".to_string(),
create_permissions_schema(),
@@ -492,7 +494,10 @@ fn create_approval_parameters(request_permission_enabled: bool) -> BTreeMap<Stri
properties
}
fn create_exec_command_tool(allow_login_shell: bool, request_permission_enabled: bool) -> 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,
);
+2 -2
View File
@@ -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,