feat(repos): per-repo dev image — repos.image_ref, digest-pinned on save, pull-if-missing at spawn #217

Merged
dominik.polakovics merged 1 commit from afk/207 into main 2026-07-23 16:14:50 +02:00

Closes #207

Each container repo can now pick the dev image its sessions run in, per the any-image + injected-tools design: nullable repos.image_ref end to end (migration → store → reposvc → API → repo-settings UI), with NULL inheriting the global --container-image — demoted from "required" to an optional default (ADR-0053).

What landed

  • Store: migrations 0018_repo_image_ref.sql (both dialects, goose, parity-checked); Repo.ImageRef *string following the container_memory nullable pattern exactly.
  • internal/imageref (new, stdlib-only): fully-qualified ref parsing (docker.io library/ shorthand normalized, podman-style short names rejected with actionable messages) and tag→digest resolution against the registry v2 API — anonymous, HTTPS-only (no http fallback, token realms must be https, redirects may not leave https), Docker-Content-Digest with a GET+sha256 fallback capped at 4 MiB. Stored form keeps the tag: host/path:tag@sha256:….
  • Pin-on-save: reposvc validates and pins via an injected PinImageRef (production: imageref.Resolver.Pin; nil fails closed — an unpinned ref is never stored). Resolution failures surface verbatim as 400s in the settings form banner.
  • Spawn: refuseContainerSpawn resolves the effective image (repo override else global; neither set → refusal naming both knobs) and new podmanx.EnsureImage runs image exists → one pull on miss — both pre-claim, so a bad or unpullable image refuses the spawn as an actionable 400 and never parks an AFK issue. The pane's RunSpec.Image carries the effective image.
  • Preflight relaxed: an unset --container-image is no longer a startup failure (per-repo-only deployments are valid); the no-effective-image case moved to spawn, where the repo is known.
  • UI: "Dev image" field in repo settings → Runner ('' ↔ null, dirty-only PATCH, server errors in the section banner).
  • Docs: ops.md (flags table, container-runner section, rewritten dev-image expectations), CONTEXT.md glossary + relationships, and ADR-0053 recording pin-on-save vs pin-at-spawn, https/anonymous-only, preflight relaxation, and pull-at-spawn vs pre-pull-at-save.

Acceptance criteria → proof

  • round-trip schema→API→UI, NULL → default: TestRepoImageRefColumnRoundTrip (both dialects), TestRepoImageRef (httpapi), Runner.test.tsx; fallback in TestRefuseContainerSpawn_effectiveImage.
  • digest-pinned on save, non-digest resolved: internal/imageref suite (58 tests, incl. Bearer token flow, GET+hash fallback, https-realm rejection), TestUpdateSettingsImageRefPinsOnSave.
  • spawns use the repo image, missing/unpullable → actionable error: TestStart_containerImageResolution (override drives pane argv + EnsureImage; pull failure → BadRequestError pre-claim, no worktree/run row), TestEnsureImage.
  • image contract documented: ops.md "Dev image expectations" incl. the read-only future-imports note; ADR-0053.
  • validation/fallback/spawn-wiring tests: above, plus preflight tests proving an unset global image is no longer a failure.

Verification

go test ./... (44 pkgs), go vet, gofmt, golangci-lint run (0 issues), go build ./...; web: tsc --noEmit, eslint clean, vitest run 933/933, vite build. The full container loop remains maintainer-validated on the dev host (ADR-0052 §consequences — the sandbox has no rootless podman).

Notes for review

  • "read-only future-imports note": the phrase appears only in the issue, so I pinned an interpretation consistent with ADR-0051/0052's language — everything lab injects arrives as a read-only mount (today /opt/lab) and future features will import additional content the same way, so images must treat lab's mount points as reserved and never depend on writing to them (ops.md + ADR-0053 pin 7). Flag if this meant something else.
  • Registry auth is out of scope per the brief (anonymous/public-pull only), noted in ops.md and ADR-0053.
  • A cold pull blocks the spawn request for the download's duration — the documented trade of pull-at-spawn (ADR-0053); subsequent spawns hit image exists.

🤖 Generated with Claude Code

Closes #207 Each container repo can now pick the dev image its sessions run in, per the any-image + injected-tools design: nullable `repos.image_ref` end to end (migration → store → reposvc → API → repo-settings UI), with NULL inheriting the global `--container-image` — demoted from "required" to an optional default (ADR-0053). ## What landed - **Store**: migrations `0018_repo_image_ref.sql` (both dialects, goose, parity-checked); `Repo.ImageRef *string` following the `container_memory` nullable pattern exactly. - **`internal/imageref`** (new, stdlib-only): fully-qualified ref parsing (docker.io `library/` shorthand normalized, podman-style short names rejected with actionable messages) and tag→digest resolution against the registry v2 API — anonymous, **HTTPS-only** (no http fallback, token realms must be https, redirects may not leave https), `Docker-Content-Digest` with a GET+sha256 fallback capped at 4 MiB. Stored form keeps the tag: `host/path:tag@sha256:…`. - **Pin-on-save**: reposvc validates and pins via an injected `PinImageRef` (production: `imageref.Resolver.Pin`; nil fails closed — an unpinned ref is never stored). Resolution failures surface verbatim as 400s in the settings form banner. - **Spawn**: `refuseContainerSpawn` resolves the effective image (repo override else global; neither set → refusal naming both knobs) and new `podmanx.EnsureImage` runs `image exists` → one `pull` on miss — both **pre-claim**, so a bad or unpullable image refuses the spawn as an actionable 400 and never parks an AFK issue. The pane's `RunSpec.Image` carries the effective image. - **Preflight relaxed**: an unset `--container-image` is no longer a startup failure (per-repo-only deployments are valid); the no-effective-image case moved to spawn, where the repo is known. - **UI**: "Dev image" field in repo settings → Runner ('' ↔ null, dirty-only PATCH, server errors in the section banner). - **Docs**: ops.md (flags table, container-runner section, rewritten dev-image expectations), CONTEXT.md glossary + relationships, and ADR-0053 recording pin-on-save vs pin-at-spawn, https/anonymous-only, preflight relaxation, and pull-at-spawn vs pre-pull-at-save. ## Acceptance criteria → proof - round-trip schema→API→UI, NULL → default: `TestRepoImageRefColumnRoundTrip` (both dialects), `TestRepoImageRef` (httpapi), `Runner.test.tsx`; fallback in `TestRefuseContainerSpawn_effectiveImage`. - digest-pinned on save, non-digest resolved: `internal/imageref` suite (58 tests, incl. Bearer token flow, GET+hash fallback, https-realm rejection), `TestUpdateSettingsImageRefPinsOnSave`. - spawns use the repo image, missing/unpullable → actionable error: `TestStart_containerImageResolution` (override drives pane argv + EnsureImage; pull failure → `BadRequestError` pre-claim, no worktree/run row), `TestEnsureImage`. - image contract documented: ops.md "Dev image expectations" incl. the read-only future-imports note; ADR-0053. - validation/fallback/spawn-wiring tests: above, plus preflight tests proving an unset global image is no longer a failure. ## Verification `go test ./...` (44 pkgs), `go vet`, `gofmt`, `golangci-lint run` (0 issues), `go build ./...`; web: `tsc --noEmit`, `eslint` clean, `vitest run` 933/933, `vite build`. The full container loop remains maintainer-validated on the dev host (ADR-0052 §consequences — the sandbox has no rootless podman). ## Notes for review - "read-only future-imports note": the phrase appears only in the issue, so I pinned an interpretation consistent with ADR-0051/0052's language — everything lab injects arrives as a read-only mount (today `/opt/lab`) and future features will import additional content the same way, so images must treat lab's mount points as reserved and never depend on writing to them (ops.md + ADR-0053 pin 7). Flag if this meant something else. - Registry auth is out of scope per the brief (anonymous/public-pull only), noted in ops.md and ADR-0053. - A cold pull blocks the spawn request for the download's duration — the documented trade of pull-at-spawn (ADR-0053); subsequent spawns hit `image exists`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(repos): per-repo dev image — nullable repos.image_ref, pinned on save, pulled at spawn
All checks were successful
ci / native (pull_request) Successful in 7m51s
f4d5d74b72
Each container repo can now name the OCI image its sessions run in
(repo settings → Runner, "Dev image"; PATCH field image_ref), with NULL
inheriting the global --container-image, demoted to an optional default
(ADR-0053).

- migrations 0018 (sqlite+postgres): nullable repos.image_ref TEXT,
  threaded through Repo/CreateRepo/RepoSettingsUpdate/scanRepo.
- new internal/imageref (stdlib-only): fully-qualified ref parsing
  (docker.io library/ shorthand kept), and tag→digest resolution against
  the registry v2 API — anonymous, HTTPS-only (no http fallback, https
  token realms, https-only redirects), Docker-Content-Digest with a
  GET+sha256 fallback capped at 4 MiB.
- reposvc pins on save via injected PinImageRef (nil fails closed —
  never stores unpinned); resolution errors surface verbatim as 400s in
  the settings form. httpapi exposes the nullable field.
- spawn: refuseContainerSpawn resolves the effective image (repo
  override else global; neither → refusal naming both knobs) and
  podmanx.EnsureImage pulls-if-missing — both pre-claim, so a bad image
  never parks an AFK issue; pull failures are actionable 400s.
- preflight no longer fails on an unset --container-image: a per-repo-
  only deployment is valid; the no-image case is a spawn refusal now.
- docs: ops.md (flags table, container-runner section, dev-image
  expectations incl. the read-only future-imports note), CONTEXT.md
  glossary, ADR-0053.

Closes #207

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS. CI green (ci/native success, 7m51s). Conventional Commits title + Closes #207 present. Diff matches issue #207 end to end: nullable repos.image_ref (migration both dialects → store → reposvc pin-on-save → httpapi → Runner UI), internal/imageref https-only anonymous tag→digest resolver, spawn-time EnsureImage pull-if-missing pre-claim, preflight relaxation, ADR-0053 + docs. Fail-closed nil-pinner, pre-claim 400 refusals, dialect-parity migrations. No out-of-scope changes.

[autoland] verdict: pass PASS. CI green (ci/native success, 7m51s). Conventional Commits title + Closes #207 present. Diff matches issue #207 end to end: nullable repos.image_ref (migration both dialects → store → reposvc pin-on-save → httpapi → Runner UI), internal/imageref https-only anonymous tag→digest resolver, spawn-time EnsureImage pull-if-missing pre-claim, preflight relaxation, ADR-0053 + docs. Fail-closed nil-pinner, pre-claim 400 refusals, dialect-parity migrations. No out-of-scope changes.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!217
No description provided.