Skip to content

fix(nesting): fetch the smoke image at build time, materialise it at deploy (the sandbox boundary) - #15

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/build-time-oci-fetch
Oct 20, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/build-time-oci-fetch

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

Head e49d7e736cc23b136ab8d0995e950a06bb42899e.

The build-time pre-pull cannot populate the nested store on a sandboxed BuildKit worker: the sandbox denies unshare(CLONE_NEWUSER), so podman pull dies at startup with Error determining cgroups mode and skopeo copy … containers-storage: dies with Error during unshare(CLONE_NEWUSER): Operation not permitted. Any box building this candy fails that step (charly#991 — the distro-cachyos box/crabbox-e2e build died there twice).

The candy now splits the work:

phase step command
build (root) the fetch mkdir -p /usr/share/charly/nesting-smoke && skopeo copy docker://quay.io/libpod/alpine:latest dir:/usr/share/charly/nesting-smoke — a pure image fetch needs no user namespace, so it runs on the sandboxed worker
build check nesting-smoke-layout file: /usr/share/charly/nesting-smoke/manifest.json exists
deploy (${USER}, context: [runtime]) nesting-smoke-materialise skopeo copy dir:/usr/share/charly/nesting-smoke containers-storage:quay.io/libpod/alpine:latest — the container's own user namespace is available there

The runtime copy has no network dependency (the bytes are baked), so a host whose outer container cannot resolve quay.io still gets a warm nested store, and nested-podman-run (podman run --pull=never) passes.

The skill (.agents/skills/container-nesting/SKILL.md) gains a "Build-time store population (the sandbox boundary)" section with this RCA and the fetch/materialise table; its /tmp section now records that the old pre-pull was the trigger and is retired.

How tested

The changed build step ran live in the box that was blocked: distro-cachyos's box/crabbox-e2e, built with this branch's candy through CHARLY_REPO_OVERRIDE=github.com/opencharly/layer-container-nesting=<this worktree>:

$ charly box build crabbox-e2e
rc=0                                  # the image-build step that died at "Error determining cgroups mode" now passes
$ charly check run check-crabbox-e2e-pod
charly check run check-crabbox-e2e-pod: [image-build] START (charly box build; log: .check/check-crabbox-e2e-pod/2026.293.1700/image-build.log)
charly check run check-crabbox-e2e-pod: [feature-run-rebuild] PASS after 36.695s (log: .check/check-crabbox-e2e-pod/2026.293.1700/feature-run-rebuild.log)
charly check run check-crabbox-e2e-pod: PASS (steps=16)
rc=0

The runtime materialise ran live in the bed's kept container (a disposable: true target, run via charly shell), after removing the bed's own copy so the step's effect is visible:

$ podman rmi -f quay.io/libpod/alpine:latest
$ podman images --format '{{.Repository}}:{{.Tag}}' | grep -c alpine
0
$ skopeo copy dir:/usr/share/charly/nesting-smoke containers-storage:quay.io/libpod/alpine:latest
Copying config sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4
Writing manifest to image destination
copy rc=0
$ podman run --rm --pull=never quay.io/libpod/alpine:latest /bin/true
nested rc=0

The plan's own gate (charly check box crabbox-e2e) parses and orders every step: 120 steps: 90 passed, 0 failed, 30 skipped — the skips are the build/runtime-context-gated steps, which the two runs above cover live.

Rulebook compliance

  • R0 — /charly-distros:container-nesting and /charly-image:layer were read; the change edits the candy and the skill together, per this repo's AGENTS.md.
  • R1 — the failure (the sandbox's unshare denial) is RCA'd in the skill section; no warning or skip hides it. The residual /tmp-corruption trigger is recorded as retired.
  • R2 — one fetch path and one materialise path; the old podman pull is deleted, not kept alongside.
  • R3 — no retry, no sleep, no suppression: the steps are deterministic skopeo calls.
  • R4 — the product works with only the charly binary (the candy's plan steps), so the docs don't route around a defect.
  • R5 — the build-time podman pull is gone, proven by the diff.
  • R6 — git status clean; the work happened in a session worktree; no force-push.
  • R7 — the changed build path executes live (the box that was blocked), and the changed runtime path executes live (the kept bed's container, copy rc=0 + nested rc=0).
  • R8 — no live service boundary; the nested podman runs are real, not mocked.
  • R9 — the runs used the worktree-local binary (charly 2026.293.1538).
  • R10 — the affected disposable: true target is check-crabbox-e2e-pod; it ran green on the built tree, and the runtime step ran in its kept container.

Change classification

Candy plan + skill (runtime-visible behavior). Attribution tier: fully tested and validated — the blocked build now passes, the full e2e bed is green, and both changed paths executed live on a disposable target.

Filed issues

  • charly#991 — the sandbox/unshare build blocker this PR fixes at the candy's source; the distro-cachyos consumer pins can now carry the 0.79.0 e2e composition.

Agent: dsh-crabbox-079
Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)

…deploy (the sandbox boundary)

The build-time `podman pull` cannot work under a sandboxed BuildKit worker:
the sandbox denies `unshare(CLONE_NEWUSER)` to build processes, so podman
dies at startup with `Error determining cgroups mode`, and skopeo's
containers-storage path dies with `Error during unshare(CLONE_NEWUSER)`.

The candy now splits the work:

- build time (root): `skopeo copy docker://quay.io/libpod/alpine:latest
  dir:/usr/share/charly/nesting-smoke` — a pure image fetch needs no user
  namespace, so it runs on the sandboxed worker;
- a new `nesting-smoke-layout` check asserts the baked OCI layout;
- deploy time (`${USER}`, `context: [runtime]`): `skopeo copy
  dir:/usr/share/charly/nesting-smoke
  containers-storage:quay.io/libpod/alpine:latest` — the container's own
  user namespace is available there.

The runtime copy has no network dependency (the bytes are baked), so a host
whose outer container cannot resolve `quay.io` still gets a warm nested
store, and `nested-podman-run` (`podman run --pull=never`) passes.

Agent: dsh-crabbox-079
Assisted-by: DeepSeek Harness deepseek-v4.1-flash (fully tested and validated)
@github-actions

Copy link
Copy Markdown

Review — PASS

Head SHA: e49d7e736cc23b136ab8d0995e950a06bb42899e (branch: feat/build-time-oci-fetch)

Security screen: PASS — no secrets/egress, no guardrail weakening, public image fetch only.
Checklist: All applicable rules PASS or NA (A1–A3, R0–R10, CONC, HC, ARCH, DISP, QUAL all satisfied; no comments to intake).

Non-blocking notes: the skill/body name the two run steps nesting-smoke-fetch / nesting-smoke-materialise, but only the check carries a matching id: (nesting-smoke-layout) — the run-step labels are descriptive, not plan ids (minor doc/code naming divergence). Evidence is all from the distro-cachyos bed (the strictest sandboxed case); root-path consumers are covered by the argument that the sandboxed drive is the superset-hard case — worth a root-path bed on the next touch.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant