title: Phase 3d — Console SS-07 RALPH wrapper date: 2026-05-03 status: Accepted phase: 3d spec: docs/specs/2026-05-02-rocky-system-redesign.md §SS-07
Phase 3d — Console SS-07 RALPH wrapper
Cross-submodule design (parent + console/, with a small additive surface in ralph/).
1. Goal
Wrap the ralph serve HTTP/SSE surface (Phase 3c) as a console subsystem. Deliver the full SS-07 console surface called out in §SS-07 of the redesign spec: lib client, API routes, SS-05 RELAY route, UI page, and SS-02 dashboard panel. Keep KILN forward-compatible without embedding any KILN logic in the console.
2. Locked decisions
| # | Decision | Rationale |
|---|---|---|
| D1 | Scope: full SS-07 console surface (lib + relay route + UI + dashboard panel). | SS-07 should land in a coherent state inside the console; lib-only would leave the tenancy invariant half-implemented. |
| D2 | Transport: driver-shaped, RALPH_TRANSPORT=sidecar|remote. HEARTH-managed per-workspace driver deferred to Phase 5+. |
Self-host runs sidecar (matches "standalone CLI continues to work" promise); cloud runs remote. Driver dispatch keeps call sites stable across modes. |
| D3 | KILN scope: transport is field-agnostic; UI reserves no-op-when-absent slots for convergence_score and early_stop_reason. KILN logic lives in ralph/, never in console/. |
Per push-down policy (decision 0001), Python optimisation work belongs in the ralph submodule. The console wrapper passes KAHN events through; rendering choices are forward-compatible. |
| D4 | Prompts editor: editable, with gates G1+G2+G3(CRDT)+G4+G5+G7+G8. G6 (council/doctrine reference resolution) deferred. | Prompts are STRATT-typed units; integrity is enforced via MERIDIAN-style fingerprinting. Multi-editor concurrency is solved with a CRDT, not optimistic-lock-and-409. |
| D5 | Fingerprint storage: <file>.yaml.fp sidecar. |
Matches MERIDIAN's existing convention; keeps the YAML readable by anything that doesn't care about provenance. |
| D6 | Canonicalisation lib: vendor @stratt/fingerprint into console/vendor/stratt-fingerprint/ (sibling of existing console/vendor/stratt/). Optional vendoring of @crdt for the editor. |
Re-using the canonicalisation algorithm from MERIDIAN guarantees fingerprints computed in either system are byte-identical. The vendor-snapshot pattern is already proven for STRATT. |
| D7 | Auth ship-target: static HMAC from VAULT (gate A) for Phase 3d; designed for migration to per-workspace scoped tokens (gate C) in a follow-up phase. | C is the right end-state for multi-tenant cloud, but minting + plumbing per-workspace tokens is genuinely independent of "wrap ralph serve in a driver" and would balloon 3d. HATCH still records actor identity (Airlock session is the source). |
3. Architecture
Browser (Airlock cookie on .devarno.cloud)
│
▼
┌─────────────────────────────────────────────────┐
│ Next.js console (existing; rocky-hq/console) │
│ │
│ /ralph/runs /ralph/runs/[id] │
│ /ralph/prompts/[file] │
│ │
│ ── /api/ralph/runs (proxy POST) │
│ ── /api/ralph/runs/[id] (proxy GET) │
│ ── /api/ralph/runs/[id]/events (SSE proxy) │
│ ── /api/ralph/runs/[id]/cancel (proxy POST) │
│ ── /api/ralph/prompts/[...path] (read/save) │
│ ── /api/relay/ralph (HATCH relay) │
│ │
│ console/src/lib/ralph/ │
│ ├ client.ts (5 verbs + driver pick) │
│ ├ transport/ │
│ │ ├ sidecar.ts (loopback child proc) │
│ │ └ remote.ts (RALPH_URL fetch) │
│ ├ types.ts (KAHN events, optional │
│ │ KILN fields) │
│ ├ prompts.ts (read/save w/ G1–G8) │
│ ├ fingerprint.ts (re-export of vendored │
│ │ @stratt/fingerprint) │
│ └ crdt.ts (Yjs doc per prompt path)│
└─────────────────────────────────────────────────┘
│
▼ (HMAC bearer; loopback or RALPH_URL)
┌──────────────┐
│ ralph serve │ ← unchanged from Phase 3c (SHA 513c63c)
│ (FastAPI) │
└──────────────┘
4. Lib client surface
// console/src/lib/ralph/client.ts
export interface SubmitRunOpts {
workspace_slug: string;
prompt_path: string; // e.g. "prompts/refactor-auth.yaml"
prompts_yaml: string; // current saved file content
prompt_fingerprint: string; // pinned at submission time (gate G7)
config: RunConfig; // mirrors ralph's RunConfig
}
export interface RalphClient {
submitRun(opts: SubmitRunOpts): Promise<{ run_id: string }>;
streamEvents(run_id: string, signal?: AbortSignal): AsyncIterable<KahnEvent>;
cancelRun(run_id: string): Promise<void>;
getRun(run_id: string): Promise<RunStatus>;
listRuns(workspace_slug: string): Promise<RunSummary[]>;
}
export function getRalphClient(): RalphClient {
const transport = process.env.RALPH_TRANSPORT ?? "sidecar";
switch (transport) {
case "sidecar": return new SidecarClient(/* loopback URL + child supervisor */);
case "remote": return new RemoteClient(process.env.RALPH_URL!);
default: throw new Error(`unknown RALPH_TRANSPORT: ${transport}`);
}
}
KahnEvent mirrors ralph/contracts/transitions.schema.json. Every additive field — including convergence_score, early_stop_reason, and any future KILN-emitted field — is optional in the TypeScript type. The wrapper never branches on KILN-specific values; it forwards events to the UI which renders a no-op slot when a field is absent.
5. Transport drivers
Sidecar driver
- Console boot supervises
ralph serveon127.0.0.1:${RALPH_SIDECAR_PORT ?? 8765}. - Health-checks via
GET /healthzbefore serving the first request; first failure delays all/ralph/*routes with a "starting RALPH…" banner; persistent failure (>30s) surfaces as a stable error banner. - Auto-restarts on crash with exponential backoff capped at 30s.
- Logs to
RALPH_SIDECAR_LOG_PATH(defaults to.rocky/sidecar.log; the parent.gitignorealready excludes.rocky/). - Auth bearer is read once at boot from VAULT path
secrets/ralph/serve_tokenand sent asAuthorization: Bearer <hmac>on every request.
Remote driver
- Reads
RALPH_URLat boot. - Single
/healthzping at startup; failure surfaces as a banner. No supervision. - Same bearer mechanism. Optional mTLS (off by default; toggled by
RALPH_MTLS=1+ cert paths) — Phase 3d implements the toggle but ships the mTLS code path behind an integration test only.
Driver dispatch
getRalphClient() is the single entry point. Call sites take the RalphClient interface, never a concrete class.
6. API routes (Next.js, all under /api/ralph/*)
Every route follows the same pipeline:
- Authn: resolve Airlock session via existing
auth-server.tsmiddleware. Reject if absent. - Authz: resolve workspace from path/body, look up role (operator/observer/admin). Reject if role insufficient.
- Pre-state-change HATCH: for state-changing routes (submit, cancel, save, delete), write the corresponding HATCH event before performing the side effect (per tenancy invariant: HATCH event written before action takes effect).
- Mint bearer: Phase 3d returns the static HMAC; the function signature
mintBearer(session, workspace_slug) → stringis in place for the per-workspace successor. - Forward: call the corresponding
RalphClientmethod. - Post-state-change HATCH: for terminal events observed via SSE (run completion, cancellation accepted), emit a follow-up HATCH event with outcome metadata.
The SSE proxy (/api/ralph/runs/[id]/events) re-streams the upstream byte-for-byte; no parsing in the proxy. The browser-side iterator parses.
7. Prompts editor protocol
Storage layout
<workspace>/prompts/<name>.yaml ← STRATT-typed prompt-set unit
<workspace>/prompts/<name>.yaml.fp ← MERIDIAN-style canonical hash sidecar
Both files are committed to the workspace's git tree (mediated by the existing SS-04 WORKSPACE store).
Read flow (G2)
GET /api/ralph/prompts/<path> returns:
{
content: string;
fingerprint_stored: string | null; // from .fp sidecar
fingerprint_computed: string; // recomputed from content
status: "verified" | "tampered" | "missing";
}
UI renders a FingerprintBadge React component, ported from MERIDIAN's FingerprintBadge.astro (the Astro original is not directly importable; the visual contract — verified/tampered/missing + truncated hash — is what's preserved).
Save flow (G1+G2+G3+G4+G7+G8)
Steps in order; failure at any step aborts with no on-disk change:
- G8 path safety. Path is resolved against
<workspace>/prompts/. Reject.., absolute paths, symlinks, or any escape. Standard path-traversal defense. - G1 STRATT schema validation. Parse with extended
console/src/lib/stratt/parser.tsagainst a newprompt-setunit schema (console/src/lib/stratt/schemas/prompt-set.ts). Reject on invalid. - G3 CRDT publish. Save = "publish" of the converged Yjs doc snapshot. The CRDT doc is keyed by
(workspace_slug, prompt_path)and held by a Next.js websocket route. Concurrent editors converge automatically; the fingerprint is computed on the publish snapshot, not on every keystroke. - G2 fingerprint compute + compare. Canonicalise via vendored
@stratt/fingerprint, hash, compare with prior.fpsidecar. If client-suppliedexpected_prior_fingerprint≠ on-disk, return 409. CRDT prevents this in the happy path; 409 is the safety net for non-CRDT edits (CLI commits, direct git pushes). - Write atomically.
<file>.yamland<file>.yaml.fpwritten via temp-file + rename pair. No partial state visible. - G4 HATCH event.
prompt.editedto/api/relay/ralphwith{actor, workspace, prompt_path, old_fp, new_fp, diff_summary: {lines_added, lines_removed, sections_changed}}. Body content is never logged in the HATCH event. - G7 run-pin (downstream). When a subsequent
submitRunreferences this file, the API route reads the on-disk.fpat submission time and embeds it in the run'sstartevent payload. Every run is fingerprint-pinned; replaying or auditing a run later proves which exact prompts content it ran against.
Role gate (G5)
| Role | Read | Edit | Delete | Submit run | Cancel run |
|---|---|---|---|---|---|
| observer | ✓ | – | – | – | – |
| operator | ✓ | ✓ | – | ✓ | – |
| admin | ✓ | ✓ | ✓ | ✓ | ✓ |
Enforced at the API route. UI mirrors with disabled controls + tooltips ("requires operator role").
Deferred from this phase
- G6 (council/doctrine reference resolution): depends on the
prompt-setSTRATT schema being finalised and the existingconsole/src/lib/stratt/registry.tsexposing aresolveHandle()verb. Trivial follow-up; lands in 3e.
8. UI surface
| Route | Content |
|---|---|
/ralph |
Landing: list of recent runs (status, started_at, prompt fingerprint, actor, duration). |
/ralph/runs |
Full runs table with filtering (workspace, status, date range). |
/ralph/runs/[id] |
Run detail: live SSE log as vertical timeline of KAHN node events, per-node done_when_results, convergence-score sparkline (renders if convergence_score present, hidden otherwise), early-stop reason badge (if early_stop_reason present), plan.md/diff.patch/transcripts artifact links (resolved through SS-04 WORKSPACE), cancel button (admin only). |
/ralph/prompts/[file] |
Monaco-style YAML editor with FingerprintBadge in header, presence indicators for other CRDT editors, role-gated controls. "Submit run" button reads current saved file → submitRun → redirect to /ralph/runs/[id]. |
Dashboard panel ralph.runs
Tile in existing SS-02 dashboards system. Rolling 24h: runs total, pass rate, mean convergence-score (if KILN landed) or mean attempt count (otherwise), top failing prompts.
Design system
No new design system. Reuses Tailwind + radix-ui per spec.
9. HATCH events emitted
| Event | When | Payload (high-signal fields only; body content never logged) |
|---|---|---|
ralph.run.submitted |
After POST /api/ralph/runs accepts |
actor, workspace, run_id, prompt_path, prompt_fingerprint |
ralph.run.cancelled |
After POST /.../cancel accepts |
actor, workspace, run_id, reason |
ralph.run.terminal |
When SSE proxy sees terminal node event | workspace, run_id, status, duration_s, attempts, early_stop_reason? |
prompt.edited |
After successful save | actor, workspace, prompt_path, old_fp, new_fp, diff_summary |
prompt.deleted |
After successful delete (admin only) | actor, workspace, prompt_path, last_fp |
All routed through /api/relay/ralph (the new SS-05 RELAY route per redesign §SS-07).
10. AUTH-EVOLUTION
Phase 3d ships a static HMAC bearer; the mintBearer(session, workspace_slug) → string interface is the migration seam.
| Phase | Bearer scope | Storage |
|---|---|---|
| 3d (this phase) | One global HMAC | VAULT secrets/ralph/serve_token |
| Auth-follow-up | Per (workspace_slug, role) JWT, short-lived (~5min), refreshed on Airlock session refresh |
VAULT-held HMAC signing key + per-call mint |
ralph-side: Phase 3c accepts a single bearer today. The follow-up adds a validate_bearer(token, workspace_slug) hook that the static-HMAC path implements as a string compare and the JWT path implements as signature + claims verify. Call sites in ralph/serve/auth.py are unchanged.
11. Testing
- Lib unit tests. Transport drivers (mock fetch + mock child_process), client interface, prompts read/save with mocked fs + fingerprint.
- API route tests. Authn gate, role gate (G5), path-safety (G8), fingerprint mismatch returns 409, HATCH event shape per route.
- SSE replay test. Mock ralph emits N events; proxy passes through byte-for-byte; client iterator yields N events.
- CRDT convergence test. Two parallel Yjs doc edits on the same
(workspace, path)produce a deterministic merged snapshot; saved fingerprint reflects the converged state. - E2E (Playwright). Observer cannot click "Edit"; operator save triggers fingerprint update; admin cancels in-flight run.
- Cross-submodule integration test. Boots real
ralph serve(Phase 3c) on a loopback port; console drives a complete run end-to-end; asserts terminal event + HATCH events.
12. Deferred / out-of-scope
| Item | Phase |
|---|---|
| KILN Phases 0–2 (diagnostic feedback, convergence_score, early termination) | Separate ralph-internal phase. This design ensures their schema additions land without re-cutting 3d. |
| G6 council/doctrine reference resolution | Phase 3e. Depends on prompt-set schema landing in 3d. |
| Per-workspace scoped JWT tokens | Auth-follow-up phase. Migration seam is in place. |
| HEARTH-managed per-workspace ralph driver | Phase 5+. Adds a third entry to getRalphClient() switch; no other changes. |
| Server-side run-history aggregation for the dashboard panel | Stretch goal for 3d, otherwise 3e. May need an SS-04 WORKSPACE store extension. |
13. Acceptance criteria
A Phase 3d PR is complete when:
RALPH_TRANSPORT=sidecarandRALPH_TRANSPORT=remoteboth pass the cross-submodule integration test.- Observer / operator / admin role gates verified at API + UI.
- Prompts save flow: schema-valid → fingerprint updated → HATCH event emitted → on-disk content +
.fpsidecar match canonicalised hash. - Two parallel CRDT editors converge to the same fingerprint after publish.
- SSE proxy passes terminal event through with
convergence_scoreandearly_stop_reasonif present, hidden but not crashing if absent (verified by emitting events with and without those fields in test fixtures). /api/relay/ralphwrites all five HATCH events listed in §9.- SS-02
ralph.runspanel renders against fixture data with both KILN-on and KILN-off event shapes.
14. References
- Spec:
docs/specs/2026-05-02-rocky-system-redesign.md§SS-07. - Phase 3c plan:
ralph/docs/plans/2026-05-02-ralph-serve-phase-3c.md(the HTTP/SSE surface this wrapper consumes). - Decision 0001 — push-down policy (per-language work lives in the relevant submodule).
- Decision 0002 — KAHN integration; the event types this wrapper passes through.
- MERIDIAN doctrine (
console/vendor/stratt/doctrines/stratt/MERIDIAN.doctrine.md) — fingerprint mechanism. - STRATT registry (
console/src/lib/stratt/registry.ts,console/vendor/stratt/).