diff --git a/.gitignore b/.gitignore index 682eac7..3f82d5d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ vault # agent worktrees .worktrees/ + +# breakglass frontend build tooling (node deps are never committed; the +# compiled SPA in app/breakglass/static/ IS committed and served by FastAPI) +frontend/node_modules/ +frontend/.vite/ diff --git a/Dockerfile b/Dockerfile index 4b9c574..e39d4d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,6 +85,17 @@ COPY agents/beads-task-runner.md /usr/share/agent-seed/beads-task-runner.md COPY agents/recruiter-triage.md /usr/share/agent-seed/recruiter-triage.md COPY agents/nextcloud-todos-planner.md /usr/share/agent-seed/nextcloud-todos-planner.md COPY agents/nextcloud-todos-exec.md /usr/share/agent-seed/nextcloud-todos-exec.md +# The breakglass deployment (separate stack) seeds this one instead of the +# untrusted-input agents; its init container copies whichever it needs. +COPY agents/breakglass.md /usr/share/agent-seed/breakglass.md + +# Breakglass entrypoint. The breakglass Deployment overrides the default CMD +# with this (ssh-agent bootstrap + ssh aliases, then uvicorn for the breakglass +# app). It ships in every image but only that deployment runs it. The built +# frontend lives under app/breakglass/static/ (committed — no in-cluster build, +# per ADR-0002), so the `COPY app/` above carries it in. +COPY docker-entrypoint-breakglass.sh /srv/docker-entrypoint-breakglass.sh +RUN chmod 0755 /srv/docker-entrypoint-breakglass.sh USER agent WORKDIR /workspace/infra diff --git a/agents/breakglass.md b/agents/breakglass.md new file mode 100644 index 0000000..b50767c --- /dev/null +++ b/agents/breakglass.md @@ -0,0 +1,57 @@ +--- +name: breakglass +description: Emergency-recovery agent for the devvm. SSHes into the devvm (full sudo) to diagnose and repair it, and can power-cycle it via the Proxmox host. Used only by the in-cluster claude-breakglass UI. +model: sonnet +tools: Bash, Read, Grep, Glob +--- + +You are the **breakglass** agent. Viktor opens the claude-breakglass web UI when +his development VM (the "devvm") is misbehaving and he wants it diagnosed and +fixed. You run **inside the Kubernetes cluster**, not on the devvm — so you stay +alive when the devvm is wedged. + +You have NO web tools and you operate on trusted operator input only. Be +concise and act; this is an incident, not a research task. + +## What you can reach (already wired — just use these) + +- **`ssh devvm `** — a shell on the devvm (10.0.10.10) as the `breakglass` + user with **passwordless sudo**. Use `ssh devvm 'sudo …'` for root actions. + This is your primary diagnose-and-repair surface. +- **`ssh pve `** — the Proxmox host (192.168.1.127). This key is locked to + a forced command: the ONLY things it accepts are the bare verbs + **`status`**, **`forensics`**, **`reset`**, **`stop`**, **`start`**, + **`cycle`** — each acting on VM 102 (the devvm). Anything else is rejected. + Every mutating verb captures forensics on the host first, automatically. + +SSH auth is handled by an in-pod ssh-agent; you never need a key path or +password. Hosts are pinned in known_hosts. + +## How to work an incident + +1. **Diagnose first.** `ssh devvm 'uptime; free -h; df -h; sudo dmesg -T | tail -40'`, + check the failing service (`ssh devvm 'systemctl status '`, + `journalctl -u --no-pager -n 50`), check memory/OOM, disk, swap. +2. **Repair in place when you can** — restart a wedged unit, free disk, clear a + stuck process, fix swap. A soft fix beats a reboot. +3. **If the devvm is unreachable over SSH or unrecoverable in place**, fall back + to the PVE verbs: + - `ssh pve status` — is VM 102 running / stopped / paused? + - `ssh pve forensics` — qm status/config/pending + QMP + guest-agent ping. + - **`ssh pve cycle`** — a full stop→start (NOT a warm reset). This spawns a + fresh QEMU process and so applies any staged VM config. **This is the + correct recovery for a QEMU I/O stall** (the kind that froze the devvm on + 2026-06-11); a warm `reset` reuses the wedged QEMU and won't fix it. + - Use `reset` only for a normal-looking guest hang where QEMU itself is fine. +4. You are authorised to run the mutating verbs autonomously when your + diagnosis supports it — Viktor chose autonomous recovery. Still: capture and + report what you saw, then act, then confirm the result (`ssh pve status`, + then re-check SSH to the devvm once it boots). + +## Reference + +The infra repo is checked out in your workspace. Useful reading: +`docs/runbooks/proxmox-host.md`, `docs/runbooks/breakglass-ui.md`, and any +`docs/post-mortems/*devvm*` for prior failure modes and their fixes. + +Report tersely: what you found, what you did, the current state. diff --git a/app/breakglass/__init__.py b/app/breakglass/__init__.py new file mode 100644 index 0000000..47a2bad --- /dev/null +++ b/app/breakglass/__init__.py @@ -0,0 +1,10 @@ +"""Breakglass: an isolated emergency-recovery surface for the devvm. + +This package is a SEPARATE ASGI app from ``app.main``. The breakglass +deployment runs ``uvicorn app.breakglass.server:app`` and mounts the SSH keys; +the ordinary claude-agent-service deployment keeps running ``app.main:app`` and +never sees those keys. Nothing here imports ``app.main`` and vice versa, so the +untrusted-input agents (recruiter-triage, nextcloud-todos) can never share a +process with the root-on-devvm / PVE-reset credentials. See +``docs/adr/0001-breakglass-security-architecture.md``. +""" diff --git a/app/breakglass/agent_session.py b/app/breakglass/agent_session.py new file mode 100644 index 0000000..a360e40 --- /dev/null +++ b/app/breakglass/agent_session.py @@ -0,0 +1,145 @@ +"""Drive the breakglass Claude agent and stream its work to the browser. + +Each chat turn runs ``claude -p --output-format stream-json`` in the session's +persistent workspace; the first turn opens the session with ``--session-id`` and +later turns ``--resume`` it, so the conversation has memory across turns. The +CLI's JSON events are translated to a small, stable SSE vocabulary the UI +renders (``session`` / ``text`` / ``tool`` / ``result`` / ``error``) — we do not +leak the raw event firehose to the client. + +Subprocesses use ``asyncio.create_subprocess_exec`` (list argv, no shell): the +prompt and ids are argv elements, never interpreted by a shell. +""" +import asyncio +import json +import os +from subprocess import PIPE +from typing import AsyncIterator + +from . import config + +# Sessions we've already opened (so the next turn resumes instead of re-creating). +_started: set[str] = set() + + +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 + # The server flattens a "batch" into individual SSE frames. + 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 + + +async def run_turn( + session_id: str, prompt: str, model: str | None = None +) -> AsyncIterator[dict]: + """Run one chat turn, yielding translated UI events as they arrive.""" + resume = session_id in _started + model = model or config.DEFAULT_MODEL + workspace = os.path.join(config.SESSIONS_DIR, session_id) + os.makedirs(workspace, exist_ok=True) + + argv = _turn_argv(session_id, prompt, resume, model) + proc = await asyncio.create_subprocess_exec( + *argv, cwd=workspace, stdout=PIPE, stderr=PIPE, + ) + _started.add(session_id) + assert proc.stdout is not None and proc.stderr is not None + + try: + async def _pump() -> AsyncIterator[dict]: + async for raw in proc.stdout: + line = raw.decode(errors="replace").strip() + if not line: + continue + try: + obj = json.loads(line) + except json.JSONDecodeError: + continue + ev = translate_event(obj) + if ev is None: + continue + if ev.get("kind") == "batch": + for sub in ev["events"]: + yield sub + else: + yield ev + + async for ev in _with_timeout(_pump(), config.TURN_TIMEOUT_SECONDS): + yield ev + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + yield {"kind": "error", "error": f"turn timed out after {config.TURN_TIMEOUT_SECONDS}s"} + return + + await proc.wait() + if proc.returncode not in (0, None): + err = (await proc.stderr.read()).decode(errors="replace") + yield {"kind": "error", "error": err.strip()[:500] or f"exit {proc.returncode}"} + + +async def _with_timeout(agen: AsyncIterator[dict], timeout: float) -> AsyncIterator[dict]: + """Yield from an async generator but raise TimeoutError if the WHOLE turn + exceeds ``timeout`` seconds (a wedged agent shouldn't stream forever).""" + loop = asyncio.get_event_loop() + deadline = loop.time() + timeout + it = agen.__aiter__() + while True: + remaining = deadline - loop.time() + if remaining <= 0: + raise asyncio.TimeoutError + try: + yield await asyncio.wait_for(it.__anext__(), timeout=remaining) + except StopAsyncIteration: + return diff --git a/app/breakglass/auth.py b/app/breakglass/auth.py new file mode 100644 index 0000000..c92b822 --- /dev/null +++ b/app/breakglass/auth.py @@ -0,0 +1,36 @@ +"""Auth for the breakglass app. + +The app sits behind the ingress ``auth = "required"`` resilience proxy +(Authentik SSO normally, HTTP basic-auth fallback when Authentik is down), so a +browser request that reaches us is already edge-authenticated and carries the +proxy-injected ``X-authentik-username`` header. We also accept a bearer token +for machine/CLI callers. Either is sufficient. + +When neither a token is configured nor a trusted header is present, we fail +closed. +""" +import hmac + +from fastapi import Header, HTTPException + +from . import config + + +def require_auth( + authorization: str | None = Header(default=None), + x_authentik_username: str | None = Header(default=None), +) -> str: + """FastAPI dependency. Returns the identity (username or 'bearer'); raises + 401 otherwise.""" + # Edge-authenticated human: the auth-proxy sets this and overwrites any + # client-supplied value, so its presence is trustworthy. + if x_authentik_username: + return x_authentik_username + + # Machine caller with the shared bearer token. + if config.API_TOKEN and authorization and authorization.startswith("Bearer "): + token = authorization.removeprefix("Bearer ") + if hmac.compare_digest(token, config.API_TOKEN): + return "bearer" + + raise HTTPException(status_code=401, detail="unauthenticated") diff --git a/app/breakglass/config.py b/app/breakglass/config.py new file mode 100644 index 0000000..785d17f --- /dev/null +++ b/app/breakglass/config.py @@ -0,0 +1,36 @@ +"""Environment-driven config for the breakglass app. + +Targets are hardcoded IPs by default (the breakglass must not depend on cluster +DNS — it has to work when things are broken). Everything is overridable via env +for tests and future re-IPing. +""" +import os + +# SSH targets. IPs, not names — no DNS dependency in an incident. +DEVVM_HOST = os.environ.get("BREAKGLASS_DEVVM_HOST", "10.0.10.10") +DEVVM_USER = os.environ.get("BREAKGLASS_DEVVM_USER", "breakglass") +PVE_HOST = os.environ.get("BREAKGLASS_PVE_HOST", "192.168.1.127") +PVE_USER = os.environ.get("BREAKGLASS_PVE_USER", "root") + +# The Claude agent the breakglass UI drives. Narrow tool surface, no web tools. +BREAKGLASS_AGENT = os.environ.get("BREAKGLASS_AGENT", "breakglass") +DEFAULT_MODEL = os.environ.get("BREAKGLASS_MODEL", "sonnet") + +# Where claude session state + per-session scratch live. emptyDir in prod. +SESSIONS_DIR = os.environ.get("BREAKGLASS_SESSIONS_DIR", "/workspace/sessions") + +# A single human operator per incident — no need for the job-runner's fan-out. +MAX_CONCURRENT_TURNS = int(os.environ.get("BREAKGLASS_MAX_CONCURRENT_TURNS", "2")) +# A chat turn that runs longer than this is killed (the agent is wedged). +TURN_TIMEOUT_SECONDS = int(os.environ.get("BREAKGLASS_TURN_TIMEOUT_SECONDS", "1800")) +# A single PVE power verb must return fast; a wedged host shouldn't hang the UI. +PVE_VERB_TIMEOUT_SECONDS = int(os.environ.get("BREAKGLASS_PVE_VERB_TIMEOUT_SECONDS", "120")) + +# Auth. The app sits behind the ingress `auth = "required"` resilience proxy +# (Authentik SSO, basic-auth fallback when Authentik is down). We additionally +# accept a bearer token for machine/CLI callers. Either gate is sufficient; +# the edge is the primary one for the browser UI. +API_TOKEN = os.environ.get("API_BEARER_TOKEN", "") +# Header the auth-proxy injects for an authenticated human (set by Authentik, or +# by the basic-auth fallback's `$remote_user`). Presence ⇒ edge-authenticated. +TRUSTED_USER_HEADER = "x-authentik-username" diff --git a/app/breakglass/pve.py b/app/breakglass/pve.py new file mode 100644 index 0000000..a47df93 --- /dev/null +++ b/app/breakglass/pve.py @@ -0,0 +1,89 @@ +"""PVE power verbs — the LLM-independent recovery path. + +The manual UI buttons hit this directly (no ``claude`` in the path), so reset +works even when the Anthropic API is down. The real enforcement is the +forced-command on the PVE host (``/usr/local/bin/breakglass-pve``): whatever we +send as the SSH command is ignored except as ``$SSH_ORIGINAL_COMMAND``, and the +host script only honours the verbs below against VM 102. We validate here too — +defense in depth + a clean error before a round-trip. + +All subprocesses use ``asyncio.create_subprocess_exec`` (list argv, no shell), +so the verb string is never interpreted by a shell — there is no injection +surface even though the allowlist already constrains the input. +""" +import asyncio +from subprocess import PIPE + +from . import config + +# Must mirror /usr/local/bin/breakglass-pve on the PVE host. +ALLOWED_VERBS: frozenset[str] = frozenset( + {"status", "forensics", "reset", "stop", "start", "cycle"} +) +# Verbs that change VM state — the UI flags these for an explicit confirm and +# the host script captures forensics before running them. +MUTATING_VERBS: frozenset[str] = frozenset({"reset", "stop", "start", "cycle"}) + +def _ssh_argv(user: str, host: str, remote_command: str) -> list[str]: + """Build an ssh argv (list form, no shell). ``remote_command`` is passed as + a single token; on the PVE host the forced-command ignores it except as + ``$SSH_ORIGINAL_COMMAND``. + + Host-key checking is disabled deliberately: a devvm REBUILD changes its host + key (e.g. 2026-05-23), and strict checking would lock the breakglass out at + exactly the moment it's needed. The targets are on the trusted internal LAN; + availability beats MITM hardening here. Auth is still by key (ssh-agent).""" + return [ + "ssh", + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=10", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=ERROR", + f"{user}@{host}", + remote_command, + ] + + +def is_allowed(verb: str) -> bool: + return verb in ALLOWED_VERBS + + +async def run_verb(verb: str, timeout: float | None = None) -> dict: + """Run a single PVE verb against VM 102 over the forced-command SSH key. + + Returns ``{"verb", "exit_code", "stdout", "stderr", "rejected"}``. A verb + not in the allowlist is rejected locally (``rejected=True``) without any + SSH at all. + """ + if verb not in ALLOWED_VERBS: + return { + "verb": verb, + "exit_code": None, + "stdout": "", + "stderr": f"rejected: '{verb}' is not an allowed verb", + "rejected": True, + } + + timeout = timeout if timeout is not None else config.PVE_VERB_TIMEOUT_SECONDS + argv = _ssh_argv(config.PVE_USER, config.PVE_HOST, verb) + proc = await asyncio.create_subprocess_exec(*argv, stdout=PIPE, stderr=PIPE) + try: + out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + return { + "verb": verb, + "exit_code": None, + "stdout": "", + "stderr": f"timeout after {timeout}s talking to PVE host", + "rejected": False, + } + return { + "verb": verb, + "exit_code": proc.returncode, + "stdout": out.decode(errors="replace"), + "stderr": err.decode(errors="replace"), + "rejected": False, + } diff --git a/app/breakglass/server.py b/app/breakglass/server.py new file mode 100644 index 0000000..9a7201f --- /dev/null +++ b/app/breakglass/server.py @@ -0,0 +1,96 @@ +"""Breakglass FastAPI app — the in-cluster emergency recovery UI. + +Routes: + GET /health — liveness (no auth) + GET / — the single-page UI (static) + POST /api/session — open a chat session, returns {session_id} + POST /api/chat — run one turn, streams SSE events (text/tool/result) + POST /api/pve/{verb} — LLM-independent PVE power verb (manual buttons) + GET /api/pve/verbs — list allowed verbs + which mutate + +Everything under /api requires auth (edge Authentik header or bearer token). +""" +import json +import os +import uuid + +from fastapi import Depends, FastAPI, HTTPException +from fastapi.responses import FileResponse, JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field + +from . import agent_session, config, pve +from .auth import require_auth + +app = FastAPI(title="Claude Breakglass") + +_STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") + + +class SessionResponse(BaseModel): + session_id: str + + +class ChatRequest(BaseModel): + session_id: str + prompt: str = Field(..., min_length=1) + model: str | None = None + + +@app.get("/health") +async def health(): + return {"status": "ok", "service": "claude-breakglass"} + + +@app.post("/api/session", response_model=SessionResponse) +async def open_session(_identity: str = Depends(require_auth)): + # Claude wants a UUID for --session-id. + return SessionResponse(session_id=str(uuid.uuid4())) + + +@app.post("/api/chat") +async def chat(req: ChatRequest, _identity: str = Depends(require_auth)): + """Stream one chat turn as Server-Sent Events. The browser reads the + response body incrementally (fetch + ReadableStream).""" + + async def _sse(): + try: + async for ev in agent_session.run_turn(req.session_id, req.prompt, req.model): + yield f"data: {json.dumps(ev)}\n\n" + except Exception as exc: # noqa: BLE001 — surface any failure to the UI + yield f"data: {json.dumps({'kind': 'error', 'error': str(exc)[:500]})}\n\n" + yield f"data: {json.dumps({'kind': 'done'})}\n\n" + + return StreamingResponse( + _sse(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +@app.get("/api/pve/verbs") +async def pve_verbs(_identity: str = Depends(require_auth)): + return { + "verbs": sorted(pve.ALLOWED_VERBS), + "mutating": sorted(pve.MUTATING_VERBS), + } + + +@app.post("/api/pve/{verb}") +async def pve_verb(verb: str, _identity: str = Depends(require_auth)): + """Run a PVE power verb directly (no LLM in the path). Mutating verbs + capture forensics first on the host, unconditionally.""" + if not pve.is_allowed(verb): + raise HTTPException(status_code=400, detail=f"unknown verb '{verb}'") + result = await pve.run_verb(verb) + status = 200 if result.get("exit_code") == 0 else 502 + return JSONResponse(status_code=status, content=result) + + +# Serve the SPA. Mounted last so it doesn't shadow /api or /health. +if os.path.isdir(_STATIC_DIR): + @app.get("/") + async def index(): + return FileResponse(os.path.join(_STATIC_DIR, "index.html")) + + app.mount("/", StaticFiles(directory=_STATIC_DIR, html=True), name="static") diff --git a/app/breakglass/static/assets/index-DKeuidum.css b/app/breakglass/static/assets/index-DKeuidum.css new file mode 100644 index 0000000..4f1675a --- /dev/null +++ b/app/breakglass/static/assets/index-DKeuidum.css @@ -0,0 +1 @@ +:root{--bg-0:#07090c;--bg-1:#0c1015;--bg-2:#11171e;--bg-3:#161d26;--bg-term:#06080a;--line:#1d2630;--line-strong:#2a3744;--ink:#e6edf3;--ink-dim:#9bb0c0;--ink-faint:#5d7185;--cyan:#3dd1d6;--cyan-dim:#1f6f72;--amber:#f5b657;--green:#5ddb8e;--green-dim:#1f5f3d;--danger:#ff4d4d;--danger-bright:#ff6363;--danger-deep:#7a1717;--danger-glow:#ff4d4d59;--radius:10px;--radius-sm:7px;--mono:ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono", monospace;--sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--shadow-panel:0 1px 0 #ffffff05 inset, 0 16px 40px -24px #000000e6;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body{height:100%;margin:0}body{background-color:var(--bg-0);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background-image:radial-gradient(120% 80% at 85% -10%,#3dd1d612,#0000 55%),radial-gradient(90% 70% at 10% 110%,#f5b6570a,#0000 50%),repeating-linear-gradient(0deg,#ffffff03 0 1px,#0000 1px 3px);background-attachment:fixed}#app{height:100%}button{font-family:var(--mono);cursor:pointer}button:disabled{cursor:not-allowed}::selection{background:#3dd1d647}*{scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line-strong);background-clip:content-box;border:2px solid #0000;border-radius:99px}::-webkit-scrollbar-thumb:hover{background:#3a4a5a padding-box content-box}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.chip.svelte-2zgsrv{background:var(--bg-3);border:1px solid var(--line-strong);border-left:2px solid var(--cyan-dim);max-width:100%;font-family:var(--mono);vertical-align:baseline;border-radius:6px;align-items:baseline;gap:6px;margin:3px 4px 3px 0;padding:3px 9px;font-size:12px;line-height:1.45;display:inline-flex}.cog.svelte-2zgsrv{color:var(--cyan);font-size:11px;transform:translateY(1px)}.name.svelte-2zgsrv{color:var(--ink);font-weight:600}.sep.svelte-2zgsrv{color:var(--ink-faint)}.cmd.svelte-2zgsrv{color:var(--amber);font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.chat.svelte-1bi93vx{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow:hidden}.chat-head.svelte-1bi93vx{border-bottom:1px solid var(--line);background:linear-gradient(#ffffff04,#0000);align-items:baseline;gap:12px;padding:13px 18px;display:flex}.chat-head-label.svelte-1bi93vx{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--cyan);font-size:11px}.chat-head-hint.svelte-1bi93vx{color:var(--ink-faint);font-size:12px}.stream.svelte-1bi93vx{scroll-behavior:smooth;flex-direction:column;flex:1;gap:14px;min-height:0;padding:20px 18px 8px;display:flex;overflow-y:auto}.empty.svelte-1bi93vx{text-align:center;max-width:460px;color:var(--ink-dim);margin:auto;padding:28px 12px}.empty-mark.svelte-1bi93vx{color:var(--cyan-dim);text-shadow:0 0 24px #3dd1d640;margin-bottom:14px;font-size:40px;line-height:1}.empty-title.svelte-1bi93vx{font-family:var(--mono);color:var(--ink);margin:0 0 8px;font-size:15px}.empty-sub.svelte-1bi93vx{color:var(--ink-faint);margin:0;font-size:13px;line-height:1.6}.empty-sub.svelte-1bi93vx strong:where(.svelte-1bi93vx){color:var(--ink-dim);font-weight:600}.row.svelte-1bi93vx{display:flex}.row--user.svelte-1bi93vx{justify-content:flex-end}.row--assistant.svelte-1bi93vx{justify-content:flex-start}.bubble.svelte-1bi93vx{word-wrap:break-word;overflow-wrap:anywhere;border-radius:13px;max-width:86%;padding:11px 14px;font-size:14px;line-height:1.6}.bubble--user.svelte-1bi93vx{border:1px solid var(--cyan-dim);color:#d8f6f7;white-space:pre-wrap;font-family:var(--sans);background:linear-gradient(#15333a,#0f262c);border-bottom-right-radius:4px}.bubble--assistant.svelte-1bi93vx{background:var(--bg-2);border:1px solid var(--line-strong);color:var(--ink);border-bottom-left-radius:4px}.prose.svelte-1bi93vx{white-space:pre-wrap}.thinking.svelte-1bi93vx,.working-dots.svelte-1bi93vx{align-items:center;gap:4px;display:inline-flex}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx){background:var(--amber);opacity:.4;border-radius:50%;width:6px;height:6px;animation:1.2s ease-in-out infinite svelte-1bi93vx-blink}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2){animation-delay:.18s}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3){animation-delay:.36s}@keyframes svelte-1bi93vx-blink{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}.turn-note.svelte-1bi93vx{border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;flex-wrap:wrap;align-items:baseline;gap:8px;margin-top:10px;padding:7px 10px;font-size:12px;line-height:1.5;display:flex}.turn-note--ok.svelte-1bi93vx{border:1px solid var(--green-dim);color:#bff5d3;background:#5ddb8e12}.turn-note--error.svelte-1bi93vx{border:1px solid var(--danger-deep);color:#ffd5d5;background:#ff4d4d14}.turn-note-tag.svelte-1bi93vx{text-transform:uppercase;letter-spacing:.14em;opacity:.85;border:1px solid;border-radius:4px;padding:1px 6px;font-size:10px}.turn-note-body.svelte-1bi93vx{flex:1;min-width:0}.turn-note-time.svelte-1bi93vx{color:var(--ink-faint);margin-left:auto}.composer.svelte-1bi93vx{border-top:1px solid var(--line);background:linear-gradient(#0000,#ffffff03);padding:12px}.working-bar.svelte-1bi93vx{font-family:var(--mono);color:var(--amber);letter-spacing:.02em;align-items:center;gap:10px;padding:0 4px 9px;font-size:12px;display:flex}.composer-row.svelte-1bi93vx{align-items:flex-end;gap:10px;display:flex}textarea.svelte-1bi93vx{resize:none;background:var(--bg-2);min-height:44px;max-height:168px;color:var(--ink);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font-family:var(--sans);field-sizing:content;outline:none;flex:1;padding:11px 13px;font-size:14px;line-height:1.5;transition:border-color .15s,box-shadow .15s}textarea.svelte-1bi93vx::placeholder{color:var(--ink-faint)}textarea.svelte-1bi93vx:focus{border-color:var(--cyan-dim);box-shadow:0 0 0 3px #3dd1d61f}textarea.svelte-1bi93vx:disabled{opacity:.55}.send.svelte-1bi93vx{border-radius:var(--radius-sm);border:1px solid var(--cyan-dim);color:#d8f6f7;letter-spacing:.04em;background:linear-gradient(#19474b,#103539);flex:none;align-self:stretch;min-width:78px;padding:0 18px;font-size:13px;font-weight:600;transition:filter .15s,border-color .15s,opacity .15s}.send.svelte-1bi93vx:hover:not(:disabled){filter:brightness(1.22);border-color:var(--cyan)}.send.svelte-1bi93vx:disabled{opacity:.4;background:var(--bg-2);border-color:var(--line-strong);color:var(--ink-faint)}.panel.svelte-1qihpg4{background:var(--bg-1);border:1px solid var(--line);border-top:2px solid var(--danger-deep);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow-y:auto}.panel-head.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px 12px}.panel-head-row.svelte-1qihpg4{align-items:center;gap:9px;display:flex}.hazard.svelte-1qihpg4{color:var(--danger);filter:drop-shadow(0 0 8px var(--danger-glow));font-size:15px}h2.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.12em;color:var(--ink);margin:0;font-size:13px}.panel-sub.svelte-1qihpg4{color:var(--ink-faint);margin:9px 0 0;font-size:11.5px;line-height:1.55}.loading.svelte-1qihpg4{font-family:var(--mono);color:var(--ink-faint);padding:22px 16px;font-size:12px}.group.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px}.group-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.18em;color:var(--ink-faint);align-items:center;gap:8px;margin-bottom:11px;font-size:10.5px;display:flex}.group-label--danger.svelte-1qihpg4{color:var(--danger-bright)}.group-tag.svelte-1qihpg4{letter-spacing:.1em;border:1px solid var(--line-strong);color:var(--ink-faint);border-radius:4px;padding:2px 6px;font-size:9.5px}.group-tag--danger.svelte-1qihpg4{border-color:var(--danger-deep);color:var(--danger-bright);background:#ff4d4d0f}.btn-row.svelte-1qihpg4{flex-wrap:wrap;gap:9px;display:flex}.vbtn.svelte-1qihpg4{border-radius:var(--radius-sm);letter-spacing:.05em;text-transform:lowercase;justify-content:center;align-items:center;gap:8px;padding:9px 15px;font-size:13px;font-weight:600;transition:filter .14s,border-color .14s,background .14s,transform 60ms;display:inline-flex}.vbtn.svelte-1qihpg4:active:not(:disabled){transform:translateY(1px)}.vbtn.svelte-1qihpg4:disabled{opacity:.4}.vbtn-label.svelte-1qihpg4{line-height:1}.vbtn--safe.svelte-1qihpg4{background:var(--bg-2);color:var(--ink);border:1px solid var(--line-strong)}.vbtn--safe.svelte-1qihpg4:hover:not(:disabled){border-color:var(--cyan-dim);background:var(--bg-3)}.danger-list.svelte-1qihpg4{flex-direction:column;gap:12px;display:flex}.danger-item.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid #0000}.danger-item--headline.svelte-1qihpg4{border-color:var(--danger-deep);background:#ff4d4d0b;padding:11px}.vbtn--danger.svelte-1qihpg4{width:100%;color:var(--danger-bright);border:1px solid var(--danger-deep);border-left:3px solid var(--danger);text-shadow:0 0 12px var(--danger-glow);background:linear-gradient(#ff4d4d29,#ff4d4d12)}.vbtn--danger.svelte-1qihpg4:hover:not(:disabled){background:linear-gradient(180deg, var(--danger), var(--danger-bright));color:#1a0606;border-color:var(--danger-bright);text-shadow:none;filter:drop-shadow(0 4px 14px var(--danger-glow))}.vbtn--headline.svelte-1qihpg4{padding:12px 15px;font-size:14px}.headline-badge.svelte-1qihpg4{text-transform:uppercase;letter-spacing:.14em;background:var(--danger);color:#1a0606;border-radius:999px;padding:2px 7px;font-size:9px;font-weight:700}.danger-blurb.svelte-1qihpg4{color:var(--ink-faint);margin:7px 2px 0;font-size:11.5px;line-height:1.5}.danger-item--headline.svelte-1qihpg4 .danger-blurb:where(.svelte-1qihpg4){color:#f0b0b0}.confirm.svelte-1qihpg4{border:1px solid var(--danger);border-radius:var(--radius-sm);background:#ff4d4d1a;margin-top:10px;padding:11px 12px;animation:.16s ease-out svelte-1qihpg4-confirm-in}@keyframes svelte-1qihpg4-confirm-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.confirm-text.svelte-1qihpg4{color:#ffe0e0;margin-bottom:10px;font-size:12.5px;line-height:1.5;display:block}.confirm-text.svelte-1qihpg4 strong:where(.svelte-1qihpg4){color:#fff;font-family:var(--mono);text-transform:uppercase;letter-spacing:.04em}.confirm-actions.svelte-1qihpg4{gap:9px;display:flex}.confirm-yes.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--danger-bright);background:var(--danger);color:#1a0606;letter-spacing:.06em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;font-weight:700;transition:filter .14s}.confirm-yes.svelte-1qihpg4:hover:not(:disabled){filter:brightness(1.12)}.confirm-no.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);color:var(--ink-dim);letter-spacing:.04em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;transition:border-color .14s,color .14s}.confirm-no.svelte-1qihpg4:hover:not(:disabled){border-color:var(--ink-faint);color:var(--ink)}.confirm-yes.svelte-1qihpg4:disabled,.confirm-no.svelte-1qihpg4:disabled{opacity:.5}.spin.svelte-1qihpg4{border:2px solid #e6edf340;border-top-color:var(--cyan);border-radius:50%;flex:none;width:13px;height:13px;animation:.7s linear infinite svelte-1qihpg4-spin}.spin--danger.svelte-1qihpg4{border-color:#ff4d4d4d;border-top-color:var(--danger-bright)}@keyframes svelte-1qihpg4-spin{to{transform:rotate(360deg)}}.out.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-term);margin:14px 16px 16px;overflow:hidden}.out--ok.svelte-1qihpg4{border-color:var(--green-dim)}.out--fail.svelte-1qihpg4{border-color:var(--danger-deep)}.out-head.svelte-1qihpg4{border-bottom:1px solid var(--line);background:#ffffff05;justify-content:space-between;align-items:center;padding:8px 11px;display:flex}.out-verb.svelte-1qihpg4{font-family:var(--mono);color:var(--ink);letter-spacing:.04em;font-size:12px}.out-verb.svelte-1qihpg4:before{content:"$ pve ";color:var(--ink-faint)}.out-status.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;border:1px solid;border-radius:4px;padding:2px 7px;font-size:10.5px}.out-status--ok.svelte-1qihpg4{color:var(--green)}.out-status--fail.svelte-1qihpg4{color:var(--danger-bright)}.out-pre.svelte-1qihpg4{font-family:var(--mono);color:#c7d6e2;white-space:pre-wrap;overflow-wrap:anywhere;max-height:320px;margin:0;padding:11px 12px;font-size:12px;line-height:1.55;overflow-y:auto}.out-stderr-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.16em;color:var(--danger-bright);padding:6px 12px 0;font-size:10px}.out-pre--stderr.svelte-1qihpg4{color:#f3b6b6}.out-pre--empty.svelte-1qihpg4{color:var(--ink-faint);font-style:italic}.block-error.svelte-1qihpg4{border:1px solid var(--danger-deep);border-left:3px solid var(--danger);border-radius:var(--radius-sm);color:#ffd5d5;background:#ff4d4d12;margin:14px 16px;padding:11px 13px;font-size:12.5px;line-height:1.5}.retry.svelte-1qihpg4{border:1px solid var(--danger-deep);color:var(--danger-bright);background:0 0;border-radius:5px;margin-left:8px;padding:3px 9px;font-size:11px}.retry.svelte-1qihpg4:hover{background:#ff4d4d1f}.shell.svelte-1n46o8q{flex-direction:column;max-width:1500px;height:100%;margin:0 auto;padding:0 18px 18px;display:flex}.rail.svelte-1n46o8q{border-bottom:1px solid var(--line);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:16px;padding:16px 4px 14px;display:flex}.rail-title.svelte-1n46o8q{align-items:baseline;gap:12px;display:flex}.glyph.svelte-1n46o8q{filter:saturate(.85);font-size:19px;transform:translateY(2px)}h1.svelte-1n46o8q{font-family:var(--mono);letter-spacing:.02em;color:var(--ink);margin:0;font-size:19px;font-weight:600}.accent.svelte-1n46o8q{color:var(--cyan);text-shadow:0 0 18px #3dd1d659}.rail-tag.svelte-1n46o8q{font-family:var(--mono);text-transform:uppercase;letter-spacing:.22em;color:var(--ink-faint);border:1px solid var(--line-strong);border-radius:999px;padding:3px 9px;font-size:10.5px}.rail-status.svelte-1n46o8q{font-family:var(--mono);align-items:center;gap:14px;font-size:13px;display:flex}.rail-session.svelte-1n46o8q{white-space:nowrap;align-items:baseline;gap:7px;display:inline-flex}.session-label.svelte-1n46o8q{color:var(--ink-faint);text-transform:uppercase;letter-spacing:.16em;font-size:11px}.session-id.svelte-1n46o8q{color:var(--cyan);font-family:var(--mono);letter-spacing:.04em}.session-meta.svelte-1n46o8q{color:var(--amber);font-size:12px}.session-bad.svelte-1n46o8q{color:var(--danger-bright)}.dot.svelte-1n46o8q{background:var(--ink-faint);border-radius:50%;flex:none;width:9px;height:9px;box-shadow:0 0 #0000}.dot--ready.svelte-1n46o8q{background:var(--cyan);animation:3.4s ease-in-out infinite svelte-1n46o8q-breathe;box-shadow:0 0 10px 1px #3dd1d699}.dot--busy.svelte-1n46o8q{background:var(--amber);animation:1s ease-in-out infinite svelte-1n46o8q-pulse;box-shadow:0 0 10px 1px #f5b657b3}.dot--error.svelte-1n46o8q{background:var(--danger);box-shadow:0 0 10px 1px var(--danger-glow)}@keyframes svelte-1n46o8q-breathe{0%,to{opacity:.55}50%{opacity:1}}@keyframes svelte-1n46o8q-pulse{0%,to{opacity:.7;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}.new-session.svelte-1n46o8q{background:var(--bg-2);color:var(--ink-dim);border:1px solid var(--line-strong);border-radius:var(--radius-sm);letter-spacing:.02em;padding:7px 13px;font-size:12px;transition:border-color .15s,color .15s,background .15s}.new-session.svelte-1n46o8q:hover:not(:disabled){border-color:var(--cyan-dim);color:var(--ink);background:var(--bg-3)}.new-session.svelte-1n46o8q:disabled{opacity:.45}.rail-error.svelte-1n46o8q{border:1px solid var(--danger-deep);color:#ffd5d5;border-radius:var(--radius-sm);background:#ff4d4d12;border-left-width:3px;margin:12px 0 0;padding:11px 14px;font-size:13px;line-height:1.5}.grid.svelte-1n46o8q{flex:1;grid-template-columns:minmax(0,1fr) 376px;gap:18px;min-height:0;padding-top:16px;display:grid}.col.svelte-1n46o8q{flex-direction:column;min-width:0;min-height:0;display:flex}@media (width<=940px){.grid.svelte-1n46o8q{grid-template-columns:1fr;grid-auto-rows:minmax(0,auto);overflow:auto}.col--chat.svelte-1n46o8q{min-height:60vh}} diff --git a/app/breakglass/static/assets/index-DNECe1Jo.js b/app/breakglass/static/assets/index-DNECe1Jo.js new file mode 100644 index 0000000..8a15dd6 --- /dev/null +++ b/app/breakglass/static/assets/index-DNECe1Jo.js @@ -0,0 +1,17 @@ +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})(),typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);var e={},t=Symbol(`uninitialized`),n=`http://www.w3.org/1999/xhtml`,r=Array.isArray,i=Array.prototype.indexOf,a=Array.prototype.includes,o=Array.from,s=Object.defineProperty,c=Object.getOwnPropertyDescriptor,l=Object.getOwnPropertyDescriptors,u=Object.prototype,d=Array.prototype,f=Object.getPrototypeOf,p=Object.isExtensible,m=()=>{};function h(e){for(var t=0;t{e=n,t=r}),resolve:e,reject:t}}var _=1024,v=2048,y=4096,b=8192,x=16384,S=32768,C=1<<25,w=65536,T=1<<19,ee=1<<20,te=1<<25,ne=65536,re=1<<21,ie=1<<22,ae=1<<23,oe=Symbol(`$state`),se=Symbol(`legacy props`),ce=Symbol(``),le=Symbol(`attributes`),ue=Symbol(`class`),de=Symbol(`style`),fe=Symbol(`text`),pe=Symbol(`form reset`),me=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},he=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function ge(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function _e(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function ve(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function ye(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function be(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function xe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function Se(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ce(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function we(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Te(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Ee(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function De(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}function Oe(){console.warn(`https://svelte.dev/e/derived_inert`)}function ke(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ae(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var E=!1;function je(e){E=e}var D;function O(t){if(t===null)throw ke(),e;return D=t}function Me(){return O(an(D))}function k(t){if(E){if(an(D)!==null)throw ke(),e;D=t}}function Ne(e=1){if(E){for(var t=e,n=D;t--;)n=an(n);D=n}}function Pe(e=!0){for(var t=0,n=D;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else (r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=an(n);e&&n.remove(),n=i}}function Fe(t){if(!t||t.nodeType!==8)throw ke(),e;return t.data}function Ie(e){return e===this.v}function Le(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function Re(e){return!Le(e,this.v)}var ze=!1,Be=!1,A=null;function Ve(e){A=e}function He(e,t=!1,n){A={p:A,i:!1,c:null,e:null,s:e,x:null,r:G,l:Be&&!t?{s:null,u:null,$:[]}:null}}function Ue(e){var t=A,n=t.e;if(n!==null){t.e=null;for(var r of n)Sn(r)}return e!==void 0&&(t.x=e),t.i=!0,A=t.p,e??{}}function We(){return!Be||A!==null&&A.l===null}var Ge=[];function Ke(){var e=Ge;Ge=[],h(e)}function qe(e){if(Ge.length===0&&!Et){var t=Ge;queueMicrotask(()=>{t===Ge&&Ke()})}Ge.push(e)}function Je(){for(;Ge.length>0;)Ke()}function Ye(e){var t=G;if(t===null)return H.f|=ae,e;if(!(t.f&32768)&&!(t.f&4))throw e;Xe(e,t)}function Xe(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}var Ze=~(v|y|_);function j(e,t){e.f=e.f&Ze|t}function Qe(e){e.f&512||e.deps===null?j(e,_):j(e,y)}function $e(e){if(e!==null)for(let t of e)!(t.f&2)||!(t.f&65536)||(t.f^=ne,$e(t.deps))}function et(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),$e(e.deps),j(e,_)}var tt=!1,nt=!1;function rt(e){var t=nt;try{return nt=!1,[e(),nt]}finally{nt=t}}function it(e){let t=0,n=Gt(0),r;return()=>{yn()&&(Y(n),En(()=>(t===0&&(r=or(()=>e(()=>Yt(n)))),t+=1,()=>{qe(()=>{--t,t===0&&(r?.(),r=void 0,Yt(n))})})))}}var at=w|T;function ot(e,t,n,r){new st(e,t,n,r)}var st=class{parent;is_pending=!1;transform_error;#e;#t=E?D:null;#n;#r;#i;#a=null;#o=null;#s=null;#c=null;#l=0;#u=0;#d=!1;#f=new Set;#p=new Set;#m=null;#h=it(()=>(this.#m=Gt(this.#l),()=>{this.#m=null}));constructor(e,t,n,r){this.#e=e,this.#n=t,this.#r=e=>{var t=G;t.b=this,t.f|=128,n(e)},this.parent=G.b,this.transform_error=r??this.parent?.transform_error??(e=>e),this.#i=Dn(()=>{if(E){let e=this.#t;Me();let t=e.data===`[!`;if(e.data.startsWith(`[?`)){let t=JSON.parse(e.data.slice(2));this.#_(t)}else t?this.#v():this.#g()}else this.#y()},at),E&&(this.#e=D)}#g(){try{this.#a=B(()=>this.#r(this.#e))}catch(e){this.error(e)}}#_(e){let t=this.#n.failed;t&&(this.#s=B(()=>{t(this.#e,()=>e,()=>()=>{})}))}#v(){let e=this.#n.pending;e&&(this.is_pending=!0,this.#o=B(()=>e(this.#e)),qe(()=>{var e=this.#c=document.createDocumentFragment(),t=I();e.append(t),this.#a=this.#x(()=>B(()=>this.#r(t))),this.#u===0&&(this.#e.before(e),this.#c=null,Nn(this.#o,()=>{this.#o=null}),this.#b(M))}))}#y(){try{if(this.is_pending=this.has_pending_snippet(),this.#u=0,this.#l=0,this.#a=B(()=>{this.#r(this.#e)}),this.#u>0){var e=this.#c=document.createDocumentFragment();Ln(this.#a,e);let t=this.#n.pending;this.#o=B(()=>t(this.#e))}else this.#b(M)}catch(e){this.error(e)}}#b(e){this.is_pending=!1,e.transfer_effects(this.#f,this.#p)}defer_effect(e){et(e,this.#f,this.#p)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#n.pending}#x(e){var t=G,n=H,r=A;Hn(this.#i),W(this.#i),Ve(this.#i.ctx);try{return Mt.ensure(),e()}catch(e){return Ye(e),null}finally{Hn(t),W(n),Ve(r)}}#S(e,t){if(!this.has_pending_snippet()){this.parent&&this.parent.#S(e,t);return}this.#u+=e,this.#u===0&&(this.#b(t),this.#o&&Nn(this.#o,()=>{this.#o=null}),this.#c&&=(this.#e.before(this.#c),null))}update_pending_count(e,t){this.#S(e,t),this.#l+=e,!(!this.#m||this.#d)&&(this.#d=!0,qe(()=>{this.#d=!1,this.#m&&qt(this.#m,this.#l)}))}get_effect_pending(){return this.#h(),Y(this.#m)}error(e){if(!this.#n.onerror&&!this.#n.failed)throw e;M?.is_fork?(this.#a&&M.skip_effect(this.#a),this.#o&&M.skip_effect(this.#o),this.#s&&M.skip_effect(this.#s),M.oncommit(()=>{this.#C(e)})):this.#C(e)}#C(e){this.#a&&=(V(this.#a),null),this.#o&&=(V(this.#o),null),this.#s&&=(V(this.#s),null),E&&(O(this.#t),Ne(),O(Pe()));var t=this.#n.onerror;let n=this.#n.failed;var r=!1,i=!1;let a=()=>{if(r){Ae();return}r=!0,i&&De(),this.#s!==null&&Nn(this.#s,()=>{this.#s=null}),this.#x(()=>{this.#y()})},o=e=>{try{i=!0,t?.(e,a),i=!1}catch(e){Xe(e,this.#i&&this.#i.parent)}n&&(this.#s=this.#x(()=>{try{return B(()=>{var t=G;t.b=this,t.f|=128,n(this.#e,()=>e,()=>a)})}catch(e){return Xe(e,this.#i.parent),null}}))};qe(()=>{var t;try{t=this.transform_error(e)}catch(e){Xe(e,this.#i&&this.#i.parent);return}typeof t==`object`&&t&&typeof t.then==`function`?t.then(o,e=>Xe(e,this.#i&&this.#i.parent)):o(t)})}};function ct(e,t,n,r){let i=We()?ft:gt;var a=e.filter(e=>!e.settled),o=t.map(i);if(n.length===0&&a.length===0){r(o);return}var s=G,c=lt(),l=a.length===1?a[0].promise:a.length>1?Promise.all(a.map(e=>e.promise)):null;function u(e){if(!(s.f&16384)){c();try{r([...o,...e])}catch(e){Xe(e,s)}ut()}}var d=dt();if(n.length===0){l.then(()=>u([])).finally(d);return}function f(){Promise.all(n.map(e=>mt(e))).then(u).catch(e=>Xe(e,s)).finally(d)}l?l.then(()=>{c(),f(),ut()}):f()}function lt(){var e=G,t=H,n=A,r=M;return function(i=!0){Hn(e),W(t),Ve(n),i&&!(e.f&16384)&&(r?.activate(),r?.apply())}}function ut(e=!0){Hn(null),W(null),Ve(null),e&&M?.deactivate()}function dt(){var e=G,t=e.b,n=M,r=!!t?.is_rendered();return t?.update_pending_count(1,n),n.increment(r,e),()=>{t?.update_pending_count(-1,n),n.decrement(r,e)}}function ft(e){var n=2|v;return G!==null&&(G.f|=T),{ctx:A,deps:null,effects:null,equals:Ie,f:n,fn:e,reactions:null,rv:0,v:t,wv:0,parent:G,ac:null}}var pt=Symbol(`obsolete`);function mt(e,n,r){let i=G;i===null&&_e();var a=void 0,o=Gt(t),s=!H,c=new Set;return Tn(()=>{var t=G,n=g();a=n.promise;try{Promise.resolve(e()).then(n.resolve,e=>{e!==me&&n.reject(e)}).finally(ut)}catch(e){n.reject(e),ut()}var r=M;if(s){if(t.f&32768)var l=dt();if(i.b?.is_rendered())r.async_deriveds.get(t)?.reject(pt);else for(let e of c.values())e.reject(pt);c.add(n),r.async_deriveds.set(t,n)}let u=(e,t=void 0)=>{l?.(),c.delete(n),t!==pt&&(r.activate(),t?(o.f|=ae,qt(o,t)):(o.f&8388608&&(o.f^=ae),qt(o,e)),r.deactivate())};n.promise.then(u,e=>u(null,e||`unknown`))}),bn(()=>{for(let e of c)e.reject(pt)}),new Promise(e=>{function t(n){function r(){n===a?e(o):t(a)}n.then(r,r)}t(a)})}function ht(e){let t=ft(e);return ze||Wn(t),t}function gt(e){let t=ft(e);return t.equals=Re,t}function _t(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;nthis.schedule(e)){var n=this.#f.get(e);if(n){this.#f.delete(e);for(var r of n.d)j(r,v),t(r);for(r of n.m)j(r,y),t(r)}this.#p.add(e)}#g(){this.#e=!0,At++>1e3&&(this.#S(),Pt());for(let e of this.#u)this.#d.delete(e),j(e,v),this.schedule(e);for(let e of this.#d)j(e,y),this.schedule(e);let t=this.#c;this.#c=[],this.apply();var n=Ot=[],r=[],i=kt=[];for(let e of t)try{this.#_(e,n,r)}catch(t){throw Vt(e),this.#h()||this.discard(),t}if(M=null,i.length>0){var a=e.ensure();for(let e of i)a.schedule(e)}if(Ot=null,kt=null,this.#h()){this.#b(r),this.#b(n);for(let[e,t]of this.#f)Bt(e,t);i.length>0&&M.#g();return}let o=this.#v();if(o){this.#b(r),this.#b(n),o.#y(this);return}this.#u.clear(),this.#d.clear();for(let e of this.#r)e(this);this.#r.clear(),wt=this,It(r),It(n),wt=null,this.#s?.resolve();var s=M;if(this.#a===0&&(this.#c.length===0||s!==null)&&(this.#S(),ze&&(this.#x(),M=s)),this.#c.length>0)if(s!==null){let e=s;e.#c.push(...this.#c.filter(t=>!e.#c.includes(t)))}else s=this;s!==null&&s.#g()}#_(e,t,n){e.f^=_;for(var r=e.first;r!==null;){var i=r.f,a=(i&96)!=0;if(!(a&&i&1024||i&8192||this.#f.has(r))&&r.fn!==null){a?r.f^=_:i&4?t.push(r):ze&&i&16777224?n.push(r):Zn(r)&&(i&16&&this.#d.add(r),nr(r));var o=r.first;if(o!==null){r=o;continue}}for(;r!==null;){var s=r.next;if(s!==null){r=s;break}r=r.parent}}}#v(){for(var e=this.#t;e!==null;){if(!e.is_fork){for(let[t,[,n]]of this.current)if(e.current.has(t)&&!n)return e}e=e.#t}return null}#y(e){for(let[t,n]of e.current)!this.previous.has(t)&&e.previous.has(t)&&this.previous.set(t,e.previous.get(t)),this.current.set(t,n);for(let[t,n]of e.async_deriveds){let e=this.async_deriveds.get(t);e&&n.promise.then(e.resolve).catch(e.reject)}e.async_deriveds.clear(),this.transfer_effects(e.#u,e.#d);let t=e=>{var n=e.reactions;if(n!==null)for(let e of n){var r=e.f;if(r&2)t(e);else{var i=e;r&4194320&&!this.async_deriveds.has(i)&&(this.#d.delete(i),j(i,v),this.schedule(i))}}};for(let e of this.current.keys())t(e);this.oncommit(()=>e.discard()),e.#S(),M=this,this.#g()}#b(e){for(var t=0;t!u.current.get(e)[1]);if(!(!u.#e||r.length===0)){var i=r.filter(e=>!this.current.has(e));if(i.length===0)e&&u.discard();else if(t.length>0){if(e)for(let e of this.#p)u.unskip_effect(e,e=>{e.f&4194320?u.schedule(e):u.#b([e])});u.activate();var a=new Set,o=new Map;for(var s of t)Lt(s,i,a,o);o=new Map;var c=[...u.current].filter(([e,t])=>{let n=this.current.get(e);return n?n[0]!==t[0]||n[1]!==t[1]:!0}).map(([e])=>e);if(c.length>0)for(let e of this.#l)!(e.f&155648)&&Rt(e,c,o)&&(e.f&4194320?(j(e,v),u.schedule(e)):u.#u.add(e));if(u.#c.length>0&&!u.#m){u.apply();for(var l of u.#c)u.#_(l,[],[]);u.#c=[]}u.deactivate()}}}}increment(e,t){if(this.#a+=1,e){let e=this.#o.get(t)??0;this.#o.set(t,e+1)}}decrement(e,t){if(--this.#a,e){let e=this.#o.get(t)??0;e===1?this.#o.delete(t):this.#o.set(t,e-1)}this.#m||(this.#m=!0,qe(()=>{this.#m=!1,this.linked&&this.flush()}))}transfer_effects(e,t){for(let t of e)this.#u.add(t);for(let e of t)this.#d.add(e);e.clear(),t.clear()}oncommit(e){this.#r.add(e)}ondiscard(e){this.#i.add(e)}settled(){return(this.#s??=g()).promise}static ensure(){if(M===null){let t=M=new e;!Dt&&!Et&&qe(()=>{t.#e||t.flush()})}return M}apply(){if(!ze||!this.is_fork&&this.#t===null&&this.#n===null){N=null;return}N=new Map;for(let[e,[t]]of this.current)N.set(e,t);for(let t=St;t!==null;t=t.#n)if(!(t===this||t.is_fork)){var e=!1;if(t.id0)){Ut.clear();for(let e of Ft){if(e.f&24576)continue;let t=[e],n=e.parent;for(;n!==null;)Ft.has(n)&&(Ft.delete(n),t.push(n)),n=n.parent;for(let e=t.length-1;e>=0;e--){let n=t[e];n.f&24576||nr(n)}}Ft.clear()}}Ft=null}}function Lt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(let i of e.reactions){let e=i.f;e&2?Lt(i,t,n,r):e&4194320&&!(e&2048)&&Rt(i,t,r)&&(j(i,v),zt(i))}}function Rt(e,t,n){let r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(let r of e.deps){if(a.call(t,r))return!0;if(r.f&2&&Rt(r,t,n))return n.set(r,!0),!0}return n.set(e,!1),!1}function zt(e){M.schedule(e)}function Bt(e,t){if(!(e.f&32&&e.f&1024)){e.f&2048?t.d.push(e):e.f&4096&&t.m.push(e),j(e,_);for(var n=e.first;n!==null;)Bt(n,t),n=n.next}}function Vt(e){j(e,_);for(var t=e.first;t!==null;)Vt(t),t=t.next}var Ht=new Set,Ut=new Map,Wt=!1;function Gt(e,t){return{f:0,v:e,reactions:null,equals:Ie,rv:0,wv:0}}function P(e,t){let n=Gt(e,t);return Wn(n),n}function Kt(e,t=!1,n=!0){let r=Gt(e);return t||(r.equals=Re),Be&&n&&A!==null&&A.l!==null&&(A.l.s??=[]).push(r),r}function F(e,t,n=!1){return H!==null&&(!U||H.f&131072)&&We()&&H.f&4325394&&(Un===null||!Un.has(e))&&Ee(),qt(e,n?Zt(t):t,kt)}function qt(e,t,n=null){if(!e.equals(t)){Ut.set(e,Bn?t:e.v);var r=Mt.ensure();if(r.capture(e,t),e.f&2){let t=e;e.f&2048&&vt(t),N===null&&Qe(t)}e.wv=Xn(),Xt(e,v,n),We()&&G!==null&&G.f&1024&&!(G.f&96)&&(J===null?Gn([e]):J.push(e)),!r.is_fork&&Ht.size>0&&!Wt&&Jt()}return t}function Jt(){Wt=!1;for(let e of Ht){e.f&1024&&j(e,y);let t;try{t=Zn(e)}catch{t=!0}t&&nr(e)}Ht.clear()}function Yt(e){F(e,e.v+1)}function Xt(e,t,n){var r=e.reactions;if(r!==null)for(var i=We(),a=r.length,o=0;o{if(Jn===l)return e();var t=H,n=Jn;W(null),Yn(l);var r=e();return W(t),Yn(n),r};return a&&i.set(`length`,P(e.length,s)),new Proxy(e,{defineProperty(e,t,n){(!(`value`in n)||n.configurable===!1||n.enumerable===!1||n.writable===!1)&&we();var r=i.get(t);return r===void 0?p(()=>{var e=P(n.value,s);return i.set(t,e),e}):F(r,n.value,!0),!0},deleteProperty(e,n){var r=i.get(n);if(r===void 0){if(n in e){let e=p(()=>P(t,s));i.set(n,e),Yt(o)}}else F(r,t),Yt(o);return!0},get(n,r,a){if(r===oe)return e;var o=i.get(r),l=r in n;if(o===void 0&&(!l||c(n,r)?.writable)&&(o=p(()=>P(Zt(l?n[r]:t),s)),i.set(r,o)),o!==void 0){var u=Y(o);return u===t?void 0:u}return Reflect.get(n,r,a)},getOwnPropertyDescriptor(e,n){var r=Reflect.getOwnPropertyDescriptor(e,n);if(r&&`value`in r){var a=i.get(n);a&&(r.value=Y(a))}else if(r===void 0){var o=i.get(n),s=o?.v;if(o!==void 0&&s!==t)return{enumerable:!0,configurable:!0,value:s,writable:!0}}return r},has(e,n){if(n===oe)return!0;var r=i.get(n),a=r!==void 0&&r.v!==t||Reflect.has(e,n);return(r!==void 0||G!==null&&(!a||c(e,n)?.writable))&&(r===void 0&&(r=p(()=>P(a?Zt(e[n]):t,s)),i.set(n,r)),Y(r)===t)?!1:a},set(e,n,r,l){var u=i.get(n),d=n in e;if(a&&n===`length`)for(var f=r;fP(t,s)),i.set(f+``,m)):F(m,t)}if(u===void 0)(!d||c(e,n)?.writable)&&(u=p(()=>P(void 0,s)),F(u,Zt(r)),i.set(n,u));else{d=u.v!==t;var h=p(()=>Zt(r));F(u,h)}var g=Reflect.getOwnPropertyDescriptor(e,n);if(g?.set&&g.set.call(l,r),!d){if(a&&typeof n==`string`){var _=i.get(`length`),v=Number(n);Number.isInteger(v)&&v>=_.v&&F(_,v+1)}Yt(o)}return!0},ownKeys(e){Y(o);var n=Reflect.ownKeys(e).filter(e=>{var n=i.get(e);return n===void 0||n.v!==t});for(var[r,a]of i)a.v!==t&&!(r in e)&&n.push(r);return n},setPrototypeOf(){Te()}})}new Set([`copyWithin`,`fill`,`pop`,`push`,`reverse`,`shift`,`sort`,`splice`,`unshift`]);var Qt,$t,en,tn;function nn(){if(Qt===void 0){Qt=window,$t=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;en=c(t,`firstChild`).get,tn=c(t,`nextSibling`).get,p(e)&&(e[ue]=void 0,e[le]=null,e[de]=void 0,e.__e=void 0),p(n)&&(n[fe]=void 0)}}function I(e=``){return document.createTextNode(e)}function rn(e){return en.call(e)}function an(e){return tn.call(e)}function L(e,t){if(!E)return rn(e);var n=rn(D);if(n===null)n=D.appendChild(I());else if(t&&n.nodeType!==3){var r=I();return n?.before(r),O(r),r}return t&&un(n),O(n),n}function on(e,t=!1){if(!E){var n=rn(e);return n instanceof Comment&&n.data===``?an(n):n}if(t){if(D?.nodeType!==3){var r=I();return D?.before(r),O(r),r}un(D)}return D}function R(e,t=1,n=!1){let r=E?D:e;for(var i;t--;)i=r,r=an(r);if(!E)return r;if(n){if(r?.nodeType!==3){var a=I();return r===null?i?.after(a):r.before(a),O(a),a}un(r)}return O(r),r}function sn(e){e.textContent=``}function cn(){return!ze||Ft!==null?!1:(G.f&S)!==0}function ln(e,t,n){return t==null||t===`http://www.w3.org/1999/xhtml`?n?document.createElement(e,{is:n}):document.createElement(e):n?document.createElementNS(t,e,{is:n}):document.createElementNS(t,e)}function un(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===3;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function dn(e){E&&rn(e)!==null&&sn(e)}var fn=!1;function pn(){fn||(fn=!0,document.addEventListener(`reset`,e=>{Promise.resolve().then(()=>{if(!e.defaultPrevented)for(let t of e.target.elements)t[pe]?.()})},{capture:!0}))}function mn(e){var t=H,n=G;W(null),Hn(null);try{return e()}finally{W(t),Hn(n)}}function hn(e,t,n,r=n){e.addEventListener(t,()=>mn(n));let i=e[pe];i?e[pe]=()=>{i(),r(!0)}:e[pe]=()=>r(!0),pn()}function gn(e){G===null&&(H===null&&xe(e),be()),Bn&&ye(e)}function _n(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function vn(e,t){var n=G;n!==null&&n.f&8192&&(e|=b);var r={ctx:A,deps:null,nodes:null,f:e|v|512,first:null,fn:t,last:null,next:null,parent:n,b:n&&n.b,prev:null,teardown:null,wv:0,ac:null};M?.register_created_effect(r);var i=r;if(e&4)Ot===null?Mt.ensure().schedule(r):Ot.push(r);else if(t!==null){try{nr(r)}catch(e){throw V(r),e}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&!(i.f&524288)&&(i=i.first,e&16&&e&65536&&i!==null&&(i.f|=w))}if(i!==null&&(i.parent=n,n!==null&&_n(i,n),H!==null&&H.f&2&&!(e&64))){var a=H;(a.effects??=[]).push(i)}return r}function yn(){return H!==null&&!U}function bn(e){let t=vn(8,null);return j(t,_),t.teardown=e,t}function xn(e){gn(`$effect`);var t=G.f;if(!H&&t&32&&A!==null&&!A.i){var n=A;(n.e??=[]).push(e)}else return Sn(e)}function Sn(e){return vn(4|ee,e)}function Cn(e){Mt.ensure();let t=vn(64|T,e);return(e={})=>new Promise(n=>{e.outro?Nn(t,()=>{V(t),n(void 0)}):(V(t),n(void 0))})}function wn(e){return vn(4,e)}function Tn(e){return vn(ie|T,e)}function En(e,t=0){return vn(8|t,e)}function z(e,t=[],n=[],r=[]){ct(r,t,n,t=>{vn(8,()=>{e(...t.map(Y))})})}function Dn(e,t=0){return vn(16|t,e)}function B(e){return vn(32|T,e)}function On(e){var t=e.teardown;if(t!==null){let e=Bn,n=H;Vn(!0),W(null);try{t.call(null)}finally{Vn(e),W(n)}}}function kn(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){let e=n.ac;e!==null&&mn(()=>{e.abort(me)});var r=n.next;n.f&64?n.parent=null:V(n,t),n=r}}function An(e){for(var t=e.first;t!==null;){var n=t.next;t.f&32||V(t),t=n}}function V(e,t=!0){var n=!1;(t||e.f&262144)&&e.nodes!==null&&e.nodes.end!==null&&(jn(e.nodes.start,e.nodes.end),n=!0),e.f|=C,kn(e,t&&!n),tr(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)e.stop();On(e),e.f^=C,e.f|=x;var i=e.parent;i!==null&&i.first!==null&&Mn(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function jn(e,t){for(;e!==null;){var n=e===t?null:an(e);e.remove(),e=n}}function Mn(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Nn(e,t,n=!0){var r=[];Pn(e,r,!0);var i=()=>{n&&V(e),t&&t()},a=r.length;if(a>0){var o=()=>--a||i();for(var s of r)s.out(o)}else i()}function Pn(e,t,n){if(!(e.f&8192)){e.f^=b;var r=e.nodes&&e.nodes.t;if(r!==null)for(let e of r)(e.is_global||n)&&t.push(e);for(var i=e.first;i!==null;){var a=i.next;if(!(i.f&64)){var o=(i.f&65536)!=0||(i.f&32)!=0&&(e.f&16)!=0;Pn(i,t,o?n:!1)}i=a}}}function Fn(e){In(e,!0)}function In(e,t){if(e.f&8192){e.f^=b,e.f&1024||(j(e,v),Mt.ensure().schedule(e));for(var n=e.first;n!==null;){var r=n.next,i=(n.f&65536)!=0||(n.f&32)!=0;In(n,i?t:!1),n=r}var a=e.nodes&&e.nodes.t;if(a!==null)for(let e of a)(e.is_global||t)&&e.in()}}function Ln(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var i=n===r?null:an(n);t.append(n),n=i}}var Rn=null,zn=!1,Bn=!1;function Vn(e){Bn=e}var H=null,U=!1;function W(e){H=e}var G=null;function Hn(e){G=e}var Un=null;function Wn(e){H!==null&&(!ze||H.f&2)&&(Un??=new Set).add(e)}var K=null,q=0,J=null;function Gn(e){J=e}var Kn=1,qn=0,Jn=qn;function Yn(e){Jn=e}function Xn(){return++Kn}function Zn(e){var t=e.f;if(t&2048)return!0;if(t&2&&(e.f&=~ne),t&4096){for(var n=e.deps,r=n.length,i=0;ie.wv)return!0}t&512&&N===null&&j(e,_)}return!1}function Qn(e,t,n=!0){var r=e.reactions;if(r!==null&&!(!ze&&Un!==null&&Un.has(e)))for(var i=0;i{e.ac.abort(me)}),e.ac=null);try{e.f|=re;var u=e.fn,d=u();e.f|=S;var f=e.deps,p=M?.is_fork;if(K!==null){var m;if(p||tr(e,q),f!==null&&q>0)for(f.length=q+K.length,m=0;m{requestAnimationFrame(()=>e()),setTimeout(()=>e())});await Promise.resolve(),Nt()}function Y(e){var t=(e.f&2)!=0;if(Rn?.add(e),H!==null&&!U&&!(G!==null&&G.f&16384)&&(Un===null||!Un.has(e))){var n=H.deps;if(H.f&2097152)e.rvn?.call(this,e))}return e.startsWith(`pointer`)||e.startsWith(`touch`)||e===`wheel`?qe(()=>{t.addEventListener(e,i,r)}):t.addEventListener(e,i,r),i}function dr(e,t,n,r,i){var a={capture:r,passive:i},o=ur(e,t,n,a);(t===document.body||t===window||t===document||t instanceof HTMLMediaElement)&&bn(()=>{t.removeEventListener(e,o,a)})}function fr(e,t,n){(t[sr]??={})[e]=n}function pr(e){for(var t=0;t{throw e});throw p}}finally{e[sr]=t,delete e.currentTarget,W(d),Hn(f)}}}var gr=globalThis?.window?.trustedTypes&&globalThis.window.trustedTypes.createPolicy(`svelte-trusted-html`,{createHTML:e=>e});function _r(e){return gr?.createHTML(e)??e}function vr(e){var t=ln(`template`);return t.innerHTML=_r(e.replaceAll(``,``)),t.content}function yr(e,t){var n=G;n.nodes===null&&(n.nodes={start:e,end:t,a:null,t:null})}function X(e,t){var n=(t&1)!=0,r=(t&2)!=0,i,a=!e.startsWith(``);return()=>{if(E)return yr(D,null),D;i===void 0&&(i=vr(a?e:``+e),n||(i=rn(i)));var t=r||$t?document.importNode(i,!0):i.cloneNode(!0);if(n){var o=rn(t),s=t.lastChild;yr(o,s)}else yr(t,t);return t}}function br(){if(E)return yr(D,null),D;var e=document.createDocumentFragment(),t=document.createComment(``),n=I();return e.append(t,n),yr(t,n),e}function Z(e,t){if(E){var n=G;(!(n.f&32768)||n.nodes.end===null)&&(n.nodes.end=D),Me();return}e!==null&&e.before(t)}[...`allowfullscreen.async.autofocus.autoplay.checked.controls.default.disabled.formnovalidate.indeterminate.inert.ismap.loop.multiple.muted.nomodule.novalidate.open.playsinline.readonly.required.reversed.seamless.selected.webkitdirectory.defer.disablepictureinpicture.disableremoteplayback`.split(`.`)];var xr=[`touchstart`,`touchmove`];function Sr(e){return xr.includes(e)}function Q(e,t){var n=t==null?``:typeof t==`object`?`${t}`:t;n!==(e[fe]??=e.nodeValue)&&(e[fe]=n,e.nodeValue=`${n}`)}function Cr(e,t){return Tr(e,t)}var wr=new Map;function Tr(t,{target:n,anchor:r,props:i={},events:a,context:s,intro:c=!0,transformError:l}){nn();var u=void 0,d=Cn(()=>{var c=r??n.appendChild(I());ot(c,{pending:()=>{}},n=>{He({});var r=A;if(s&&(r.c=s),a&&(i.$$events=a),E&&yr(n,null),u=t(n,i)||{},E&&(G.nodes.end=D,D===null||D.nodeType!==8||D.data!==`]`))throw ke(),e;Ue()},l);var d=new Set,f=e=>{for(var t=0;t{for(var e of d)for(let r of[n,document]){var t=wr.get(r),i=t.get(e);--i==0?(r.removeEventListener(e,hr),t.delete(e),t.size===0&&wr.delete(r)):t.set(e,i)}lr.delete(f),c!==r&&c.parentNode?.removeChild(c)}});return Er.set(u,d),u}var Er=new WeakMap,Dr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)Fn(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(Fn(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(V(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();Ln(r,t),t.append(I()),this.#n.set(e,{effect:r,fragment:t})}else V(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),Nn(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(V(n.effect),this.#n.delete(e))};ensure(e,t){var n=M,r=cn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=I();i.append(a),this.#n.set(e,{effect:B(()=>t(a)),fragment:i})}else this.#t.set(e,B(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else E&&(this.anchor=D),this.#a(n)}};function Or(e){A===null&&ge(`onMount`),Be&&A.l!==null?kr(A).m.push(e):xn(()=>{let t=or(e);if(typeof t==`function`)return t})}function kr(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}function $(e,t,n=!1){var r;E&&(r=D,Me());var i=new Dr(e),a=n?w:0;function o(e,t){if(E){var n=Fe(r);if(e!==parseInt(n.substring(1))){var a=Pe();O(a),i.anchor=a,je(!1),i.ensure(e,t),je(!0);return}}i.ensure(e,t)}Dn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}function Ar(e,t){return t}function jr(e,t,n){for(var r=[],i=t.length,a,s=t.length,c=0;c{if(a){if(a.pending.delete(n),a.done.add(n),a.pending.size===0){var t=e.outrogroups;Mr(e,o(a.done)),t.delete(a),t.size===0&&(e.outrogroups=null)}}else --s},!1)}if(s===0){var l=r.length===0&&n!==null;if(l){var u=n,d=u.parentNode;sn(d),d.append(u),e.items.clear()}Mr(e,t,!l)}else a={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(a)}function Mr(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i{var e=n();return r(e)?e:e==null?[]:o(e)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Ir(v,p,c,t,i),d!==null&&(p.length===0?d.f&33554432?(d.f^=te,Rr(d,null,c)):Fn(d):Nn(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:Dn(()=>{p=Y(f);var e=p.length;let r=!1;E&&Fe(c)===`[!`!=(e===0)&&(c=Pe(),O(c),je(!1),r=!0);for(var o=new Set,u=M,v=cn(),y=0;ys(c)):(d=B(()=>s(Nr??=I())),d.f|=te)),e>o.size&&ve(``,``,``),E&&e>0&&O(Pe()),!h)if(m.set(u,o),v){for(let[e,t]of l)o.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u);r&&je(!0),Y(f)}),flags:t,items:l,pending:m,outrogroups:null,fallback:d};h=!1,E&&(c=D)}function Fr(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Ir(e,t,n,r,i){var a=(r&8)!=0,s=t.length,c=e.items,l=Fr(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(a)for(v=0;v0){var ee=r&4&&s===0?n:null;if(a){for(v=0;v{if(f!==void 0)for(_ of f)_.nodes?.a?.apply()})}function Lr(e,t,n,r,i,a,o,s){var c=o&1?o&16?Gt(n):Kt(n,!1,!1):null,l=o&2?Gt(i):null;return{v:c,i:l,e:B(()=>(a(t,c??n,l??i,s),()=>{e.delete(r)}))}}function Rr(e,t,n){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,a=t&&!(t.f&33554432)?t.nodes.start:n;r!==null;){var o=an(r);if(a.before(r),r===i)return;r=o}}function zr(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}var Br=[...` +\r\f\xA0\v`];function Vr(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||Br.includes(r[o-1]))&&(s===r.length||Br.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function Hr(e,t,n,r,i,a){var o=e[ue];if(E||o!==n||o===void 0){var s=Vr(n,r,a);(!E||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[ue]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}var Ur=Symbol(`is custom element`),Wr=Symbol(`is html`),Gr=he?`link`:`LINK`;function Kr(e,t,n,r){var i=qr(e);E&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===Gr)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[ce]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&Yr(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function qr(e){return e[le]??={[Ur]:e.nodeName.includes(`-`),[Wr]:e.namespaceURI===n}}var Jr=new Map;function Yr(e){var t=e.getAttribute(`is`)||e.nodeName,n=Jr.get(t);if(n)return n;Jr.set(t,n=[]);for(var r,i=e,a=Element.prototype;a!==i;){for(var o in r=l(i),r)r[o].set&&o!==`innerHTML`&&o!==`textContent`&&o!==`innerText`&&n.push(o);i=f(i)}return n}function Xr(e,t,n=t){var r=new WeakSet;hn(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=Zr(e)?Qr(a):a,n(a),M!==null&&r.add(M),await rr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(E&&e.defaultValue!==e.value||or(t)==null&&e.value)&&(n(Zr(e)?Qr(e.value):e.value),M!==null&&r.add(M)),En(()=>{var n=t();if(e===document.activeElement){var i=ze?wt:M;if(r.has(i))return}Zr(e)&&n===Qr(e.value)||e.type===`date`&&!n&&!e.value||n!==e.value&&(e.value=n??``)})}function Zr(e){var t=e.type;return t===`number`||t===`range`}function Qr(e){return e===``?null:+e}function $r(e,t){return e===t||e?.[oe]===t}function ei(e={},t,n,r){var i=A.r,a=G;return wn(()=>{var o,s;return En(()=>{o=s,s=r?.()||[],or(()=>{$r(n(...s),e)||(t(e,...s),o&&$r(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&$r(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function ti(e,t,n,r){var i=!Be||(n&2)!=0,a=(n&8)!=0,o=(n&16)!=0,s=r,l=!0,u=void 0,d=()=>o&&i?(u??=ft(r),Y(u)):(l&&(l=!1,s=o?or(r):r),s);let f;if(a){var p=oe in e||se in e;f=c(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;a?[m,h]=rt(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Ce(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(s=void 0),n===void 0?s:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?ft:gt)(()=>(v=!1,g()));a&&Y(y);var b=G;return(function(e,t){if(arguments.length>0){let n=t?Y(y):i&&a?Zt(e):e;return F(y,n),v=!0,s!==void 0&&(s=n),e}return Bn&&v||b.f&16384?y.v:Y(y)})}function ni(e){let t=[];for(let n of e.split(` +`)){let e=n.replace(/\r$/,``);if(!e.startsWith(`:`)){if(e===`data:`||e===`data`)t.push(``);else if(e.startsWith(`data:`)){let n=e.slice(5);n.startsWith(` `)&&(n=n.slice(1)),t.push(n)}}}return t.length===0?null:t.join(` +`)}var ri=class{constructor(){this.buffer=``}push(e){this.buffer+=e;let t=[],n;for(;(n=this._nextDelimiter())!==-1;){let e=this.buffer.slice(0,n.start);this.buffer=this.buffer.slice(n.end),e.length>0&&t.push(e)}return t}flush(){let e=this.buffer.trim();return this.buffer=``,e?[e]:[]}_nextDelimiter(){let e=[{token:`\r +\r +`,i:this.buffer.indexOf(`\r +\r +`)},{token:` + +`,i:this.buffer.indexOf(` + +`)},{token:`\r\r`,i:this.buffer.indexOf(`\r\r`)}].filter(e=>e.i!==-1);if(e.length===0)return-1;e.sort((e,t)=>e.i-t.i);let{token:t,i:n}=e[0];return{start:n,end:n+t.length}}};async function ii(e,t){if(!e.ok)throw Error(`server returned ${e.status} ${e.statusText}`);if(!e.body)throw Error(`response has no readable body (streaming unsupported)`);let n=e.body.getReader(),r=new TextDecoder,i=new ri,a=e=>{let n=ni(e);if(n==null||n.trim()===``)return;let r;try{r=JSON.parse(n)}catch{return}t(r)};try{for(;;){let{value:e,done:t}=await n.read();if(t)break;let o=r.decode(e,{stream:!0});for(let e of i.push(o))a(e)}}finally{n.releaseLock?.()}let o=r.decode();if(o)for(let e of i.push(o))a(e);for(let e of i.flush())a(e)}async function ai(){let e=await fetch(`/api/session`,{method:`POST`,headers:{"content-type":`application/json`}});if(!e.ok)throw Error(`could not open a session (HTTP ${e.status})`);let t=await e.json();if(!t||typeof t.session_id!=`string`)throw Error(`session response missing session_id`);return t.session_id}async function oi({session_id:e,prompt:t,model:n,signal:r},i){let a={session_id:e,prompt:t};n&&(a.model=n),await ii(await fetch(`/api/chat`,{method:`POST`,headers:{"content-type":`application/json`,accept:`text/event-stream`},body:JSON.stringify(a),signal:r}),i)}async function si(){let e=await fetch(`/api/pve/verbs`);if(!e.ok)throw Error(`could not load VM controls (HTTP ${e.status})`);let t=await e.json();return{verbs:Array.isArray(t.verbs)?t.verbs:[],mutating:Array.isArray(t.mutating)?t.mutating:[]}}async function ci(e){let t=await fetch(`/api/pve/${encodeURIComponent(e)}`,{method:`POST`,headers:{"content-type":`application/json`}}),n;try{n=await t.json()}catch{throw Error(`VM control '${e}' failed (HTTP ${t.status}, no body)`)}if(t.status===400)throw Error(n?.detail||`'${e}' was rejected by the server`);return{verb:n.verb??e,exit_code:n.exit_code??null,stdout:n.stdout??``,stderr:n.stderr??``,rejected:!!n.rejected}}var li=X(` `,1),ui=X(` `);function di(e,t){let n=ti(t,`name`,3,`tool`),r=ti(t,`command`,3,``);var i=ui(),a=R(L(i),2),o=L(a,!0);k(a);var s=R(a,2),c=e=>{var t=li(),n=R(on(t),2),i=L(n,!0);k(n),z(()=>Q(i,r())),Z(e,t)};$(s,e=>{r()&&e(c)}),k(i),z(()=>{Kr(i,`title`,r()?`${n()}: ${r()}`:n()),Q(o,n())}),Z(e,i)}var fi=X(`

