From 2fbe846771a0c853c802d90d9ef45e2565d296c3 Mon Sep 17 00:00:00 2001 From: jtenniswood Date: Wed, 2 Sep 2026 11:44:13 +0100 Subject: [PATCH] docs(studio): documentation reconciliation + tip-parity sweep The closing PR of the stacked #618 landing. studio/CLAUDE.md reaches its final state (accurate now that every rule's subject exists, with the studio ADR references at their renumbered 0288/0289 ids); the readiness tracker's Studio row reads shipped with the honest residuals named; user-docs gets its completeness pass with the re-attach limit stated honestly (any tab re-attaches via the durable session watch; the driving tab's own prompt stream still cancels on disconnect); llms.txt regenerated; the Docusaurus site builds clean. The parity sweep verifies the series against the original branch: studio's src, tests, and scripts are byte-identical to feat/studio-atrium's tip except the enumerated deviations recorded across the series. Co-Authored-By: Claude Fable 5 --- studio/CLAUDE.md | 135 ++++++++++++++++--- user-docs/building/what-you-get/studio.md | 157 +++++++--------------- 2 files changed, 167 insertions(+), 125 deletions(-) diff --git a/studio/CLAUDE.md b/studio/CLAUDE.md index 9113f988eb..c91717be52 100644 --- a/studio/CLAUDE.md +++ b/studio/CLAUDE.md @@ -1,22 +1,123 @@ -# CLAUDE.md — studio/ +# CLAUDE.md — Mecatl Studio -Studio is mecatl's web client (the Atrium workspace): a Next.js **Node module, -never a Go module** — not in `go.work`, the layering DAG, depguard, or the -api-compat gate. It consumes only the daemon's public HTTP/SSE API. See ADR -0287 (`docs/adr/0288-studio-atrium-module.md`). +The web client for the mecatl harness: a Next.js app (App Router) serving the +Atrium workspace — Chats · Scheduled · Skills · Memory · Settings — against a +`mecated` daemon. See ADR 0288 (module + posture) and ADR 0289 (server-backed +chats). -> Studio is landing as a stacked PR series; this file grows with the module. -> Until the series completes, the rules below cover what is in the tree. +## Commands + +Run through the root Taskfile, not bare npm: + +```sh +task build # repo root first — studio's managed mode spawns ../bin/mecated +task studio:dev # start Studio + its mecated supervisor (background; logs in studio/dev.log) +task studio:stop # stop web server + controller + the mecated it supervises +task studio:test # vitest run + the hermetic server-tier suite (builds first) +task studio:lint # biome +task studio:typecheck +``` + +`npm run dev` = managed mode via `scripts/dev-local.mjs` (controller on :8788, +web on :3000). `npm run dev:web` = bare `next dev` (external mode or against an +already-running controller). Setting `MECATL_BASE_URL` selects external mode. +The hermetic suite (`npm run test:server`) builds Next first — a stale build is +the usual reason it fails mysteriously. + +## Module shape + +- `src/lib/protocol/` — the ONLY reader of raw daemon JSON: event decode + + StreamEvent translation, session inventory/transcript decoders, schedule + decode/encode. Generated TS proto bindings are deferred; this seam plus its + vitest suite is the stopgap. +- `src/lib/harness/client.ts` — browser transport: fetch + SSE buffering + + stream robustness (120s idle timeout, saw-result guard). +- `src/lib/server-proxy.ts` + `src/app/api/mecatl{,-control}/[...path]` — the + server tier: origin trust, header allowlists, bearer + workspace injection, + external-mode 409 policy. +- `scripts/local-controller.mjs` — managed-mode sidecar (supervises `mecated`, + owns model-router/MCP-gateway config + OAuth); policy helpers in + `src/lib/controller-security.mjs`. +- `src/features/agent/` — runtime-status provider + the daemon-backed hooks; + `src/app/workspace/**` — the five surfaces. ## Rules that have teeth -- **Daemon-only.** No mock layer, no demo fallbacks: an unreachable daemon is a - rendered offline state. Never add fixture content behind a probe failure. -- **npm, pinned.** Node from `.nvmrc`; the lockfile is regenerated only with - the pinned npm (`npx -y npm@10.9.4 install`) — npm 11 rewrites it into a - shape CI's npm 10 rejects. Installs run `--ignore-scripts`. -- **License headers.** Everything under `studio/` is `Apache-2.0`; CI greps - away any stray Proprietary SPDX header regression. -- **Gates.** `npm run lint` (Biome), `npm run typecheck`, `npm run knip` - (dead code/exports/deps), `npx vitest run`, `npm run build` — all green - before commit. Root docs gates still apply to any Markdown change. +Each rule is backed by a test; break the rule and its test names you. + +1. **Daemon-only: an unreachable daemon renders offline, never demo data.** + There are no fixtures to fall back to — do not add any. The one sanctioned + exception is the explicit, default-off, clearly-labeled Labs mock content + (Settings → Labs → "Show mock features", `src/features/agent/mock-tour.ts`) + — an opt-in demo the user turns on, never a fallback for an unreachable + daemon. + (`tests/rendered-html.test.mjs`: unreachable daemon → friendly 503.) +2. **The workspace is resolved, never hardcoded and never browser-supplied.** + Managed: controller `/status`; external: `MECATL_WORKSPACE`; injected + server-side into session/team/schedule creation. SERVER-ASSIGNED remote + deployments (ADR 0237 — any network-facing listener) refuse a client + workspace outright: leave `MECATL_WORKSPACE` unset there, and the proxy + deliberately injects nothing (the correct empty-workspace create); a + refused injection is rewritten with the unset-the-variable fix. + (hermetic: session creation carries the deployment workspace.) +3. **Credentials never cross the browser/controller boundary.** No key-paste + UI anywhere; `mecated` reads `~/.config/mecatl/auth.yaml`. The proxy's + header allowlist excludes `authorization` from the browser. + (hermetic: bearer injected server-side.) +4. **Controller mutations are server-only.** They require the server-set + `x-mecatl-studio-request` header, a loopback Host, and an allowlisted + Origin. (hermetic: CSRF/DNS-rebinding truth table.) +5. **External mode owns nothing locally.** Every control write answers 409. + (hermetic: control writes 409.) +6. **A failed turn renders as failed.** `result.stop === "error"` with no text + must never become a quiet success — the run_result event always reaches the + UI. (`src/lib/protocol/events.test.ts`.) +7. **Unknown event kinds are surfaced, never dropped.** A new daemon + capability shows up as "not rendered yet". (`events.test.ts`.) +8. **The memory panel is read-only.** A value typed into the UI would land in + turn-0 context bypassing injection scanning; the daemon has no write API by + design. Do not add an editor. +9. **Session rows obey the store.** Eligibility comes from row capabilities + (omitted = denied); a row is removed only by a complete inventory walk or a + 404; renames adopt the daemon's clamped echo. + (`src/lib/protocol/sessions.test.ts` + `use-agent-sessions`.) +10. **Schedule PUT replaces the whole spec.** Fields the form cannot edit ride + the row's `carried` spec and are re-encoded, or they are silently deleted. + (`src/lib/protocol/schedules.test.ts`: carried round-trip.) +11. **Requests are protojson; responses are stdlib JSON.** Never echo a decoded + response back as a request body. (`schedules.test.ts`: the asymmetry test.) +12. **Skills are project-scoped only.** The controller pins `--skills-dir` and + never passes `--skills-conventional`. + +## Gotchas + +- `npm test` runs vitest in watch mode; CI and `task studio:test` use + `npx vitest run` + `npm run test:server`. +- The controller restarts `mecated` on every config write; in-flight runs and + session ids die with it. Surfaces warn before writes that restart. Startup + rides the daemon's ready file (`--ready-file` + an ephemeral `--http-addr` + + a mkfifo lifetime pipe — Node's stdio "pipe" is a socketpair mecated + rejects); `/status` reports the ready doc's `apiMajor`/`features`/ + `deployment`. +- FireNow (`POST /v1/schedules/{name}/fire`) is synchronous — the request lasts + the whole agent run. +- Live re-attach to a running session rides the durable watch + (`GET /v1/sessions/{id}/watch`, ADR 0250; gate on the + `watch_session_events` feature): SSE `{event, cursor, phase}` envelopes — + replay from the cursor (empty = the beginning), one event-less + `phase: "live"` boundary frame, then live follow. The client is + `src/lib/harness/watch.ts`; `use-agent-chat` attaches it when the + inventory reads running/awaiting and Studio is not itself driving the run. + Residual: `POST /prompt` still cancels its run on client disconnect, so a + reload of the DRIVING tab still ends the run — the watch covers runs + driven elsewhere (schedules, other tabs/clients) and parked approvals. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/user-docs/building/what-you-get/studio.md b/user-docs/building/what-you-get/studio.md index 3a083dd941..beeb2a4855 100644 --- a/user-docs/building/what-you-get/studio.md +++ b/user-docs/building/what-you-get/studio.md @@ -10,12 +10,6 @@ surfaces on one rail — **Chats**, **Scheduled**, **Skills**, **Memory**, and **Settings**. It is a client like `mecatui`: the daemon owns every record, and Studio reads and writes the daemon's state rather than keeping its own. -:::note Landing in progress -Studio is landing as a stacked series of pull requests; the full workspace, -including the mobile round, is now in the tree, with a closing documentation -pass to follow. -::: - ## Starting it Managed mode (the default) supervises a `mecated` from your checkout: @@ -38,105 +32,52 @@ MECATL_WORKSPACE=/srv/workspace \ npm run start ``` -In external mode there is no local controller: every local control surface -answers 409 as owned by the deployment. - -Studio is **daemon-only** by design: when the daemon is unreachable it renders -an offline state that names the fix — never simulated content. The browser -never holds a daemon address or credential; Studio's own server tier pins -Host/Origin, injects the bearer and the session workspace server-side, and -allowlists headers in both directions. The decision record is ADR 0288 -(`docs/adr/0288-studio-atrium-module.md` in the repo). - -## Chats - -Chats are the daemon's sessions — there is no separate Studio store. The -sidebar lists the daemon's session inventory bucketed by recency, with -capability-gated rename and delete. A new chat is a draft until you send: -the session is minted on first send, so abandoned drafts never litter the -daemon. The transcript streams live (text, reasoning, tool activity, and -delegation badges), failed turns surface as alerts instead of vanishing, and -permission asks render as three-way approvals (allow once / always / deny) -that withdraw if the daemon retracts them. @-mentions offer the daemon's -agent roster and slash commands its command list. - -## Scheduled - -Scheduled tasks live on the daemon; Studio's Scheduled surface lists them -with their fire history. Authoring supports the two real shapes — a cron -schedule (with timezone and an optional max-fires cap) or a one-shot (with -retry) — and mutating runs are an explicit opt-in in the form, so the -invalid mutating/plan pairing cannot be constructed. Editing round-trips the -schedule's carried spec (the daemon's update replaces the whole spec), and -each fire links its real transcript. - -## Skills - -The Skills surface lists the daemon's skills with provenance, browses folder -skills (including the SKILL.md inline), and — in managed mode — authors them -through the local controller: create a skill in two steps, upload a zip or a -folder, enable/disable (a disabled skill moves to a holding area rather than -being deleted), or delete. Skill names pass one shared validator on both the -browser and the controller. In external mode the list is read-only: skill -management belongs to the deployment. - -## Memory and Settings - -Memory is read-only by design: the daemon has no memory write API (a -hand-typed value would enter turn-0 context without injection scanning), so -Studio shows the memory table with honest disabled/empty states, per-entry -detail, the store footprint, and the consolidate action. Settings carries -Personalize (text size, interface scale, session-list side, notifications), -the agent identity card (name and avatar are browser-local cosmetics — the -agent learns your name in conversation), and the learning review page. - -## Providers, model router, MCP gateway - -In managed mode Studio administers the daemon's runtime configuration -through the local controller: add a provider (built-in kinds take a base-URL -override; custom gateways are validated), test a key, switch the active -provider, remove one, and edit the model router over the daemon's whole -model inventory. Provider credentials live in the daemon's auth file, owned -server-side — Studio shows status booleans and key-shape hints, and key -material never crosses to the browser. The MCP gateway URL is user-entered -but always validated, and egress is HTTPS-only (loopback HTTP sits behind an -operator env opt-in). In external mode all of this reads as owned by the -deployment. The About-this-daemon card reports the safe identity probe. - -## External-mode sign-in (OIDC) - -When an external daemon sits behind an OIDC-aware gateway, the settings -provider page offers sign-in: Studio's server tier runs the PKCE flow itself -and holds tokens in process memory only — nothing token-shaped reaches the -browser or disk. With OIDC unconfigured, the static `MECATL_AUTH_TOKEN` path -is untouched. Managed mode never uses either. - -## Live re-attach - -Open a driving session in a second tab (or refresh mid-run) and Studio -re-attaches to the live run through the daemon's durable session watch. One -limit to know: the tab that *started* the run still cancels it if that tab's -prompt connection drops — re-attach covers every other tab. - -## Modes, model switching, debug sessions - -The composer's mode selector switches the session's permission mode live -(with rollback if the daemon refuses), and the model selector forks the -conversation onto another model mid-chat, keeping the transcript. Where the -daemon advertises session debugging, a chat's menu offers Debug with AI -behind an explicit consent dialog; debug sessions carry a badge in the -sidebar. - -## Labs - -Settings → Labs holds the clearly-labeled mock extras, default-off: a mock -Projects section in the chat sidebar and a canned feature tour. These are -browser-local demo content only — a mock id is hard-gated from ever reaching -the daemon — and with the toggle off none of it exists at runtime. - -## On a phone - -Studio installs as a PWA and adapts below tablet width: bottom tab -navigation, sheet-based side panels, a docked composer that plays well with -the on-screen keyboard, long-press row actions, and swipe-to-dismiss. -Desktop rendering is unchanged. +In external mode there is no local controller: provider, model-router, and +MCP-gateway settings show as owned by the deployment. + +Studio is **daemon-only**. If the daemon is unreachable you get an offline +banner naming the fix — never simulated content. + +## What each surface does + +- **Chats** — the daemon's session store, live. The sidebar is the session + inventory (a chat renamed or deleted here is renamed or deleted for every + client); opening a chat reads its authoritative transcript; a new chat + creates its daemon session on the first message. Streaming shows tool calls, + reasoning, delegation badges when the run hands work to subagents or teams, + and permission asks with three-way verdicts (allow once / always / deny). A + failed run renders as failed, with a retry. +- **Scheduled** — the schedule registry: create and edit schedules (cron with + timezone, or one-shot), pause/resume/fire, and audit each schedule's fire + history down to the per-fire session transcript. Write-capable schedules + require an explicit opt-in; the default posture is read-only plan mode. +- **Skills** — the daemon's resolved skill inventory (name, summary, + provenance). Read-only today; authoring is a follow-up. +- **Memory** — the user model: durable facts the agent has stored about you. + Read-only by design — the agent curates memory through injection-scanned + tool calls, so Studio never offers an editor. +- **Settings** — appearance and notifications, plus (managed mode) the + provider status, the semantic model router, and the MCP gateway connection + (bearer token or OAuth). Credentials are never typed into Studio: `mecated` + reads them from `~/.config/mecatl/auth.yaml`. + +## Environment variables + +| Variable | Meaning | +| --- | --- | +| `MECATL_BASE_URL` | External daemon base URL; presence selects external mode | +| `MECATL_AUTH_TOKEN` | Bearer for the external daemon (server-side only) | +| `MECATL_WORKSPACE` | Workspace path in external mode (server-side only) | +| `MECATL_STUDIO_PUBLIC_ORIGIN` | Comma-separated origins Studio is served from (CSRF gate) | +| `MECATL_STUDIO_ORIGINS` | Controller's Origin allowlist (managed mode) | +| `MECATL_STUDIO_PROVIDER` | Managed provider: `mock`, `openrouter`, or `toolhive` | +| `MECATL_ALLOW_INSECURE_LOOPBACK_MCP` | `1` permits a loopback-HTTP MCP gateway | + +## Limits worth knowing + +- Studio re-attaches live to running sessions through the daemon's durable + session watch (any tab, including a scheduled fire in progress). One + residual: the tab that *started* a run still cancels it if that tab's + prompt connection drops. +- Config writes in managed mode restart the daemon, which ends in-flight runs. +- There is no cost display: the daemon accounts tokens, not currency.