Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,19 @@ updates:
update-types:
- minor
- patch

# Studio (the Node web client) — npm.
- package-ecosystem: npm
directory: "/studio"
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: "chore(deps)"
labels:
- dependencies
groups:
npm-minor-patch:
update-types:
- minor
- patch
63 changes: 63 additions & 0 deletions .github/workflows/studio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Studio

on:
pull_request:
paths:
- "studio/**"
- ".github/workflows/studio.yml"
push:
branches: [main]
paths:
- "studio/**"
- ".github/workflows/studio.yml"

permissions:
contents: read

jobs:
checks:
name: Build, test, lint, typecheck, audit
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: studio/.nvmrc
cache: npm
cache-dependency-path: studio/package-lock.json

# No package in the tree needs install scripts (Next/sharp ship prebuilt
# binaries), so scripts stay off — supply-chain surface CI never runs.
- name: Install
working-directory: studio
run: npm ci --ignore-scripts

- name: Lint, typecheck, dead-code
working-directory: studio
run: |
npm run lint
npm run typecheck
npm run knip

- name: Unit tests
working-directory: studio
run: npx vitest run

- name: Build
working-directory: studio
run: npm run build

- name: Audit
working-directory: studio
run: npm audit --audit-level=high

- name: License headers
run: |
if git grep -l "SPDX-License-Identifier: Proprietary" -- studio/; then
echo "Proprietary SPDX headers are not allowed in studio/" >&2
exit 1
fi
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ coverage.*
# Personal, local-only Claude Code instructions and running-state notes
/CLAUDE.local.md
/HANDOFF.md

# Studio managed-mode local state (controller-pinned skills/memory dirs).
/.mecatl/