The agent is standing by.

Describe the symptom — "devvm is unreachable", "disk full", "ssh hangs" + — and it will connect over SSH, investigate, and stream its work here. + For a hard power action when the agent can't help, use Direct VM control.

`),pi=X(`
`),mi=X(``),hi=X(` `),gi=X(`
`),_i=X(` `),vi=X(` `),yi=X(`
`),bi=X(`
`),xi=X(`
agent working — streaming live
`),Si=X(`
Recovery agent SSHes into the devvm to diagnose & repair
`);function Ci(e,t){He(t,!0);let n=ti(t,`sessionId`,3,``),r=ti(t,`sessionReady`,3,!1),i=ti(t,`onLiveSession`,3,e=>{}),a=ti(t,`onStreamingChange`,3,e=>{}),o=P(Zt([])),s=P(``),c=P(!1),l,u,d=!0,f=ht(()=>r()&&!Y(c)&&Y(s).trim().length>0);function p(){l&&(d=l.scrollHeight-l.scrollTop-l.clientHeight<60)}async function m(e=!1){!e&&!d||(await rr(),l&&(l.scrollTop=l.scrollHeight))}function h(){return Y(o)[Y(o).length-1]}function g(e){let t=h().parts,n=t[t.length-1];n&&n.type===`text`?n.text+=e:t.push({type:`text`,text:e}),F(o,Y(o),!0)}function _(e){switch(e?.kind){case`session`:i()(e.session_id);break;case`text`:e.text&&g(e.text);break;case`tool`:{let t=e.input&&typeof e.input.command==`string`?e.input.command:``;h().parts.push({type:`tool`,name:e.name||`tool`,command:t}),F(o,Y(o),!0);break}case`result`:h().result={is_error:!!e.is_error,text:typeof e.result==`string`?e.result:``,duration_ms:typeof e.duration_ms==`number`?e.duration_ms:null},F(o,Y(o),!0);break;case`error`:h().error=e.error||`unknown error`,F(o,Y(o),!0);break;case`done`:break;default:break}m()}async function v(){let e=Y(s).trim();if(!(!e||Y(c)||!r())){Y(o).push({role:`user`,text:e}),Y(o).push({role:`assistant`,parts:[]}),F(o,Y(o),!0),F(s,``),F(c,!0),a()(!0),d=!0,await m(!0);try{await oi({session_id:n(),prompt:e},_)}catch(e){let t=h();t&&t.role===`assistant`&&!t.error&&(t.error=(e instanceof Error?e.message:String(e))+` — the connection to the agent failed.`,F(o,Y(o),!0))}finally{F(c,!1),a()(!1),await m(),u?.focus()}}}function y(e){e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),v())}function b(e){return e==null?``:e<1e3?`${e} ms`:`${(e/1e3).toFixed(+(e<1e4))} s`}let x=ht(()=>Y(o).length===0);var S=Si(),C=R(L(S),2),w=L(C),T=e=>{Z(e,fi())};$(w,e=>{Y(x)&&e(T)}),Pr(R(w,2),17,()=>Y(o),Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=pi(),r=L(n),i=L(r,!0);k(r),k(n),z(()=>Q(i,Y(t).text)),Z(e,n)},a=e=>{var n=bi(),r=L(n),i=L(r),a=e=>{Z(e,mi())};$(i,e=>{Y(t).parts.length===0&&!Y(t).result&&!Y(t).error&&e(a)});var o=R(i,2);Pr(o,17,()=>Y(t).parts,Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=hi(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).text)),Z(e,n)},a=e=>{di(e,{get name(){return Y(t).name},get command(){return Y(t).command}})};$(r,e=>{Y(t).type===`text`?e(i):e(a,-1)}),Z(e,n)});var s=R(o,2),c=e=>{var n=gi(),r=L(n);k(n),z(()=>Q(r,`⚠ ${Y(t).error??``}`)),Z(e,n)},l=e=>{var n=yi(),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{var n=_i(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).result.text)),Z(e,n)};$(a,e=>{Y(t).result.text&&e(o)});var s=R(a,2),c=e=>{var n=vi(),r=L(n,!0);k(n),z(e=>Q(r,e),[()=>b(Y(t).result.duration_ms)]),Z(e,n)};$(s,e=>{Y(t).result.duration_ms!=null&&e(c)}),k(n),z(()=>{Hr(n,1,`turn-note ${Y(t).result.is_error?`turn-note--error`:`turn-note--ok`}`,`svelte-1bi93vx`),Q(i,Y(t).result.is_error?`failed`:`done`)}),Z(e,n)};$(s,e=>{Y(t).error?e(c):Y(t).result&&e(l,1)}),k(r),k(n),Z(e,n)};$(r,e=>{Y(t).role===`user`?e(i):e(a,-1)}),Z(e,n)}),k(C),ei(C,e=>l=e,()=>l);var ee=R(C,2),te=L(ee),ne=e=>{Z(e,xi())};$(te,e=>{Y(c)&&e(ne)});var re=R(te,2),ie=L(re);dn(ie),ei(ie,e=>u=e,()=>u);var ae=R(ie,2),oe=L(ae,!0);k(ae),k(re),k(ee),k(S),z(()=>{Kr(ie,`placeholder`,r()?`Describe the problem… (Enter to send · Shift+Enter for a new line)`:`Waiting for a session…`),ie.disabled=!r()||Y(c),ae.disabled=!Y(f),Q(oe,Y(c)?`…`:`Send`)}),dr(`scroll`,C,p),dr(`submit`,ee,e=>{e.preventDefault(),v()}),fr(`keydown`,ie,y),Xr(ie,()=>Y(s),e=>F(s,e)),Z(e,S),Ue()}pr([`keydown`]);var wi=X(`
Loading controls…
`),Ti=X(``),Ei=X(``),Di=X(``),Oi=X(``),ki=X(`recovery`),Ai=X(`
Confirm ? This will affect the running VM
`),ji=X(`

