breakglass UI v2: attachable sessions (tmux model) + mobile-first redesign
Full audit-driven rework. Keeps the proven SSE-translation + verb logic; everything else upgraded for phone-primary use.
Backend — server owns the session, clients attach (Viktor's tmux idea):
- session.py: SessionManager + Session with an event log, subscriber pub/sub, and turns that run DETACHED (keep going if the client disconnects).
- GET /api/session/{id}/stream = attach (SSE): replays the transcript then tails live; per-event id: lines so an EventSource auto-reconnect resumes from Last-Event-ID (free re-attach). POST /{id}/prompt starts a detached turn; POST /{id}/cancel = Stop. Replaces the old one-shot /api/chat.
- agent_session trimmed to the argv + translate_event helpers; 21 new/updated tests (replay, Last-Event-ID resume, broadcast, detached turn, resume, cancel, routes) — 53 green.
Frontend — mobile-first via the frontend-design skill (emergency-console aesthetic):
- EventSource attach (native auto-reconnect, zero client reconnect logic); transcript.js folds events->messages with id-dedupe so replays never double-render (30 unit assertions).
- Installable PWA: manifest + icons (wrench/break-glass mark) + apple-mobile-web-app meta + theme-color; viewport-fit=cover + safe-area; 100dvh; 16px composer (no iOS zoom).
- One-tap diagnosis presets (Triage / Memory-OOM / Disk / Services / QEMU-wedged) mapped to the devvm's real failure modes; Stop button while a turn runs.
- Foldable VM-control sheet, cycle the dominant recovery action w/ confirm, output capped 46vh.
- a11y: fixed --ink-faint contrast 3.6:1 -> 6.1:1 (WCAG AA); >=44px tap targets. Deleted the obsolete fetch-reader sse.js (EventSource replaces it).
Verified: 53 backend tests + 30 transcript assertions; Playwright @390x844 (input on-screen y=721-821, presets/sheet/fold/cap); local integration smoke vs the real backend (attach->caught-up, 404, verbs, PWA served).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 19:19:03 +00:00
|
|
|
"""Claude CLI argv + stream-json → UI-event translation for the breakglass agent.
|
breakglass: in-cluster emergency-recovery UI for the devvm
Viktor wanted a web UI on the claude service to act as his breakglass when
the devvm is down: open it, have Claude SSH in to diagnose/repair, and
power-cycle the VM via the Proxmox host if needed. This is the app half
(the infra stack + host bootstrap live in the infra repo).
New, ISOLATED ASGI app under app/breakglass/ (never imports app.main, so the
untrusted-input agents — recruiter-triage, nextcloud-todos — can't share a
process with the root-on-devvm / PVE-reset SSH key):
- pve.py: the LLM-independent power-verb path (status|forensics|reset|stop|
start|cycle on VM 102), whitelist-validated client-side, executed over the
forced-command SSH key (list argv, no shell).
- agent_session.py: multi-turn streamed chat — claude -p --session-id /
--resume with --output-format stream-json, translated to a small SSE
vocabulary (session/text/tool/result/error/done).
- auth.py: edge Authentik header OR bearer; fail-closed.
- server.py: FastAPI (session/chat-SSE/pve-verb routes) + serves the Svelte UI.
- Svelte SPA (frontend/, built into app/breakglass/static/ and committed — no
in-cluster build, per ADR-0002): streamed chat + danger-styled manual VM
controls with confirm-on-mutate.
- agents/breakglass.md: narrow tools (Bash/Read/Grep/Glob, no web), taught the
ssh devvm / ssh pve aliases and cycle-vs-reset.
- docker-entrypoint-breakglass.sh: ssh-agent bootstrap from the mounted key +
ssh aliases, then uvicorn app.breakglass.server. The breakglass Deployment
overrides the image CMD with this; the existing service is untouched.
26 new tests (verb whitelist incl. injection attempts, stream-json→SSE
translation, auth gating, route behaviour); full suite 58 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:36:05 +00:00
|
|
|
|
breakglass UI v2: attachable sessions (tmux model) + mobile-first redesign
Full audit-driven rework. Keeps the proven SSE-translation + verb logic; everything else upgraded for phone-primary use.
Backend — server owns the session, clients attach (Viktor's tmux idea):
- session.py: SessionManager + Session with an event log, subscriber pub/sub, and turns that run DETACHED (keep going if the client disconnects).
- GET /api/session/{id}/stream = attach (SSE): replays the transcript then tails live; per-event id: lines so an EventSource auto-reconnect resumes from Last-Event-ID (free re-attach). POST /{id}/prompt starts a detached turn; POST /{id}/cancel = Stop. Replaces the old one-shot /api/chat.
- agent_session trimmed to the argv + translate_event helpers; 21 new/updated tests (replay, Last-Event-ID resume, broadcast, detached turn, resume, cancel, routes) — 53 green.
Frontend — mobile-first via the frontend-design skill (emergency-console aesthetic):
- EventSource attach (native auto-reconnect, zero client reconnect logic); transcript.js folds events->messages with id-dedupe so replays never double-render (30 unit assertions).
- Installable PWA: manifest + icons (wrench/break-glass mark) + apple-mobile-web-app meta + theme-color; viewport-fit=cover + safe-area; 100dvh; 16px composer (no iOS zoom).
- One-tap diagnosis presets (Triage / Memory-OOM / Disk / Services / QEMU-wedged) mapped to the devvm's real failure modes; Stop button while a turn runs.
- Foldable VM-control sheet, cycle the dominant recovery action w/ confirm, output capped 46vh.
- a11y: fixed --ink-faint contrast 3.6:1 -> 6.1:1 (WCAG AA); >=44px tap targets. Deleted the obsolete fetch-reader sse.js (EventSource replaces it).
Verified: 53 backend tests + 30 transcript assertions; Playwright @390x844 (input on-screen y=721-821, presets/sheet/fold/cap); local integration smoke vs the real backend (attach->caught-up, 404, verbs, PWA served).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 19:19:03 +00:00
|
|
|
The session lifecycle (running turns, attaching clients) lives in ``session.py``;
|
|
|
|
|
this module is just the two helpers it builds on:
|
|
|
|
|
* ``_turn_argv`` — the no-shell list argv for one ``claude -p`` turn.
|
|
|
|
|
* ``translate_event`` — map a raw stream-json event to the small UI vocabulary
|
|
|
|
|
(session / text / tool / result), dropping the hook/thinking-token noise.
|
breakglass: in-cluster emergency-recovery UI for the devvm
Viktor wanted a web UI on the claude service to act as his breakglass when
the devvm is down: open it, have Claude SSH in to diagnose/repair, and
power-cycle the VM via the Proxmox host if needed. This is the app half
(the infra stack + host bootstrap live in the infra repo).
New, ISOLATED ASGI app under app/breakglass/ (never imports app.main, so the
untrusted-input agents — recruiter-triage, nextcloud-todos — can't share a
process with the root-on-devvm / PVE-reset SSH key):
- pve.py: the LLM-independent power-verb path (status|forensics|reset|stop|
start|cycle on VM 102), whitelist-validated client-side, executed over the
forced-command SSH key (list argv, no shell).
- agent_session.py: multi-turn streamed chat — claude -p --session-id /
--resume with --output-format stream-json, translated to a small SSE
vocabulary (session/text/tool/result/error/done).
- auth.py: edge Authentik header OR bearer; fail-closed.
- server.py: FastAPI (session/chat-SSE/pve-verb routes) + serves the Svelte UI.
- Svelte SPA (frontend/, built into app/breakglass/static/ and committed — no
in-cluster build, per ADR-0002): streamed chat + danger-styled manual VM
controls with confirm-on-mutate.
- agents/breakglass.md: narrow tools (Bash/Read/Grep/Glob, no web), taught the
ssh devvm / ssh pve aliases and cycle-vs-reset.
- docker-entrypoint-breakglass.sh: ssh-agent bootstrap from the mounted key +
ssh aliases, then uvicorn app.breakglass.server. The breakglass Deployment
overrides the image CMD with this; the existing service is untouched.
26 new tests (verb whitelist incl. injection attempts, stream-json→SSE
translation, auth gating, route behaviour); full suite 58 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:36:05 +00:00
|
|
|
"""
|
|
|
|
|
from . import config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _turn_argv(session_id: str, prompt: str, resume: bool, model: str) -> list[str]:
|
|
|
|
|
argv = [
|
|
|
|
|
"claude", "-p",
|
|
|
|
|
"--agent", config.BREAKGLASS_AGENT,
|
|
|
|
|
"--dangerously-skip-permissions",
|
|
|
|
|
"--output-format", "stream-json",
|
|
|
|
|
"--verbose", # required for stream-json output
|
|
|
|
|
"--model", model,
|
|
|
|
|
]
|
|
|
|
|
# --session-id opens a brand-new session with that id; --resume continues it.
|
|
|
|
|
argv += (["--resume", session_id] if resume else ["--session-id", session_id])
|
|
|
|
|
argv.append(prompt)
|
|
|
|
|
return argv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def translate_event(obj: dict) -> dict | None:
|
|
|
|
|
"""Map one raw stream-json event to a UI event, or None to drop it.
|
|
|
|
|
|
|
|
|
|
Pure function — the unit tests pin this contract. Keeps the noisy
|
|
|
|
|
hook/thinking-token/system chatter off the wire and exposes only what an
|
|
|
|
|
operator watching a recovery needs: which session, assistant prose, which
|
|
|
|
|
tools ran, and the final result.
|
|
|
|
|
"""
|
|
|
|
|
etype = obj.get("type")
|
|
|
|
|
|
|
|
|
|
if etype == "system":
|
|
|
|
|
if obj.get("subtype") == "init":
|
|
|
|
|
return {"kind": "session", "session_id": obj.get("session_id", "")}
|
|
|
|
|
return None # hook_started/hook_response/thinking_tokens/etc. — noise
|
|
|
|
|
|
|
|
|
|
if etype == "assistant":
|
|
|
|
|
events: list[dict] = []
|
|
|
|
|
for block in obj.get("message", {}).get("content", []) or []:
|
|
|
|
|
btype = block.get("type")
|
|
|
|
|
if btype == "text" and block.get("text"):
|
|
|
|
|
events.append({"kind": "text", "text": block["text"]})
|
|
|
|
|
elif btype == "tool_use":
|
|
|
|
|
events.append({
|
|
|
|
|
"kind": "tool",
|
|
|
|
|
"name": block.get("name", ""),
|
|
|
|
|
"input": block.get("input", {}),
|
|
|
|
|
})
|
|
|
|
|
if not events:
|
|
|
|
|
return None
|
breakglass UI v2: attachable sessions (tmux model) + mobile-first redesign
Full audit-driven rework. Keeps the proven SSE-translation + verb logic; everything else upgraded for phone-primary use.
Backend — server owns the session, clients attach (Viktor's tmux idea):
- session.py: SessionManager + Session with an event log, subscriber pub/sub, and turns that run DETACHED (keep going if the client disconnects).
- GET /api/session/{id}/stream = attach (SSE): replays the transcript then tails live; per-event id: lines so an EventSource auto-reconnect resumes from Last-Event-ID (free re-attach). POST /{id}/prompt starts a detached turn; POST /{id}/cancel = Stop. Replaces the old one-shot /api/chat.
- agent_session trimmed to the argv + translate_event helpers; 21 new/updated tests (replay, Last-Event-ID resume, broadcast, detached turn, resume, cancel, routes) — 53 green.
Frontend — mobile-first via the frontend-design skill (emergency-console aesthetic):
- EventSource attach (native auto-reconnect, zero client reconnect logic); transcript.js folds events->messages with id-dedupe so replays never double-render (30 unit assertions).
- Installable PWA: manifest + icons (wrench/break-glass mark) + apple-mobile-web-app meta + theme-color; viewport-fit=cover + safe-area; 100dvh; 16px composer (no iOS zoom).
- One-tap diagnosis presets (Triage / Memory-OOM / Disk / Services / QEMU-wedged) mapped to the devvm's real failure modes; Stop button while a turn runs.
- Foldable VM-control sheet, cycle the dominant recovery action w/ confirm, output capped 46vh.
- a11y: fixed --ink-faint contrast 3.6:1 -> 6.1:1 (WCAG AA); >=44px tap targets. Deleted the obsolete fetch-reader sse.js (EventSource replaces it).
Verified: 53 backend tests + 30 transcript assertions; Playwright @390x844 (input on-screen y=721-821, presets/sheet/fold/cap); local integration smoke vs the real backend (attach->caught-up, 404, verbs, PWA served).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 19:19:03 +00:00
|
|
|
# The session log flattens a "batch" into individual events.
|
breakglass: in-cluster emergency-recovery UI for the devvm
Viktor wanted a web UI on the claude service to act as his breakglass when
the devvm is down: open it, have Claude SSH in to diagnose/repair, and
power-cycle the VM via the Proxmox host if needed. This is the app half
(the infra stack + host bootstrap live in the infra repo).
New, ISOLATED ASGI app under app/breakglass/ (never imports app.main, so the
untrusted-input agents — recruiter-triage, nextcloud-todos — can't share a
process with the root-on-devvm / PVE-reset SSH key):
- pve.py: the LLM-independent power-verb path (status|forensics|reset|stop|
start|cycle on VM 102), whitelist-validated client-side, executed over the
forced-command SSH key (list argv, no shell).
- agent_session.py: multi-turn streamed chat — claude -p --session-id /
--resume with --output-format stream-json, translated to a small SSE
vocabulary (session/text/tool/result/error/done).
- auth.py: edge Authentik header OR bearer; fail-closed.
- server.py: FastAPI (session/chat-SSE/pve-verb routes) + serves the Svelte UI.
- Svelte SPA (frontend/, built into app/breakglass/static/ and committed — no
in-cluster build, per ADR-0002): streamed chat + danger-styled manual VM
controls with confirm-on-mutate.
- agents/breakglass.md: narrow tools (Bash/Read/Grep/Glob, no web), taught the
ssh devvm / ssh pve aliases and cycle-vs-reset.
- docker-entrypoint-breakglass.sh: ssh-agent bootstrap from the mounted key +
ssh aliases, then uvicorn app.breakglass.server. The breakglass Deployment
overrides the image CMD with this; the existing service is untouched.
26 new tests (verb whitelist incl. injection attempts, stream-json→SSE
translation, auth gating, route behaviour); full suite 58 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:36:05 +00:00
|
|
|
return events[0] if len(events) == 1 else {"kind": "batch", "events": events}
|
|
|
|
|
|
|
|
|
|
if etype == "result":
|
|
|
|
|
return {
|
|
|
|
|
"kind": "result",
|
|
|
|
|
"is_error": bool(obj.get("is_error")),
|
|
|
|
|
"result": obj.get("result", ""),
|
|
|
|
|
"duration_ms": obj.get("duration_ms"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return None
|