Owner: architecture agent. Autonomous (P6). Upstream: specs/spec.md. Downstream: TASKS.
Decisions are technical and documented with reasoning (Ben delegates the technical).
Budgets are numbers. Inherits memory:brand-tokens-as-code,js-budget-gate,
aa-on-brand-color,aa-proof-kinetics(NOTwrangler-node-pin— see §7).
Designed on top of a deep reference study (§A) — embed, don't reinvent.
Four reference repos shallow-cloned and read directly (not web-search synthesis). Concrete,
attributed patterns we build on. Embed/borrow the proven mechanics; reinvent nothing.
*Source: github.com/nesquena/hermes-webui (read at /tmp/ref-hermes). Factual: Python**stdlib
http.server only, deps = pyyaml + cryptography, WebAuthn hand-rolled** on cryptography.*
.rail + .sidebar (panel-view sheets) + .main + toggled .workspace-panel; panel state driven by documentElement.dataset.workspacePanel set
pre-paint (index.html:27,157,172,383). Adopt the rail + dataset-driven panel toggle.
queue.Queue(maxsize=16) drop-on-full, text/event-stream + X-Accel-Buffering:no + : heartbeat, never Connection: close
on the stream (route_approvals.py:51,89; routes.py:9985,10061). Adopt verbatim.
0600, lazy-pruned; atomic-write JSON state for all secrets (auth.py:418-462; passkeys.py:69-82). Adopt for our session +
passkey-credential store.
approval_id) (route_approvals.py:106; index.html:465). Bank for tier-2 (Review).
?v= query string (no hashing, stays no-build) (index.html:1522).SIGPIPE→SIG_IGN, response-side credential-redaction regex (server.py:33; helpers.py:209). Adopt the redaction idea.
routes.py + 17k-line i18n.js are accreted debt — **split routesby domain from day one; it's chat-first — we invert to instrument-first** (live state is
the hero, not conversation); tighten CORS/CSRF (it's * on OPTIONS).
Source: github.com/tsl0922/ttyd (read at /tmp/ref-ttyd, src/http.c, man page).
X-Frame-Options/CSP frame-ancestors (verified src/http.c) → asame-origin iframe under a Caddy sub-path works with no override.
-O check-origin OFF when iframing cross-path (else the WS drops); avoid -a/--url-arg (injection surface).
ttyd -i 127.0.0.1 -p 7681 -b /terminal -W -c "$U:$P" -t disableLeaveAlert=true bash-H <header> lets ttyd trust a reverse-proxy-verified header (compose with our passkey via Caddy).Source: github.com/langfuse/langfuse (read at /tmp/ref-langfuse). Three pure passes —
we reimplement, we do not import (drop tRPC/Next/ClickHouse/Decimal.js):
Map<id,node> → link children → BFS depth; orphan rows (parent not in id-set) become roots (lib/tree-building.ts:88,110-175).
inDegree queue → O(N) cost/token rollup (tree-building.ts:183-312).
timeline-calculations.ts:27-50): startOffset = ((start−traceStart)/totalSpan)W, barWidth = (latency/totalSpan)W; position
by margin-left+width; zero-width floor ||10px, dashed border = open span (TimelineBar.tsx:54,141).
treeLines: boolean[] bitmask for the connector lines + a collapsed set (timeline-flattening.ts:27-139; TimelineRow.tsx). Metrics computed once, not per render.
level (DEBUG<DEFAULT<WARNING<ERROR) — **restyle to amber/graphite** by swapping classes (lib/helpers.ts:159-185).
Source: github.com/All-Hands-AI/OpenHands (read at /tmp/ref-openhands). We embed ttyd, so we
skip its xterm bridge; we borrow the resizable-panel mechanics:
getBoundingClientRect + clamped clientX→%, applied as inline width:X% — pure DOM, portable to vanilla (hooks/use-resizable-panels.ts:36).
cursor/userSelect during drag; disable transitions while dragging (ui/resize-handle.tsx:11). Persist width to localStorage
(use-resizable-panels.ts:17,54).
{id: renderFn} tab map, only active mounts (conversation-tab-content.tsx:20).for a no-build cockpit that iframes ttyd.
| Layer | Choice | Why |
|---|---|---|
| Frontend | Vanilla JS, no build — native ES modules + tokens-as-code CSS | hermes ethos, but ES modules avoid hermes' global-window accretion (A.1 diverge); per-domain files, boot.js orchestrator. |
| Server | FastAPI (Python 3.11+), routes split by domain | Orion's stack (one language on the VPS); we use a WebAuthn library + split routes rather than hermes' hand-rolled 11k-line monolith (A.1). |
| Live updates | SSE (not WebSockets) | hermes pattern (A.1); read-only observe fits a one-way stream; simpler than WS. |
| Terminal | ttyd embedded via same-origin iframe under /terminal | A.2 — ttyd ships its own xterm; nothing to build. |
| TLS / proxy | Caddy (auto-TLS), fronts Studio + ttyd same-origin | trivial config; enables the same-origin iframe (A.2). |
| Network | Tailscale (WireGuard mesh) | zero open public ports (NFR-5). |
| App auth | WebAuthn / passkey (py_webauthn lib) + HMAC-signed session cookie, 0600 store | hermes pattern (A.1) but library-backed, not hand-rolled. |
| HTTP client | httpx (async) → Observe API + Directus | read-only proxy; no DB driver to Orion. |
| Container | Docker + docker-compose, non-root | hermes/Orion ethos (A.1 Dockerfile shape). |
Decision: the Studio server is a **thin read-proxy that calls the Orion Observe API over HTTP
(Tailscale) — it does not** import Orion code or open Orion's Postgres. The contract
(spec.md §4.1) is the seam; the server needs only the Observe API URL + token, no DB creds.
SSE from the Observe API (if present) is passed through to the browser via our own SSE
endpoint (A.1 hardening). The D-1 stub is a FixtureObserveClient with the same interface.
operator ──passkey──▶ Studio (FastAPI + vanilla ES-module JS)
│ httpx (read-only, Tailscale) SSE passthrough ▲
├──▶ Orion Observe API (runs, traces) [Orion-owned, D-1]
├──▶ Directus REST (product docs) [read-only token]
└──▶ repo files (memory/, products/*)
workspace panel ──same-origin iframe──▶ /terminal ─Caddy▶ ttyd(127.0.0.1:7681) ─▶ shell
[Tailscale · Caddy TLS · passkey · ttyd -c]
Decision: authored in ai-studio/products/orion-studio/implementation/ (PRISM product of
record), deployed beside orion-os on the VPS, coupled to Orion only via the Observe API.
This realizes the brief's locked "both": *authored in PRISM, co-located at runtime, API-coupled
(not DB-coupled).* Keeps the PRISM audit trail, honors P10, runs at Orion's side for low latency.
The Studio persists exactly one thing: the operator's passkey credential + session map
(JSON 0600, A.1 pattern). This is the Studio's own auth data, not Orion/PRISM truth — it does
not violate FR-5. Orion/PRISM data stays read-through; an optional in-memory cache (TTL ≤ 3s)
for the runs list is never authoritative, never persisted. **No Orion-data persistence layer
exists in the codebase** (AC-Trust).
products/orion-studio/implementation/
server/ # FastAPI, routes split by domain (NOT a monolith — A.1 diverge)
main.py # app wiring + static host
config.py # pydantic Settings (env-driven, Orion pattern)
routes/observe.py # /api/runs, /api/runs/{id}, /api/runs/stream (SSE passthrough)
routes/prism.py # /api/products, /api/memory (repo + Directus)
routes/auth.py # /api/auth/* WebAuthn register/verify
observe_client.py # httpx → Observe API; FixtureObserveClient (D-1 stub), same interface
directus_client.py # httpx → Directus (read-only)
repo_reader.py # memory/ + products/* (FR-3)
session.py # HMAC-signed cookie + 0600 store, lazy-prune (A.1)
sse.py # bounded queue, heartbeat, no Connection:close (A.1)
web/ # NO build — native ES modules
index.html # .rail / .sidebar / .main / .workspace-panel (A.1); ?v= cache-bust
css/tokens.css # brand.md tokens VERBATIM (single source of truth)
css/cockpit.css # 3-panel layout + instrument styles
js/boot.js # orchestrator (loaded last)
js/router.js js/runs.js js/trace.js js/prism.js js/terminal.js
js/panels.js # resizable split + localStorage width (A.4)
js/motion.js # breathe/tick, reduced-motion gate (aa-proof-kinetics)
js/sse.js # EventSource {withCredentials:true} + polling fallback (A.1)
fonts/ # IBM Plex Sans + Mono (self-hosted .woff2)
fixtures/observe/ # §4.1 contract fixtures (D-1 stub)
deploy/
Dockerfile docker-compose.yml Caddyfile tailscale.md
tests/ # contract tests + AC checks (a11y/budget/drift/port/auth/resilience)
README.md
trace.js implements A.3's 3-pass tree-build + two timing formulas; panels.js implements A.4's
resize math. The terminal is an iframe — no xterm code of our own.
js-budget-gate adapted).aa-on-brand-color); reduced-motion honored (aa-proof-kinetics).Decision: Docker on the VPS, fronted by Caddy, reached over Tailscale. A static
Pages deploy can't host ttyd or reach a private VPS, and the Studio must sit beside Orion's data —
so wrangler-node-pin does not apply (first PRISM web product that is self-hosted, not Pages).
ttyd (A.2):
ttyd -i 127.0.0.1 -p 7681 -b /terminal -W -c "$TTYD_USER:$TTYD_PASS" -t disableLeaveAlert=true bash
Caddyfile (A.2 — same origin so the iframe is same-origin):
studio.<tailnet-name>.ts.net {
handle_path /terminal/* {
reverse_proxy 127.0.0.1:7681 {
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
}
}
reverse_proxy /* 127.0.0.1:8000 # FastAPI Studio
}
Auth layers (outer→inner, each fails closed): Tailscale → Caddy TLS → passkey (app) → ttyd -c.
-O check-origin stays OFF (iframe is cross-path); localhost mode binds 127.0.0.1 only.
Each client (observe_client, directus_client, ttyd iframe) maps unreachable/error → a typed
calm-failure state; server returns 503 + {source, reachable:false}; UI renders the right
calm message per panel (FR-1.4) — never a spinner-forever or blank.
observe_client (live + fixture) conforms to spec.md §4.1.all routes (AC-Sec-2), ≤1 accent/viewport scan (AC-Calm), AA + keyboard + reduced-motion
(AC-A11y), JS-budget gate, resilience 3-case (AC-Resilience).
superpowers:test-driven-development) for server logic + the A.3 tree/timing functions(pure → unit-testable); visual/brand checks at REVIEW.
Real dependency = Orion builds the Observe API (Observe tier, Orion-owned, P10) per
architecture/observe-api-contract.md. Fixtures (shaped to the real agent_runs/tasks/
shadow_logs columns) unblock the entire build; live cutover = config flip. PRISM proposes a
published schema + drift-guard mirroring prism_task.v1.
patterns/sse-live-updates (hermes SSE hardening — heartbeat, bounded queue, no Connection:close).patterns/trace-waterfall-plain-js (A.3 3-pass + two timing formulas, framework-free).patterns/ttyd-iframe-embed (A.2 invocation + Caddy + auth layering).patterns/vanilla-resizable-panels (A.4 % split, no library).brand-isolation: **fact-check fonts/colors against each prior product's shipped brand.md, not memory** (caught the Space Grotesk overlap at the BRAND gate).