Skip to content

Add an ACP agent so Buzz (and other ACP clients) can talk to Fleet #984

Description

@bradflaugher

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

  • fleet acp speaks ACP over stdio; a canned client can initialize / session/new / session/prompt and get a governed Fleet turn (visible in chat or task run log)
  • Assistant text reaches the client via session/update; cancel works
  • buzz-acp with BUZZ_ACP_AGENT_COMMAND=fleet BUZZ_ACP_AGENT_ARGS=acp produces a reply in a Buzz workspace (manual checklist in the PR)
  • Daemon-down / 401 / timeout produce clear client-visible errors
  • Seam-purity / entrypoint-conformance tests stay green — no second loop
  • docs/ACP.md with an Honest scope section; spec pin; CHANGELOG [Unreleased] entry
  • Tests against the fake-LLM seam; no live keys, no live Buzz in CI
  • SECURITY: bot token not logged; secrets in env only

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

MModerate: few days, one subsystemP2Nice to haveenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions