Agent Client Protocol (ACP)

LibreFang ships an Agent Client Protocol adapter so editors like Zed, VS Code, and JetBrains can embed a LibreFang agent natively. The editor renders approval modals, streams prompt deltas, and routes file references through its own UI rather than the LibreFang dashboard.


How to use it

LibreFang exposes the ACP server as a CLI subcommand:

librefang acp

librefang acp runs the ACP server on stdio until the editor closes the connection. There is no network port — the editor spawns this binary as a child process and pipes JSON-RPC frames through stdin/stdout.

Specifying an agent

By default the server binds to the agent named assistant (the same default used by the dashboard and TUI). To embed a different agent:

librefang acp --agent reviewer
librefang acp --agent 7b3f9c2e-1d4a-4f8b-9e2a-1b8c3d4e5f60

Both human-readable names and UUIDs are accepted. A typo surfaces as agent not found immediately, before the JSON-RPC handshake.


Two execution modes

LibreFang automatically selects between in-process and daemon-attached depending on whether a daemon is already running on the machine.

In-process

When no daemon is running, librefang acp boots a fresh kernel inside the same process and serves ACP on stdio. State (agent history, remembered approval decisions, sessions) lives only as long as the editor keeps the child process alive.

┌─────────┐     stdio JSON-RPC     ┌──────────────────────┐
  Editor ◄────────────────────►  librefang acp
  (Zed)  │                         │  ├─ in-process       │
└─────────┘  kernel
  └─ ACP server
                                    └──────────────────────┘

Suitable for one-shot use without running librefang start first. Each editor tab gets its own kernel instance, so there's no cross-tab state sharing.

Daemon-attached (Unix)

When the daemon is already running and its ACP UDS listener is up at ~/.librefang/acp.sock, librefang acp becomes a transparent stdio↔socket pipe. JSON-RPC frames flow directly between the editor and the long-running daemon kernel.

┌─────────┐  stdio  ┌──────────────┐  UDS   ┌─────────────────────┐
  Editor ◄─────► librefang acp│ ◄────►  librefang start
  (Zed)  │         │  (proxy)     │        │  ├─ ACP listener    │
└─────────┘         └──────────────┘  ├─ shared kernel
  └─ approval policy
                                            └─────────────────────┘

Multiple editor tabs share one kernel, agent history, and allow_always decisions. A stale socket file from a crashed daemon is detected and the CLI falls back to in-process mode automatically.

Daemon-attached mode is Unix-only for now (Linux, macOS). Windows requires named pipes — tracked as a follow-up issue.


Editor configuration

Zed

Zed exposes ACP-compatible agents through its agent_servers configuration. Add LibreFang as an external agent in ~/.config/zed/settings.json:

{
  "agent_servers": {
    "librefang": {
      "command": "librefang",
      "args": ["acp"]
    }
  }
}

Pass --agent <name> in args to bind a non-default agent. The Zed agent panel will show "librefang" in the picker; selecting it spawns librefang acp and starts a session.

VS Code (Claude Code extension)

The Claude Code extension's external-agent settings accept ACP servers. Configure in the extension's Custom Agent Servers setting:

{
  "claude-code.customAgents": [
    {
      "id": "librefang",
      "label": "LibreFang",
      "command": "librefang",
      "args": ["acp"]
    }
  ]
}

JetBrains

Open Settings → Tools → AI Assistant → External Agents and add a new entry pointing to librefang acp.


What's wired up

The adapter implements the ACP methods most editors actually use:

MethodDirectionNotes
initializeclient → agentDeclares AgentCapabilities (load_session, session list/resume/close) and PromptCapabilities (image/audio/embedded all false for now).
session/newclient → agentMints a fresh ACP SessionId, allocates a backing LibreFang session.
session/loadclient → agentMaps an existing ACP SessionId onto a fresh LibreFang session. History replay across daemon restarts is a follow-up.
session/listclient → agentLists active ACP sessions; supports cwd filtering.
session/resumeclient → agentSame shape as load until full history replay lands.
session/closeclient → agentDrops the session-store entry.
session/promptclient → agentPumps StreamEvent from the agent loop as session/update notifications, returns PromptResponse with the final StopReason.
session/cancelclient → agentCancels the in-flight prompt via per-session cancellation token.
session/request_permissionagent → clientForwarded from kernel ApprovalEvent::Created events; outcome maps back to ApprovalDecision. allow_always / reject_always decisions are persisted via the approval cache.

