Skip to content

fix(nesting)!: drop the retired builder-selector env var from the candy - #17

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/build-engine-16
Oct 29, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/build-engine-16

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Summary

Head 71762b829b058f041f91ebcd64e3487b7195f758 (base 7d31a504a8404f23d433e683532eb7875718a23b).

The container-nesting candy's env: map still set the retired builder selector. The contract retired it: EngineConfig.Build, ResolvedRuntime.BuildEngine and the CHARLY_BUILD_ENGINE key are gone from spec/hostenv, and a leftover engine.build: docker is INERT (spec/hostenv/retired_build_selector_test.go). The variable therefore did nothing in every image this candy bakes — it was a name in a map, not a knob.

One cutover, three surfaces in this repo:

surface what changed
charly.yml the line is deleted from the container-nesting: candy's env: map
.agents/skills/container-nesting/SKILL.md the Env row of the property table
README.md the Environment row of the property table

What stays, deliberately: CHARLY_RUN_ENGINE=podman (the RUN engine — live) and the hidden pair _CONTAINERS_USERNS_CONFIGURED="" + BUILDAH_ISOLATION=chroot (the two contracts that make rootless-in-rootless podman work; both are asserted by the candy's own baked check: steps). BUILDAH_ISOLATION configures the NESTED podman/buildah inside the container — a different mechanism from charly's retired builder selector, and it is NOT in scope here.

The claim-keyed grep over the tree returns nothing outside CHANGELOG/:

$ git grep -n "CHARLY_BUILD_ENGINE" HEAD -- .
$ echo $?
1

The corpus projection (opencharly/marketplace/distros/skills/container-nesting/SKILL.md) and the docs recipe (opencharly/docs/src/content/docs/recipes/distros/container-nesting.md) still carry the old row. Both are GENERATED projections of this repo's skill body: they are fixed by the corpus regeneration after this repo tags, never by hand-editing them.

How tested

The change class is candy config (charly.yml's env: map) plus prose: the env: map is runtime-visible (it lands in the emitted image's ENV block and in the ai.opencharly.env_candy OCI label), so the gate is the pre-flight plus the bed whose image stacks this candy.

1 — Pre-flight on the committed tree

$ charly box validate
charly box validate: OK — checked 1 candy, 7 distros, 7 builders; 0 warnings, 0 errors

2 — The claim, and the proof that it fails without the change

The retired name on the parent tree (3 hits, the claim):

$ git grep -n "CHARLY_BUILD_ENGINE" 7d31a504a8404f23d433e683532eb7875718a23b -- .
7d31a50:.agents/skills/container-nesting/SKILL.md:37:| Env | `CHARLY_BUILD_ENGINE=podman`, `CHARLY_RUN_ENGINE=podman`, `_CONTAINERS_USERNS_CONFIGURED=""`, `BUILDAH_ISOLATION=chroot` |
7d31a50:README.md:30:| Environment | `CHARLY_BUILD_ENGINE=podman`, `CHARLY_RUN_ENGINE=podman`, `_CONTAINERS_USERNS_CONFIGURED=""`, `BUILDAH_ISOLATION=chroot` |
7d31a50:charly.yml:143:            CHARLY_BUILD_ENGINE: "podman"

On the committed tree: no hits (exit 1).

3 — The emitted artifact, at its real boundary

The generated Containerfile of the box that composes this candy (opencharly/distro-cachyos's crabbox-e2e), generated from the parent tree and from this tree — the diff is exactly the retired name and nothing else:

$ diff -u CF.before CF.after
@@ -38,7 +38,6 @@
 # Layer environment variables
 ENV BUILDAH_ISOLATION="chroot"
-ENV CHARLY_BUILD_ENGINE="podman"
 ENV CHARLY_RUN_ENGINE="podman"
 ENV NODE_ENV="production"
 ENV NPM_CONFIG_PREFIX="/home/user/.npm-global"
@@ -394,7 +393,7 @@
-LABEL ai.opencharly.env_candy='{"BUILDAH_ISOLATION":"chroot","CHARLY_BUILD_ENGINE":"podman","CHARLY_RUN_ENGINE":"podman","NODE_ENV":"production",…}'
+LABEL ai.opencharly.env_candy='{"BUILDAH_ISOLATION":"chroot","CHARLY_RUN_ENGINE":"podman","NODE_ENV":"production",…}'

4 — The R10 bed, live, on the committed tree

check-crabbox-e2e-pod in opencharly/distro-cachyos — the disposable bed whose image stacks this candy. The candy was taken from THIS tree through CHARLY_REPO_OVERRIDE, so the changed env map is what the image was built from.

$ export CHARLY_REPO_OVERRIDE=github.com/opencharly/layer-container-nesting=<this worktree>
$ export CHARLY_BUILD_REGISTRY_MIRROR=127.0.0.1:5000
$ charly check run check-crabbox-e2e-pod
[image-build] PASS after 1m6.56s
[check-image] PASS after 40.812s
[deploy-add] PASS after 17.772s
[config] PASS after 4.733s
[start] PASS after 3.736s
[wait-ready] PASS after 1.545s
[bring-up-members] PASS after 44ms
[check-live] PASS after 47.572s
[feature-run] PASS after 14.172s
[update] PASS after 34.946s
[rebuild-ready] PASS after 25.68s
[check-live-rebuild] PASS after 41.405s
[wait-ready] PASS after 55.317s
[feature-run-rebuild] PASS after 15.88s
[cleanup] PASS after 1.861s
[cleanup-members] PASS after 0s
charly check run check-crabbox-e2e-pod: PASS (steps=16, skipped_checks=34)
$ echo $?
0

The run's own summary (.check/check-crabbox-e2e-pod/2026.302.0824/summary.yml) — every step ok: true, status: PASS:

bed: check-crabbox-e2e-pod
calver: 2026.302.0824
driver_version: 2026.302.0357
steps:
  - name: image-build        duration_seconds: 67  ok: true  status: PASS
  - name: check-image        duration_seconds: 41  ok: true  status: PASS
  - name: deploy-add         duration_seconds: 18  ok: true  status: PASS
  - name: config             duration_seconds: 5   ok: true  status: PASS
  - name: start              duration_seconds: 4   ok: true  status: PASS
  - name: wait-ready         duration_seconds: 2   ok: true  status: PASS
  - name: bring-up-members   duration_seconds: 0   ok: true  status: PASS
  - name: check-live         duration_seconds: 48  ok: true  status: PASS
  - name: feature-run        duration_seconds: 14  ok: true  status: PASS
  - name: update             duration_seconds: 35  ok: true  status: PASS
  - name: rebuild-ready      duration_seconds: 26  ok: true  status: PASS
  - name: check-live-rebuild duration_seconds: 41  ok: true  status: PASS
  - name: wait-ready         duration_seconds: 55  ok: true  status: PASS
  - name: feature-run-rebuild duration_seconds: 16 ok: true status: PASS
  - name: cleanup            duration_seconds: 2   ok: true  status: PASS
  - name: cleanup-members    duration_seconds: 0   ok: true  status: PASS

The changed candy's own baked checks, in the image the bed built (from summary.yml):

plan:candy:github.com/opencharly/layer-container-nesting:2  PASS  — the podman container engine is installed for in-container use
plan:candy:github.com/opencharly/layer-container-nesting:3  PASS  — the buildah image-build tool is installed
plan:candy:github.com/opencharly/layer-container-nesting:4  PASS  — the skopeo image-copy tool is installed
plan:candy:github.com/opencharly/layer-container-nesting:5  PASS  — containers.conf pins userns=host so the inner /proc mount is not rejected by the kernel mount_too_revealing check
plan:candy:github.com/opencharly/layer-container-nesting:6  PASS  — policy.json is present so podman pull/run accepts an image signature policy
plan:candy:github.com/opencharly/layer-container-nesting:7  PASS  — newuidmap carries the cap_setuid file capability so a nested user namespace can be delegated
plan:candy:github.com/opencharly/layer-container-nesting:9  PASS  — /etc/subuid declares the username:start:count delegation ranges the inner podman maps from
plan:candy:github.com/opencharly/layer-container-nesting:10 SKIP  — a fresh rootless `podman run` of an inner container succeeds end-to-end … (agent step, no grader bound)

The 34 skipped checks are the bed's declared checks that "did not run" for a structural reason (agent step (no grader bound), context [runtime] not active in box mode) — the bed's own idiom, named in the verdict line. No warning, no failed step, no unmapped skip.

Rulebook compliance

  • R0 — /charly-distros:container-nesting (this repo's own skill) and /charly-build:validate were read before the edit; the candy and the skill body move together, per this repo's AGENTS.md.
  • R1 — two failures surfaced in this gate; both were RCA'd before any remediation, and both are filed (see ## Filed issues).
    1. The first bed run failed at the FIRST BASE RESOLVE of the image chain, not in the changed candy: failed to resolve source metadata for ghcr.io/opencharly/arch:2026.302.0815: not found. Mechanism: a local multi-stage build bridges each built stage into the local charly OCI registry (CHARLY_BUILD_REGISTRY_MIRROR) so the next stage's FROM resolves locally; with the knob unset the drive pushes nothing, the resolve went upstream to ghcr.io, and the private opencharly/arch package denied it. Missed control: the drive has no preflight that names the mirror when a chain's base resolves upstream, so the error points at GHCR rather than at the missing knob — the same handshake skew as opencharly/plugin-build#35, in the opposite direction (there the DAEMON lacked the mirror; here the DRIVE did). Blast radius: every local multi-stage build in a shell without the knob, not this change. Root fix applied: the knob is exported for the gate run, and the evidence is on opencharly/plugin-build#35.
    2. A standalone charly box build crabbox-e2e (a redundant step of the gate script, not the bed) was SIGKILLed with signal: killed after 7 m 41 s, while the buildkitd daemon's own log shows the solve still running (Solve returned error: rpc error: code = Canceled desc = context canceled, with per-exec probes up to 10 s before the cancel). Mechanism: charly box build is a plugin→plugin call, and that leg arms charly's idle no-progress bound (charly/plugin_dispatch_reverse.go:159 → idleBoundedContext, class ClassBuild → the 90 s spec fallback). Its progress sources are host reverse legs and the peer plugin's live process-tree CPU — and the CPU source is dead for a compiled-in peer (providerPid → 0 for anything but a *grpcProvider, and plugin-build is compiled in), while the build's real work lives in the separate buildkitd daemon's tree. The bound cancelled the context; the SDK's exec.CommandContext SIGKILLed the buildctl child; and because the build reports its failure as a reply payload (spec.BuildReply.Error), the cancel cause — which carries the bound and the stall report — never reached the operator. Missed control: the CPU source assumed the peer is a separate process (#699's tree walk), and the reply-payload path drops the cause (#1003's complaint, recurring on a leg its fix did not cover). Blast radius: every long cold build on that path; each such failure is reported as an unqualified signal: killed. Root fix: charly core — a live progress source for a compiled-in peer, and the bound named on the reply-payload path. Filed as opencharly/charly#1088. The bed's own build of the same image was warm and green, so this is non-blocking for this change.
  • R2 — one env map, one property table, one README row: the retired name is deleted from every surface that carried it in this repo, not kept alongside a second spelling.
  • R3 — no workaround, no retry, no suppression: the gate is the bed, run once, on the committed tree.
  • R4 — the product is fixed before the prose: the env: map (the product surface) goes in the same commit as the two documents that describe it, and every documented command works with nothing but the charly binary.
  • R5 — the legacy name is gone from the tree, proven by the claim-keyed grep above, and by the emitted-artifact diff above: the generated Containerfile loses exactly the one ENV line and exactly the one OCI-label key, and nothing else.
  • R6 — git status was clean before the edit; the work happened in a session worktree (.worktrees/lcn-16/layer-container-nesting) branched off fresh origin/main; no force-push, no amend after the first push, no hook bypass, and no edit inside any submodule. The two generated .containerignore/.dockerignore files the build left in the worktree were removed again — the build writing them into the source checkout is filed as opencharly/plugin-build#39.
  • R7 — coverage that fails without the change, at the real boundary: the claim-keyed grep finds 3 hits on the parent tree and none on this one; and the emitted artifact changes exactly as the claim says — the generated Containerfile for the box that composes this candy (distro-cachyos's crabbox-e2e) loses ENV CHARLY_BUILD_ENGINE="podman" and the CHARLY_BUILD_ENGINE key of the ai.opencharly.env_candy label, byte-identical everywhere else. The changed path then ran LIVE: the bed below built that image from this candy and exercised it in a disposable: true pod.
  • R8 — the bed crosses a real service boundary (nested rootless podman inside a pod) and runs against it; nothing is mocked.
  • R9 — the gate ran on the worktree-local, CalVer-stamped binary (charly 2026.302.0357, built from opencharly/charly @ 1637a5e); no shared install.
  • R10 — the affected disposable: true target is check-crabbox-e2e-pod (in distro-cachyos), the bed whose image stacks this candy; it ran green on the committed tree below, with zero warnings, and the pre-flight charly box validate is green on the same tree.

Change classification

Candy config (charly.yml's env: map) + prose. The env: map is runtime-visible — it lands in the emitted image's ENV block and in the ai.opencharly.env_candy OCI label — so the class is candy config, not documentation-only, and the gate is the bed that composes the changed entity. Attribution tier: fully tested and validated — the candy-config pre-flight and the bed that stacks the candy both passed on the final tree, and the changed artifact was diffed before/after at its real boundary.

Filed issues

  • charly#1087 — .env.example still offers the retired CHARLY_BUILD_ENGINE knob in a live example file; it lives in another repo with its own gate, so it is genuinely separable from this cutover and is filed in its owning repo.
  • charly#1088 — the plugin→plugin idle bound cancels a long build and the operator sees a bare signal: killed with no bound named (the RCA of finding 2 above).
  • plugin-build#35 — the build registry mirror is read by the drive but the daemon and the drive can disagree about it; the evidence from this gate (the opposite skew: the daemon carries the mirror, the drive does not, so the base resolve went upstream and the error named GHCR instead of the missing knob) is added there rather than filed twice.
  • plugin-build#39 — a build writes the generated .containerignore/.dockerignore into the candy's source checkout, leaving the repo dirty.
  • marketplace#600 — the corpus projection still publishes the retired name; it is a GENERATED projection and is fixed by advancing the ref and regenerating, producer-first, after this repo tags.
  • docs#204 — the published recipe still carries the retired row; same projection chain, one leg further downstream.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

The candy's `env:` map still set the retired builder selector. The contract
retired it — `EngineConfig.Build`, `ResolvedRuntime.BuildEngine` and the
`CHARLY_BUILD_ENGINE` key are gone from `spec/hostenv`, and a leftover
`engine.build: docker` is INERT — so the variable did nothing in every image
this candy bakes.

It is deleted from `charly.yml`, the skill body and the README in one
cutover; the claim-keyed grep over the tree returns nothing outside
`CHANGELOG/`.

The live env contracts stay exactly as they were: `CHARLY_RUN_ENGINE` (the
run engine) and the hidden pair `_CONTAINERS_USERNS_CONFIGURED` +
`BUILDAH_ISOLATION`, which are what make rootless-in-rootless podman work.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@github-actions

Copy link
Copy Markdown

Review — PASS

What I verified against the current state

Diff matches the body's claimed surfaces. Three files changed, exactly the three named in the body's table:

  • charly.yml — the single CHARLY_BUILD_ENGINE: "podman" line deleted from the container-nesting: candy's env: map; CHARLY_RUN_ENGINE and the hidden _CONTAINERS_USERNS_CONFIGURED / BUILDAH_ISOLATION pair are retained, as the body says they must be.
  • .agents/skills/container-nesting/SKILL.md — Env row loses exactly one name.
  • README.md — Environment row loses exactly one name.

Line numbers in the body's evidence are internally consistent with the diff. The body's parent-tree grep cites SKILL.md:37, README.md:30, charly.yml:143. The hunks are @@ -34,7 +34,7 @@ (Env row at 34+3 = 37), @@ -27,7 +27,7 @@ (Environment row at 27+3 = 30), and @@ -140,7 +140,6 @@ (deletion at 143). These agree exactly, so the evidence was taken from this tree, not copied forward from a stale state.

The emitted-artifact diff is coherent with the source diff. The body's CF.before/CF.after diff removes exactly one ENV line and exactly one key from the ai.opencharly.env_candy label, with hunk offsets (@@ -38,7 +38,6 @@, @@ -394,7 +393,7 @@) consistent with a one-line deletion upstream of both. The claim "exactly the retired name and nothing else" is reproducible from the diff shape.

No contradiction between the "breaking" marker and the change. The commit is fix(nesting)!. The body argues the variable was inert, which would make ! unnecessary — but the removal is genuinely observable at the image boundary (ENV block, OCI label), so a consumer keying off that label or env var would see a change. Marking it breaking is conservative, not dishonest.

Scope is clean. No leftover duplicate spelling, no second config path, no dead code, no secret/injection/privilege surface introduced. This is a pure deletion of a name plus its two prose descriptions. The out-of-repo projections (marketplace, docs) and the stale .env.example in charly are explicitly deferred to their owning repos with filed issues, which is the correct producer-first ordering rather than hand-editing generated artifacts.

Comments

There are no comments in the thread, so there is no sign-off to weigh and no claim to re-derive. The change stands on its own evidence, as required when no designated maintainer has signed off.

Non-blocking notes (not gating)

  1. No test ships in this repo for the removal. The "coverage that fails without the change" here is the claim-keyed grep plus the live bed run — both reproducible, both documented with exact commands and outputs, but neither is a committed artifact. A one-line assertion (e.g. in charly box validate's scope, or a grep-based lint over the candy config) would prevent the retired name from silently reappearing. This is a hardening suggestion, not a defect: the change deletes dead config, and the bed run exercises the real boundary.
  2. Minor wording mismatch in the body. "The claim-keyed grep over the tree returns nothing outside CHANGELOG/" is followed by a command whose exit status of 1 means no hits anywhere, including CHANGELOG/. The shown output is the stronger, authoritative statement, so this is an understatement rather than a false claim — worth tightening for clarity but not a correctness problem.
  3. Findings 1 and 2 under R1 are correctly dispositioned as out-of-scope. The missing CHARLY_BUILD_REGISTRY_MIRROR preflight and the plugin→plugin idle-bound SIGKILL are both in charly/plugin-build, both reproduced with concrete mechanism (file:line references and daemon logs), and both filed (charly#1088, plugin-build#35). Neither is caused by, nor fixable in, this PR.

Conclusion

The diff does exactly what the body claims: it removes one retired, inert name from the candy's env: map and from the two documents that describe it, changing nothing else, and the evidence at the artifact boundary (generated Containerfile + OCI label) and at the runtime boundary (the check-crabbox-e2e-pod bed) is presented with enough specificity to be re-derived. No correctness, security, or scope problem found.

Verdict: PASS


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

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 29, 2026 09:24
@github-actions
github-actions Bot merged commit 50da777 into main Oct 29, 2026
3 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 29, 2026
@aitrawog

Copy link
Copy Markdown
Contributor Author

RESOLVED — the validator's PASS landed this PR (squash 50da7774, tag v2026.302.0925), and opencharly/layer-container-nesting#16 is closed with it. Disposition of the two non-blocking notes, both read in full:

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0

Note 1 (no committed control keeps the retired name from reappearing) — accepted, and FILED. The claim-keyed grep and the live bed run are reproducible evidence, but neither is a committed artifact, so the name can come back silently. The fix belongs to the ORG, not to this repo: one committed control that keeps every retired variable name out of the org's trees, with the list of retired names owned by the spec (where spec/hostenv/retired_build_selector_test.go already names CHARLY_BUILD_ENGINE as inert). It is filed as opencharly/opencharly#799 — including the constraint a solution has to respect: the umbrella's gates see only the materialized submodules, so a sweep that skips an unmaterialized path proves nothing about that path (the same coverage rule charly task verify already applies).

Note 2 (the wording "nothing outside CHANGELOG/") — clarified with the evidence, not a defect. The body's claim-keyed grep is the exit-1 form, so it proves the stronger statement: no hit anywhere, CHANGELOG/ included. The mention of CHANGELOG/ was the R5 convention for a history-exempt surface (history is allowed to name what it retired), and here the exemption is moot because the grep returns nothing at all. The pasted command and its exit status are the authoritative evidence; the sentence above them is an understatement of it, and the tree carries the retired name nowhere.

Note 3 (the R1 findings correctly dispositioned as out of scope) — agreed, with the two filed issues already carrying the RCAs (opencharly/charly#1088 for the plugin→plugin idle bound, opencharly/plugin-build#35 for the mirror skew).

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

github-actions Bot pushed a commit to opencharly/opencharly that referenced this pull request Oct 29, 2026
…cutover (#801)

Three leaf pins advance to the commits their owning repos' merged mains
already serve. The scope is derived, not written: the product's own currency
report names the pins that are not current, and the wave advances exactly
those. No pin was hand-picked, and no pin was hand-pinned.

| pin | recorded | served | producer |
|---|---|---|---|
| layer-container-nesting | 7d31a504a8 | 3946ee1923 | opencharly/layer-container-nesting#17 (squash 50da7774, tag v2026.302.0925) |
| marketplace | 197f42a57a | 3cf744b9ef | opencharly/marketplace#601 (squash b53310d, tag v2026.302.0951) |
| docs | 84595508dd | a99557f1e6 | opencharly/docs#205 (squash a4675b56, tag v2026.302.1030) |

The retired CHARLY_BUILD_ENGINE builder selector is gone from the spec's
hostenv contract; each of these three repos still named the commits from
before its producer leg landed, so each carried the retired selector one
step further than the repo that had already fixed it.

RCA

Mechanism — the umbrella records every org repo as a gitlink; the pin is the
INDEX entry and the checkout follows it. Nothing advanced these three pins
after their producer legs landed, so the umbrella's view of those repos stayed
on the pre-cutover commits while the repos themselves had moved on.

Missed control — the currency audit (charly task pins --param MODE=currency)
REPORTS a behind pin and does not fail, because "behind" is an advance
available NOW rather than a violation: the remedy is a pin-wave PR, not a
gate. So nothing turned red, and the pins stayed behind until a wave ran.

Blast radius — every consumer of the umbrella's view: the harness skill
bindings in .dsh/skills (symlinks into the corpus checkout), the org map, and
every repo that reads the umbrella as its source of pins.

Root fix — advance the leaf pins in the index with the sanctioned wave
(charly task sync), materialize the checkouts at the new pins (charly task
init), and judge the committed tree (charly task verify). No hand-pin, no
branch checkout.

The wave's order: sync (the pins move in the index; the checkouts that exist
are rolled) -> init (those checkouts are refreshed to the pins the index
records) -> verify (every pin judged from the index, on the committed tree).

The wave's scope was exactly these three leaves. The verb's own report is the
scope control: it rolled 7 leaf submodule(s) to their default-branch HEAD and
advanced 396 leaf pin(s) in the index, and the 6 pin_map edges moved 0,
because the edge half follows charly's own box/* gitlinks and the charly leaf
is already current.

A caveat this commit states rather than hides: for each producer the newest
tag points at the MERGE commit, and the served head the umbrella pins is
exactly ONE commit above that tag, and that one commit adds only
CHANGELOG/<version>.md. So these pins are MERGED and their tags exist, but the
served head is not itself a tagged commit. The measurement is on
#793.

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
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