Repository navigation
fix(argo): take implement off its workdir PVC, delete the dead Go cache - #1194
Conversation
Last agent template on a Hetzner volume (#1047). Six steps were six pods spread across nodes, so the RWO volume moved between them on every step: six attach/detach pairs per run with Multi-Attach errors on each, and with the DevLoop driving ~20 implement runs a day that is ~120 volume operations daily on one template. A wedged detach leaves a pod Pending, and a Pending pod holding mctl-gitops-main-writes is the 2026-07-30 cascade. emptyDir + artifact handoff, same recipe as #1042/#1046/#1048: - Each pod clones mctl-gitops for itself in an initContainer; the standalone clone step is gone. - run-implementer collects the .status.yaml files it changed into an Argo artifact at a fixed workflow-scoped S3 key, read literally by commit-and-push. Both primary and fallback write the same key, so no step reference has to resolve through a skipped step. Paths are split on NUL in Python and a path that is not a .status.yaml under agents-state is fatal. - commit-and-push takes its OWN fresh clone, validates the handoff before any of it reaches the checkout, and applies deletions explicitly. That closes #983 P1 for this template and lets safe.directory and `git reset -q origin/main` go with the shared worktree. - activity travels to onExit as an optional artifact instead of a file on the PVC. No workflow.outputs.parameters is introduced (#530/#531, #1186). - ephemeral-storage requests/limits on every step, because an emptyDir counts against the pod rather than being invisible to the node. restore-go-cache and save-go-cache are deleted rather than ported. They never worked once: the agent overrides GOPATH to $HOME/go on every run (178 export statements in one run's audit log) and the save-side emptiness check (`find -maxdepth 3 -type f`) cannot see a Go module cache even when full. Both ended in an unconditional exit 0, so six months of cold starts went unnoticed and MinIO never held a mctl-agents/gocache/ object. Their ExternalSecret in argo-workflows had no other consumer and goes too; the per-tenant openclaw minio-cache-creds secrets are separate and untouched. A cache that actually works is a different design — a per-service partitioned artifact at the path the agent really uses, plus per-language images so the Go/Zig/musl-cross bootstrap stops being paid at all — and is tracked separately. Refs #1047, #856, #983 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011zuKeZdjt1ZfF4SaK3rEkf
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude finished @mashkovd's task in 6m 6s —— View job Review complete for PR #1194
Verdict: APPROVED — 0 P1/P2 findings, 1 P3 (non-blocking). This is a faithful port of the already-validated recipe from #1042/#1046/#1048: per-pod initContainer clone (no more shared workdir volume), artifact-based Specifically verified:
One P3 posted inline: |
Antigravity (agy) review — informationalModel: gemini-3.1-pro-high · commit Severity: P2 Scenario: The workflow replaces the shared PVC with an Argo artifact created by a Python script running at the very end of the Proposed Fix: Configure Argo to directly archive the Severity: P3 Scenario: The PR introduces complex new shell and Python logic to compute git diffs, handle renames/deletions, and securely overlay artifacts onto a pristine Git checkout. There is no test visible in this diff to verify that these new handoff boundaries correctly process edge cases or reject malicious paths as intended. Proposed Fix: Add unit or E2E tests validating the Python artifact collection script and the strict |
|
|
||
| # Second layer, on the checkout: the same expression the PVC era used, now | ||
| # against a pristine clone holding only what the copy above laid down. | ||
| NON_STATUS_CHANGES="$(git status --porcelain -z -uall -- ':(exclude,glob)platform-gitops/agents-state/**/.status.yaml' | tr '\0' '\n' | cut -c4-)" |
There was a problem hiding this comment.
P3: This second-layer guard parses git status --porcelain -z with tr '\0' '\n' | cut -c4-, blindly stripping 4 chars off every NUL-separated record. The Python collector earlier in this same file (and its comment at the top of the COLLECT heredoc) explicitly calls out why that's wrong for a detected rename/copy: XY new\0old\0 puts the status prefix only on the first field, so this cut chops 3 real characters off the old path continuation line (the exact class of bug fixed as claude P2 on #1046). Not exploitable today — the handoff onto this checkout is purely additive from paths the Python side already validated (no control chars), so a same-repo rename between two already-allowed .status.yaml paths is the only way to reach this code, and both endpoints would already be in-scope. Flagging because it's a latent inconsistency: if this guard is ever relied on more broadly (e.g. pathspec widened), it silently reintroduces the bug the NUL-safe Python parsing was written to close. Same pattern already exists in cwft-mctl-agents-shepherd.yaml, so this isn't new to this PR — just noting it since it's not blocking here.
There was a problem hiding this comment.
No P1/P2 findings (1 P3). Good to merge.
Reviewed against the pinned diff (dcaf93b...aea8563, two files: cwft-mctl-agents-implement.yaml and the deleted mctl-agents-minio-cache.yaml ExternalSecret). This is a faithful port of the already-validated recipe from #1042/#1046/#1048 (per-pod initContainer clone, artifact-based handoff at a fixed workflow-scoped S3 key, commit-and-push on its own fresh clone, activity via onExit artifact, ephemeral-storage accounting for the new emptyDir).
Checked specifically:
- No leftover references to the removed
clone-gitopstop-level template,restore-go-cache/save-go-cache, orGOPATH— clean removal. minio-cache-credsExternalSecret deletion: confirmed no other consumer inargo-workflowsnamespace; the per-tenant openclaw secrets of the same name are separate declarations and untouched.- Handoff path validation (Python collector, NUL-safe) correctly blocks paths outside
agents-state/**/.status.yaml, symlinks,.., and control characters before anything reaches the checkout. commit-and-pushtakes its own fresh clone, never touches the agent's worktree (closes #983 P1 for this template), and the mutex/retryStrategy pairing matchescwft-mctl-agents-run.- Resource requests/limits (cpu/memory/ephemeral-storage) added consistently across the new initContainers and the emptyDir-backed main containers.
One P3 posted inline: the second-layer NON_STATUS_CHANGES/STATUS_CHANGES guard in commit-and-push parses git status --porcelain -z with a blind cut -c4-, which is the same rename/copy mis-parse class the Python collector's own comment calls out. Not exploitable in this flow (upstream validation already excludes control chars, so a rename can only occur between two already-in-scope paths) and it's an existing pattern already merged in cwft-mctl-agents-shepherd.yaml, so not novel to this PR and not blocking.
|
Answering agy's two findings, since the P2 describes a real mechanism even though its consequence does not follow. P2 — SIGKILL loses the handoff. Mechanism confirmed, consequence does not hold.Every link up to the last one is true, and it is worth stating plainly: an Where the finding breaks is "causing duplicate PR attempts on the next run". The residual cost is one wasted attempt in the narrower case where a The proposed fix is self-defeating and says so in its own parenthetical: archiving Worth being explicit about scope: this property is identical in P3 — no tests for the handoff logicFair, and it is the same gap for the three templates already carrying this code. There is no harness in this repo for CWFT shell or for a heredoc'd Python collector; claude[bot]'s P3Accurate and correctly scoped as non-blocking: the second-layer guard's Gate: claude[bot] APPROVED, 0 P1/P2. Codex hit its usage limit and never read the diff. agy is the non-blocking pilot and both of its findings are answered above. |
|
Correcting the gate line at the end of my previous comment, and adding the part of the P2 rebuttal I had not yet verified in code. The gate on this PR is agy, not claude[bot]I wrote "agy is the non-blocking pilot". That is the general rule and it is the wrong rule for this class of PR. On gitops#982 — shell inside a ClusterWorkflowTemplate, exactly what this PR is — claude[bot] returned APPROVED, "No P1/P2 findings", five rounds running, while agy found 8 real issues across those same rounds including three P1, two of which were defects introduced by the previous round's fix. claude is reliably blind to the trust boundaries between Argo steps: what reaches So The P2 rebuttal, now verified against the code rather than a commentPreviously I cited the comment at And there is a point in the emptyDir design's favour that I missed: on What genuinely regresses is narrower than the finding states: a Merge sequencingRecording this on the PR because it is part of the change, not of the review: the last time this exact port was merged — #676/#677, take-reconcile-off-the-PVC, clean CI and a clean claude review — the first live cron tick after ArgoCD synced failed two production crons and both had to be reverted (#679). CI here validates schema, not behaviour; So this does not get merged and left alone. After merge: wait for ArgoCD to sync (Argo expands |
Closes the last
volumeClaimTemplatesin the agent fleet (#1047). With it, no mctl-agents workflow creates a Hetzner volume any more.Why now
Hetzner's volume action history for the last week (
/v1/volumes/actions, cross-referenced withkube_persistentvolumeclaim_info) attributes every volume created since 2026-09-08 to this one template:Six steps were six pods spread across nodes, so the RWO volume moved between them on every step — six attach/detach pairs per run,
Multi-Attach errorin the events on each. At ~20 DevLoop-driven runs a day that is ~120 volume operations daily on one template. The cost is trivial (€0.0572/GB/month); the risk is not: a wedged detach leaves a pod Pending, and a Pending pod holdingmctl-gitops-main-writesis the 2026-07-30 cascade.The port
Same recipe as #1042 / #1046 / #1048, carried over in full:
clone-gitopsstep is gone; each pod that needs the checkout takes its own in an initContainer.commit-and-push. Both primary and fallback write the same key, so no reference has to resolve through a skipped step.optional: true, and the output directory is created only once there is something to put in it, so an attempt that produced nothing cannot clobber the previous one's object..status.yamlunderagents-stateis fatal rather than skipped.commit-and-pushtakes its OWN fresh clone and validates the handoff before any of it reaches the checkout — the.git/hooksboundarygit statusstructurally cannot see. This closes commit-and-push runs git as root inside a worktree the agent can write #983 P1 for this template and letssafe.directoryandgit reset -q origin/maingo with the shared worktree.activityreaches onExit as an optional artifact, not a file on the PVC and not aworkflow.outputs.parametersentry (fix(agents): skip Telegram notify on no-op Succeeded runs #530/fix(agents): pass activity via workdir PVC, not workflow.outputs #531, and shepherd: post-deploy-verify reports "unknown (verification did not complete)" on runs where it completed cleanly #1186 for why the one remaining slot did not work either).ephemeral-storagerequests and limits on every step, because an emptyDir counts against the pod rather than being invisible to the node.retryStrategyoncommit-and-push, matchingcwft-mctl-agents-run.The Go cache is deleted, not ported
restore-go-cache/save-go-cacheran onminio/mcand bridged the shared volume to MinIO. The investigation recorded on #1047 found they have never worked:GOPATHto$HOME/goon every run — 178export GOPATH=$HOME/gostatements in one run's audit log — so the container'sGOPATH=/workdir/gocache/gopathis set correctly and then discarded.find /workdir/gocache/gopath -maxdepth 3 -type fcannot see a Go module cache even when it is full; the first file sits deeper.Both steps end in an unconditional
exit 0and are documented as non-fatal, so six months of "starting cold" went unnoticed, andplatform-cachein MinIO has never held amctl-agents/gocache/object. Removing them is removing dead code, not a regression. TheirExternalSecretinargo-workflowshad no other consumer and goes with them; the per-tenant openclawminio-cache-credssecrets are a separate declaration and are untouched.A cache that actually works is a different design — a per-service/per-toolchain partitioned artifact at the path the agent really uses (
/home/app/go), plus per-language images so the Go/Zig/musl-cross/gcc bootstrap stops being paid at all, which is the larger half of the cold start. Tracked separately from this port.Validation
argo lint --offlineclean on the wholecluster-templates/directory, with the CLI pinned to the cluster's v3.7.11. (The v4 CLI rejectssynchronization.mutexon every template in the repo, including untouched ones — a schema change, not a finding.)scripts/validate-local-workdir.py,scripts/validate-shell-param-interpolation.py,scripts/validate-agent-platform.py --selftestall pass.What a merge does NOT prove
#676/#677 were green and were reverted on a misattribution, so a green run is not the acceptance criterion. Before this is called done:
kubectl get pvc -n argo-workflowsempty during the run, noSuccessfulAttachVolumein pod events, nocreate_volumein Hetzner's action history — and the PR it was implementing still lands.activityvalue (yes/none), asserted on the value and not on the exit code.agents-statefails the step and pushes nothing, while the pre-change template accepts the same payload.Refs #1047, #856, #983
🤖 Generated with Claude Code
https://claude.ai/code/session_011zuKeZdjt1ZfF4SaK3rEkf