feat: add Reject approval policy with granular prompt rejection controls (#12087)

## Why

We need a way to auto-reject specific approval prompt categories without
switching all approvals off.

The goal is to let users independently control:
- sandbox escalation approvals,
- execpolicy `prompt` rule approvals,
- MCP elicitation prompts.

## What changed

- Added a new primary approval mode in `protocol/src/protocol.rs`:

```rust
pub enum AskForApproval {
    // ...
    Reject(RejectConfig),
    // ...
}

pub struct RejectConfig {
    pub sandbox_approval: bool,
    pub rules: bool,
    pub mcp_elicitations: bool,
}
```

- Wired `RejectConfig` semantics through approval paths in `core`:
  - `core/src/exec_policy.rs`
    - rejects rule-driven prompts when `rules = true`
    - rejects sandbox/escalation prompts when `sandbox_approval = true`
- preserves rule priority when both rule and sandbox prompt conditions
are present
  - `core/src/tools/sandboxing.rs`
- applies `sandbox_approval` to default exec approval decisions and
sandbox-failure retry gating
  - `core/src/safety.rs`
- keeps `Reject { all false }` behavior aligned with `OnRequest` for
patch safety
    - rejects out-of-root patch approvals when `sandbox_approval = true`
  - `core/src/mcp_connection_manager.rs`
    - auto-declines MCP elicitations when `mcp_elicitations = true`

- Ensured approval policy used by MCP elicitation flow stays in sync
with constrained session policy updates.

- Updated app-server v2 conversions and generated schema/TypeScript
artifacts for the new `Reject` shape.

## Verification

Added focused unit coverage for the new behavior in:
- `core/src/exec_policy.rs`
- `core/src/tools/sandboxing.rs`
- `core/src/mcp_connection_manager.rs`
- `core/src/safety.rs`
- `core/src/tools/runtimes/apply_patch.rs`

Key cases covered include rule-vs-sandbox prompt precedence, MCP
auto-decline behavior, and patch/sandbox retry behavior under
`RejectConfig`.
This commit is contained in:
Michael Bolin
2026-02-19 11:41:49 -08:00
committed by GitHub
Unverified
parent f6c06108b1
commit 425fff7ad6
37 changed files with 1490 additions and 117 deletions
@@ -127,7 +127,13 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
}
fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool {
!matches!(policy, AskForApproval::Never)
match policy {
AskForApproval::Never => false,
AskForApproval::Reject(reject_config) => !reject_config.rejects_sandbox_approval(),
AskForApproval::OnFailure => true,
AskForApproval::OnRequest => true,
AskForApproval::UnlessTrusted => true,
}
}
// apply_patch approvals are decided upstream by assess_patch_safety.
@@ -159,3 +165,29 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::protocol::RejectConfig;
#[test]
fn wants_no_sandbox_approval_reject_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,
mcp_elicitations: false,
}))
);
assert!(
runtime.wants_no_sandbox_approval(AskForApproval::Reject(RejectConfig {
sandbox_approval: false,
rules: false,
mcp_elicitations: false,
}))
);
}
}
+60 -3
View File
@@ -156,6 +156,8 @@ impl ExecApprovalRequirement {
/// - Never, OnFailure: do not ask
/// - OnRequest: ask unless sandbox policy is DangerFullAccess
/// - Reject: ask unless sandbox policy is DangerFullAccess, but auto-reject
/// when `sandbox_approval` rejection is enabled.
/// - UnlessTrusted: always ask
pub(crate) fn default_exec_approval_requirement(
policy: AskForApproval,
@@ -163,14 +165,23 @@ pub(crate) fn default_exec_approval_requirement(
) -> ExecApprovalRequirement {
let needs_approval = match policy {
AskForApproval::Never | AskForApproval::OnFailure => false,
AskForApproval::OnRequest => !matches!(
AskForApproval::OnRequest | AskForApproval::Reject(_) => !matches!(
sandbox_policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
),
AskForApproval::UnlessTrusted => true,
};
if needs_approval {
if needs_approval
&& matches!(
policy,
AskForApproval::Reject(reject_config) if reject_config.rejects_sandbox_approval()
)
{
ExecApprovalRequirement::Forbidden {
reason: "approval policy rejected sandbox approval prompt".to_string(),
}
} else if needs_approval {
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
@@ -224,7 +235,13 @@ pub(crate) trait Approvable<Req> {
/// Decide we can request an approval for no-sandbox execution.
fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool {
!matches!(policy, AskForApproval::Never | AskForApproval::OnRequest)
match policy {
AskForApproval::OnFailure => true,
AskForApproval::UnlessTrusted => true,
AskForApproval::Never => false,
AskForApproval::OnRequest => false,
AskForApproval::Reject(reject_config) => !reject_config.sandbox_approval,
}
}
fn start_approval_async<'a>(
@@ -313,6 +330,7 @@ impl<'a> SandboxAttempt<'a> {
mod tests {
use super::*;
use codex_protocol::protocol::NetworkAccess;
use codex_protocol::protocol::RejectConfig;
use pretty_assertions::assert_eq;
#[test]
@@ -344,4 +362,43 @@ mod tests {
}
);
}
#[test]
fn default_exec_approval_requirement_rejects_sandbox_prompt_when_configured() {
let policy = AskForApproval::Reject(RejectConfig {
sandbox_approval: true,
rules: false,
mcp_elicitations: false,
});
let requirement =
default_exec_approval_requirement(policy, &SandboxPolicy::new_read_only_policy());
assert_eq!(
requirement,
ExecApprovalRequirement::Forbidden {
reason: "approval policy rejected 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,
mcp_elicitations: true,
});
let requirement =
default_exec_approval_requirement(policy, &SandboxPolicy::new_read_only_policy());
assert_eq!(
requirement,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
}
);
}
}