mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(acp): add PermissionMode::Auto + contradiction matrix rows (#4938)
Add the Auto variant to PermissionMode (wire string 'auto'; #4557 adds the same variant from the claude-config arc — this commit establishes the contradiction logic ahead of that merge so the rebase is mechanical). Auto mode = fully autonomous execution; model-gated (requires supportsAutoMode); the adapter self-approves all tool calls internally and never emits session/request_permission. Mode matrix: - allow + auto → compatible (transmit as-is; both want unattended approval) - ask + auto → startup error (card never fires — ask becomes a dead letter) - reject + auto → startup error (inverted-security worst case: policy says deny while adapter silently auto-approves everything) Tests: 4 new pinned tests (allow+auto ok, ask+auto error, reject+auto error, wire string correct). Total: 724 passing. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -6254,4 +6254,51 @@ mod tests {
|
||||
let cfg = ResolvedPermissionConfig::resolve(PermissionPolicy::Allow, None).unwrap();
|
||||
assert_eq!(cfg.effective_mode.as_wire_str(), "default");
|
||||
}
|
||||
|
||||
// ── Pinned amendment: PermissionMode::Auto matrix row ────────────────────
|
||||
//
|
||||
// `auto` = "fully autonomous execution" — the adapter self-approves all
|
||||
// tool calls internally and never emits `session/request_permission`.
|
||||
// - allow + auto → compatible (transmit as-is; both want unattended approval)
|
||||
// - ask + auto → startup error (card never fires — ask becomes dead letter)
|
||||
// - reject + auto → startup error (inverted security: policy says deny, adapter
|
||||
// auto-approves everything)
|
||||
|
||||
#[test]
|
||||
fn resolved_permission_config_allow_plus_explicit_auto_is_ok() {
|
||||
// allow + auto is compatible: both want unattended approval.
|
||||
let cfg =
|
||||
ResolvedPermissionConfig::resolve(PermissionPolicy::Allow, Some(PermissionMode::Auto))
|
||||
.unwrap();
|
||||
assert_eq!(cfg.effective_mode, PermissionMode::Auto);
|
||||
assert_eq!(cfg.effective_mode.as_wire_str(), "auto");
|
||||
assert_eq!(cfg.mode_source, ModeSource::Explicit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_permission_config_ask_plus_explicit_auto_is_startup_error() {
|
||||
// ask + auto: adapter self-approves internally, card never fires.
|
||||
let result =
|
||||
ResolvedPermissionConfig::resolve(PermissionPolicy::Ask, Some(PermissionMode::Auto));
|
||||
assert!(result.is_err(), "ask + auto must be a startup error");
|
||||
let msg = format!("{}", result.unwrap_err());
|
||||
assert!(msg.contains("auto"), "error must mention auto, got: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_permission_config_reject_plus_explicit_auto_is_startup_error() {
|
||||
// reject + auto: inverted-security worst case — policy says deny but
|
||||
// adapter auto-approves everything internally.
|
||||
let result =
|
||||
ResolvedPermissionConfig::resolve(PermissionPolicy::Reject, Some(PermissionMode::Auto));
|
||||
assert!(result.is_err(), "reject + auto must be a startup error");
|
||||
let msg = format!("{}", result.unwrap_err());
|
||||
assert!(msg.contains("auto"), "error must mention auto, got: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_mode_auto_wire_string_is_correct() {
|
||||
assert_eq!(PermissionMode::Auto.as_wire_str(), "auto");
|
||||
assert!(!PermissionMode::Auto.is_default());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@ impl std::fmt::Display for RespondTo {
|
||||
/// `configId: "mode"` (e.g. `claude-agent-acp`).
|
||||
///
|
||||
/// - `default` — agent's built-in behaviour (permission requests per tool call).
|
||||
/// - `auto` — fully autonomous execution; model-gated (requires `supportsAutoMode`);
|
||||
/// the adapter degrades gracefully to `default` when the active model does not
|
||||
/// support it. The adapter self-approves all tool calls internally — no
|
||||
/// `session/request_permission` ever crosses ACP under this mode.
|
||||
/// - `acceptEdits` — auto-approve file edits, still ask for other tools.
|
||||
/// - `dontAsk` — never prompt; reject anything that would require permission.
|
||||
/// - `plan` — planning-only mode (no tool execution).
|
||||
@@ -123,6 +127,15 @@ pub enum PermissionMode {
|
||||
/// Agent default — permission requests per tool call.
|
||||
#[value(alias = "default")]
|
||||
Default,
|
||||
/// Fully autonomous execution; model-gated (requires `supportsAutoMode`).
|
||||
///
|
||||
/// The adapter self-approves all tool calls internally and never emits
|
||||
/// `session/request_permission`, so this mode is incompatible with
|
||||
/// `ask` (card never fires) and `reject` (policy is a dead letter while
|
||||
/// the adapter auto-approves — the inverted-security worst case).
|
||||
/// Compatible with `allow` (both want unattended approval).
|
||||
#[value(alias = "auto")]
|
||||
Auto,
|
||||
/// Auto-approve file edits, still ask for other tools.
|
||||
#[value(alias = "acceptEdits")]
|
||||
AcceptEdits,
|
||||
@@ -140,6 +153,7 @@ impl PermissionMode {
|
||||
pub fn as_wire_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "default",
|
||||
Self::Auto => "auto",
|
||||
Self::AcceptEdits => "acceptEdits",
|
||||
Self::DontAsk => "dontAsk",
|
||||
Self::Plan => "plan",
|
||||
@@ -244,6 +258,10 @@ impl ResolvedPermissionConfig {
|
||||
/// - `ask` + explicit `dontAsk` — harness would want the agent to
|
||||
/// escalate, but `dontAsk` makes the agent self-deny internally.
|
||||
/// - `allow` + explicit `dontAsk` — same contradiction.
|
||||
/// - `ask` + explicit `auto` — adapter self-approves internally, so the
|
||||
/// card never fires; the `ask` policy becomes a silent dead letter.
|
||||
/// - `reject` + explicit `auto` — inverted-security worst case: policy says
|
||||
/// "deny" but the adapter auto-approves everything internally.
|
||||
pub fn resolve(
|
||||
policy: PermissionPolicy,
|
||||
explicit_mode: Option<PermissionMode>,
|
||||
@@ -257,6 +275,20 @@ impl ResolvedPermissionConfig {
|
||||
dontAsk makes the agent self-deny internally before Buzz can answer"
|
||||
)));
|
||||
}
|
||||
// Fail on ask/reject + auto: `auto` makes the adapter self-approve
|
||||
// internally so `session/request_permission` never crosses ACP.
|
||||
// Under `ask` the card never fires; under `reject` the policy is a dead
|
||||
// letter while the adapter silently grants everything (inverted security).
|
||||
// `allow` + auto is compatible: both policies want unattended approval.
|
||||
if matches!(policy, PermissionPolicy::Ask | PermissionPolicy::Reject)
|
||||
&& explicit_mode == Some(PermissionMode::Auto)
|
||||
{
|
||||
return Err(ConfigError::ConfigFile(format!(
|
||||
"permission_policy={policy} conflicts with permission_mode=auto: \
|
||||
auto makes the adapter self-approve internally before Buzz can answer \
|
||||
(ask: card never fires; reject: policy becomes a dead letter)"
|
||||
)));
|
||||
}
|
||||
|
||||
let (effective_mode, mode_source) = match explicit_mode {
|
||||
Some(m) => (m, ModeSource::Explicit),
|
||||
|
||||
Reference in New Issue
Block a user