From b5f15b4bc37361a06fe5c33890b7d5c134d68cbb Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Mon, 17 Aug 2026 17:37:22 +0800 Subject: [PATCH] =?UTF-8?q?docs(adr):=20Part=20D=20=E2=80=94=20Studio-brok?= =?UTF-8?q?ered=20fs,=20exec-backed=20(MCP=20server=20=3D=20federation=20f?= =?UTF-8?q?allback)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revises the remote file editor's backend after the design call. fs stays Studio-brokered (agent never holds the credential; `oab` grant stays management-only), but the backend is chosen by one axis — **does Studio own the target's runtime?** - Backend 1 (primary, every agent in the fleet today): Studio's platform exec channel — ECS `ExecuteCommand` / k8s `pods/exec` — with a control- plane credential Studio already holds. Ships now, no openab dependency. - Backend 2 (fallback, federation): the target-hosted fs MCP server over the `oab` relay, for an agent Studio can reach only over `/acp` but whose platform it does not own. Depends on openab; deferred until such an agent exists. Consequence: the fleet's fs write is no longer gated on an upstream fs server — that item drops from "gating dependency" to "federation fallback." The trade the exec backend buys, stated plainly: gating (`roots`/`writable`) becomes Studio-enforced (path validation + argv-form exec), not resource- enforced, and a control-plane credential enters the fs path — held only by Studio's broker, never by an agent. - §5 Part D rewritten around the two-backend model + the deciding axis. - §6/§7 re-pointed: read+write ship over exec for owned runtimes; the MCP server is the deferred federation fallback; security consequences updated (enforcement locus, control-plane cred in the fs path). - §8: OQ#1 re-resolved (two backends); new OQ#2 on the exec backend's enforcement locus. Closing slices: (4) read+write over exec (no openab), (5) federation MCP backend when needed. Supersedes the Part D mechanism locked in #70 (fs = target-exposed MCP server as the sole backend). Docs-only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- docs/adr/agent-consoles.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/adr/agent-consoles.md b/docs/adr/agent-consoles.md index 21eaabc..6e15233 100644 --- a/docs/adr/agent-consoles.md +++ b/docs/adr/agent-consoles.md @@ -73,38 +73,44 @@ 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 (an MCP files server, management-plane) +## 5. Decision — Part D: the remote file editor (Studio-brokered fs — exec-backed for owned runtimes, MCP-server for federation) -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.) +Studio needs to **list / read / write** files on a remote agent's filesystem. **Decision: fs is a Studio-brokered capability with two interchangeable backends behind one surface. The default backend for a runtime Studio controls is the platform's exec channel (ECS `ExecuteCommand`, k8s `pods/exec`); the fallback for a runtime Studio does *not* control (a foreign agent reachable only over `/acp`) is an fs MCP server the target agent exposes.** The `oab` tool + the UI source are identical across both. -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.) +Two things are fixed regardless of backend; only the backend varies: -- **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. +- **Topology — Studio-brokered (unchanged, decided).** The management agent never holds a target's exec credential or bearer. Its fs call arrives as a Studio reverse-MCP `oab` tool; **Studio** performs the operation with the credential *it* holds. **Credentials and policy stay in Studio; the `oab` grant stays management-only** (least privilege, Part A). The rejected alternative — handing the agent the exec cred / each target's bearer directly — concentrates cluster-grade RCE in an LLM and breaks least privilege. +- **Actor — unchanged.** fs is not only Studio's UI reading a disk; the driver is the **management agent reaching into a target to manage it**, editing a file being one op. Both consumers (the agent as an `oab` tool, the UI as the read-only browser) hit the same Studio surface — one fs policy point. + +**The deciding axis is: does Studio own the target's runtime?** + +- **Backend 1 — Studio platform-exec (primary; every agent in the fleet today).** Studio reaches the target's filesystem via the orchestrator's exec channel — ECS `ExecuteCommand` (SSM), k8s `pods/exec` — using a control-plane credential Studio already holds (it is the plane that scales these services). **Ships now; no openab dependency.** This covers 100% of the current fleet (all agents run on our own ECS/k8s). +- **Backend 2 — target-hosted fs MCP server (fallback; un-owned runtimes / federation).** For an agent Studio can reach only over `/acp` but whose platform it does **not** control, fs rides a small files server the agent exposes (`list`/`read`/`write`/`stat`), relayed through the `oab` tool over the token in `agents.toml`. This is the previously-decided MCP mechanism; it **depends on openab** and is **deferred until such an agent exists** — exec cannot reach a platform Studio doesn't own, so this is the escape hatch, not the day-1 path. + +- **Editable-root scoping — same intent, different enforcement locus.** `writable` defaults **off**; no `/`-wide default; browsing/writes are bounded to declared root(s). **Enforcement locus differs by backend and this is the material trade the exec backend buys:** the MCP server enforces roots *at the resource* (the agent declares roots, the server refuses outside them); **exec has no OS root-fence, so with the exec backend `roots`/`writable` become Studio-side path validation + safe argv-form command construction — advisory, not resource-enforced.** A Studio path-validation bug is therefore a full-container write. Mitigations: hard path canonicalization + prefix checks, argv-form exec (never shell-string interpolation), `writable` default-off, explicit Apply. We accept this for owned runtimes because Studio is *already* the control plane holding a fleet-management credential — exec is the same trust tier, not a new principal — and the agent still never receives that credential. +- **Apply semantics (unchanged).** `write` persists the file. Some files hot-reload (the agent watches); others need a restart. 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 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. +- ✅ **In, once the exec provider lands:** the remote file editor's read *and* write paths for **owned runtimes** — over Studio's platform-exec backend (ECS/k8s), consumed by the UI and the `oab` fs tool (Part D). No openab dependency; covers the whole current fleet. +- ⛔ **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, backend 2) is in this bucket but is now the **federation fallback**, not the gating dependency — deferred until a Studio-un-owned agent exists. - 🚫 **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 **`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). +- ✅ 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 tool** (Studio → exec backend, or → target fs MCP server for federation). +- ✅ **The fleet's fs write ships without openab.** The exec backend uses a credential Studio already holds, so the remote file editor is no longer gated on an upstream fs server; that upstream item drops from "gating dependency for write" to "federation fallback, deferred." fs and management still ride **one** brokered channel (agent → `oab` → target), one fs policy point. +- ⚠️ **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 `writable` default-off, explicit Apply, no `/`-wide default. **With the exec backend this gating is Studio-enforced (path validation + argv-form exec), not resource-enforced** — a real downgrade from the MCP server's server-side roots, accepted for owned runtimes because Studio is already the control plane and the credential never reaches the agent. (c) The exec backend puts a **control-plane / orchestrator credential in the fs path** — the very credential the earlier draft kept fs away from; it is held **only by Studio's broker**, never granted to an agent or an agent console. (d) Reverse-MCP fleet-control tools — including the fs tool — stay **least-privilege**, published only to the management binding. (e) 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 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. +- ⚠️ The **new implementation weight is Studio's exec provider** — per-platform backends (ECS `ExecuteCommand` over SSM, k8s `pods/exec`), safe argv construction, and `ls`/`stat`/`cat`/`tee`-style op mapping with binary/large-file handling. The MCP-server backend (federation) reuses the `oab` relay and lands only when a Studio-un-owned agent needs it. ## 8. Open questions -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. +1. ~~**`fs/*` wire** — native ACP methods or an agent-served MCP files server?~~ ~~Resolved: MCP files server, Studio-brokered.~~ **Re-resolved: Studio-brokered fs with two backends — platform-exec (primary, owned runtimes, ships now) + target-hosted fs MCP server (fallback, federation, deferred) (Part D).** Remaining sub-questions: the exact `oab` fs tool set/params (one `fs.*` family fanning out by target name, vs. per-op tools) — shared by both backends; and, for the federation backend, whether the target *runtime* ships the MCP server or Studio injects it. **No longer the gating dependency for write** — exec ships it. +2. **Enforcement locus for the exec backend** — with exec there is no server-side roots capability, so `roots`/`writable` are enforced by Studio: path canonicalization + prefix checks + argv-form exec. Open: how strict (a fixed allow-list of roots per agent in `agents.toml`? a `chroot`/`--workdir` confinement where the platform allows it?), and whether to surface roots to the UI up-front (grey out non-editable paths) or only enforce on write. For the federation (MCP) backend this is the server's **roots capability**, resource-enforced. 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 **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. +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 a source-agnostic read, shipped read-only against fixtures (#68; the bespoke `fs/*` client explored in the slice-3 draft was dropped per Part D); (4) **read+write/apply over the exec backend** — Studio's platform-exec provider (ECS/k8s) behind the `oab` fs tool, consumed by both the UI and the management agent; lights up slice 3's read on real owned endpoints and adds write/Apply. No openab dependency. (5, when needed) **federation backend** — the target-hosted fs MCP server over the `oab` relay, for a Studio-un-owned agent.