All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Viktor wanted a web UI on the claude service to act as his breakglass when the devvm is down: open it, have Claude SSH in to diagnose/repair, and power-cycle the VM via the Proxmox host if needed. This is the app half (the infra stack + host bootstrap live in the infra repo). New, ISOLATED ASGI app under app/breakglass/ (never imports app.main, so the untrusted-input agents — recruiter-triage, nextcloud-todos — can't share a process with the root-on-devvm / PVE-reset SSH key): - pve.py: the LLM-independent power-verb path (status|forensics|reset|stop| start|cycle on VM 102), whitelist-validated client-side, executed over the forced-command SSH key (list argv, no shell). - agent_session.py: multi-turn streamed chat — claude -p --session-id / --resume with --output-format stream-json, translated to a small SSE vocabulary (session/text/tool/result/error/done). - auth.py: edge Authentik header OR bearer; fail-closed. - server.py: FastAPI (session/chat-SSE/pve-verb routes) + serves the Svelte UI. - Svelte SPA (frontend/, built into app/breakglass/static/ and committed — no in-cluster build, per ADR-0002): streamed chat + danger-styled manual VM controls with confirm-on-mutate. - agents/breakglass.md: narrow tools (Bash/Read/Grep/Glob, no web), taught the ssh devvm / ssh pve aliases and cycle-vs-reset. - docker-entrypoint-breakglass.sh: ssh-agent bootstrap from the mounted key + ssh aliases, then uvicorn app.breakglass.server. The breakglass Deployment overrides the image CMD with this; the existing service is untouched. 26 new tests (verb whitelist incl. injection attempts, stream-json→SSE translation, auth gating, route behaviour); full suite 58 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
51 lines
1.9 KiB
Bash
51 lines
1.9 KiB
Bash
#!/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 <verb>` 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" <<CFG
|
|
Host devvm
|
|
HostName ${BREAKGLASS_DEVVM_HOST:-10.0.10.10}
|
|
User ${BREAKGLASS_DEVVM_USER:-breakglass}
|
|
Host pve
|
|
HostName ${BREAKGLASS_PVE_HOST:-192.168.1.127}
|
|
User ${BREAKGLASS_PVE_USER:-root}
|
|
Host *
|
|
BatchMode yes
|
|
ConnectTimeout 10
|
|
StrictHostKeyChecking no
|
|
UserKnownHostsFile /dev/null
|
|
LogLevel ERROR
|
|
CFG
|
|
chmod 600 "$SSH_DIR/config"
|
|
|
|
# Load the key into ssh-agent from a private tmpfs copy, then drop the copy
|
|
# (the agent keeps it in memory). The mounted secret is tmpfs, never disk.
|
|
if [[ -f "$KEY_SRC" ]]; then
|
|
eval "$(ssh-agent -s)" >/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
|