Skip to content

Add --acp: serve the Agent Client Protocol over stdio - #1611

Open
espetro wants to merge 1 commit into
CodebuffAI:mainfrom
espetro:feature/acp-agent
Open

espetro wants to merge 1 commit into
CodebuffAI:mainfrom
espetro:feature/acp-agent

Conversation

@espetro

@espetro espetro commented Nov 2, 2026

Copy link
Copy Markdown

Summary

Adds --acp: the CLI serves the Agent Client Protocol v1 over stdio (newline-delimited JSON-RPC, via the official @agentclientprotocol/sdk), so editors like Zed can drive Freebuff as an agent subprocess. This is the in-tree answer to #319.

Why in-tree, not a wrapper

While prototyping this as an external SDK wrapper, a run with the user's device token through a non-CLI harness was refused by the backend:

Freebuff free mode only works through the official Freebuff CLI, Freebuff Desktop and freebuff.com. This request came from a third-party client, harness or proxy, so it was refused. (warning, then ban)

So the ACP surface has to live inside the official binary. That is also the minimal-diff path: the adapter reuses the exact run plumbing the TUI uses (getCodebuffClient() → SDK run() with PrintModeEvent handlers), so free mode's session admission, freebuff_instance_id metadata and device signing are byte-identical and the allowlist still sees an official Freebuff CLI.

What it does

  • freebuff --acp (and codebuff --acp) skips renderer setup and serves the protocol on stdin/stdout instead of the TUI.
  • Sessions are in-memory per connection, continued across turns via the SDK's previousRun state.
  • Event mapping (PrintModeEvent → session/update):
    • text → agent_message_chunk
    • reasoning_delta → agent_thought_chunk
    • tool_call → tool_call (pending, with kind from a name table)
    • tool_result → tool_call_update (completed, output as content)
    • error → notice
    • finish → the session/prompt response resolves with end_turn
  • session/cancel aborts the in-flight SDK run. Tool calls a turn leaves open are closed (failed on cancel, completed otherwise) so clients never spin forever.

What it deliberately does not do

  • No auth flow over ACP (authMethods: []): the CLI's own keychain/credentials login is the auth path; session/prompt returns auth_required when signed out.
  • No session/load/session/list (in-memory sessions die with the process; agentCapabilities.loadSession: false).
  • No permission round-trips yet: the run uses the CLI's default tool behavior. A follow-up can map overrideTools to session/request_permission.
  • No plan/todo passthrough, no usage_update.

Tests

  • Unit: the event mapping (each PrintModeEvent variant, unknown tools, media outputs).
  • In-process: an ACP client drives the agent through initialize → session/new → session/prompt and asserts the streamed updates.
  • Transport: the test spawns the real CLI with --acp and completes the handshake over actual stdio.

One drive-by: picocolors was imported by cli/src/index.tsx but missing from cli/package.json (resolved transitively in the private tree; fails to run from a fresh clone of this mirror).

Related

Fixes the ACP half of CodebuffAI#319. `freebuff --acp` serves ACP v1 (stdio
JSON-RPC, newline-delimited) using the official
\@agentclientprotocol/sdk, so editors such as Zed can drive the CLI as
an agent subprocess.

The ACP surface is a second front end over the same conversation
machinery the TUI uses: `getCodebuffClient()` + the SDK's `run()` with
PrintModeEvent handlers. Free mode's session admission, instance
metadata and device signing are unchanged, so the server's client
allowlist still sees an official Freebuff CLI - the third-party-refusal
gate that keeps an external SDK wrapper from driving free mode (seen
while prototyping) never trips.

Mapping: text -> agent_message_chunk, reasoning_delta ->
agent_thought_chunk, tool_call -> tool_call (pending), tool_result ->
tool_call_update (completed), error -> notice, finish -> the
session/prompt response (end_turn). session/cancel aborts the in-flight
SDK run; tool calls left open by a turn end failed/completed so clients
never spin forever.

Tests cover the event mapping, the protocol lifecycle in-process, and
the real stdio transport (spawn the CLI, initialize, session/new).
@codebuff-team

Copy link
Copy Markdown
Contributor

Good instinct and a genuinely useful feature — reusing getCodebuffClient()/run() to keep free-mode admission and device signing identical is exactly the right call given the backend's allowlist, and the three-tier test suite (pure event-mapping unit tests, in-process protocol round-trip, real stdio spawn) is more thorough than most PRs this size.

The blocking bug: FreebuffAcpAgent's constructor takes (options: { logger?: Logger; runTurn?: ... }), but cli/src/acp/serve.ts calls new FreebuffAcpAgent(log) — passing the Logger instance directly instead of { logger: log }. Since logger and runTurn are both optional, TypeScript won't catch this, and at runtime options.logger is silently undefined, so the custom logger is dropped and the agent falls back to the default logger import every time. The unit test in acp-agent.test.ts makes the same mistake (info, error, warn etc. as top-level fields instead of nested under logger), which is why this slipped through — the tests never actually exercise the real constructor shape they're mocking. Worth fixing both.

Other things to flag before porting:

  • defaultRunTurn hardcodes agent: 'base2-free' and costMode: AGENT_MODE_TO_COST_MODE.LITE, ignoring --lite/--max/initialMode entirely. That may be an intentional v1 simplification, but it silently diverges from what the TUI does with the same flags — worth a comment or a TODO at minimum, ideally threading initialMode through.
  • Several as never casts in the test files suggest the type surface wasn't fully reconciled with the implementation; worth tightening so the next person doesn't inherit silent mismatches like the one above.
  • The picocolors drive-by fix is good and should probably be split into its own tiny PR so it doesn't get lost if this one needs rework.

None of this touches forbidden paths, and the overall shape is portable — fix the constructor bug and clarify the hardcoded agent/costMode choice and this is in good shape.

@codebuff-team codebuff-team added bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written labels Nov 2, 2026

This branch has not been deployed

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

Labels

bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants