Repository navigation
fix(deploy): carry the authored security block into the persisted deploy node - #44
Conversation
…loy node `charly deploy add` persisted a deploy node's per-host entry without its authored `security:` block: the entry's security came only from the image labels plus auto-detected devices, so an authored `cap_add` never reached `charly config`'s MergeDeployOntoMetadata step and the emitted quadlet had no `AddCapability=` line. The container then ran with a narrower boundary than its tree declares (opencharly/pod-crabbox-k3s#6: `ip link add k3s0 type dummy` -> RTNETLINK Operation not permitted). Seed the authored block into the per-host entry after a successful dispatch (the same position and locked cycle as the selector persist), MERGING it into any existing block: list fields are unioned (AppendUnique, so a device detection already recorded is never emitted twice), authored scalars win, and a field the tree leaves unset keeps the existing value. A node with no `security:` is a no-op, so the device-detection path is byte-identical, and a fresh image-bearing pod entry is seeded so `deploy add` then `charly config` works on a first-ever deploy. Refs #42 Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated)
|
STATUS — opened for review.
Agent: |
Review — PASSHead SHA: Security screen: PASS — no scope mismatch (all 4 files accounted for), no secrets/exfiltration, no weakened guardrails, no supply-chain changes; does not touch the validator spec / rulebook / gates (T4 not triggered, no maintainer sign-off required). Checklist: All applicable rules PASS or NA — A1 (summary, how-tested, tier, rule-compliance, footer all present and consistent with the diff), A2 (code change → repo's declared gate + disposable-fixture live e2e on the changed path; repo declares no eval beds), A3 ( Comment intake: 1 comment (by @aitrawog, Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
… carry (#1111) * fix(pins): adopt plugin-fleet v0.2026309.2138 for the deploy security carry Refs opencharly/plugin-fleet#44 A deploy node's authored security block (security.cap_add, security_opt, devices, ...) was dropped by 'charly deploy add': plugin-fleet before v0.2026309.2138 persisted nothing of it, so 'charly config' derived the quadlet boundary from image labels plus auto-detected devices and the emitted unit carried no AddCapability= line. Measured on a pod: node authoring security.cap_add: [NET_ADMIN]: the old pin emits no AddCapability, the new pin emits AddCapability=NET_ADMIN. The R10 pod bed check-pod-overlay cannot run on this tree: every project load fails validate on two remote candy refs inside the imported cachyos namespace (opencharly/sdk#488), identically at the old and the new pin. Assisted-by: Claude Code deepseek-v4.1-flash:cloud (analysed on a live system) * fix(pins): adopt sdk v0.2026309.2334 so imported-namespace candy refs resolve Refs opencharly/sdk#489 Refs opencharly/sdk#488 The R10 pod bed check-pod-overlay could not build because every project load failed validate on two remote candy refs inside the imported cachyos namespace (opencharly/sdk#488). sdk v0.2026309.2334 makes CollectRemoteRefsOpts walk cfg.Namespaces recursively, so an imported namespace's kind:local remote candy refs reach the fetch set. The tools/* modules carrying the shared sdk pin are moved to the same version by 'charly task mods-tidy', keeping tools/gomod-canonical's TestContractPinsMatchShared anchor green. Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated) * chore(pr): re-freeze for the round-3 disposable proof The round-3 deliverable is EVIDENCE: the plugin-fleet security carry is proven on a disposable-marked target (before: no AddCapability=; after: AddCapability=NET_ADMIN), and a round-3 check bed was withdrawn because it could not fail — the check-bed runner seeds the per-host security entry itself (PersistBedDeployOverrides), so a bed asserting AddCapability passes with or without the plugin-fleet pin. No tracked file changes; the tree and CalVer are unchanged, so the round-2 gate and bed results remain the evidence for this exact tree. Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated) * chore(pr): re-freeze citing the shipped regression coverage Round 4 adds no tracked change: the shipped coverage for the deploy security carry is opencharly/plugin-fleet's own deploy_node_security_test.go (three TestDeployAddSecurity tests), present only at v0.2026309.2138 and failing with the fix file removed (undefined: persistDeployNodeSecurity). The consumer pin adopts the release that carries it; the tree and CalVer are unchanged. Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated) --------- Co-authored-by: Andreas Trawoeger <atrawog@users.noreply.github.com>
Summary
charly deploy addpersisted a deploy node's per-host entry without its authoredsecurity:block. The entry's security came only from the image labels plus auto-detected devices, so an authoredcap_addnever reachedcharly config'sMergeDeployOntoMetadatastep and the emitted quadlet had noAddCapability=line. The container then ran with a narrower boundary than its tree declares — the measured symptom in opencharly/pod-crabbox-k3s#6, whereip link add k3s0 type dummyfailed withRTNETLINK answers: Operation not permitted.The fix seeds the authored block into the per-host entry after a successful
deploy adddispatch, at the same position and through the same locked cycle as the existing selector persist. It MERGES into any existing block instead of replacing it: list fields (cap_add,devices,security_opt,group_add,mount) are unioned withdeploykit.AppendUnique, so a device detection already recorded is never emitted twice; an authored scalar orprivileged: truewins; a field the tree leaves unset keeps the existing value.charly configthen runs its unchanged path:MergeDeployOntoMetadatafolds the entry onto the image metadata andAppendUniqueadds the detected devices. A node with nosecurity:is a no-op, so the detection-only path is byte-identical; a first-ever image-bearing pod entry is seeded so the documenteddeploy addthencharly configsequence works. Dotted in-substrate child paths and host-rooted / external in-place nodes are skipped, mirroringdeploykit.PersistBedDeployOverrides' own guards.The change is four files:
candy/plugin-fleet/deploy_node_security.go— new: the merge and the seeded persist.candy/plugin-fleet/walk.go— one call after a successful dispatch.candy/plugin-fleet/deploy_node_security_test.go— new: the regression tests.candy/plugin-fleet/bed_persist_writer_test.go— the test-scoped node decoder learns thesecurity:block the tests assert.Closes #42.
Head
bd97a334a973f5c446603658f19b55f7073ed879How tested
R9 binary. The binary under test is built from the umbrella's
charlycheckout (clean;HEADb0454eb5bd326eb877b065e7cc85f53b2c3d1484, newest tagv2026.309.1009) with the plugin-fleet worktree on the workspace, CalVer-stamped. It is not a shared install.The repo's change-class gate, on this committed tree. This repo declares no Go CI workflow (
.github/workflows/holds onlytag-on-merge.yml; the merge gate is the org-widecharly/pr-validator), so the gate is the repo's own declared minimum (plugin-fleet/AGENTS.md): build, vet, test,gofmt -l, andcharly box validate.The regression tests.
The first test is the #42 reproduction at the persist boundary: the entry already carries the detected devices a previous
charly configrecorded, the tree declarescap_add+cgroupns+security_opt, and/dev/kvmis declared by BOTH — all three authored fields must come back with the detected devices and/dev/kvmexactly once. The second pins "no authored security ⇒ the detection path is byte-identical" (the mutation reports no change and the file is not rewritten). The third pins the freshdeploy add-then-charly configsequence.It fails on
origin/main. A detached worktree atorigin/main(d623e1495f8d113f5da3422663badb4e359926e0) received the new test file and the decoder change (the test references the new symbol, so this is the compile-time witness; the live witness is the negative control below):End to end on this host — a disposable fixture. The fixture is a temporary project whose
pod:node is markeddisposable: trueand authorscap_add: [NET_ADMIN],cgroupns: host,security_opt: [label=disable]; its box re-bases the already-builtghcr.io/opencharly/probe-engine-image(the same keepalive image the pod beds use, so no build is needed and the image labels carry nocap_addof their own):Negative control — the same sequence with the umbrella
charlybinary (v2026.309.1009, pinnedplugin-fleetv0.2026286.700, i.e. without this branch). The persisted entry carries the detected devices only, and the emitted quadlet has no capability or security-opt line:Positive arm — the same project and sequence with the branch binary.
deploy addalone already carries the authored block;charly configunions the detected devices into it and the quadlet emits all three authored fields:Teardown — both arms were removed with the sanctioned verbs; the container was never started (only
deploy add+configran):The negative-control arm was torn down the same way (
remove_rc=0,del_rc=0), and the test deploys' temporary overlays live under/tmp— the operator's real~/.config/charly/charly.ymlwas never written by this work.R10 bed. This repo declares no
disposable: truebed: its rootcharly.ymlisdiscover:-only and the candy'splan:is a build-context check (plugin-fleet/AGENTS.md: the live R10 roster belongs to the consumer repos, anddeploy addis every bed's entry point). The live proof is therefore the disposable fixture deployment above, driven on the changed path (deploy add→config→ emitted quadlet boundary), with the teardown quoted.Rulebook compliance
git-workflow,strict-policy,root-cause-analyzer,check,plugin, andinstall-plan(the setplugin-fleet/AGENTS.mdnames); the repo's ownAGENTS.mdwas read first.security:werecharly config(image labels + detected devices) anddeploykit.PersistBedDeployOverrides; nothing carried the deploy node's authored block. One separable finding was filed ascharly deploy add --disposable(and a node'sdisposable: true) is never persisted — the documented flag is inert #43 (--disposableis inert); it does not affect this change's claim or gate. The gate ran zero warnings.deploykit.AppendUnique, the shared locked cycledeploykit.MutateDeployConfig, and this package's existingloadDeployConfig/saveDeployConfigcallbacks; it adds no second lock and no second device-detection implementation (detection stays inplugin-deploy-podand still runs, unchanged, at config time).charly deploy add,charly config,charly remove, andcharly deploy del.charly deploy addthencharly configsequence now carries the authored security block, and every command used works with thecharlybinary alone.git status --shortwas empty before and after the commit; thecharlysubmodule checkout is clean and was not edited; the work is onfix/deploy-add-carry-securityofforigin/maind623e1495f8d113f5da3422663badb4e359926e0; no force-push, no bypass, no direct push tomain.origin/main(pasted above) and pass on this tree; the changed path executed live on this host, and the emitted quadlet — the artifact boundary — was inspected withgrep AddCapability.plugin-deploy-podprovider and the real host device-detection probe./tmp/pf42-charly versionprints2026.309.1009, the checkout's newest tagv2026.309.1009, and it carries the new code (thestrings | grep -cline in the R9 fence prints1).disposable: truetarget, so the live R10 form is the disposable fixture deployment on the changed path, torn down withcharly removerc 0 andcharly deploy delrc 0 (pasted above).Change classification
Bug fix, additive, no contract change.
charly deploy addnow persists a deploy node's authoredsecurity:block into the per-host overlay entry, merged with any existing block, socharly config's existingMergeDeployOntoMetadatastep emits the declared capabilities, devices, and security options into the quadlet. A node with nosecurity:keeps the previous behaviour exactly; dotted in-substrate child paths and host-rooted / external in-place nodes are skipped. No schema, wire type, CLI flag, or config format changed.Breaking change + rollback: none — the change only ADDS a persisted field that every binary built from
mainalready understands (MergeDeployOntoMetadatahas mergedoverlay.Securityfield-by-field since before this branch), and no existing entry is rewritten except to union the authored block into it. Rollback is a revert of the squash commit; entries already written remain valid because they only carry the values the tree itself declares.Filed issues
charly deploy add --disposable(and a node'sdisposable: true) is never persisted — the documented flag is inert #43 —charly deploy add --disposable(and a node'sdisposable: true) is never persisted; the documented flag is inert. Found on the same run; separable from deploy add drops the authored security.cap_add, so the container runs without a capability the tree declares #42 (the security carry does not depend on it).Agent:
containerd-crabbox-program/pf42· session8b1c01cf-7f79-4a75-bff8-240415a06208Assisted-by: Claude Code deepseek-v4.1-flash:cloud (fully tested and validated)