Permission flow

When a tool needs approval (any deferred-path tool the agent calls), the kernel's ApprovalManager broadcasts an ApprovalEvent::Created. The ACP permission bridge:

  1. Filters by the LibreFang SessionId it owns.
  2. Sends session/request_permission with the tool name, action summary, and four options: Allow once, Allow always, Deny, Deny always.
  3. Awaits the editor's response with a 60-second timeout (timed-out approvals default to denied).
  4. Persists _always decisions through AcpKernel::remember_decision so the next (agent, tool_name) invocation short-circuits.
  5. Calls KernelHandle::resolve_tool_approval, which spawns the deferred tool execution.

The audit log records decided_by = "acp" so operators can distinguish editor-side approvals from dashboard or TUI approvals after the fact.


What's wired up vs. follow-up

Wired up:

  • fs/read_text_file / fs/write_text_file agent → client requests. The runtime's file_read / file_write builtins route through the editor when an AcpFsClient is registered for the session.
  • terminal/{create,output,wait_for_exit,kill,release} agent → client requests. shell_exec runs the command's PTY in the editor's terminal panel when an AcpTerminalClient is registered.
  • Editor-declared cwd: terminal/create is dispatched without an explicit cwd so the editor uses the cwd it declared at session/new time (the user's project root).
  • Windows named pipes: daemon-attached mode works on Windows via \\.\pipe\librefang-acp in addition to the Unix UDS at ~/.librefang/acp.sock.
  • session/load / session/resume honour the supplied ACP session id by deriving the LibreFang SessionId deterministically (Uuid::new_v5 keyed on the ACP id), so a reconnecting editor rejoins the same kernel-side session and picks up its persisted history without an explicit replay step.
  • Multimodal content blocks (image / audio / embedded resource) are declared as unsupported in PromptCapabilities; if a defensive editor still sends them, the adapter emits a visible session/update message chunk telling the user the attachment was dropped.

Follow-up issues (not yet shipped):

  • session/update history replay stream — sending past assistant messages back as session/update notifications so the editor's chat panel rehydrates immediately on reconnect (the agent already has the history through the kernel; this is just a UX polish).
  • True multimodal pipeline — image / audio / embedded resource content actually reaching the LLM driver. Cross-cutting work in librefang-llm-drivers is required.
  • tool_use_id on synchronous (request_approval) approvals and dashboard manual approvals — today only the deferred path carries it; the others fall back to a clearly-namespaced approval-{req_id} ToolCallId.
  • Persisting tool_use_id and DeferredToolExecution.session_id across daemon restarts — needs a column on pending_approvals and a small migration.
  • End-to-end integration tests for fs/* and terminal/* reverse-RPC routing under a duplex transport with mock client handlers.

If your editor ships an ACP feature outside the supported set above, the adapter returns JSON-RPC method_not_found (-32601) for the request — your editor should skip the optional feature gracefully.


Troubleshooting

The editor reports "agent crashed" immediately. Run librefang acp from a terminal directly — any kernel boot failure (config parse error, port conflict from a previous daemon, missing API keys) prints to stderr.

Allow once resolves but the tool never runs. This is the bug fixed by routing through KernelHandle::resolve_tool_approval. If you see it on a recent build, file an issue with the audit log entry — the resolve path should always spawn handle_approval_resolution for deferred tools.

Daemon-attached mode never engages. Run librefang start and confirm ~/.librefang/acp.sock exists. The CLI falls back to in-process mode if the socket file is missing or the daemon process is gone.

Multimodal input gets dropped. The current build advertises prompt_capabilities = { image: false, audio: false, embedded_context: false }. Editors honoring the capability declaration won't try; editors that ignore it will see the text portion only.