buzz-acp: no permission mode lets an agent publish its reply — mute turns report outcome="ok"
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
With `buzz-acp`'s default `--permission-mode dont-ask`, a Codex or Claude Code agent connects to the relay, subscribes, receives an `@mention`, runs a full turn — and publishes nothing. The harness logs `agent_returned outcome="ok"` and no error is raised anywhere, so the failure is indistinguishable from "the agent had nothing to say."
Both runtimes fail for the same structural reason but through two different mechanisms, and neither is reachable through the current `PermissionMode` enum.
## Why it's silent
`handle_session_update` logs `agent_message_chunk` and does not publish it:
https://github.com/block/buzz/blob/main/crates/buzz-acp/src/acp.rs#L1733
```rust
"agent_message_chunk" => {
if let Some(text) = update["content"]["text"].as_str() {
tracing::info!(target: "acp::stream", "{text}");
}
false
}
```
That is by design — `base_prompt.md` tells the agent the `buzz` CLI is its primary interface, so replying means shelling out to `buzz messages send`. The consequence is that **an agent that cannot run a shell command cannot speak at all**, and `dont-ask` is exactly that agent.
## Repro — Claude Code
```
buzz-acp --agent-command claude-agent-acp --channels \
--agent-owner --respond-to allowlist --respond-to-allowlist
```
`@mention` the agent. It runs a turn, then explains the problem in its own prose — which is only visible in `acp::stream` logs, never in the channel:
> 채널에 메시지를 보내려면 `buzz messages send` (Bash)가 필요한데, 이 세션이 "don't ask" 모드라 Bash 실행이 거부됐습니다.
> (*"Replying in the channel needs `buzz messages send` (Bash), but this session is in "don't ask" mode so Bash execution was denied."*)
`claude-agent-acp` describes the mode as **"Don't prompt for permissions, deny if not pre-approved"** — so it denies the shell call outright. The modes it advertises in `session/new`:
```
auto | default | acceptEdits | plan | dontAsk | bypassPermissions
```
`auto` ("Use a model classifier to approve/deny permission prompts") is the lightest mode that permits publishing, and `PermissionMode` has no variant for it.
## Repro — Codex
Same invocation with `--agent-command codex-acp`. The agent composes the right command and the shell call fails:
```
tool_call: buzz messages send --channel --reply-to --content '…'
-> failed
{"error":"network_error",
"message":"network error: error sending request for url (http://localhost:3000/query):
client error (Connect): tcp connect error: Operation not permitted (os error 1)",
"retryable":true}
```
`codex_network_env` already anticipates this and injects `CODEX_CONFIG={"sandbox_workspace_write":{"network_access":true}}`. Injection succeeds — `injecting CODEX_CONFIG network_access for relay host host="localhost"` appears in the log — **but it has no effect**, because `codex-acp` pins the session to the `agent` preset and the preset wins over the config override. Setting `sandbox_mode` in the parent env's `CODEX_CONFIG` does not help either.
The only thing that works is the session mode. Driving the adapter directly:
```
session/set_config_option {configId: "mode", value: "agent-full-access"}
→ CALL: curl -s -o /dev/null -w '%{http_code}' http://localhost:3000
-> completed {"formatted_output": "200", "exit_code": 0}
```
`codex-acp` advertises that mode in `session/new` (`modes.availableModes[].id` = `read-only | agent | agent-full-access`), so the existing `agent_supports_mode` gate already handles it correctly — only the enum value is missing.
## What fixed it locally
Adding two variants to `PermissionMode` (`crates/buzz-acp/src/config.rs`) and passing the matching flag:
| Variant | Wire value | Runtime |
|---|---|---|
| `Auto` | `auto` | `claude-agent-acp` |
| `AgentFullAccess` | `agent-full-access` | `codex-acp` |
No change to `agent_supports_mode` was needed, and agents that don't advertise these ids are skipped as before. Both agents then published normally.
I also tried `bypassPermissions` and reverted it — `test_permission_mode_rejects_unattended_bypass` asserts it must be rejected ("must not disable the ACP permission boundary"). `auto` keeps a classifier in the loop, which seems like the right side of that line.
## Suggestions
1. **Add the missing modes.** At minimum `auto`; `agent-full-access` for Codex is a sandbox widening, so it may deserve an explicit opt-in or a warning at startup.
2. **Consider selecting the mode per runtime.** The current enum is Claude-flavoured (`acceptEdits`, `dontAsk`) but is sent verbatim to every adapter, and Codex's ids don't overlap at all. `KnownAcpRuntime` already carries per-runtime metadata in the desktop crate.
3. **Don't let a mute turn look like a successful one.** A turn that ends with no published event, when the trigger was a direct `@mention`, is almost always a failure. `base_prompt.md` already calls this "a silent failure" — a warning log (or a channel-visible note) when `agent_returned outcome="ok"` coincides with zero published messages would have made both of these diagnosable in seconds instead of hours.
## Environment
- `block/buzz` @ `02f640bc4`, macOS 15 (arm64), Docker via Colima
- `@agentclientprotocol/codex-acp` 1.1.14, Codex CLI authenticated via ChatGPT
- `@agentclientprotocol/claude-agent-acp` 0.66.0, Claude Code 2.1.222 authenticated via claude.ai
Happy to open a PR with the two enum variants and tests if that's the direction you'd want.
Contributor guide
Assessment
This issue has not been assessed yet.