Skip to content

fix(acp): let an unattended bridge answer session/request_permission - #26

Merged
mattbalza merged 1 commit into
mainfrom
fix/acp-permission-policy
Aug 7, 2026
Merged

fix(acp): let an unattended bridge answer session/request_permission#26
mattbalza merged 1 commit into
mainfrom
fix/acp-permission-policy

Conversation

@mattbalza

Copy link
Copy Markdown
Owner

Why

The 2026-08-07 08:46 UTC deploy of bfa150be5 (the 136-commit upstream ingest) took all four server agents mute. Upstream ad538bfb1 flipped session/request_permission from auto-approve to an unconditional reject_once and deleted PermissionMode::BypassPermissions.

That is the correct posture for an attended client. For our droplet bridges it is a silent outage:

  • nobody is awake to answer the prompt, so every tool call an adapter asks about is refused;
  • @codex publishes its reply through mcp__buzz__buzz_send_message, so a refused tool call is not an error — it is an invisible turn. The journal logged matched turn starting / turn complete pairs, the workers burned CPU, and the channels stayed silent.

We are keeping upstream's hardening. BypassPermissions is not coming back.

What

Adds the opt-in upstream is already writing: BUZZ_ACP_PERMISSION_POLICY from block/buzz PR block#4938 (branch duncan/permission-policy), using upstream's exact value names (allow | ask | reject) and its reject default. When an ingest carries that PR, this patch is a deletion, not a merge reconciliation — both hunks carry a header comment saying so.

allow selects the request's unique allow_once option and nothing else:

request shape outcome
exactly one allow_once with a string optionId selected
no allow_once denied (reject_once)
several allow_once denied — we would be choosing between distinct grants
allow_once with missing/non-string optionId denied
allow_always present never selected
policy reject (default) or ask byte-for-byte today's output

ask exists so an upstream-shaped config value can never fail clap parsing and take a bridge down; with no prompt surface it denies like reject.

The policy is published once by Config::from_args into a process-wide fail-closed AtomicBool, because handle_permission_request answers on the agent's I/O task and never sees a Config. The decision itself is a pure function (permission_response(allow, id, options)), so the tests never touch the global.

The boundary

Our permission boundary is the OS sandbox — bubblewrap, one UID per identity, [permissions.*] in each config.toml, network.enabled = false, and the broker socket — not an ACP prompt no human is there to answer. That sentence is in the patch comment.

Logging blind spot

Both permission log lines target acp::permission, and RUST_LOG=buzz_acp=info filters targets by prefix — so neither ever appeared, which is why this shipped invisibly. The request line goes debug!info! and the denial goes info!warn!. The matching acp::permission=info directive lands on the four bridge units in the ERP repo, asserted by provision-buzz-isolation.sh verify.

Test plan

cargo test -p buzz-acp722 passed, 0 failed, including:

  • allow_policy_selects_the_unique_allow_once
  • allow_policy_denies_when_allow_once_is_ambiguous
  • allow_policy_with_no_options_is_cancelled
  • allow_policy_denies_allow_once_without_string_option_id
  • allow_policy_never_selects_allow_always
  • reject_policy_is_identical_to_the_unconditional_denial (numeric id, string id, empty options)
  • only_allow_flips_the_published_policy
  • test_permission_policy_value_enum_is_lowercase, test_summary_includes_permission_policy, test_summary_permission_policy_default_is_reject, test_only_allow_policy_permits_selection

test_permission_mode_rejects_unattended_bypass is untouched and still passes. cargo clippy -p buzz-acp --all-targets is clean.

Prod round-trip (the check the 08:46 deploy skipped) runs at deploy: a human mentions @codex in a channel and gets a kind:9 reply, cross-checked against the rollout JSONL under /var/lib/buzz-acp-codex/codex/sessions/ where every mcp_tool_call_end must carry a result rather than {"Err":"user rejected MCP tool call"}.

Upstream ad538bf (in the 136-commit ingest, bfa150b) flipped
`session/request_permission` from auto-approve to an unconditional
`reject_once` and deleted `PermissionMode::BypassPermissions`. That is the
right posture for an attended client and the wrong one for our four droplet
bridges: nobody is awake to answer the prompt, and @codex publishes its reply
*through* `mcp__buzz__buzz_send_message`, so a refused tool call is not an
error — it is an invisible turn. All four agents went mute on 2026-08-07 with
matched `turn starting` / `turn complete` pairs in the journal.

Keep the hardening; add the opt-in upstream is already writing. This is
`BUZZ_ACP_PERMISSION_POLICY` from block/buzz PR block#4938, with upstream's exact
value names and its `reject` default, so the ingest that carries that PR
deletes this patch rather than merging around it. Both hunks say so in a
header comment.

`allow` selects the request's *unique* `allow_once` option and nothing else:
zero candidates, several candidates, or a missing/non-string `optionId` all
fall through to the same denial as before. Never `allow_always`, never a
hardcoded optionId.

Our permission boundary is the OS sandbox — bubblewrap, one UID per identity,
`[permissions.*]` in each config.toml, `network.enabled = false` and the broker
socket — not an ACP prompt no human is there to answer.

Also raise the two permission log lines out of the blind spot that hid this:
they log to target `acp::permission`, and `RUST_LOG=buzz_acp=info` filters
targets by prefix, so neither ever appeared. The request line goes debug->info
and the denial goes info->warn; the unit files gain the matching directive.
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@mattbalza
mattbalza merged commit 0c4f48e into main Aug 7, 2026
30 checks passed
@mattbalza
mattbalza deleted the fix/acp-permission-policy branch August 7, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant