src/handler/api/static/ was a committed build artifact: Next's content-hashed
chunk names churn on every build, so any two branches touching frontend/ were
guaranteed merge conflicts there, PR diffs drowned in generated churn, and a
forgotten `npm run export` could silently ship a UI older than its source.
- gitignore the export (plus frontend/out and .next were already covered) and
remove the 52 tracked files.
- Dockerfile grows a `ui` stage (npm ci + npm run build) whose output is copied
into the packaged tree before pip install, so the image published by docker.yml
always carries a UI built from exactly that commit's source — the frontend
build is now effectively part of CI with no new workflow.
- .dockerignore excludes frontend artifacts and any stale local export: COPY
into src/handler/api/static merges, so a checkout copy must never leak in.
- pyproject: hatchling skips VCS-ignored files, so `artifacts` re-includes the
export when present; absent it, the wheel builds fine and the API just runs
headless (it only mounts static/ when the directory exists).
- README documents the two build paths (Docker stage vs `npm run export` for
source installs) and the headless fallback.
Verified: wheel with the export present ships all 52 files (memory page
included); wheel without it builds clean and create_app() skips the UI mount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqkoYPX8NAo1V2KyXr1pk
Wires the phase-1 headless machinery behind runner=headless (default
stays tmux; legacy agents, session_id null, keep the tmux paths):
- spawn: branches tmux vs headless.launch; extracts _agent_env (shared
with resume - a headless resume is a new process needing identity/
credential env); headless spawns require a task (no idle-REPL mode),
enforced at spawn and as a 400 in the API
- resume: headless path materializes the session archive from the DB
onto whichever worker claimed the command, then claude -p --resume;
falls back to a fresh session with DB-re-injected context (visible
worker event) when no transcript survives anywhere; refuses while a
run is live. Undeliverable resumes now raise -> command FAILED,
fixing silent input loss on both runners
- kill: headless path flags cancel_requested; the owning supervisor
SIGTERMs its own child (cross-worker safe)
- worker: stable per-container ids, DB-driven run slots (full workers
skip claiming spawn/resume/mise_init, leaving them for less-loaded
workers), credsync refresh in the main loop
- settings_gen: permissions block (defaultMode + allowlist) for
headless runs - -p auto-denies anything that would prompt; hooks
remain the hard gate
- credsync + migration 0009 (runtime_secrets): login publishes the
Fernet-encrypted claude credential bundle; every worker materializes
it (merge-safe for local trust state); login_submit pinned to the
login_start worker via commands.target_worker
Suite 270 -> 290 green, including the cross-worker resume linchpin
(clean-HOME materialize + --resume against the fake binary).
Two changes so an operator can stand up and authenticate Handler entirely
from the browser, with a self-contained control image.
Bundle executables in the control image (Dockerfile.control)
- Node.js (NodeSource) + the Claude Code CLI, mise (official apt repo), and
forge (git-pkgs/forge, built in a Go stage) join the existing git/tmux/ssh.
No more bring-your-own binaries: live agent spawning, the verification gate,
CI resolution, and the login flow all work out of the box. Installed under
/usr so the /var/lib/handler VOLUME never masks them; mise apt source pinned
to $TARGETARCH for the multi-arch (amd64/arm64) build.
Claude login from the web UI
- New login_start / login_submit command types (migration 0005) drive the
interactive `claude /login` through the same enqueue→worker handoff every
other control action uses — the API container has no claude binary.
- control/login.py opens `claude` in a dedicated tmux session, sends /login,
selects the subscription account, and scrapes the claude.com authorization
URL (tmux.capture_pane, -pJ so a wrapped URL rejoins); a second command feeds
back the pasted code. Fully mockable via the tmux seam.
- API: POST /login/start, POST /login/submit (admin-gated).
- Dashboard: a "Claude Login" pane — a button that starts the flow, embeds the
URL in an iframe (with a new-tab fallback, since claude.com may refuse
framing), and takes the code to finish.
Also un-ignores frontend/lib/ (a broad Python `lib/` rule was swallowing the
UI's own api client + formatters, breaking rebuilds from a fresh clone) and
reconstructs those two source files; rebuilt static export committed.
Tests: control/login unit tests (tmux faked), worker dispatch, and API route
tests. Full suite green (195 tests), ruff clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:
- Credential resolution/injection (control/credentials.py): credential_ref pointers
(env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
forge-host-scoped git credential helper reading the token from env (never on disk / in
the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
`handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
`approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).
Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.