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.
ACP is a JSON-RPC 2.0 protocol over a duplex byte stream — typically the agent process's stdio. Editor and agent ship state changes (prompt chunks, tool calls, permission requests) as session/update notifications, with bidirectional request/response for permission decisions.
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:
| Method | Direction | Notes |
|---|---|---|
initialize | client → agent | Declares AgentCapabilities (load_session, session list/resume/close) and PromptCapabilities (image/audio/embedded all false for now). |
session/new | client → agent | Mints a fresh ACP SessionId, allocates a backing LibreFang session. |
session/load | client → agent | Maps an existing ACP SessionId onto a fresh LibreFang session. History replay across daemon restarts is a follow-up. |
session/list | client → agent | Lists active ACP sessions; supports cwd filtering. |
session/resume | client → agent | Same shape as load until full history replay lands. |
session/close | client → agent | Drops the session-store entry. |
session/prompt | client → agent | Pumps StreamEvent from the agent loop as session/update notifications, returns PromptResponse with the final StopReason. |
session/cancel | client → agent | Cancels the in-flight prompt via per-session cancellation token. |
session/request_permission | agent → client | Forwarded 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:
- Filters by the LibreFang
SessionIdit owns. - Sends
session/request_permissionwith the tool name, action summary, and four options:Allow once,Allow always,Deny,Deny always. - Awaits the editor's response with a 60-second timeout (timed-out approvals default to denied).
- Persists
_alwaysdecisions throughAcpKernel::remember_decisionso the next(agent, tool_name)invocation short-circuits. - 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_fileagent → client requests. The runtime'sfile_read/file_writebuiltins route through the editor when anAcpFsClientis registered for the session.terminal/{create,output,wait_for_exit,kill,release}agent → client requests.shell_execruns the command's PTY in the editor's terminal panel when anAcpTerminalClientis registered.- Editor-declared
cwd:terminal/createis dispatched without an explicitcwdso the editor uses thecwdit declared atsession/newtime (the user's project root). - Windows named pipes: daemon-attached mode works on Windows via
\\.\pipe\librefang-acpin addition to the Unix UDS at~/.librefang/acp.sock. session/load/session/resumehonour the supplied ACP session id by deriving the LibreFangSessionIddeterministically (Uuid::new_v5keyed 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 visiblesession/updatemessage chunk telling the user the attachment was dropped.
Follow-up issues (not yet shipped):
session/updatehistory replay stream — sending past assistant messages back assession/updatenotifications 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-driversis required. tool_use_idon synchronous (request_approval) approvals and dashboard manual approvals — today only the deferred path carries it; the others fall back to a clearly-namespacedapproval-{req_id}ToolCallId.- Persisting
tool_use_idandDeferredToolExecution.session_idacross daemon restarts — needs a column onpending_approvalsand a small migration. - End-to-end integration tests for
fs/*andterminal/*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.