mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
f6c06108b1
commit
425fff7ad6
@@ -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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user