← Ledger


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

Remote driver

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:

  1. Authn: resolve Airlock session via existing auth-server.ts middleware. Reject if absent.
  2. Authz: resolve workspace from path/body, look up role (operator/observer/admin). Reject if role insufficient.
  3. 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).
  4. Mint bearer: Phase 3d returns the static HMAC; the function signature mintBearer(session, workspace_slug) → string is in place for the per-workspace successor.
  5. Forward: call the corresponding RalphClient method.
  6. 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:

  1. G8 path safety. Path is resolved against <workspace>/prompts/. Reject .., absolute paths, symlinks, or any escape. Standard path-traversal defense.
  2. G1 STRATT schema validation. Parse with extended console/src/lib/stratt/parser.ts against a new prompt-set unit schema (console/src/lib/stratt/schemas/prompt-set.ts). Reject on invalid.
  3. 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.
  4. G2 fingerprint compute + compare. Canonicalise via vendored @stratt/fingerprint, hash, compare with prior .fp sidecar. If client-supplied expected_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).
  5. Write atomically. <file>.yaml and <file>.yaml.fp written via temp-file + rename pair. No partial state visible.
  6. G4 HATCH event. prompt.edited to /api/relay/ralph with {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.
  7. G7 run-pin (downstream). When a subsequent submitRun references this file, the API route reads the on-disk .fp at submission time and embeds it in the run's start event 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

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

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:

  1. RALPH_TRANSPORT=sidecar and RALPH_TRANSPORT=remote both pass the cross-submodule integration test.
  2. Observer / operator / admin role gates verified at API + UI.
  3. Prompts save flow: schema-valid → fingerprint updated → HATCH event emitted → on-disk content + .fp sidecar match canonicalised hash.
  4. Two parallel CRDT editors converge to the same fingerprint after publish.
  5. SSE proxy passes terminal event through with convergence_score and early_stop_reason if present, hidden but not crashing if absent (verified by emitting events with and without those fields in test fixtures).
  6. /api/relay/ralph writes all five HATCH events listed in §9.
  7. SS-02 ralph.runs panel renders against fixture data with both KILN-on and KILN-off event shapes.

14. References