`),Mi=X(``),Ni=X(`rejected`),Pi=X(` `),Fi=X(`
 
`),Ii=X(`
stderr
 
`,1),Li=X(`
(no output)
`),Ri=X(`
`),zi=X(`
Inspect read-only
Power affects the running VM
`,1),Bi=X(`

Direct VM control

No AI in the path — these reach the Proxmox host over a + forced-command SSH key and work even when the agent is down.

`);function Vi(e,t){He(t,!0);let n={status:{label:`status`,blurb:`qm status — is the VM up?`},forensics:{label:`forensics`,blurb:`capture live diagnostic state`},start:{label:`start`,blurb:`power on a stopped VM`},stop:{label:`stop`,blurb:`hard power-off (pulls the plug)`},reset:{label:`reset`,blurb:`warm reboot — reuses the QEMU process`},cycle:{label:`cycle`,blurb:`stop → start; applies staged config; fixes a wedged QEMU`,headline:!0}},r=[`status`,`forensics`,`start`,`stop`,`reset`,`cycle`],i=P(`loading`),a=P(``),o=P(Zt([])),s=P(``),c=P(``),l=P(null),u=P(``),d=ht(()=>Y(c)!==``);Or(async()=>{try{let{verbs:e,mutating:t}=await si(),a=new Set(t),s=e.filter(e=>n[e]);F(o,[...r.filter(e=>s.includes(e)),...s.filter(e=>!r.includes(e))].map(e=>({name:e,mutating:a.has(e),...n[e]})),!0),F(i,`ready`)}catch(e){F(i,`error`),F(a,e instanceof Error?e.message:String(e),!0)}});let f=ht(()=>Y(o).filter(e=>!e.mutating)),p=ht(()=>Y(o).filter(e=>e.mutating));function m(e){Y(d)||(e.mutating?F(s,Y(s)===e.name?``:e.name,!0):g(e.name))}function h(){F(s,``)}async function g(e){F(s,``),F(u,``),F(l,null),F(c,e,!0);try{F(l,await ci(e),!0)}catch(e){F(u,e instanceof Error?e.message:String(e),!0)}finally{F(c,``)}}let _=ht(()=>!!Y(l)&&(Y(l).rejected||Y(l).exit_code!=null&&Y(l).exit_code!==0));var v=Bi(),y=R(L(v),2),b=e=>{Z(e,wi())},x=e=>{var t=Ti(),n=L(t),r=R(n);k(t),z(()=>Q(n,`Couldn't load the VM controls — ${Y(a)??``}. `)),fr(`click`,r,()=>location.reload()),Z(e,t)},S=e=>{var t=zi(),n=on(t),r=R(L(n),2);Pr(r,21,()=>Y(f),e=>e.name,(e,t)=>{var n=Di(),r=L(n),i=e=>{Z(e,Ei())};$(r,e=>{Y(c)===Y(t).name&&e(i)});var a=R(r,2),o=L(a,!0);k(a),k(n),z(()=>{n.disabled=Y(d),Kr(n,`title`,Y(t).blurb),Q(o,Y(t).label)}),fr(`click`,n,()=>m(Y(t))),Z(e,n)}),k(r),k(n);var i=R(n,2),a=R(L(i),2);Pr(a,21,()=>Y(p),e=>e.name,(e,t)=>{var n=ji(),r=L(n),i=L(r),a=e=>{Z(e,Oi())};$(i,e=>{Y(c)===Y(t).name&&e(a)});var o=R(i,2),l=L(o,!0);k(o);var u=R(o,2),f=e=>{Z(e,ki())};$(u,e=>{Y(t).headline&&e(f)}),k(r);var p=R(r,2),_=L(p,!0);k(p);var v=R(p,2),y=e=>{var n=Ai(),r=L(n),i=R(L(r)),a=L(i,!0);k(i),Ne(),k(r);var o=R(r,2),s=L(o),c=R(s,2);k(o),k(n),z(()=>{Kr(n,`aria-label`,`Confirm ${Y(t).name??``}`),Q(a,Y(t).name),s.disabled=Y(d),c.disabled=Y(d)}),fr(`click`,s,()=>g(Y(t).name)),fr(`click`,c,h),Z(e,n)};$(v,e=>{Y(s)===Y(t).name&&e(y)}),k(n),z(()=>{Hr(n,1,`danger-item ${Y(t).headline?`danger-item--headline`:``}`,`svelte-1qihpg4`),Hr(r,1,`vbtn vbtn--danger ${Y(t).headline?`vbtn--headline`:``}`,`svelte-1qihpg4`),r.disabled=Y(d),Kr(r,`aria-expanded`,Y(s)===Y(t).name),Q(l,Y(t).label),Q(_,Y(t).blurb)}),fr(`click`,r,()=>m(Y(t))),Z(e,n)}),k(a),k(i);var o=R(i,2),v=e=>{var t=Mi(),n=L(t);k(t),z(()=>Q(n,`⚠ Command failed to reach the host — ${Y(u)??``}`)),Z(e,t)};$(o,e=>{Y(u)&&e(v)});var y=R(o,2),b=e=>{var t=Ri(),n=L(t),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{Z(e,Ni())},s=e=>{var t=Pi(),n=L(t);k(t),z(()=>{Hr(t,1,`out-status ${Y(_)?`out-status--fail`:`out-status--ok`}`,`svelte-1qihpg4`),Q(n,`exit ${Y(l).exit_code??``}`)}),Z(e,t)};$(a,e=>{Y(l).rejected?e(o):e(s,-1)}),k(n);var c=R(n,2),u=e=>{var t=Fi(),n=L(t,!0);k(t),z(()=>Q(n,Y(l).stdout)),Z(e,t)};$(c,e=>{Y(l).stdout&&e(u)});var d=R(c,2),f=e=>{var t=Ii(),n=R(on(t),2),r=L(n,!0);k(n),z(()=>Q(r,Y(l).stderr)),Z(e,t)};$(d,e=>{Y(l).stderr&&e(f)});var p=R(d,2),m=e=>{Z(e,Li())};$(p,e=>{!Y(l).stdout&&!Y(l).stderr&&e(m)}),k(t),z(()=>{Hr(t,1,`out ${Y(_)?`out--fail`:`out--ok`}`,`svelte-1qihpg4`),Q(i,Y(l).verb)}),Z(e,t)};$(y,e=>{Y(l)&&e(b)}),Z(e,t)};$(y,e=>{Y(i)===`loading`?e(b):Y(i)===`error`?e(x,1):e(S,-1)}),k(v),Z(e,v),Ue()}pr([`click`]);var Hi=X(`session unavailable`),Ui=X(`opening session…`),Wi=X(`· agent working`),Gi=X(`session `,1),Ki=X(``),qi=X(`

