← 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 2026-05-02-redesign-bootstrap), 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