The arm64 control image build failed installing mise: it pulled the amd64
.deb (dpkg "architecture (amd64) does not match system (arm64)"). Cause: the
`ARG TARGETARCH=amd64` default overrode the per-platform value buildx injects,
so the arm64 stage wrote an amd64 apt source. Derive the arch from
`dpkg --print-architecture` (the image's own arch) instead — correct on both
platforms and independent of buildx arg wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
The control image build failed: forge v0.6.0 requires Go >= 1.26 but the
forge-builder stage pinned golang:1.22 (GOTOOLCHAIN=local, so no auto-fetch).
Track the latest stable Go via golang:1-bookworm and set GOTOOLCHAIN=auto so a
future forge release needing a newer toolchain resolves itself.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
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
Make credentials/hosts, projects, agents, and approvals manageable from the
dashboard. The API and control layer are separate containers, so the API can't
run control actions directly (no git/tmux/claude, doesn't own the tmux
sessions). Instead the API enqueues a command and a worker in the control
container executes it and writes the result back.
Data model (migration 0003):
- `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a
nullable approver id + `actor` so operator verdicts are first-class.
Control worker:
- `control/worker.py` claims commands and dispatches to the existing control
functions (spawn/kill/resume/record_approval/write_skills/poller.sweep),
plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the
control image's default command (subsumes `poll-ci --watch`).
API:
- `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent
spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the
API tried to send tmux keys to a session in the control container); new
approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints.
Credentials/hosts:
- host->token-env lookup consults the `forge_hosts` registry first (built-in
map is the fallback); `resolve()` refactored to a scheme dispatch reserving
`db:` for a future encrypted store. Web input restricts credential_ref to
env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands).
Dashboard:
- New tabs for projects, agents (spawn/kill with live command-status polling),
approvals, hosts, and an activity/audit view; shared context is now writable.
Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating,
cmd: rejection, host-aware credentials, and an API->queue->worker->spawn
end-to-end). README gains a Web management section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
Add a second image for the handler control CLI (spawn/list/kill, approve/
reject, forge-init, CI poller) alongside the existing API image. It shares
the package, database, and /var/lib/handler volume but runs the control
process instead of uvicorn.
- Dockerfile.control: git + tmux baked in for live spawning; default CMD is
the `poll-ci --watch` loop; RUN_MIGRATIONS toggle reuses docker-entrypoint.sh.
- docker-control.yml: builds/pushes ghcr.io/<repo>/control (multi-arch),
scoped gha cache so it doesn't clobber the API build.
- docker-compose.yml: new `control` service, RUN_MIGRATIONS=false, depends on
the API (which owns migrations) being healthy.
- README: Containers section documenting both images and compose usage.