From 02bd52cd8da03db173de9f8bf448fe48c59700e0 Mon Sep 17 00:00:00 2001 From: brettchien Date: Mon, 17 Aug 2026 11:49:11 +0800 Subject: [PATCH] =?UTF-8?q?docs(adr):=20Part=20D=20=E2=80=94=20fs=20is=20a?= =?UTF-8?q?n=20MCP=20files=20server,=20Studio-brokered=20(not=20bespoke=20?= =?UTF-8?q?fs/*)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks the remote file editor's mechanism after the design call with Brett: fs is an MCP surface a target agent exposes, consumed by the management agent (and by Studio's UI), not a bespoke `fs/*` method set on `/acp`. - §5 Part D rewritten: the actor is the management agent reaching into a target to manage it (fs = one op of that channel), which is the ordinary agent→MCP direction — so a bespoke wire would build the management channel twice. Topology decided as **Studio-brokered**: management agent → Studio `oab` reverse-MCP tool → Studio dials the target `/acp` with the registry-held token → target fs MCP. Bearers + policy stay in Studio; the `oab` grant stays management-only. Roots/ writable/apply gating moves to tool-level in the fs server. - §6/§7: the dependency + security bullets re-pointed from the `fs/*` wire to the fs MCP server + `oab` fs-relay; adds the "one channel, one policy point" win. - §8: OQ#1 (native ACP vs MCP files server) resolved → MCP, Studio-brokered, with the remaining sub-questions (fs tool set, `oab` relay shape, runtime vs injected server); OQ#2 roots via MCP roots capability; OQ#3 (agents.toml) marked shipped. - Closing slices: (3) read = browser over MCP-backed read (replaces the slice-3 bespoke `fs.rs` draft); (4) write/apply = fs MCP server + `oab` relay. Co-Authored-By: Claude Opus 4.8 --- docs/adr/agent-consoles.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/adr/agent-consoles.md b/docs/adr/agent-consoles.md index 10fd4ce..21eaabc 100644 --- a/docs/adr/agent-consoles.md +++ b/docs/adr/agent-consoles.md @@ -73,34 +73,38 @@ Selecting an agent (from the roster or the registry) opens its console with two 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 +## 5. Decision — Part D: the remote file editor (an MCP files server, management-plane) -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). +Studio needs to **list / read / write** files on a remote agent's filesystem. **Decision: fs is an MCP surface — a files server the target agent exposes — not a bespoke `fs/*` method set on `/acp`.** (This resolves Part D's original open question in favour of the agent-served MCP variant.) -- **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. +The reason is the *actor*. fs is not only Studio's UI reading a disk; the real driver is the **management agent reaching into a target agent to manage it**, of which editing a file is one operation. That is the ordinary MCP direction — an agent consuming tools — and the management→target control channel has to exist regardless. A bespoke `fs/*` RPC would build that channel a second time, just for files. (An earlier draft preferred bespoke `fs/*` by scoping fs as *Studio-reads-agent*, which is the reverse MCP direction; once the actor is the management agent, that objection dissolves.) + +- **Mechanism — an fs MCP server on the target agent.** Each agent exposes a small files server (`list` / `read` / `write` / `stat` tools), roots-scoped (below). It is consumed by the **management agent** as tools, and by **Studio's UI** for the read-only browser — one fs surface, one policy point, no second wire. (Whether the target *runtime* ships this server or it is injected is coordinated with openab, the way streaming/`tool_call` are; but the shape is now settled as MCP, not native ACP methods.) +- **Topology — Studio-brokered (decided).** The management agent does **not** hold every target's bearer. Its fs/management call arrives as a Studio reverse-MCP `oab` tool; Studio then dials the target's `/acp` with the token it already holds in `agents.toml` and relays to that agent's fs server. **Bearers and policy stay in Studio; the `oab` grant stays management-only** (least privilege, Part A). Two MCP hops — the management agent's MCP into Studio, Studio's relay into the target's MCP ("MCP in MCP"). The rejected alternative — the management agent connecting to each target directly — spreads N bearers out of Studio and breaks least privilege. +- **Editable-root scoping (agent-declared).** The agent advertises its editable root(s) via the files server's roots capability; Studio / `oab` never assume `/`. `writable` defaults **off**; writes outside the declared root are refused server-side. This bounds a very powerful capability (arbitrary write into a running agent = arbitrary persona/behaviour change) — now enforced as **tool-level gating in the fs server**, not in a bespoke wire. +- **Apply semantics.** `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. +- ⚠️ **Sequenced after the fs server lands:** the remote file editor's write path — depends on the target agent's **fs MCP server** + Studio's `oab` relay tool (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 MCP files server** on the target agent (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). +- ✅ 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 **`oab` fs-relay tool** (Studio → target fs MCP server). +- ✅ fs and management ride **one** channel (the management agent → `oab` → target), not two. Studio's file browser and the management agent consume the same fs server, so there is a single fs policy point instead of a bespoke wire duplicated per consumer. +- ⚠️ **Security surface grows materially.** (a) The registry holds **N bearer secrets**, not one — and they stay in Studio (brokered topology), not fanned out to the management agent. (b) Remote **file write into a live agent** is among the most powerful actions Studio can take — hence agent-declared editable roots, `writable` default-off, explicit Apply, and no `/`-wide default, enforced as tool-level gating in the fs server. (c) Reverse-MCP fleet-control tools — now including the fs relay — 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. +- ⚠️ Depends on openab for the target's **fs MCP server**; the write path can't ship until that exists. The architecture and read-only browser 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. +1. ~~**`fs/*` wire** — native ACP methods or an agent-served MCP files server?~~ **Resolved: MCP files server, Studio-brokered (Part D).** Remaining sub-questions: the exact tool set/params of the fs server, and the shape of the `oab` fs-relay tool (one `fs.*` tool family fanning out by target name, vs. per-op tools). Coordinate with openab on whether the target *runtime* ships the server or Studio injects it. **Still the gating dependency for the write path.** +2. **Editable-root declaration** — the agent advertises its editable root(s) via the fs server's **roots capability** (standard MCP), so Studio / `oab` can't overreach. Open: whether roots are also surfaced to the UI up-front (to grey out non-editable paths) or only enforced on write. +3. ~~**Registry vs `remote.toml`**~~ **Resolved & shipped:** new `agents.toml` with a `remote.toml` back-compat shim (slice 1, #66); the in-app editor now targets `agents.toml` (#69). 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. +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 **read path** — the browser UI over an MCP-backed read (source-agnostic; the bespoke `fs/*` client explored in the slice-3 draft is replaced by the fs MCP server per Part D); (4) **write/apply path** — the target's fs MCP server + Studio's `oab` fs-relay tool, consumed by both the UI and the management agent.