# Ad-hoc local attachment drops.
/attachments/
6 changes: 6 additions & 0 deletions .matlatlignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ website/CLAUDE.md
# Exclude both the committed reports and their ignored comparison copies.
sdk/typescript/etc/*.api.md
sdk/typescript/.api-extractor-temp/
# Studio's managed-mode working state (the controller pins the project skills
# and memory dirs here) and ad-hoc local attachment drops — machine-local
# state, not documentation. CI checkouts never contain them; ignoring them
# keeps local `check`/`index` runs byte-identical to CI's.
/.mecatl/
/attachments/
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ the opt-in `provider/*` submodules (ADR 0093), and the root module all move in l
- `contracts/proto/mecatl/v1/` — gRPC contract (source of truth); `contracts/proto/mecatl/driver/v1/` — the driver protocol (SessionStoreService/MemoryStoreService stores; SkillSourceService/SoulSourceService/AgentSourceService/CommandSourceService content sources) a remote driver process implements; `contracts/gen/` is generated, **never hand-edit**.
- `cmd/mecated/` — standalone server (composition root): flags, TLS/auth/rate-limit, HTTP + metrics listeners. `cmd/mecademo/` — the offline demo. `cmd/mecatequi/` — single-shot HEADLESS composition root (peer of mecademo over `app.Build`): one prompt → a git-diff patch + a JSON Summary + an optional JSONL log + an exit code. It is **FORGE-AGNOSTIC** — knows nothing about GitHub; the glue that turns an issue into a PR lives ONLY in `.github/` + shell, NEVER the binary or `engine/`, and keeps a **split-privilege token boundary** (the agent job holds NO GitHub write token; the publish job runs NO agent code, applies the patch as DATA). See `docs/adr/0028-mecatequi.md`. `cmd/mecak8s/` — storage-free k8s-native agent (ADR 0048), a thin peer of mecated that composes `app.Build` with k8s-native defaults (Redis store + k8s lease + drain gate); no PVC, no local state — state is a managed service (Redis + k8s API server). The four real-provider mains share credential/base-URL wiring via `internal/cliconfig`.
- `cmd/mecatui/` — optional gRPC **client** TUI; by default hosts a `mecated` in-process over a UNIX socket. `ui`/`theme`/`client` import no `engine/...` or `internal/...` and no proto directly — they render from relayed proto `Event`s. See `docs/tui.md`.
- `studio/` — the web client: a Next.js **Node module**, never a Go module (not in `go.work`, the layering DAG, depguard, or api-compat). A CLIENT like mecatui, consuming only the public HTTP/SSE API through its own server-side proxies; daemon-only (an unreachable daemon renders offline, never demo data). Commands run via `task studio:*` (npm underneath). **A breaking HTTP/SSE wire change owes a Studio update in the same PR.** See ADR 0288/0289 + `studio/CLAUDE.md`.
- `perf/` — the OFFLINE scenario perf harness (perf-tracking Phase 2, `task perf:scenarios`, NOT part of `task test`): `perf/kpi` (stdlib-ONLY KPI capture — `ScenarioResult`/`Capture`/`/proc` RSS sampler; never imports `engine/...` or `internal/...`) + `perf/scenarios` (external-test `testing.B` whole-loop benchmarks over `engine/...` + `engine/adapter/*`, never `internal/...`). The TUI scrollback render bench lives in `cmd/mecatui/ui/scrollback_bench_test.go` (perf/kpi imported in the `_test` file only). `perf/cmd/perfconvert` (Phase 3; stdlib + `perf/kpi` only) reshapes the scenario JSON into the two github-action-benchmark suites the CI gate consumes, and `perf/cmd/allocsgate` (stdlib only, tested, FAIL-CLOSED) is the deterministic allocs/op gate over `task bench` — the gate DECISION (benchstat is the local human A/B tool only, never the CI gate); the gate is `.github/workflows/perf.yml` (split: allocsgate over `task bench` + github-action-benchmark over the scenarios; PR fails-but-never-pushes, main pushes the `gh-pages` trend store). See `docs/adr/0019-perf-tracking.md`.

## The layering rule (the thing to get right)
Expand Down
3 changes: 3 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ includes:
sdk:
taskfile: sdk/typescript/Taskfile.yml
dir: sdk/typescript
studio:
taskfile: studio/Taskfile.yml
dir: studio
mecak8s:
taskfile: deploy/mecak8s-kind/Taskfile.yml
dir: .
Expand Down
108 changes: 108 additions & 0 deletions docs/adr/0288-studio-atrium-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# ADR 0288 — Studio: the Atrium workspace as mecatl's daemon-only web client

- Status: Accepted
- Date: 2026-08-18
- Scope: `studio/` — what the web client is, what it may talk to, how it deploys,
and what it deliberately does not do

> History: this decision subsumes the unmerged ADR drafts from PR #548 ("Studio
> module", numbered 0225 there before that id was taken on main) and carries its
> security posture forward under the new UI. PR #548 is superseded by the change
> that lands this ADR.

## Context

Mecatl needed a web client. Two candidates existed side by side: the original
Studio module on the unmerged `feat/studio-module` branch — a single-page chat
with a hardened server tier (origin-pinned proxies, a managed-`mecated`
supervisor, a typed wire seam, behavior tests) but a one-file UI — and the
Atrium workspace prototype in `stacklok/enterprise-ui-prototypes`, a designed
five-surface product (Chats · Scheduled · Skills · Memory · Settings) that
already spoke mecated's SSE protocol, but grew inside a ToolHive console fork:
its own OIDC stack, a mock server, generated clients for services mecatl does
not have, and per-surface demo fallbacks that rendered fabricated content
whenever the daemon was away.

Neither was shippable alone. The prototype had the product; the module had the
posture.

## Decision

**Studio is the Atrium workspace UI mounted on the original module's server
tier, in-repo at `studio/`, and it is daemon-only.**

- **A Node module, never a Go module.** Studio is a CLIENT of the daemon like
`mecatui`: it is not in `go.work`, the layering DAG, depguard, or the
api-compat gate. It consumes only the public HTTP/SSE API, through its own
server-side route handlers — the browser never holds a daemon address or
credential.
- **Two pure deployment modes.** Managed: `scripts/local-controller.mjs`
supervises a `mecated` it spawns from `../bin/mecated` on a random loopback
port with a generated bearer token. External: `MECATL_BASE_URL` selects a
remote daemon; no controller runs and every local control surface answers 409
as owned by the deployment. Nothing in between.
- **Daemon-only.** The prototype's mock server, demo fixtures, and per-hook
fallbacks are excluded at import. An unreachable daemon is a rendered state —
a shared runtime-status provider polls the daemon and controller, shows the
offline banner, and gates every surface's loads. Probe failure must never
produce fabricated content. (The managed controller may still run
`mecated --mock`: that is a real daemon with a mock LLM provider, which is
what offline development means here.)
- **One typed wire seam.** `src/lib/protocol/` is the only reader of raw daemon
JSON: structural decoders that throw on malformed frames, surface unknown
event kinds as visible notices, and encode the request/response asymmetry
(protojson requests, stdlib-JSON responses). Generated TypeScript bindings
from `contracts/proto` remain deferred; the seam plus its behavior tests is
the stopgap, and a breaking wire change owes a Studio update in the same PR.
- **The security posture is inherited, not re-derived.** Host/Origin/CSRF
pinning at the Next tier (`MECATL_STUDIO_PUBLIC_ORIGIN`); bearer injection
server-side only; controller mutations require the server-set
`x-mecatl-studio-request` header, loopback Host, and an allowlisted Origin;
bounded bodies; MCP gateway egress is HTTPS-only (loopback HTTP behind an
operator env opt-in) with the gateway URL user-entered but always validated;
the session workspace is resolved server-side (controller status or
`MECATL_WORKSPACE`) and injected into create bodies so the browser never
learns or chooses paths.
- **Deliberate non-features.** No provider-credential entry anywhere: mecated
reads keys from its auth file, and Studio's provider card only reports
status. Memory is read-only (the daemon has no write API, by design — a
hand-typed value would enter turn-0 context without injection scanning).
Agent-definition authoring, team runs, and the plan-approval flow are named
follow-ups, not silent gaps. Live re-attach to a running session rides the
durable session watch (`GET /v1/sessions/{id}/watch`,
[ADR 0250](./0250-durable-cursors-and-watch.md)) with one residual: the
driving tab's own `POST …/prompt` stream still cancels the run on
disconnect.
- **Toolchain.** npm with a committed lockfile (security pins carried from the
prototype as npm `overrides`), Node 22 LTS via `.nvmrc`, Biome for
lint+format, vitest for unit/decoder tests, and a hermetic `node --test`
harness that boots the production build in external mode against a fake
recording daemon to prove the server tier's behavior. CI runs all of it plus
`npm audit --audit-level=high` and a license-header guard; installs run with
scripts disabled.

## Consequences

One product instead of two halves: the designed workspace, on the hardened
tier, with the daemon as the single source of truth.

The costs, stated plainly:

- A fresh checkout without `bin/mecated` shows an offline screen, not a demo.
The screen names the fix (`task build`, then `task studio:dev`); losing the
zero-setup demo is the price of never rendering fabricated state.
- The deferred surfaces are real feature regressions against the prototype's
dormant code (authoring flows existed there, unmounted) and stay out until
they can land controller-mediated with the same posture.
- Vendoring a designed UI brings a large dependency tree (~40 runtime
packages) into the repo's audit surface; dependabot and the audit gate own
that from here.
- The same-PR rule now binds a much larger client: a daemon wire change costs
a Studio change in the same PR, every time.

## See also

- ADR 0289 — the chat list is the daemon's session store (lands with the Chats
surface; back-links here)
- [docs/architecture.md](../architecture.md) — the Studio client section
- `user-docs/building/what-you-get/studio.md` — what operating it looks like
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE
- [0087 — Staged mecatui transport migration](./0087-mecatui-staged-transport-migration.md) *(superseded by 0089)*
- [0088 — Explicit daemon.yaml (listener topology config)](./0088-daemon-config-file.md)
- [0222 — mecatui: ctrl+t routes by ask type; full-screen ask-args view](./0222-mecatui-ask-args-view.md)
- [0288 — Studio: the Atrium workspace as mecatl's daemon-only web client](./0288-studio-atrium-module.md)
- [0247 — mecatui generated status lines](./0247-mecatui-status-line.md)
- [0280 — Automatic light theme selection in mecatui](./0280-mecatui-light-theme-autodetect.md)

Expand Down
16 changes: 16 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,22 @@ HTTPS, origin, CA, hostname, and redirect safeguards. Kind remote login is avail
the public CA, but is a live qualification path, not ordinary offline-test coverage.
See [ADR 0275](adr/0275-bounded-scoped-https-keepalive-oidc.md), [ADR 0277](adr/0277-remote-mecatui-oidc.md), [ADR 0287](adr/0287-target-aware-mecatui-tls.md), and [ADR 0274](adr/0274-remote-mecatui-logout-budget.md).

**Studio — the web client (`studio/`).** An optional Next.js *client* of the public
HTTP/SSE API, in-repo as a Node module (never a Go module — not in `go.work`, the
layering DAG, depguard, or the api-compat gate). The browser talks only to Studio's
own server-side proxy routes (`/api/mecatl/*`, `/api/mecatl-control/*`), which pin
Host/Origin, inject the bearer token and the session workspace server-side, and
allowlist headers in both directions. Two pure deployment modes: managed (a local
controller supervises a `mecated` spawned from `bin/mecated` on a random loopback
port with a generated bearer) or external (`MECATL_BASE_URL`; every local control
surface answers 409 as deployment-owned). Studio is daemon-only — an unreachable
daemon renders as an offline state, never demo content — and decodes the wire in
one typed seam (`studio/src/lib/protocol/`) that surfaces unknown event kinds
instead of dropping them. Live re-attach to a running session is a stated non-goal
today: the live tail is gRPC-only (`StreamSessionLive`), so Studio shows running
state from the session inventory and reads the transcript when the run ends. A
breaking wire change owes a Studio update in the same PR. See ADR 0288/0289.

**mecatequi — the single-shot headless runner (`cmd/mecatequi`).** A fourth composition
root and a *peer of `mecademo`* over the same `app.Build`: it runs **one** prompt against
an in-process `server.Service`, drives it to a terminal state, and emits three
Expand Down
1 change: 1 addition & 0 deletions docs/design/PRODUCTION-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ record; current behaviour is in the linked [architecture](../architecture.md) do
| mecak8s (storage-free k8s-native agent) | ✅ shipped (MVP) · ✅ OPT-IN `/metrics` loopback scrape + OTLP push (ADR 0098) · ✅ verified external Redis TLS/ACL with transactional projected-file reload + last-valid generations (ADR 0240) · ✅ Helm 0.3.0 secure real-provider in-pod TLS+OIDC or edge-terminated TLS+OIDC (ClusterIP h2c), nullable spend ceilings, and pod scheduling controls · ⛔ CRD/Operator · ⛔ HPA (custom-metrics on active-runs) · ⛔ managed Redis provisioning (ElastiCache/MemoryStore — endpoint only) · ⛔ fix `mecated`'s unbounded `GracefulStop` (pre-existing, follow-up) | [mecak8s.md](../adr/0048-mecak8s.md) · [0098](../adr/0098-headless-telemetry.md) · [0240](../adr/0240-mecak8s-credential-reload-and-chart-security.md) · [0278](../adr/0278-mecak8s-edge-terminated-tls.md) · [MECAK8S-PLAN.md](./MECAK8S-PLAN.md) | [overview](../architecture.md) |
| ACP adapter (editor stdio surface) | ✅ Phase 1+2 + bounded Phase 3 + multimodal shipped · ⛔ Phase 3 long-tail (rule persistence, grep-over-buffers, fs/* on resume) | [0001-acp-adapter.md](../adr/0001-acp-adapter.md) | [api surface](../architecture/api-surface.md) |
| Conversation fork (peer session from a history snapshot) | ✅ shipped · ✅ effort override (mid-conversation effort switch, keeps the transcript — [0068](../adr/0068-effort-change-via-fork.md)) · ⛔ cross-provider/model fork (v2: replay-blob stripping) · ⛔ workspace-branching fork · ⛔ fork-from-event-log-at-arbitrary-point · ⛔ fork lineage (`forked_from` label) | [0065-conversation-fork.md](../adr/0065-conversation-fork.md) | [overview](../architecture.md) |
| Studio (web client) | 🚧 landing as a stacked PR series: ✅ module foundation (vendored Atrium UI kit, root chrome, npm/Biome/knip/vitest toolchain, CI checks incl. the license-header gate) · ⛔ server tier (proxy + managed-mode controller) · ⛔ protocol seam + harness · ⛔ the five surfaces (Chats · Scheduled · Skills · Memory · Settings) · ⛔ browser e2e | [0288](../adr/0288-studio-atrium-module.md) | [overview](../architecture.md) |
| _Historical / retired_ | — | [ARCHITECTURE.md](../adr/0004-v1-architecture.md) · [STEP-CHAIN.md](../adr/0006-v1-step-chain.md) · [TWELVE-PATTERNS-AUDIT.md](../adr/0007-twelve-patterns-audit.md) · [REPOMAP-TREE-SITTER.md](../adr/0029-repomap-tree-sitter.md) | — |

## Security
Expand Down
Loading
Loading