• feat(web): in-browser debug companion — issue #16 (v0.15.0)

    vh released this 2026-05-27 19:03:50 -07:00 | 238 commits to main since this release

    Browser-based debug companion to the Ratatoskr TUI, reusing the
    existing wire-layer modules unchanged. Same five surfaces (transcript,
    thinking, tools, debug, persona) over the same Worldtree Conversation
    API SSE wire, viewable from any device on the operator's LAN.

    Per docs/contracts/issues/16.contract.md (full v2.1 module contract
    with 11 FN blocks + 9 invariants + Heid panel review pass merged).

    Architecture:

    • New module ratatoskr.web with server.py (Starlette app, ~250 LOC),
      entrypoint.py (lazy-import gate, ~100 LOC), static/index.html
      (single-page vanilla JS UI, ~360 LOC)
    • Optional-deps group [web] = starlette + uvicorn[standard]; dev
      pulls these in transitively
    • New console script ratatoskr-web
    • Streaming via browser-native EventSource GET; prompt-submit is a
      separate POST (load-bearing Hulda finding from R13 panel — EventSource
      is GET-only)
    • Small in-memory turn registry maps (session_id, turn_id) → upstream
      request handle for cancel + browser-disconnect cleanup

    Endpoint surface (9 routes):

    • GET / → static index.html
    • GET /static/* → static assets
    • GET /version → {"ratatoskr": ""}
    • GET /api/agents → upstream /agents + local Tier 3 merge
    • POST /api/sessions → upstream POST /sessions
    • GET /api/agents/{id}/persona_state → upstream persona-state
    • POST /api/turns/{sid} → allocate turn_id, register in turn registry
    • GET /api/turns/{sid}/stream?turn_id=N → proxy upstream SSE to browser
    • POST /api/turns/{sid}/cancel?turn_id=N → upstream cancel

    Trust model: internal LAN debug surface. Binds 0.0.0.0:8765 default;
    no auth, no CORS guard (operator direction). What stays disciplined
    regardless of network trust:

    • Transcript HTML-escapes assistant content (INV-004 — model output
      is untrusted text; adversarial HTML must not execute in browser)
    • Upstream API key never reaches browser DOM (INV-003 — proxy-only)

    Lifecycle:

    • Browser disconnect mid-stream → upstream cancel (INV-005;
      asyncio.CancelledError caught in stream handler)
    • Server Ctrl-C → lifespan shutdown drains turn registry within 5s
      budget (INV-006; structured-log line on timeout)

    Tests (37 new, 356 total; previous 319 baseline preserved):

    • tests/test_web_server.py (23 cases): endpoint contract via Starlette
      TestClient + respx mocks; covers each endpoint, browser-disconnect →
      upstream cancel, lifespan shutdown draining the registry
    • tests/test_web_presentation_contract.py (11 cases): proxy
      serialization matches tests/fixtures/presentation_contract.json
      for one of each Event type — drift detection between server-side
      serializer and the JS presenter without forcing a shared abstraction
    • tests/test_web_packaging.py (4 cases): static asset packaging via
      importlib.resources; AST-checked lazy-import discipline (no top-
      level starlette/uvicorn import in entrypoint.py); missing-API-key
      exit-11 path; missing-extras exit-12 path

    Provenance:

    • Scope v1 → Heid panel review (Gróa + Hulda, R13) → 8 load-bearing
      corrections (POST→GET split, Starlette > FastAPI, lazy-import
      discipline, browser-disconnect → upstream cancel, presentation-
      contract fixture, error event contract, static-asset packaging,
      escaped plain-text Markdown deferred) merged into scope v2
    • Operator direction: internal-LAN debug surface; auth + CORS
      deliberately omitted

    Not yet (deferred to v0.16.x+):

    • Cross-reload session resume via Last-Event-ID
    • Tier 3 lifecycle UI (define/patch/delete in browser)
    • Markdown rendering with vendored safe-subset renderer
    • TLS + real auth (only if a non-LAN use case ever surfaces)
    Downloads