Summary
Make Fleet speak the Agent Client Protocol (agentclientprotocol.com — Zed/JetBrains ACP, not IBM's old Agent Communication Protocol that merged into A2A) so an ACP client can drive a governed Fleet turn.
The motivating client is Buzz: a human messages a Fleet-backed agent in a Buzz workspace and gets a real Fleet reply. The same fleet acp binary also works with Zed, JetBrains, or any other ACP client. Buzz is a consumer of this surface, not a protocol Fleet has to implement.
Framing that keeps this maintainable: ACP is a protocol-shaped translation of a seam we already ship, not a new seam and not a Buzz-specific bridge. Same doctrine as A2A (#1279 / ADR-0051): one external caller in → one governed run → one outcome out. No second loop (ADR-0001).
What this is not
| Tempting approach |
Why not |
| A Buzz/Nostr HTTP shim that POSTs Fleet's chat API |
Reimplements what buzz-acp already does. Couples Fleet to Buzz's relay, keys, and event kinds. |
| An ACP client inside Fleet |
Wrong direction. An ACP client launches an agent. We want Fleet to be the agent. Outbound “drive Claude Code / OpenCode from Fleet” is a different issue. |
| Fleet speaking Nostr / hosting a Buzz relay |
Out of scope. buzz-acp already owns Buzz identity, relay subscription, and message delivery. |
| Reusing A2A as the Buzz front door |
Buzz does not speak A2A. A2A is HTTP JSON-RPC, task-scoped, agent-to-agent. ACP is JSON-RPC over stdio, session-scoped, client-to-agent. Complementary surfaces. |
How Buzz actually works
Human in Buzz
─WS/Nostr─► Buzz relay
│
─WS─► buzz-acp (ACP *client* + Buzz identity)
│
─stdio ACP─► agent process
buzz-acp already:
- holds the agent's Nostr key and talks to the relay
- listens for @mentions
- launches
BUZZ_ACP_AGENT_COMMAND + BUZZ_ACP_AGENT_ARGS as a subprocess
- speaks ACP (
initialize, session/new, session/prompt) over stdio
- posts the agent's reply back via the Buzz CLI
That is exactly how OpenCode lands in Buzz: OpenCode ships a native ACP entrypoint (opencode acp), no *-acp adapter package, and Buzz just spawns it (block/buzz#2370). Goose is goose acp. Hermes is hermes acp. Fleet should be fleet acp.
Setup for a Buzz workspace is then:
export BUZZ_ACP_AGENT_COMMAND=fleet
export BUZZ_ACP_AGENT_ARGS=acp
# plus the usual BUZZ_PRIVATE_KEY / BUZZ_RELAY_URL
buzz-acp
A later, optional courtesy: a Buzz Desktop catalog entry so Fleet shows up next to OpenCode/Goose. Not required for v1 — custom command works today.
Architecture
Fleet is a long-running daemon (sandbox, budgets, MCP broker, DB). OpenCode is the agent process. So fleet acp is an in-tree stdio ACP agent in the fleet binary that maps protocol methods onto an existing governed seam. The subprocess is protocol translation, not a second executor and not a Buzz product.
buzz-acp ──ACP stdio──► fleet acp ──existing seam──► agentcore.Run
(ACP client) (ACP agent) (one governed loop)
How the stdio process reaches the daemon is an implementation detail of the adapter (loopback HTTP to the local orchestrator, or in-process if fleet acp is invoked on the same host with the same config). Either way the run still goes through agentcore.Run with the same policy, ceilings, audit, sandbox, and MCP credential gates. Guarded by the existing seam-purity / entrypoint-conformance tests — the ACP adapter stays data-supplying, not loop-forking.
Protocol scope (v1)
Pin the ACP spec version in one place (internal/acp.SpecVersion + docs). Prefer generated/vendored types (evaluate coder/acp-go-sdk the way A2A evaluated a2a-go: types + framing only, no second executor).
| ACP method |
Fleet behavior |
initialize |
Advertise agent info + honest capabilities (text prompts; no fs/terminal in v1) |
session/new |
Open a Fleet conversation (or a task-backed session — spike decides; chat is the more natural map because ACP is multi-turn) |
session/prompt |
One governed turn; stream assistant text via session/update; return a stop reason |
session/cancel |
Cancel the in-flight turn |
session/load |
Deferred unless the chosen seam already has durable session ids we can round-trip honestly |
Auth: operator-provisioned Fleet credential for a dedicated bot user / task key, resolved host-side from env. Not caller-chosen personas/models/ceilings — same posture as A2A and webhook triggers.
Capability surface (v1)
- Text in / text out. Tool traces summarized as short
session/update text, full detail stays in the Fleet run log.
- Approvals stay in the Fleet UI. If a turn parks on an allow/deny card, ACP replies with a pointer (
approval needed in Fleet: <url>), not a reimplementation of cards over session/request_permission.
- Failure modes (daemon down, 401, timeout) become clear ACP errors / text replies, not hung sessions.
Implementation track
| Step |
Deliverable |
| 0 |
Spike: initialize + session/new + echo session/prompt against a fake-LLM fleet. Confirm buzz-acp will spawn fleet acp. Decide chat-conversation vs task seam. |
| 1 |
Real turn mapping + streaming session/update + cancel |
| 2 |
Approval / error text, bot-user auth, docs/ACP.md (honest scope) + CHANGELOG |
| 3 |
Manual Buzz checklist: fresh workspace, buzz-acp pointed at fleet acp, @mention → Fleet run log entry → reply in Buzz |
Non-goals (v1)
- Fleet hosting a Buzz relay, or any Nostr protocol in-tree.
- An ACP client (Fleet launching other ACP agents).
- Full ACP fs / terminal / permission-prompt parity with Zed.
- Mapping every Buzz user onto a Fleet user.
- A Buzz Desktop first-party catalog patch (nice follow-up; custom command is enough).
- Treating this as a replacement for A2A. A2A stays the HTTP agent-to-agent surface (
docs/A2A.md).
Acceptance checklist
Size
M if the chat (or task) seam maps cleanly and buzz-acp accepts fleet acp as a custom command — which it should, that is the documented extension point. L only if we discover ACP session semantics that force a new persistence model.
References
Assigning to @KristianYe.
Summary
Make Fleet speak the Agent Client Protocol (agentclientprotocol.com — Zed/JetBrains ACP, not IBM's old Agent Communication Protocol that merged into A2A) so an ACP client can drive a governed Fleet turn.
The motivating client is Buzz: a human messages a Fleet-backed agent in a Buzz workspace and gets a real Fleet reply. The same
fleet acpbinary also works with Zed, JetBrains, or any other ACP client. Buzz is a consumer of this surface, not a protocol Fleet has to implement.Framing that keeps this maintainable: ACP is a protocol-shaped translation of a seam we already ship, not a new seam and not a Buzz-specific bridge. Same doctrine as A2A (#1279 / ADR-0051): one external caller in → one governed run → one outcome out. No second loop (ADR-0001).
What this is not
buzz-acpalready does. Couples Fleet to Buzz's relay, keys, and event kinds.buzz-acpalready owns Buzz identity, relay subscription, and message delivery.How Buzz actually works
buzz-acpalready:BUZZ_ACP_AGENT_COMMAND+BUZZ_ACP_AGENT_ARGSas a subprocessinitialize,session/new,session/prompt) over stdioThat is exactly how OpenCode lands in Buzz: OpenCode ships a native ACP entrypoint (
opencode acp), no*-acpadapter package, and Buzz just spawns it (block/buzz#2370). Goose isgoose acp. Hermes ishermes acp. Fleet should befleet acp.Setup for a Buzz workspace is then:
A later, optional courtesy: a Buzz Desktop catalog entry so Fleet shows up next to OpenCode/Goose. Not required for v1 — custom command works today.
Architecture
Fleet is a long-running daemon (sandbox, budgets, MCP broker, DB). OpenCode is the agent process. So
fleet acpis an in-tree stdio ACP agent in thefleetbinary that maps protocol methods onto an existing governed seam. The subprocess is protocol translation, not a second executor and not a Buzz product.How the stdio process reaches the daemon is an implementation detail of the adapter (loopback HTTP to the local orchestrator, or in-process if
fleet acpis invoked on the same host with the same config). Either way the run still goes throughagentcore.Runwith the same policy, ceilings, audit, sandbox, and MCP credential gates. Guarded by the existing seam-purity / entrypoint-conformance tests — the ACP adapter stays data-supplying, not loop-forking.Protocol scope (v1)
Pin the ACP spec version in one place (
internal/acp.SpecVersion+ docs). Prefer generated/vendored types (evaluate coder/acp-go-sdk the way A2A evaluateda2a-go: types + framing only, no second executor).initializesession/newsession/promptsession/update; return a stop reasonsession/cancelsession/loadAuth: operator-provisioned Fleet credential for a dedicated bot user / task key, resolved host-side from env. Not caller-chosen personas/models/ceilings — same posture as A2A and webhook triggers.
Capability surface (v1)
session/updatetext, full detail stays in the Fleet run log.approval needed in Fleet: <url>), not a reimplementation of cards oversession/request_permission.Implementation track
initialize+session/new+ echosession/promptagainst a fake-LLM fleet. Confirmbuzz-acpwill spawnfleet acp. Decide chat-conversation vs task seam.session/update+ canceldocs/ACP.md(honest scope) + CHANGELOGbuzz-acppointed atfleet acp, @mention → Fleet run log entry → reply in BuzzNon-goals (v1)
docs/A2A.md).Acceptance checklist
fleet acpspeaks ACP over stdio; a canned client caninitialize/session/new/session/promptand get a governed Fleet turn (visible in chat or task run log)session/update; cancel worksbuzz-acpwithBUZZ_ACP_AGENT_COMMAND=fleetBUZZ_ACP_AGENT_ARGS=acpproduces a reply in a Buzz workspace (manual checklist in the PR)docs/ACP.mdwith an Honest scope section; spec pin; CHANGELOG[Unreleased]entrySize
M if the chat (or task) seam maps cleanly and
buzz-acpacceptsfleet acpas a custom command — which it should, that is the documented extension point. L only if we discover ACP session semantics that force a new persistence model.References
opencode acp, no adapter package)docs/A2A.md/ ADR-0051 (inbound A2A), Outbound A2A client: fleet delegating to other A2A agents (#1279 Phase 3) #1368 / ADR-0056 (outbound A2A)docs/BUILDING-ON-FLEET.mdAssigning to @KristianYe.