Skip to content

fix(pod): honor a deploy's authored engine selector in config, start and teardown - #47

Merged
github-actions[bot] merged 3 commits into
mainfrom
feat/pod-engine-selector
Nov 5, 2026
Merged

github-actions[bot] merged 3 commits into
mainfrom
feat/pod-engine-selector

Conversation

@aitrawog

@aitrawog aitrawog commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This is the deploy:pod leg of opencharly/charly#1109. Four defects in one
coherent change:

  1. The authored selector was never persisted. PrepareVenue shipped a
    state patch containing only resolved_image, so a pod: node authored
    engine: containerd produced a per-host entry with no engine, and
    charly config fell back to the host-wide engine.run (podman). The patch
    now carries the node's Engine, plus its box name and the pod target so
    an engine-only no-overlay entry is a complete image-backed pod entry (and
    never the targetless group: the node-form writer would otherwise emit).
  2. The run mode came from the wrong engine. config/start/remove
    branched on rt.RunMode, resolved for rt.RunEngine (the host-wide
    engine.run). runtimeForSelection re-derives the mode from the SELECTED
    engine's capability row (container.EngineRunModeFor via
    kit.DetectRunMode), so a containerd deploy emits and starts the engine's
    systemd-unit (nerdctl run), never a podman quadlet. direct is
    engine-agnostic and is preserved.
  3. A locally-built bed image could not reach the containerd store.
    build:ensure received no project context, so the local build fallback
    failed buildkitdrive: build: no context. ensureImagePresent now threads
    the project dir (through the existing deploy-plugins-connect preamble)
    into the build:ensure request.
  4. The emitted host unit CHDIR'd to the container's workdir. The first live
    containerd start failed status=200/CHDIR: writeSystemdUnitConfig passed
    the CONTAINER working dir (/home/user) as the engine unit's
    WorkingDirectory=, which systemd applies on the HOST. The container workdir
    is already the engine argv's run -w <path>; the host unit now omits it.

