anvilsign in

collin/anvil · 6410f75b

Record remote-runner status and next steps in CLAUDE.md

Collin Richards · 2026-08-24 08:14 UTC · 6410f75b6580db7498455d44c9c491a0d26f5fcd · parent 35ea3553 · browse files

modifiedCLAUDE.md+56 −0
⋯ 54 unchanged lines
5555
5656 note that we can't use portless for this app because of a websocets bug with http/2
5757
58+## Remote runners: where this stands
59+
60+anvil does **not** execute CI. Runners dial out, claim jobs, and run them on
61+their own Docker daemon. Design and rationale: `docs/remote-runners.md`.
62+
63+M1 is done: `anvil-job` (wire format), `anvil-docker` (connect/ensure_image),
64+`anvil-worker` (the runner binary), five endpoints under `/-/runner/`,
65+in-memory leases, and no Docker socket on the deployed container.
66+
67+**Invariants to not break.** These are the reasons the split is shaped the way
68+it is, and each is easy to undo by accident:
69+
70+- The runner never parses `.anvil/ci.yml`. Image resolution, the allowlist
71+ check and script assembly happen in `build_job`, so a runner cannot widen
72+ what it is permitted to run.
73+- `store_artifact` stays server-side. The runner uploads a raw tar; `browse`
74+ decides whether that becomes a servable directory tree, which is anvil's
75+ call, not a runner's.
76+- A job's secrets live on its lease, not the vault, once dispatched.
77+ `Vault::take` fails after the repo's unlock TTL lapses, so re-reading at
78+ finish time would silently skip log masking on exactly the long runs whose
79+ logs most need it.
80+- `anvil-job` depends on serde and nothing else. It exists so the runner does
81+ not link toasty, SQLite and gix.
82+
83+**Next, in order:**
84+
85+1. **M2, platform.** The plumbing is live end to end (`JobSpec.platform` reaches
86+ both bollard option structs; runners advertise their native platform on
87+ claim). What is missing is a source: a `platform:` key in `.anvil/ci.yml`,
88+ probably a `[ci] platform` default, and routing a job to a runner that has
89+ that architecture natively. This is what stops arm64 runners testing code
90+ that ships as amd64.
91+2. **M3, the deploy agent.** Image builds cannot be CI jobs, because job
92+ containers get no Docker socket by design. `docker build --platform
93+ linux/amd64` plus `docker push` belongs to a separate process on the build
94+ host at a different trust level, triggered by the existing `deploy_webhook`.
95+ Folding it into the dial-out channel as a privileged "publish" job kind,
96+ authorized by the `is_deploy_target` check that already scopes CD to one
97+ repo, would remove the last inbound path to the build host.
98+3. **CI concurrency.** "One job at a time" was a property of the old in-process
99+ loop and is gone. Nothing bounds in-flight jobs now beyond how many runners
100+ exist. Needs a `max_concurrent` equivalent.
101+4. **Per-runner credentials.** One shared `[ci] runner_token` means one
102+ revocation for every runner, no `last_used_at`, and any holder can claim any
103+ job and receive its secrets. Wants the API-token write scope first (see
104+ TODO.md).
105+5. **Live logs.** The result POST is a single write, matching the old
106+ behaviour. Streaming needs chunked append with offsets and a UI that
107+ tolerates gaps. Newly worth doing now that a producer exists.
108+
109+**Operationally:** nothing runs until a runner is started and `[ci]
110+runner_token` is set; queued runs just sit, with a warning logged at startup.
111+Agent sessions still drive Docker locally and are the one thing the dropped
112+socket mount gives up. They are off by default.
113+
58114 Current status, resume notes, the agreed next steps (a/b/c), and the roadmap live
59115 in the TODO. Read it first:
60116
⋯ 1 unchanged line