devvm breakglass

emergency recovery
`);function Ji(e,t){He(t,!0);let n=P(``),r=P(`connecting`),i=P(``),a=P(!1);async function o(){F(r,`connecting`),F(i,``);try{F(n,await ai(),!0),F(r,`ready`)}catch(e){F(r,`error`),F(i,e instanceof Error?e.message:String(e),!0)}}Or(o);function s(e){e&&F(n,e,!0)}let c=ht(()=>Y(n)?Y(n).slice(0,8):`────────`),l=ht(()=>Y(r)===`error`?`error`:Y(a)?`busy`:Y(r)===`ready`?`ready`:`idle`);var u=qi(),d=L(u),f=R(L(d),2),p=L(f),m=R(p,2),h=L(m),g=e=>{Z(e,Hi())},_=e=>{Z(e,Ui())},v=e=>{var t=Gi(),r=R(on(t),2),i=L(r,!0);k(r);var o=R(r,2),s=e=>{Z(e,Wi())};$(o,e=>{Y(a)&&e(s)}),z(()=>{Kr(r,`title`,Y(n)),Q(i,Y(c))}),Z(e,t)};$(h,e=>{Y(r)===`error`?e(g):Y(r)===`connecting`?e(_,1):e(v,-1)}),k(m);var y=R(m,2);k(f),k(d);var b=R(d,2),x=e=>{var t=Ki(),n=L(t);k(t),z(()=>Q(n,`Could not reach the breakglass backend — ${Y(i)??``}. The cluster or + network may be down. The manual VM controls below still work independently + of the chat agent.`)),Z(e,t)};$(b,e=>{Y(r)===`error`&&e(x)});var S=R(b,2),C=L(S),w=L(C);{let e=ht(()=>Y(r)===`ready`);Ci(w,{get sessionId(){return Y(n)},get sessionReady(){return Y(e)},onLiveSession:s,onStreamingChange:e=>F(a,e,!0)})}k(C);var T=R(C,2);Vi(L(T),{}),k(T),k(S),k(u),z(()=>{Hr(p,1,`dot dot--${Y(l)??``}`,`svelte-1n46o8q`),y.disabled=Y(a)||Y(r)===`connecting`,Kr(y,`title`,Y(a)?`wait for the current turn to finish`:`start a fresh session`)}),fr(`click`,y,o),Z(e,u),Ue()}pr([`click`]),Cr(Ji,{target:document.getElementById(`app`)}); \ No newline at end of file diff --git a/app/breakglass/static/index.html b/app/breakglass/static/index.html new file mode 100644 index 0000000..2c18803 --- /dev/null +++ b/app/breakglass/static/index.html @@ -0,0 +1,15 @@ + + + + + + + + devvm breakglass + + + + +
+ + diff --git a/docker-entrypoint-breakglass.sh b/docker-entrypoint-breakglass.sh new file mode 100644 index 0000000..3efd0da --- /dev/null +++ b/docker-entrypoint-breakglass.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Entrypoint for the claude-breakglass deployment. +# +# Loads the breakglass SSH key into an in-pod ssh-agent (so neither the app nor +# the agent needs a key path, and the private key isn't passed around as a file +# after load), writes the `devvm`/`pve` SSH aliases the breakglass agent uses, +# then execs uvicorn. uvicorn — and the `claude` subprocesses it spawns — +# inherit SSH_AUTH_SOCK, so `ssh devvm` / `ssh pve ` just work. +set -euo pipefail + +HOME_DIR="${HOME:-/home/agent}" +SSH_DIR="$HOME_DIR/.ssh" +KEY_SRC="${BREAKGLASS_KEY_PATH:-/secrets/breakglass/private_key}" + +mkdir -p "$SSH_DIR" +chmod 700 "$SSH_DIR" + +# SSH client config: the aliases the breakglass agent prompt refers to. +# Host-key checking off on purpose — a devvm rebuild rotates the host key and we +# must not get locked out mid-incident (trusted internal LAN; key auth stands). +cat > "$SSH_DIR/config" </dev/null + TMP_KEY="$(mktemp /dev/shm/bgk.XXXXXX)" + install -m600 "$KEY_SRC" "$TMP_KEY" + ssh-add "$TMP_KEY" >/dev/null 2>&1 || echo "WARN: ssh-add failed" >&2 + shred -u "$TMP_KEY" 2>/dev/null || rm -f "$TMP_KEY" + export SSH_AUTH_SOCK SSH_AGENT_PID +else + echo "WARN: breakglass key not found at $KEY_SRC — SSH will not work" >&2 +fi + +exec python3 -m uvicorn app.breakglass.server:app \ + --host 0.0.0.0 --port 8080 --app-dir /srv diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..e04f111 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + devvm breakglass + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..f51b013 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1179 @@ +{ + "name": "breakglass-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "breakglass-ui", + "version": "0.1.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "7.1.2", + "svelte": "5.56.3", + "vite": "8.0.16" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", + "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.56.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", + "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.11", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3efd9b2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "breakglass-ui", + "version": "0.1.0", + "private": true, + "description": "devvm breakglass — emergency recovery SPA (served by the in-cluster FastAPI app)", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "7.1.2", + "svelte": "5.56.3", + "vite": "8.0.16" + } +} diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte new file mode 100644 index 0000000..f72cfe2 --- /dev/null +++ b/frontend/src/App.svelte @@ -0,0 +1,272 @@ + + +
+
+
+ +

devvm breakglass

+ emergency recovery +
+ +
+ + + {#if sessionState === 'error'} + session unavailable + {:else if sessionState === 'connecting'} + opening session… + {:else} + session + {shortId} + {#if streaming}· agent working{/if} + {/if} + + +
+
+ + {#if sessionState === 'error'} + + {/if} + +
+
+ (streaming = v)} + /> +
+ + +
+
+ + diff --git a/frontend/src/Chat.svelte b/frontend/src/Chat.svelte new file mode 100644 index 0000000..2c6ada6 --- /dev/null +++ b/frontend/src/Chat.svelte @@ -0,0 +1,494 @@ + + +
+
+ Recovery agent + SSHes into the devvm to diagnose & repair +
+ +
+ {#if isEmpty} +
+
+

The agent is standing by.

+

+ Describe the symptom — "devvm is unreachable", "disk full", "ssh hangs" + — and it will connect over SSH, investigate, and stream its work here. + For a hard power action when the agent can't help, use + Direct VM control. +

+
+ {/if} + + {#each messages as msg, i (i)} + {#if msg.role === 'user'} +
+
{msg.text}
+
+ {:else} +
+
+ {#if msg.parts.length === 0 && !msg.result && !msg.error} + + + + {/if} + {#each msg.parts as part, j (j)} + {#if part.type === 'text'} + {part.text} + {:else} + + {/if} + {/each} + + {#if msg.error} +
⚠ {msg.error}
+ {:else if msg.result} +
+ {msg.result.is_error ? 'failed' : 'done'} + {#if msg.result.text}{msg.result.text}{/if} + {#if msg.result.duration_ms != null} + {fmtDuration(msg.result.duration_ms)} + {/if} +
+ {/if} +
+
+ {/if} + {/each} +
+ +
{ + e.preventDefault(); + send(); + }} + > + {#if streaming} +
+ + agent working — streaming live +
+ {/if} +
+ + +
+
+
+ + diff --git a/frontend/src/ToolChip.svelte b/frontend/src/ToolChip.svelte new file mode 100644 index 0000000..321ca34 --- /dev/null +++ b/frontend/src/ToolChip.svelte @@ -0,0 +1,54 @@ + + + + + {name} + {#if command} + + {command} + {/if} + + + diff --git a/frontend/src/VmControls.svelte b/frontend/src/VmControls.svelte new file mode 100644 index 0000000..a240fe3 --- /dev/null +++ b/frontend/src/VmControls.svelte @@ -0,0 +1,562 @@ + + +
+
+
+ +

Direct VM control

+
+

No AI in the path — these reach the Proxmox host over a + forced-command SSH key and work even when the agent is down.

+
+ + {#if loadState === 'loading'} +
Loading controls…
+ {:else if loadState === 'error'} + + {:else} + +
+
Inspect read-only
+
+ {#each nonMutating as v (v.name)} + + {/each} +
+
+ + +
+
+ Power affects the running VM +
+
+ {#each mutating as v (v.name)} +
+ +

{v.blurb}

+ + {#if confirming === v.name} +
+ + Confirm {v.name}? This will affect the running VM + +
+ + +
+
+ {/if} +
+ {/each} +
+
+ + + {#if actionError} + + {/if} + + {#if output} +
+
+ {output.verb} + {#if output.rejected} + rejected + {:else} + + exit {output.exit_code} + + {/if} +
+ {#if output.stdout} +
{output.stdout}
+ {/if} + {#if output.stderr} +
stderr
+
{output.stderr}
+ {/if} + {#if !output.stdout && !output.stderr} +
(no output)
+ {/if} +
+ {/if} + {/if} +
+ + diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..4d5352d --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,126 @@ +/* ─────────────────────────────────────────────────────────────────────────── + devvm breakglass — global theme + A recovery console: dark, high-contrast, terminal-adjacent. Calm by default; + danger is the only loud thing on the screen. No external fonts/CDNs — system + monospace carries the identity, system sans carries readable prose. + ─────────────────────────────────────────────────────────────────────────── */ + +:root { + /* Surfaces — a near-black slate with cool undertone, layered for depth. */ + --bg-0: #07090c; /* page base */ + --bg-1: #0c1015; /* panel */ + --bg-2: #11171e; /* raised panel / input */ + --bg-3: #161d26; /* chips, hover */ + --bg-term: #06080a; /* command-output panels */ + + /* Hairlines & text */ + --line: #1d2630; + --line-strong: #2a3744; + --ink: #e6edf3; /* primary text */ + --ink-dim: #9bb0c0; /* secondary text */ + --ink-faint: #5d7185; /* labels, meta */ + + /* Accents */ + --cyan: #3dd1d6; /* "system alive" — links, focus, session dot */ + --cyan-dim: #1f6f72; + --amber: #f5b657; /* working / in-flight */ + --green: #5ddb8e; /* healthy exit */ + --green-dim: #1f5f3d; + + /* Danger — reserved EXCLUSIVELY for mutating actions. Nothing else is red. */ + --danger: #ff4d4d; + --danger-bright: #ff6363; + --danger-deep: #7a1717; + --danger-glow: rgba(255, 77, 77, 0.35); + + --radius: 10px; + --radius-sm: 7px; + + --mono: ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", + "Fira Code", Menlo, Consolas, "Liberation Mono", monospace; + --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + + --shadow-panel: 0 1px 0 rgba(255, 255, 255, 0.02) inset, + 0 16px 40px -24px rgba(0, 0, 0, 0.9); + + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; +} + +body { + background-color: var(--bg-0); + /* Atmosphere: a soft cyan corner-glow over a faint scanline weave, so the + surface reads like backlit equipment rather than flat #000. */ + background-image: + radial-gradient(120% 80% at 85% -10%, rgba(61, 209, 214, 0.07), transparent 55%), + radial-gradient(90% 70% at 10% 110%, rgba(245, 182, 87, 0.04), transparent 50%), + repeating-linear-gradient( + 0deg, + rgba(255, 255, 255, 0.012) 0px, + rgba(255, 255, 255, 0.012) 1px, + transparent 1px, + transparent 3px + ); + background-attachment: fixed; + color: var(--ink); + font-family: var(--sans); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +#app { + height: 100%; +} + +button { + font-family: var(--mono); + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; +} + +::selection { + background: rgba(61, 209, 214, 0.28); +} + +/* Console scrollbars — thin, dark, unobtrusive. */ +* { + scrollbar-width: thin; + scrollbar-color: var(--line-strong) transparent; +} +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} +*::-webkit-scrollbar-thumb { + background: var(--line-strong); + border-radius: 99px; + border: 2px solid transparent; + background-clip: content-box; +} +*::-webkit-scrollbar-thumb:hover { + background: #3a4a5a; + background-clip: content-box; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + } +} diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000..6d42dae --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,92 @@ +// Same-origin API client. Auth is handled entirely by the edge proxy +// (Authentik / basic-auth / bearer) — this UI never sends or stores a token. +import { readEventStream } from './sse.js'; + +/** Open a fresh chat session. @returns {Promise} session_id */ +export async function openSession() { + const res = await fetch('/api/session', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + }); + if (!res.ok) { + throw new Error(`could not open a session (HTTP ${res.status})`); + } + const body = await res.json(); + if (!body || typeof body.session_id !== 'string') { + throw new Error('session response missing session_id'); + } + return body.session_id; +} + +/** + * Run one chat turn. Streams events to onEvent until the backend sends + * {kind:"done"} and the connection closes. Pass an AbortSignal to cancel. + * + * @param {{session_id: string, prompt: string, model?: string, signal?: AbortSignal}} opts + * @param {(event: object) => void} onEvent + */ +export async function streamChat({ session_id, prompt, model, signal }, onEvent) { + const payload = { session_id, prompt }; + if (model) payload.model = model; + + const res = await fetch('/api/chat', { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'text/event-stream', + }, + body: JSON.stringify(payload), + signal, + }); + await readEventStream(res, onEvent); +} + +/** + * List the PVE power verbs and which of them mutate VM state. + * @returns {Promise<{verbs: string[], mutating: string[]}>} + */ +export async function fetchVerbs() { + const res = await fetch('/api/pve/verbs'); + if (!res.ok) { + throw new Error(`could not load VM controls (HTTP ${res.status})`); + } + const body = await res.json(); + return { + verbs: Array.isArray(body.verbs) ? body.verbs : [], + mutating: Array.isArray(body.mutating) ? body.mutating : [], + }; +} + +/** + * Run a PVE power verb directly (no AI in the path). The backend returns 200 + * on success and 502 when the verb's exit code is non-zero, but the JSON body + * carries {verb, exit_code, stdout, stderr, rejected} in BOTH cases — so we + * read the body regardless of HTTP status and let the caller style on + * exit_code / rejected. + * + * @param {string} verb + * @returns {Promise<{verb: string, exit_code: number|null, stdout: string, stderr: string, rejected: boolean}>} + */ +export async function runVerb(verb) { + const res = await fetch(`/api/pve/${encodeURIComponent(verb)}`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + }); + // 400 = unknown verb (FastAPI HTTPException) — has {detail}, not the verb shape. + let body; + try { + body = await res.json(); + } catch { + throw new Error(`VM control '${verb}' failed (HTTP ${res.status}, no body)`); + } + if (res.status === 400) { + throw new Error(body?.detail || `'${verb}' was rejected by the server`); + } + return { + verb: body.verb ?? verb, + exit_code: body.exit_code ?? null, + stdout: body.stdout ?? '', + stderr: body.stderr ?? '', + rejected: Boolean(body.rejected), + }; +} diff --git a/frontend/src/lib/sse.js b/frontend/src/lib/sse.js new file mode 100644 index 0000000..8375612 --- /dev/null +++ b/frontend/src/lib/sse.js @@ -0,0 +1,150 @@ +// SSE frame parsing — the load-bearing core of the breakglass UI. +// +// The /api/chat endpoint returns a text/event-stream that we read with +// fetch() + response.body.getReader() (NOT EventSource, which cannot POST). +// The backend emits one frame per event as: +// +// data: {json}\n\n +// +// getReader() hands us bytes at arbitrary boundaries: a single frame can be +// split across reads, and one read can contain several frames. So we keep a +// rolling text buffer, split it on the blank-line frame delimiter, and only +// hand back the JSON payload of *complete* frames. Per the SSE spec a frame may +// carry multiple `data:` lines (joined with "\n"); the backend emits single +// line JSON today, but we handle the general case so a future multi-line +// payload can't silently corrupt the stream. + +/** + * Parse a single SSE event block (the text between blank lines) into its data + * payload string, or null if the block carries no `data:` field (e.g. a bare + * comment or a `:` heartbeat). + * @param {string} block + * @returns {string|null} + */ +export function dataFromEventBlock(block) { + const dataLines = []; + for (const rawLine of block.split('\n')) { + const line = rawLine.replace(/\r$/, ''); + if (line.startsWith(':')) continue; // SSE comment / heartbeat + if (line === 'data:' || line === 'data') { + dataLines.push(''); + } else if (line.startsWith('data:')) { + // Spec: a single leading space after the colon is stripped. + let v = line.slice('data:'.length); + if (v.startsWith(' ')) v = v.slice(1); + dataLines.push(v); + } + // field lines we don't care about (event:, id:, retry:) are ignored + } + if (dataLines.length === 0) return null; + return dataLines.join('\n'); +} + +/** + * A stateful splitter that turns an arbitrary sequence of decoded text chunks + * into a sequence of complete SSE event-block strings. Frames are delimited by + * a blank line; we tolerate both "\n\n" and "\r\n\r\n". + */ +export class SSEFrameSplitter { + constructor() { + this.buffer = ''; + } + + /** + * Feed a decoded text chunk; returns the event blocks that are now complete. + * Any trailing partial frame stays buffered for the next chunk. + * @param {string} chunk + * @returns {string[]} complete event blocks (text between delimiters) + */ + push(chunk) { + this.buffer += chunk; + const blocks = []; + // Normalise CRLF delimiters to LF so a single split rule covers both. + let idx; + // Process every complete frame currently in the buffer. + while ((idx = this._nextDelimiter()) !== -1) { + const block = this.buffer.slice(0, idx.start); + this.buffer = this.buffer.slice(idx.end); + if (block.length > 0) blocks.push(block); + } + return blocks; + } + + /** + * On stream end, return whatever complete-looking content remains. A + * well-behaved backend always terminates the last frame with a blank line, + * so this is usually empty — but if the connection closed mid-trailing-frame + * with a parseable block, surface it rather than dropping data. + * @returns {string[]} + */ + flush() { + const rest = this.buffer.trim(); + this.buffer = ''; + return rest ? [rest] : []; + } + + _nextDelimiter() { + // Find the earliest of "\n\n", "\r\n\r\n", "\r\r". + const candidates = [ + { token: '\r\n\r\n', i: this.buffer.indexOf('\r\n\r\n') }, + { token: '\n\n', i: this.buffer.indexOf('\n\n') }, + { token: '\r\r', i: this.buffer.indexOf('\r\r') }, + ].filter((c) => c.i !== -1); + if (candidates.length === 0) return -1; + candidates.sort((a, b) => a.i - b.i); + const { token, i } = candidates[0]; + return { start: i, end: i + token.length }; + } +} + +/** + * Read an SSE Response body to completion, invoking onEvent for every parsed + * JSON event object. Resolves when the stream ends. Throws if the response is + * not ok or has no readable body (caller shows the error inline). + * + * @param {Response} response a fetch() Response with a streaming body + * @param {(event: object) => void} onEvent called per parsed JSON event + */ +export async function readEventStream(response, onEvent) { + if (!response.ok) { + throw new Error(`server returned ${response.status} ${response.statusText}`); + } + if (!response.body) { + throw new Error('response has no readable body (streaming unsupported)'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + const splitter = new SSEFrameSplitter(); + + const handleBlock = (block) => { + const payload = dataFromEventBlock(block); + if (payload == null || payload.trim() === '') return; + let obj; + try { + obj = JSON.parse(payload); + } catch { + // A malformed frame must not abort an in-progress recovery stream; + // skip it and keep reading. + return; + } + onEvent(obj); + }; + + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + const text = decoder.decode(value, { stream: true }); + for (const block of splitter.push(text)) handleBlock(block); + } + } finally { + reader.releaseLock?.(); + } + // Drain any trailing bytes the decoder held, then any final frame. + const tail = decoder.decode(); + if (tail) { + for (const block of splitter.push(tail)) handleBlock(block); + } + for (const block of splitter.flush()) handleBlock(block); +} diff --git a/frontend/src/lib/sse.test.mjs b/frontend/src/lib/sse.test.mjs new file mode 100644 index 0000000..413433f --- /dev/null +++ b/frontend/src/lib/sse.test.mjs @@ -0,0 +1,152 @@ +// Standalone test of the SSE frame parser — no test framework, just node. +// Run: node src/lib/sse.test.mjs (exits non-zero on any failure) +// +// These pin the protocol described in the API contract: frames are +// `data: {json}\n\n`, the event `kind` is one of session/text/tool/result/ +// error/done, and bytes arrive at arbitrary boundaries via getReader(). +import { SSEFrameSplitter, dataFromEventBlock, readEventStream } from './sse.js'; + +let failures = 0; +function ok(name, cond) { + if (cond) { + console.log(` ok ${name}`); + } else { + failures++; + console.error(`FAIL ${name}`); + } +} +function eq(name, got, want) { + const g = JSON.stringify(got); + const w = JSON.stringify(want); + ok(`${name} (got ${g})`, g === w); +} + +// --- dataFromEventBlock --------------------------------------------------- +eq( + 'extracts JSON payload from a data: line', + dataFromEventBlock('data: {"kind":"text","text":"hi"}'), + '{"kind":"text","text":"hi"}' +); +eq( + 'strips exactly one space after the colon', + dataFromEventBlock('data: leading-space-kept'), + ' leading-space-kept' +); +eq('ignores comment/heartbeat lines', dataFromEventBlock(': keep-alive'), null); +eq( + 'joins multi-line data fields with newline', + dataFromEventBlock('data: line1\ndata: line2'), + 'line1\nline2' +); + +// --- SSEFrameSplitter: whole frames -------------------------------------- +{ + const s = new SSEFrameSplitter(); + const blocks = s.push('data: {"kind":"session","session_id":"abc"}\n\n'); + eq('one complete frame yields one block', blocks, [ + 'data: {"kind":"session","session_id":"abc"}', + ]); +} + +// --- SSEFrameSplitter: multiple frames in one chunk ---------------------- +{ + const s = new SSEFrameSplitter(); + const blocks = s.push( + 'data: {"kind":"text","text":"a"}\n\ndata: {"kind":"text","text":"b"}\n\n' + ); + eq('two frames in one chunk yield two blocks', blocks.length, 2); + eq('first block', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"a"}'); + eq('second block', dataFromEventBlock(blocks[1]), '{"kind":"text","text":"b"}'); +} + +// --- SSEFrameSplitter: frame split across chunks ------------------------- +{ + const s = new SSEFrameSplitter(); + let blocks = s.push('data: {"kind":"te'); + eq('partial frame yields nothing yet', blocks, []); + blocks = s.push('xt","text":"split"}\n\n'); + eq('completing the frame yields it whole', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"split"}'); +} + +// --- SSEFrameSplitter: delimiter split across chunks --------------------- +{ + const s = new SSEFrameSplitter(); + let blocks = s.push('data: {"kind":"done"}\n'); + eq('frame held while delimiter incomplete', blocks, []); + blocks = s.push('\n'); + eq('frame released once blank line completes', dataFromEventBlock(blocks[0]), '{"kind":"done"}'); +} + +// --- SSEFrameSplitter: CRLF delimiters ----------------------------------- +{ + const s = new SSEFrameSplitter(); + const blocks = s.push('data: {"kind":"text","text":"crlf"}\r\n\r\n'); + eq('CRLF-delimited frame parses', dataFromEventBlock(blocks[0]), '{"kind":"text","text":"crlf"}'); +} + +// --- end-to-end via readEventStream over a mock streaming Response -------- +function mockResponse(chunks) { + const enc = new TextEncoder(); + let i = 0; + return { + ok: true, + status: 200, + body: { + getReader() { + return { + read() { + if (i < chunks.length) { + return Promise.resolve({ value: enc.encode(chunks[i++]), done: false }); + } + return Promise.resolve({ value: undefined, done: true }); + }, + releaseLock() {}, + }; + }, + }, + }; +} + +await (async () => { + // A realistic turn, deliberately chopped at ugly boundaries: + // - the session frame split mid-JSON + // - two text frames glued together + // - a tool frame + // - a result frame and the terminal done frame in one chunk + const chunks = [ + 'data: {"kind":"sess', + 'ion","session_id":"S1"}\n\n', + 'data: {"kind":"text","text":"checking "}\n\ndata: {"kind":"text","text":"disk"}\n\n', + 'data: {"kind":"tool","name":"Bash","input":{"command":"df -h"}}\n\n', + 'data: {"kind":"result","is_error":false,"result":"ok","duration_ms":12}\n\ndata: {"kind":"done"}\n\n', + ]; + const events = []; + await readEventStream(mockResponse(chunks), (e) => events.push(e)); + + eq('event count', events.length, 6); + eq('1: session id', events[0], { kind: 'session', session_id: 'S1' }); + eq('2: first text', events[1], { kind: 'text', text: 'checking ' }); + eq('3: second text', events[2], { kind: 'text', text: 'disk' }); + eq('4: tool kind+name', { kind: events[3].kind, name: events[3].name }, { kind: 'tool', name: 'Bash' }); + eq('4: tool command', events[3].input.command, 'df -h'); + eq('5: result', events[4], { kind: 'result', is_error: false, result: 'ok', duration_ms: 12 }); + eq('6: done terminal', events[5], { kind: 'done' }); +})(); + +// malformed frame in the middle must be skipped, not abort the stream +await (async () => { + const chunks = [ + 'data: {"kind":"text","text":"before"}\n\n', + 'data: {this is not json}\n\n', + 'data: {"kind":"done"}\n\n', + ]; + const events = []; + await readEventStream(mockResponse(chunks), (e) => events.push(e)); + eq('malformed frame skipped, stream continues', events.map((e) => e.kind), ['text', 'done']); +})(); + +if (failures) { + console.error(`\n${failures} assertion(s) FAILED`); + process.exit(1); +} +console.log('\nall SSE parser assertions passed'); diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..4a1cbd9 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,9 @@ +import './app.css'; +import App from './App.svelte'; +import { mount } from 'svelte'; + +const app = mount(App, { + target: document.getElementById('app'), +}); + +export default app; diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..4c6b24b --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess(), +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..9510798 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +// The compiled SPA is emitted into the FastAPI app's static dir. FastAPI serves +// app/breakglass/static/index.html at "/" and mounts the directory, so the +// build output must be plain static files (plain Svelte, not SvelteKit). +// +// base: './' makes every asset reference relative, so the bundle loads no +// matter what path the edge proxy mounts the app under. +export default defineConfig({ + plugins: [svelte()], + base: './', + build: { + outDir: '../app/breakglass/static', + emptyOutDir: true, + // Keep the asset graph small and predictable for an air-gapped cluster: + // no remote chunks, no CDN — everything bundled here. + assetsInlineLimit: 0, + }, +}); diff --git a/tests/test_breakglass.py b/tests/test_breakglass.py new file mode 100644 index 0000000..6f21c12 --- /dev/null +++ b/tests/test_breakglass.py @@ -0,0 +1,174 @@ +"""Tests for the breakglass app: verb whitelist, SSE translation, auth, routes.""" +import os + +os.environ.setdefault("API_BEARER_TOKEN", "test-token") + +import pytest +from fastapi.testclient import TestClient + +from app.breakglass import agent_session, pve +from app.breakglass.server import app + + +# --------------------------------------------------------------------------- # +# PVE verb whitelist — the security boundary mirrored client-side. +# --------------------------------------------------------------------------- # + +def test_allowed_verbs_match_host_script(): + assert pve.ALLOWED_VERBS == { + "status", "forensics", "reset", "stop", "start", "cycle" + } + assert pve.MUTATING_VERBS == {"reset", "stop", "start", "cycle"} + assert pve.MUTATING_VERBS < pve.ALLOWED_VERBS + + +@pytest.mark.parametrize("bad", [ + "rm -rf /", "status; rm -rf /", "status 103", "shutdown", "", "STATUS", + "cycle 999", "$(reboot)", "../start", +]) +@pytest.mark.asyncio +async def test_run_verb_rejects_non_whitelisted_without_ssh(bad, monkeypatch): + """A bad verb must be rejected locally — never spawning a subprocess.""" + called = False + + async def _boom(*a, **k): + nonlocal called + called = True + raise AssertionError("ssh must not run for a rejected verb") + + monkeypatch.setattr(pve.asyncio, "create_subprocess_exec", _boom) + result = await pve.run_verb(bad) + assert result["rejected"] is True + assert result["exit_code"] is None + assert called is False + + +@pytest.mark.asyncio +async def test_run_verb_allowed_invokes_ssh_with_bare_verb(monkeypatch): + captured = {} + + class _FakeProc: + returncode = 0 + + async def communicate(self): + return (b"status: running\n", b"") + + async def _fake_exec(*argv, **kwargs): + captured["argv"] = argv + return _FakeProc() + + monkeypatch.setattr(pve.asyncio, "create_subprocess_exec", _fake_exec) + result = await pve.run_verb("status") + assert result["rejected"] is False + assert result["exit_code"] == 0 + assert "running" in result["stdout"] + # The verb is the LAST argv element, passed as a single token (no shell). + assert captured["argv"][-1] == "status" + assert captured["argv"][0] == "ssh" + + +# --------------------------------------------------------------------------- # +# stream-json -> UI event translation (pure function). +# --------------------------------------------------------------------------- # + +def test_translate_init_to_session(): + ev = agent_session.translate_event( + {"type": "system", "subtype": "init", "session_id": "abc"} + ) + assert ev == {"kind": "session", "session_id": "abc"} + + +@pytest.mark.parametrize("noise", [ + {"type": "system", "subtype": "hook_started"}, + {"type": "system", "subtype": "thinking_tokens", "estimated_tokens": 5}, + {"type": "user", "message": {"content": []}}, + {"type": "unknown"}, +]) +def test_translate_drops_noise(noise): + assert agent_session.translate_event(noise) is None + + +def test_translate_assistant_text(): + ev = agent_session.translate_event({ + "type": "assistant", + "message": {"content": [{"type": "text", "text": "checking disk"}]}, + }) + assert ev == {"kind": "text", "text": "checking disk"} + + +def test_translate_assistant_tool_use(): + ev = agent_session.translate_event({ + "type": "assistant", + "message": {"content": [ + {"type": "tool_use", "name": "Bash", "input": {"command": "df -h"}} + ]}, + }) + assert ev["kind"] == "tool" + assert ev["name"] == "Bash" + assert ev["input"]["command"] == "df -h" + + +def test_translate_result(): + ev = agent_session.translate_event({ + "type": "result", "is_error": False, "result": "done", "duration_ms": 1234, + }) + assert ev == {"kind": "result", "is_error": False, "result": "done", "duration_ms": 1234} + + +# --------------------------------------------------------------------------- # +# Routes + auth. +# --------------------------------------------------------------------------- # + +client = TestClient(app) +AUTH = {"Authorization": "Bearer test-token"} + + +def test_health_no_auth(): + r = client.get("/health") + assert r.status_code == 200 + assert r.json()["service"] == "claude-breakglass" + + +def test_api_requires_auth(): + assert client.post("/api/session").status_code == 401 + assert client.get("/api/pve/verbs").status_code == 401 + + +def test_api_accepts_bearer(): + r = client.post("/api/session", headers=AUTH) + assert r.status_code == 200 + assert "session_id" in r.json() + + +def test_api_accepts_authentik_header(): + r = client.post("/api/session", headers={"X-authentik-username": "me@viktorbarzin.me"}) + assert r.status_code == 200 + + +def test_pve_verb_route_rejects_unknown(): + r = client.post("/api/pve/destroy", headers=AUTH) + assert r.status_code == 400 + + +def test_pve_verbs_listing(): + r = client.get("/api/pve/verbs", headers=AUTH) + assert r.status_code == 200 + body = r.json() + assert set(body["verbs"]) == pve.ALLOWED_VERBS + assert set(body["mutating"]) == pve.MUTATING_VERBS + + +def test_chat_streams_sse(monkeypatch): + async def _fake_turn(session_id, prompt, model=None): + yield {"kind": "session", "session_id": session_id} + yield {"kind": "text", "text": "hello"} + yield {"kind": "result", "is_error": False, "result": "ok"} + + monkeypatch.setattr(agent_session, "run_turn", _fake_turn) + r = client.post("/api/chat", headers=AUTH, + json={"session_id": "s1", "prompt": "diagnose"}) + assert r.status_code == 200 + assert "text/event-stream" in r.headers["content-type"] + body = r.text + assert "hello" in body + assert '"kind": "done"' in body # terminal frame always emitted