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>
36 lines
1.3 KiB
Python
36 lines
1.3 KiB
Python
"""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")
|