diff --git a/docs/adr/agent-consoles.md b/docs/adr/agent-consoles.md new file mode 100644 index 0000000..235e8e9 --- /dev/null +++ b/docs/adr/agent-consoles.md @@ -0,0 +1,106 @@ +# ADR: Two consoles — a management console and per-agent agent consoles (endpoint registry + remote file editor) + +- **Status:** Proposed +- **Date:** 2026-08-14 +- **Author:** Orca (`ecs-claude`) +- **Extends:** [Chatting with a connected agent — `session/prompt` over `/acp` (Part C)](./agent-chat-panel.md); [Fleet grouping & connection model](./fleet-grouping-and-connection-model.md); [Deployment control plane (ADR-2)](./deployment-control-plane.md) +- **Reference:** [`brettchien/katashiro`](https://github.com/brettchien/katashiro) — working ACP chat client on the same `/acp` gateway; [MCP-over-ACP tunnel contract](https://github.com/openabdev/openab/blob/main/docs/mcp-over-acp-tunnel-contract.md) + +--- + +> The [agent-chat-panel ADR](./agent-chat-panel.md) added a chat surface to Studio's **one** bound agent (single-agent, by design). This ADR steps up a level and fixes the **architecture**: Studio has **two kinds of console**. The **management console** (the current top-level view) chats with **one designated agent** that — via reverse-MCP — can *drive the fleet*. An **agent console** is a **per-agent** view where the operator can **view/edit that agent's files and apply them**, and **chat with it directly**. The chat panel from the agent-chat-panel ADR becomes a **reusable primitive** instantiated per endpoint; this ADR adds the **endpoint registry** that makes N agents reachable and the **remote file editor** that makes "view/edit config" real. + +## 1. Context + +Three things are now settled or in flight: + +- The **management console** exists: the top-level roster + a single `/acp` connection (`remote.toml`) whose chat talks to the bound agent, and whose reverse-MCP tunnel publishes Studio's `oab` tools **to** that agent so it can control the fleet (Part B, #43/#48; chat panel Part C in progress). +- That model is **deliberately single-agent** (agent-chat-panel §4). Brett now wants the complementary surface: **one console per agent**, each able to **inspect/edit that agent's config and chat with it directly** — N independent single-agent consoles, **not** a multi-agent room (no @mention/relay/loop-guard; that remains a non-goal). +- The agent-chat-panel ADR is **still under development** (Part C not landed). Rather than churn an in-flight ADR, this new ADR captures the two-console architecture and the two new mechanisms it needs (endpoint registry, remote file editor). The chat *primitive* it defines is reused unchanged. + +Operator decisions locked in this thread (Brett, 2026-08-14): + +1. **Reachability is the operator's job.** *Any* openab agent can start an `/acp` endpoint; making it reachable is a deployment concern the operator handles (expose the k8s/orbstack or docker-compose port for a local agent; an ingress/DNS for a remote one). Studio only needs, per agent, a `(url, token, cwd)` — it does not provision the endpoint. +2. **"Config" = files.** Instead of modelling "runtime spec" vs "persona files" separately, Studio ships **one remote file editor**: browse/edit the agent's files on its remote filesystem and **apply**. Both runtime config and persona (`CLAUDE.md`, `agent_profiling/…`) are just files. +3. **The management agent is not special infrastructure.** It is simply the agent bound as the management endpoint; it **may** be a member of a managed fleet, and it is **not required** to be managed by Studio. Any agent (including it) can also have an agent console. + +## 2. Decision — Part A: two consoles, one chat primitive + +| | **Management console** | **Agent console (per agent)** | +|---|---|---| +| Scope | one designated agent | any agent with an endpoint | +| Chat | ✅ (agent-chat-panel primitive) | ✅ (same primitive) | +| Reverse-MCP `oab` tools published to the agent | ✅ — **the point**: the agent drives the fleet | ⛔ **off by default** (least privilege) | +| Remote file editor | — (not its job) | ✅ view/edit/apply the agent's files | +| Roster / fleet control UI | ✅ | — | + +- **The chat panel is one component**, instantiated against a chosen endpoint. The management console instantiates it against the management binding; an agent console instantiates it against that agent's binding. Rendering, markdown sink, turn controls, autoscroll, history/resume — all from agent-chat-panel §4, unchanged. +- **Reverse-MCP publication is a per-binding capability, not automatic.** Handing an agent Studio's `oab` fleet-control tools is a grant; it stays a property of the **management** binding. An ordinary agent console chats with and configures an agent **without** giving it fleet control. (An agent console *may* opt into publishing `oab` via an explicit binding flag, but that is off by default.) + +## 3. Decision — Part B: per-agent endpoint registry + +Generalize the single `remote.toml` into a **registry of agent endpoints**. Each entry is the same shape as today's `RemoteConfig` plus an identity and capability flags: + +```toml +# ~/.config/oab-studio/agents.toml (edited in-app, same editor pattern as fleets.toml) + +[[agent]] +name = "orca" +url = "wss://orca-acp.brettchien.cc/acp" +token = "…" # /acp bearer — SECRET, never logged +cwd = "/home/node" +management = true # this entry backs the management console (reverse-MCP oab published) + +[[agent]] +name = "mira" +url = "wss://mira-acp.brettchien.cc/acp" +token = "…" +cwd = "/home/node" +# management defaults to false → agent console only, no oab tools published +``` + +- **`management` is a policy flag**, not a separate file: exactly one entry carries it (it backs the top-level console and its reverse-MCP grant). All entries (including the management one) are selectable as agent consoles. +- **Backward compatibility:** a legacy single `remote.toml` is read as one `management = true` entry, so existing setups keep working while the registry is adopted. +- **Schema lives in `crates/acp-tunnel/config.rs`** (extend `RemoteConfig` → an `AgentEndpoint` + an `AgentRegistry` wrapper); parsing/validation stay pure there. Each endpoint validates as today (WSS scheme, token present). Tokens remain secrets that surface in the editor but are never logged. +- **No auto-connect.** Endpoints are dialed on demand: the management console dials its binding on "Activate" (as today); an agent console dials its endpoint when opened, tears down when closed. Reconnect/status machinery (`remote-status` per connection) is reused per endpoint — `RemoteState` becomes keyed by agent name rather than a singleton. + +## 4. Decision — Part C: the agent console + +Selecting an agent (from the roster or the registry) opens its console with two regions: + +1. **Files / config** — a **remote file editor** (Part D) over that agent's filesystem: a tree/list scoped to an editable root, open a file into the existing CodeMirror editor, **Apply** writes it back over the endpoint. This is the concrete meaning of "view/edit each agent's config and apply." +2. **Chat** — the agent-chat-panel primitive bound to this agent's endpoint (single-shot reply behind a think-spinner today, markdown-at-finalize, stop/retry, batched queue, history + `session/resume`). + +The management console keeps its current shape (roster + fleet control + its own chat); an agent console is a **focused, single-agent** view reached by selection. Both share the tab chrome; the agent console is a new `
` + tab in the vanilla-TS `console` (render + wiring in `render.ts`/`main.ts`), keyed by the selected agent. + +## 5. Decision — Part D: the remote file editor + +The new mechanism this ADR introduces. Studio needs to **list / read / write** files on a remote agent's filesystem, over the **same `/acp` endpoint** (uniform for local and remote agents — no direct kube/docker API access, which would need orchestrator credentials and break the "just expose the `/acp` port" model). + +- **Transport — a file-ops surface on the `/acp` session (⚠️ upstream coordination with openab).** The cleanest wire is a small method set on the existing session — `fs/list` (dir → entries), `fs/read` (path → text + metadata), `fs/write` (path + text → ok), `fs/stat` — symmetric to how the reverse-MCP tunnel already rides one socket. **openab's `/acp` does not expose this today** (as with `tool_call`/streaming, this is an upstream capability, not a Studio-only slice). Studio builds a **transport-agnostic file client** (`crates/acp-tunnel`) + the UI now; the exact method names/params are settled with openab. Until the wire lands, the agent console ships **chat + read-only config display**, with the editor arriving when the fs methods do. +- **Editable-root scoping (agent-declared).** The agent declares which path(s) are editable (e.g. `~`, or an allowlist); Studio never assumes free rein over `/`. Writes outside the declared root are refused server-side. This bounds a very powerful capability (arbitrary file write into a running agent = arbitrary persona/behaviour change). +- **Apply semantics.** `fs/write` persists the file. Some files hot-reload (the agent watches); others need a restart to take effect. Studio's **Apply** = write; for restart-required files, offer an explicit **"restart instance"** action reusing the control-plane scale-cycle (scale→0→1, ADR-2). Studio does not guess reload semantics — it writes and, on request, cycles. + +## 6. Scope + +- ✅ **In (this ADR's target):** the two-console architecture; the per-agent endpoint registry (`agents.toml`, backward-compatible with `remote.toml`); the agent console shell (selector + config region + chat region); per-endpoint chat via the existing primitive; reverse-MCP publication gated to the management binding. +- ⚠️ **Sequenced after the wire lands:** the remote file editor's write path — depends on the openab `/acp` `fs/*` method set (Part D). Ships read-only until then. +- ⛔ **Blocked on openab (upstream), unchanged from agent-chat-panel:** token streaming, `tool_call`/thought rendering (only `agent_message_chunk` today); the `fs/*` file-ops surface (Part D) is a new item in this bucket. +- 🚫 **Not a goal:** the multi-agent room (@mention routing, agent→agent relay, loop guard) — N independent consoles, no cross-agent surface; accessibility (single-operator internal console, per agent-chat-panel §5). + +## 7. Consequences + +- ✅ The operator can inspect **and reconfigure** any reachable agent, and converse with each — not just the one management agent. +- ✅ Reuses the chat primitive, the CodeMirror editor, and the reconnect/status machinery; the new weight is the registry, the per-endpoint keying of `RemoteState`, and the `fs/*` client. +- ⚠️ **Security surface grows materially.** (a) The registry holds **N bearer secrets**, not one. (b) Remote **file write into a live agent** is among the most powerful actions Studio can take — hence agent-declared editable roots, explicit Apply, and no `/`-wide default. (c) Reverse-MCP fleet-control tools stay **least-privilege** — published only to the management binding, never to an arbitrary agent console. (d) Agent output rendered in chat keeps the mandatory DOMPurify sink + Tauri CSP (agent-chat-panel §6). +- ⚠️ `RemoteState` is no longer a singleton — it becomes a per-agent map of connections, each with its own status/reconnect. Bounded fan-out (operator opens a console at a time), but the lifecycle (open→dial, close→teardown) must be clean to avoid leaked sockets. +- ⚠️ Depends on openab for the `fs/*` wire; the write path can't ship until that exists. The architecture and read-only path are independent and land first. + +## 8. Open questions + +1. **`fs/*` wire** — the exact method set / params on `/acp`, and whether openab models it as native ACP methods or an agent-served MCP "files" server Studio consumes. Coordinate with openab (parallels the streaming/tool_call gaps). **This is the gating dependency for the write path.** +2. **Editable-root declaration** — does the agent advertise its editable root in the `session/new` result, a dedicated `fs/roots` call, or config Studio already holds? Leaning: agent-advertised, so Studio can't overreach. +3. **Registry vs `remote.toml`** — adopt `agents.toml` as the new file (with `remote.toml` back-compat shim), or grow `remote.toml` into an array in place. Leaning: new `agents.toml`, shim the old one. +4. **Endpoint discovery** — purely manual (operator pastes url+token per agent) for now; could later be derived from the roster/control-plane if agents publish their `/acp` address. Out of scope here. + +This is a direction-alignment ADR. Implementation lands in slices, each verified against the live gateway: (1) endpoint registry + `RemoteState` keying; (2) agent console shell + per-endpoint chat (reuses the Part C primitive); (3) remote file editor read path; (4) write/apply path once the openab `fs/*` wire exists.