Adopts spec v0.2026309.1917 and sdk v0.2026309.1926 (producer-first:
opencharly/spec#292, opencharly/sdk#487). The plugin-build producer legs the
live proof needed — local build into the requested run engine, aliasing the
build's own produced ref, and keeping a handled pull miss out of the gate log —
are opencharly/plugin-build#52. The charly pin bump follows the tags.

Diff

File Change
go.mod, go.sum adopt spec v0.2026309.1917 and sdk v0.2026309.1926
lifecycle.go, prepare_venue_state_test.go carry engine + box + pod target in the PrepareVenue state patch; tests
selection.go, selection_test.go runtimeForSelection derives the mode from the selected engine's capability row; tests
config_setup.go, resolve.go, config_remove.go use the effective runtime (the selected engine's mode)
image_ensure.go, image_ensure_test.go, resolve_ref.go, config_setup_helpers.go thread the project dir to build:ensure; capture test
systemd_unit.go omit the container workdir from the host systemd unit

How tested

All module commands ran on this head in
/home/atrawog/Sync/Atrapub/mo/claude/opencharly/.worktrees/containerd-crabbox-program/engine/plugin-deploy-pod/candy/plugin-deploy-pod
with GOWORK=off (the real adopted pins):

$ git rev-parse HEAD
45c1b42e1fc09b04754a73bd081811596256c9c6
$ GOTOOLCHAIN=go1.26.8 GOWORK=off go build ./...
build rc=0
$ GOTOOLCHAIN=go1.26.8 GOWORK=off go test ./...
ok  	github.com/opencharly/plugin-deploy-pod/candy/plugin-deploy-pod	1.360s
$ gofmt -l .
$ GOTOOLCHAIN=go1.26.8 GOWORK=off ~/go/bin/golangci-lint run ./...
0 issues.

Negative control (pre-fix) — the defect reproduced

Plugin v2026.308.1638, charly 2026.309.1009, a disposable: true pod:
bed authored engine: containerd, on this host's rootless containerd 2.3.5:

[deploy-add] PASS after 12.178s
[config] PASS after 8.462s
[start] PASS after 7.226s
[check-live] FAIL after 26.671s: exit 2
  FAIL  check the per-host entry persisted the authored engine selector  [engine-selector-persisted]
  FAIL  check charly deploy add exposes the per-deploy engine selector flag  [engine-flag-exists]
  FAIL  check the deploy's emitted unit launches the containerd engine through nerdctl  [unit-launches-nerdctl]
  FAIL  check the deploy's container is present in the rootless containerd store  [container-in-containerd]
  PASS  check a command executes inside the containerd-launched container  [in-container-command]
23 steps: 18 passed, 4 failed, 1 skipped

The unit-launches-nerdctl failure's captured stdout was the podman user
generator's unit header (the unit under
/run/user/1000/systemd/generator/ was generated by
/usr/lib/systemd/user-generators/podman-user-generator), and
container-in-containerd exited 1: the container was in podman, not containerd.

Fixed — the selector, config unit and start path ran live on containerd

A charly binary built with
scripts/bootstrap-charly.sh --dev-plugin plugin-build=<plugin-build PR #52 worktree> (plus the released spec/sdk pins and plugin-fleet v0.2026309.1933) and this branch's plugin ran the same bed. The config phase
resolved engine: containerd FROM THE PER-HOST ENTRY (a podman fallback would
have emitted a quadlet), built the image into the containerd store, and wrote
the engine's systemd unit:

$ grep -nE 'ensure-image:|Building probe-engine-image|tagged |Wrote |Reloaded systemd' .check/check-engine-containerd-pod/2026.309.2001/config.log
4:ensure-image: pulling ghcr.io/opencharly/probe-engine-image:check-engine-containerd-pod-2026.309.2001
5:ensure-image: pull ghcr.io/opencharly/probe-engine-image:check-engine-containerd-pod-2026.309.2001 failed: exit status 1
6:ensure-image: building probe-engine-image locally
9:--- Building probe-engine-image ---
10:ensure-image: tagged ghcr.io/opencharly/probe-engine-image:2026.309.2002 as ghcr.io/opencharly/probe-engine-image:check-engine-containerd-pod-2026.309.2001
11:Wrote /home/atrawog/.config/systemd/user/charly-check-engine-containerd-pod.service
12:Reloaded systemd user daemon
$ grep -c 'level=fatal' .check/check-engine-containerd-pod/2026.309.2001/config.log
0

The emitted unit is the engine's, not a podman quadlet:

$ systemctl --user cat charly-check-engine-containerd-pod.service
Description=charly deployment charly-check-engine-containerd-pod
ExecStart=nerdctl --address /run/containerd/containerd.sock --namespace default run --name charly-check-engine-containerd-pod --network charly --user 0 --runtime crun --hostname charly-check-engine-containerd-pod -w /home/user --device /dev/kvm --device /dev/vhost-net --device /dev/vhost-vsock --device /dev/fuse --device /dev/net/tun --device /dev/hwrng ghcr.io/opencharly/probe-engine-image:check-engine-containerd-pod-2026.309.2004 supervisord -n -c /etc/supervisord.conf
ExecStop=nerdctl --address /run/containerd/containerd.sock --namespace default stop charly-check-engine-containerd-pod

charly deploy add --help on the assembled binary now carries the selector:

$ ./bin/charly deploy add --help | grep -- --engine
      --engine=STRING              Container engine for this deploy (pod):

The measured venue blocker (not a finding in this diff)

The container did not start on this host, and this PR does not claim it
did. Two pre-existing venue/contract defects block it, both filed and
separable:

The bed also surfaced two cleanup/readiness gaps, both filed:
opencharly/plugin-pod#27 (charly remove leaves the containerd container, so
the next start fails name-store) and opencharly/plugin-check#142
(wait-ready probes the host engine, not the deploy's, a vacuous 90 s wait).

Tier: analysed on a live system — the changed runtime path ran live with
retained output; the full bed did not pass because of the venue's missing
crun handler, not because of this diff.

Authoring runtime evidence — the footer's model string is the runtime's own
value, read from the process environment:

$ env | grep -E '^(AI_AGENT|CLAUDE_CODE_SUBAGENT_MODEL|ANTHROPIC_BASE_URL)=' | sort
AI_AGENT=claude-code_2-1-308_agent
ANTHROPIC_BASE_URL=http://127.0.0.1:11434
CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4.1-flash:cloud

Rulebook compliance

  • R0 — loaded /charly-internals:go, /charly-internals:plugin,
    /charly-internals:go-quality, /charly-internals:git-workflow, this repo's
    AGENTS.md, and the umbrella rulebook before any edit.
  • R1 — every failure the bed surfaced is fixed here (engine persistence,
    run-mode selection, project context, host-unit CHDIR) or filed with its RCA
    (plugin-containerd#16, spec#293, plugin-pod#27, plugin-check#142);
    plugin-build#50/#51 are the pre-existing findings from the same runs.
  • R2 — one runtimeForSelection, one state-patch builder, one image-store
    path; no engine-word switch or sibling implementation.
  • R3 — no workaround in the product: the selector rides the contract, the
    mode comes from the capability table, the build gets its context, the host
    unit stops CHDIRing. The minimal-image fixture only isolates the venue's
    label-cap variable; it is disclosed above.
  • R4 — product before prose; no documented command is routed around.
  • R5 — no legacy word, alias or shim; containerd is the engine word and
    nerdctl is only the capability's binary.
  • R6 — git status was read first; the session worktree is
    .worktrees/containerd-crabbox-program/engine/plugin-deploy-pod off fresh
    origin/main; the branch advanced only by new commits, never a force-push.
  • R7 — the unit tests fail on the pre-fix code; the emitted unit is
    validated at the systemd boundary (systemctl --user cat) and the
    pre-fix/fixed bed pair is the before/after witness.
  • R8 — the live containerd service is real (rootless 2.3.5) and is not
    mocked; the crun absence is reported, not hidden.
  • R9 — the worktree-local dev/release binaries were rebuilt; the served
    plugin source is logged in the run output.
  • R10 — the disposable bed was run through charly check run on a fresh
    rebuild; its changed path is green, and the remaining red is the filed venue
    blocker above. The charly PR re-runs it on the tagged assembled stack.

Change classification

Behavior change at the pod deploy boundary: a per-deploy engine selector now
persists and drives unit emission, start/stop and config removal; an
engine-only state patch is classified as pod; build:ensure receives the
project dir; the host unit no longer carries the container workdir. No authored
key changes spelling and no migration is owed. Rollback: revert this PR and the
adopted tags; a deploy without a selector behaves exactly as before.

Breaking change + rollback: none — a deploy with no per-host engine keeps
the previous host-wide resolution, and the new state-patch fields are optional.

Filed issues

Agent: containerd-crabbox-program/engine
Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash:cloud (analysed on a live system)

…tart

PrepareVenue now carries the deploy node's authored `engine:` into the
per-host state patch (with the box name and pod target, so an engine-only
no-overlay entry is complete and valid). config/start/remove derive the
persistence mode from the SELECTED engine's capability row instead of the
host-wide engine.run, so a pod deploy authored `engine: containerd` emits the
engine's systemd unit and starts it through nerdctl, never a podman quadlet.

Adopts spec v0.2026309.1917 and sdk v0.2026309.1926 (opencharly/charly#1109).

Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash:cloud (fully tested and validated)
A disposable pod bed's image is built locally under the project's
registry/name. When the deploy's run engine is containerd, its store lacks
that image and the registry pull misses, so build:ensure must rebuild it from
the project — and it received no dir, failing 'buildkitdrive: build: no
context' before config/start could run. Thread the project dir through
ensureImagePresent via the existing deploy-plugins-connect preamble, and pin
the request with a capture test (opencharly/charly#1109).

Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash:cloud (fully tested and validated)
writeSystemdUnitConfig passed the CONTAINER working dir (/home/user) as the
engine unit's WorkingDirectory. unit_emit renders that as systemd's
WorkingDirectory=, which systemd applies on the HOST before spawning nerdctl —
so every containerd start failed status=200/CHDIR (measured on the first live
containerd pod bed). The container workdir is already the engine argv's
; the host unit needs none (opencharly/charly#1109).

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

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Review — PASS

Head SHA: 45c1b42e1fc09b04754a73bd081811596256c9c6 (branch: feat/pod-engine-selector)

Security screen: PASS — no secrets/exfil, no weakened guardrails, no scope mismatch (all 14 files map to the 4 stated defects), spec/sdk pins bumped producer-first.
Checklist: All applicable rules PASS or NA.

  • Placement: CORRECT — capability logic (runtimeForSelection, projectDirFor, detectRunMode) stays in the candy; no core alias file, no new core→sdk import, detectRunMode is a local test seam over kit.DetectRunMode.
  • R1/R10 — the red bed is attributed to genuinely separable, externally-filed defects (plugin-containerd#16 crun, spec#293 label-cap, plugin-pod#27, plugin-check#142); each changed piece has a unit test that fails pre-fix (TestPrepareVenueState_CarriesEngine, TestRuntimeForSelection_*, TestDispatchBuildEnsurePeer_CarriesProjectDir) plus live config.log / systemctl --user cat evidence. Tier analysed on a live system is honest and not inflated.
  • A1/A3 — body accounts for every file, ## Filed issues present, footer ends with the Assisted-by: line; model string matches the pasted CLAUDE_CODE_SUBAGENT_MODEL/ANTHROPIC_BASE_URL env.
  • systemd_unit.go — workingDir is not dead: it still feeds argv's run -w <path>; only the host WorkingDirectory= is dropped. No QUAL dead-param issue.
  • Comment thread: empty (0 comments) — nothing to disposition.

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.

2 participants