release-train: develop -> staging - #519
Conversation
…491) A Helm chart repo publishes only on a version change, so a template/values edit without a Chart.yaml bump reaches no installs. That is exactly how the perIngestionTables flag block shipped to staging yet never rendered (PR #472 changed the template but not the version, so the published 1.9.7 stayed stale). This blocking gate makes the bump non-optional. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… re-runs) (#501) * feat(#420): resume after reboot + schema-versioned install state Two legitimate reboots (Windows feature enablement; Docker/WSL first boot) can interrupt the install. Before, every interruption ended with "re-find and re-paste the one-liner", and each re-run re-walked the whole install. Now: - Schema-versioned state file under %USERPROFILE%\.tracebloc\install-state.json. Pure, unit-tested helpers (New-InstallState, ConvertTo-InstallState, Add-CompletedStage, Test-StateHasStage, Test-InstallStateCurrent) + thin I/O wrappers (Read/Save/Set-StageComplete/Set-InstallComplete). A corrupt or incompatible-schema file degrades to a fresh state -- never a throw. The state is ADVISORY: every stage still self-verifies (tools re-checked, cluster re-derived), so a stale checkpoint can never skip real work. - Resume-after-reboot via a RunOnce continuation. On a reboot the installer checkpoints 'features-reboot-pending' and registers HKCU RunOnce (Get-ResumeCommand reuses the #421 elevation arg-builder + -Resume) so the install resumes at next sign-in with no re-pasting -- for both auto-reboot and manual -NoReboot. -Resume is forwarded through the admin-gate self-elevation. Only the durable -File form carries -Resume; the irm|iex shim has no param block (#421), and the state file drives the one-liner path anyway. Cleared on success so it never fires spuriously. - Fast idempotent re-runs. Each of the 6 steps checkpoints on completion; on a fresh run where a prior install completed AND the tools + cluster are still present, the installer prints "already installed -- nothing to do" and exits 0. The gate verifies real presence, not just the checkpoint, so the claim is honest. Tests: pure state helpers (round-trip, corrupt/empty/wrong-schema -> fresh, dedup), Get-ResumeCommand (File form carries -File+switches+-Resume; one-liner omits -Resume), Save/Read I/O round-trip + corrupt-file degradation (mocked path, no home writes), and source guards for the -Resume plumbing, reboot-exit RunOnce, per-step checkpoints, success clear+complete, and the honest fast-path gate. Closes #420 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#420): only mark the install complete on success Bugbot (High): Set-InstallComplete ran unconditionally before the exit-code check, so a failed client state (bad_creds/crash/image_pull/image_pull_ca) still persisted completed=true. A re-run then hit the tools+cluster fast path, printed "already installed -- nothing to do" and exited 0 -- blocking the documented remediation (the summary tells the operator to re-run). Add Test-InstallSucceeded (connected/starting = success) as the SINGLE source of truth shared by the completion checkpoint and the exit code, so they can't drift. Set-InstallComplete now runs only when Test-InstallSucceeded; the exit code uses the same predicate. RunOnce is still cleared unconditionally (no reboot is pending once the walk finishes). Unit test across connected/starting + failure states, plus source guards for the gated completion and shared-predicate exit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#420): fast path requires a RUNNING, bounded cluster check Bugbot round 2 on the new fast path: - High: the probe only checked the cluster NAME appeared in `k3d cluster list`, not that it was running -- so a completed install with a STOPPED cluster (or dead node) printed "nothing to do" and exited 0, skipping New-K3dCluster's start/repair. - Medium: it ran a bare `k3d cluster list` with no deadline, so a wedged Docker engine hung the fast path at the start of every re-run (violates the bounded- external-command rule). Replace Test-ClusterPresent with Test-ClusterRunning: BOUNDED via Start-Job + Wait-JobWithProgress (15s) + Remove-Job, parsing `-o json` through a pure Test-ClusterRunningInList that requires serversRunning >= 1 for the named cluster (present-but-stopped -> false -> fall through to the repair walk). Fast-path message now states what was verified ("installed and the cluster is running"). Tests: Test-ClusterRunningInList (running/stopped/absent/corrupt/no-count), a source guard that the probe is job+deadline bounded, and the updated fast-path gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(#420): address review — connected-only completion, drop dead stages, split-account note Asad's review on #501: - Completion counted `starting`, but that's Get-NotReadyState's catch-all for a client that isn't Ready yet -- so a client that never comes up got completed=true and armed the fast path, skipping remediation. Completion now requires the client to be CONNECTED (new Test-InstallConnected); the exit code keeps the more lenient connected||starting (Test-InstallSucceeded) so a slow-but-starting client doesn't hard-fail the run. The two predicates differ on purpose and say why. - The per-stage checkpoints were written but never read (the six steps set shared $script: state, so a resume must re-walk them; speed comes from each step's own self-skip). Dropped the dead machinery -- stages array, Add-CompletedStage, Test-StateHasStage, Set-StageComplete, Test-StageComplete, per-step calls, and the features-reboot-pending checkpoint. State is now just {schema, completed}. - Resume scope clarified: the reboot happens in Step 1 during the elevating account's session, so HKCU RunOnce is correct for that account. Added a Hint for the split -DailyUser case (a different user signing in after the reboot must re-run). Tests updated: pure helpers (no stages), Set-InstallComplete round-trip, Test-InstallConnected vs Test-InstallSucceeded across states, split-account Hint guard, and a guard that the stage machinery stays gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#420): fast path verifies client health; clear stale completed on failure Bugbot round on the review rework: - High: the fast path claimed "nothing to do" from completed + tools + a running cluster WITHOUT checking the client workloads are Ready (the bash assess path requires Ready workloads), and `completed` was never cleared when a later walk failed. So a re-run started because the client is down could print "already installed" and skip the remediation. Now the fast path also requires Test-ClientHealthy (finds the release namespace via Get-InstalledClientInfo, then a SHORT bounded `kubectl rollout status --timeout=5s` per client deployment), and a walk that doesn't end connected calls Clear-InstallCompleted so a stale flag can't keep the fast path armed. Extracted Get-ClientDeploymentNames as the shared source of truth for the readiness gate and the health check (no duplication). - Low: the force-reinstall hint hard-coded ~\.tracebloc\install-state.json; it now interpolates Get-InstallStatePath so it's correct under an overridden HOST_DATA_DIR. Tests: Test-ClientHealthy (unknown/no-ns/all-ready/not-ready via mocks), Get-ClientDeploymentNames, Clear-InstallCompleted round-trip, and updated fast-path + hint-path + completion source guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two Medium Bugbot findings from the develop->staging promotion (client#499), both customer-visible on the Windows path. 1. Print-Roadmap listed five steps while the runtime ran six (the added "Install system tools" phase was never added to the up-front banner), so every later step was mis-numbered -- undercutting the honest-progress split it was meant to show. Fix: a single $script:INSTALL_STEPS source of truth. Print-Roadmap renders it, and every "Step N/total" header derives its total from INSTALL_STEPS.Count, so the roadmap and the runtime step count can't drift again. 2. The tracked winget/installer installs (Docker Desktop via winget, Docker Desktop direct, winget k3d, winget helm) had no output redirects, so a failure left only a bare exit code in the log / -Diagnose bundle -- expensive when we debug Windows installs remotely from a customer log. Fix: a shared Invoke-TrackedInstall helper captures stdout+stderr to temp files, waits with a killing deadline, folds the output into the log (stderr first, matching #423), and returns a typed outcome. All four sites route through it; the WSL / k3d-cluster-start paths already did this, so this removes an inconsistency inside one change rather than adding a new convention. Tests: INSTALL_STEPS/Print-Roadmap single-source + no hard-coded /6 + step-count parity; Invoke-TrackedInstall behavior (ok/failed/timeout/spawn-failed via mocks) + redirect/ordering source guards + all four installs routed through it. Updated the #419/#422 guards that pinned the old Start-Process shapes to the wrapper. Closes #500
…#494) * feat(ingestor): resolve the spawned tag per environment (backend#1360) dev and staging edges now spawn ingestion Jobs from the internal channels published by the matching data-ingestors branch (:dev / :stg) instead of the 0.7 release float, so an ingestor change can be validated on a real edge without a production release. Before this, the ingestor image existed only as a byproduct of a prod release -- on 2026-07-30 testing one change cost a prod PyPI publish plus an FR-gate override. - images.ingestor.tag becomes an explicit override, EMPTY by default. - images.ingestor.channelTags carries the per-environment floats (dev/stg/prod), keyed on the resolved CLIENT_ENV. - New tracebloc.ingestorTag helper mirrors tracebloc.ingestorDigest's precedence: explicit tag > channel for CLIENT_ENV > literal 0.7 (so a release predating these keys still renders under --reuse-values). - prod deliberately stays a semver float, NOT a :prod channel -- no such tag is published, and prod normally runs prodDigest anyway. - The ingestor-multiarch CI guard previously hard-failed on an empty tag; it now validates the explicit override when set plus every channelTags entry, since an edge resolves exactly one of them. Chart defaults propagate through the fleet auto-upgrade (--reset-then-reuse-values), and the installer does not pin the tag, so existing dev/staging edges pick up their channel on the next upgrade. An operator who set images.ingestor.tag explicitly keeps it. Verified: 307/307 helm unittest across 27 suites (9 new cases covering each environment, the override, an unknown CLIENT_ENV, and a channelTags-less replay); helm lint --strict clean on all four platform values files; rendered INGESTOR_IMAGE_TAG confirmed as dev/stg/0.7/0.7 for dev/stg/prod/unset; schema still rejects 'latest' in channelTags and now accepts the empty override. * fix(review): normalize CLIENT_ENV aliases before the channel lookup Asad's first note landed on a real defect, not just a stale description. The schema documents CLIENT_ENV as (dev, staging, prod) while the channel keys are dev|stg|prod, so CLIENT_ENV=staging -- the documented value -- missed channelTags entirely and fell back to the prod float. Meanwhile client-runtime normalizes staging->stg at runtime, so that edge would have talked to the stg backend while spawning the 0.7 release ingestor: exactly the split-brain client-runtime#227 was filed for, reintroduced one layer up. tracebloc.ingestorTag now normalizes development/staging/production before the lookup, mirroring proxy_config.ENV_ALIASES, and the schema description states which values are canonical and that it is load-bearing for channel selection. Also addresses the second note: values.schema.json is edited surgically in the file's own style instead of being reformatted by a json round-trip (576-line diff -> 29). Verified: 310/310 helm unittest (3 new alias cases); lint --strict clean on all four platform files; rendered tag is dev/dev/stg/stg/0.7/0.7 for dev/development/stg/staging/prod/production, and an unknown value still falls back to the float rather than rendering empty. * fix(review): one CLIENT_ENV normalizer, and unbreak the digest resolver Both from Bugbot on #494, and both caused by my own half-applied alias fix. 1. The alias normalization went into tracebloc.ingestorTag only, while tracebloc.ingestorDigest still compared the RAW CLIENT_ENV to "prod". So CLIENT_ENV=production got the prod float tag but an EMPTY digest -- silently dropping the reproducibility pin (backend#1028/#1245) on an edge that looked correctly configured, which is worse than the bug the alias fix was for. Extracted tracebloc.clientEnv as the single normalizer and pointed both helpers at it, so they cannot drift again (the reason ENV_ALIASES lives once in client-runtime proxy_config). 2. scripts/resolve-ingestor-digest.sh read images.ingestor.tag, now empty by default, so the documented no-arg / --write path exited on an empty tag -- the exact command the chart comments and the ingestor-multiarch CI error tell operators to run. It now falls back to images.ingestor.channelTags.prod on both the yq and the yq-free path, with a matching sibling awk reader scoped the same way. Verified: 311/311 helm unittest (a new case pins the production-alias digest); lint --strict clean on all four platform files; rendered tag+digest correct for prod/production/stg/staging/dev/development; the script's no-arg path resolves 0.7 -> the multi-arch digest again, and the portable reader returns 0.7 with tag empty. * style(script): keep each yq-free reader with its own header comment The new read_ingestor_prod_channel landed between read_ingestor_tag's header comment and read_ingestor_tag itself, so that header described the wrong function and read_ingestor_tag sat comment-less ~30 lines below. Moved the new function below the original; each header is now directly above the function it documents. No behaviour change -- both readers verified still returning 0.7 / empty, and the no-arg resolver path still resolves the multi-arch digest. * fix: bump the chart to 1.9.9, and strip comments before quotes in the channel parser Two things, both caught by CI/review rather than by me: - The 'chart content ⇒ Chart.yaml version bump' guard was red: this PR edits client/templates and client/values.yaml, and a Helm repo publishes only on a version change, so without a bump the whole change would reach no install -- exactly how the perIngestionTables block shipped dark in #472. Chart 1.9.8 -> 1.9.9. - read_ingestor_prod_channel stripped quotes BEFORE removing an inline comment, the reverse of read_ingestor_tag. A channelTags.prod line with a trailing comment therefore parsed as 0.7" -- a stray quote -- and the no-arg/--write resolver would look up a nonexistent ref. Reordered to match: key, comment, trim, quotes. Verified across all four forms (double/single quoted with and without a trailing comment, and bare).
…ackend#1181) (#503) * feat(chart): provision per-experiment DB credentials (RFC-0003 D10, backend#1181) The chart half that flips client-runtime#235 from inert to live: - perExperimentDbCreds value (default false, schema-typed) → renders PER_EXPERIMENT_DB_CREDS + TB_CREDMGR_USER + TB_CREDMGR_PASSWORD onto jobs-manager (password via secretKeyRef, never plain). - secrets.yaml: generate-once TB_CREDMGR_PASSWORD (upgrade-stable, same lookup pattern as POD_TOKEN_SIGNING_SECRET), emitted only when enabled. - rbac: secrets verbs gain 'delete' — jobs-manager deletes the per-job cred Secret in the revoke path. - Chart.yaml 1.9.8 -> 1.9.9 (publishes on version change). Default installs render byte-identically (all flag-gated; 300 helm tests, both sides pinned). jobs-manager self-provisions the tb_credmgr account from this Secret on startup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(chart): gate the RBAC delete verb + credmgr pin/tests (Saqlain + Bugbot #503) - rbac: the Secret `delete` verb is now flag-gated on perExperimentDbCreds and scoped to `secrets` alone (was on configmaps+secrets, every install). Default-off is byte-for-byte `["create", "get"]` again — a cluster-wide delete-on-all-Secrets ClusterRole no longer ships to every install; the verb the revoke path needs only appears when the flag is on (Saqlain #A). - secrets: TB_CREDMGR_PASSWORD gains the 3rd resolution tier — an explicit `.Values.credmgrPassword` operator pin (DR / pre-created MySQL account / forced rotation), mirroring podTokenSigningSecret; validated alphanumeric (jobs-manager's constraint) with a fail-fast. Also documents the off->on->off->on regen edge and that the pin is its fix (Saqlain #B/#C). - Chart appVersion 1.9.8 -> 1.9.9, back in lockstep with version so the `app.kubernetes.io/version` labels report the shipped chart (Bugbot). - tests: secrets.yaml + rbac.yaml helm-unittests for both flag states (credmgr key absent/present, pin flows, non-alnum rejected, delete verb gated + secrets-only). 307 helm tests pass; helm lint clean (Saqlain #D). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (backend#1184) (#504) * docs(seal-check): record k3d/k3s NetworkPolicy substrate verification (backend#1184) RFC-0003 §8.4 says 'do not assume k3d enforces NetworkPolicy'. Ran the substrate check on a throwaway k3d v5.8.3 / k3s v1.33.6+k3s1 cluster: a standalone deny-egress NetworkPolicy on a probe pod took a curl to 1.1.1.1:443 reachable -> BLOCKED -> reachable-again-after-removal (HTTP 301 -> connect failure -> HTTP 301). The block is attributable to the policy, so k3s's embedded kube-router controller does enforce egress NetworkPolicy on this k3d version — the substrate doubt is resolved. Records the result in the §8.4 runbook + the follow-ups list; the §8.3 k3d cell now reads 'substrate verified; full-probe run pending'. Recording the full-chart egress-enforcement probe run against a deployed release stays open (still backend#1184). Docs-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(seal-check): fix matrix drift + lead with unsealed framing (Saqlain #504) - Update the §8.3 k3d matrix cell (was 'verification run pending; do not assume') to 'Substrate verified; full-probe run pending', matching the prose that cross-references it — the concrete edit the PR promised but never made (Saqlain #i). - Status note now LEADS with 'still UNSEALED for the egress guarantee on k3d until the full-chart probe is recorded', honoring this doc's unsealed-never-silently-sealed philosophy — a skimmer can no longer read the bold VERIFIED as 'sealed' (Saqlain #ii). - State the run evidence (versions + reachable->blocked->reachable) ONCE in the status note; the post-runbook paragraph, the follow-ups bullet, and the matrix cell now reference it instead of restating — which is how the cell drifted out of sync in the first place (Saqlain #iii). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Records a redacted gitleaks baseline at the repo root so the full-history dispatch scan runs clean, and wires the code-quality caller to consume it via the gitleaks-baseline input. Part of tracebloc/backend#1303. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(#425): tell the truth about stuck image pulls, don't sell failures as "downloading" helm runs without --wait, so the pull-progress helper waited TB_PULL_TIMEOUT and, on expiry, always printed "Services are still downloading — they'll finish starting in the background." — even when the pulls had PERMANENTLY failed (x509 / blocked registry / auth). A hard failure was reported in success-flavored copy. Bash (scripts/lib/install-client-helm.sh): - New _pull_failure_detail: on a visible ImagePullBackOff/ErrImagePull, prints the failing pod line + the matching pull event (self-contained; no cross-lib source). - New pure _progress_end_message: maps (pulled,total,max_pulled,has_fail) -> one of done|failed|downloading|stalled. A permanent failure NEVER maps to "downloading", so it can't be sold as background progress. - _download_services_progress tracks max_pulled and, on timeout, classifies: a failure warns loudly with the event text; "downloading in the background" prints ONLY when pulls demonstrably progressed; otherwise a neutral "not pulling yet". Windows (scripts/install-k8s.ps1) — the Wait-ForClientReady path already classified pull failures (image_pull/image_pull_ca/crash) into honest red-X summary branches; "downloading" only shows for a genuine starting state. Now it also carries the event text: Get-NotReadyState captures the pull event (or the failing pod line) into $script:NotReadyDetail, and the failure summary branches print it via the new Write-NotReadyDetail helper — matching the bash acceptance on both platforms. Tests: bats for _progress_end_message (all four outcomes; failure wins over partial progress) + _pull_failure_detail (failing prints detail/0, healthy prints nothing/1) + a source guard that the end copy routes through the selector; Pester for Get-NotReadyState detail capture (x509 / non-x509 / no-event fallback) and Write-NotReadyDetail (prints under a label; no-op when empty). Closes #425 * fix(#425): scope _pull_failure_detail to pull-failure events only (Bugbot) Bugbot: the event grep matched bare x509/TLS strings alongside failed to pull/ ErrImagePull, so with tail -n 3 an UNRELATED x509 event elsewhere in the namespace could displace the real pull-failure line and show the wrong reason. Scope to failed to pull|ErrImagePull only — matching summary.sh::_diagnose_not_ready and the PowerShell path. A genuine x509 pull failure is on a "failed to pull ..." line, so its detail is still captured; an unrelated x509 event is not. Test: unrelated x509 events don't displace the real pull reason (403 survives; x509 scoped out). Also merges develop (manifest regenerated). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#425): soften stuck-pull wording + bound the failing-pod list (Asad review) - ImagePullBackOff/ErrImagePull can also be a transient blip or a registry 429 that kubelet keeps retrying, so wait_for_client_ready may still reach "connected". The absolute "this won't finish on its own" could contradict a later ✔ Connected — soften to "look stuck pulling — this usually needs action, not just more time". - _pull_failure_detail printed $bad unbounded though the header says "Bounded" and the PowerShell path caps at 3; add `| head -n 3` so a many-failing-pods cluster doesn't print a wall of lines (matches Select-Object -First 3). Test copy assertion updated to the new wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…508) * fix(#431): idempotent GPU reconfigure — no Docker restart / cluster bounce on re-run On a GPU host, every re-run of install_nvidia_container_toolkit unconditionally ran `nvidia-ctk runtime configure --runtime=docker`, then `systemctl restart docker`, then a CUDA smoke test that re-pulls nvidia/cuda. Restarting Docker takes the live k3d cluster DOWN mid-reinstall (a correctness hazard, not just wasted time). - Skip-when-satisfied: reconfigure + restart Docker only when the running daemon isn't ALREADY defaulting to the NVIDIA runtime (_docker_default_runtime_is_nvidia via `docker info`). A re-run on a configured host does nothing here — no restart, no cluster bounce. - When a restart IS needed and the cluster is live, say so and sequence it: warn, restart Docker, then `k3d cluster start` to bring the cluster back deterministically rather than relying only on the nodes' restart policy. Cluster-running is detected jq-free via _k3d_cluster_running. - Cache the smoke test: record the toolkit+driver signature (_gpu_stack_signature) of the last PASSING test; skip (no image pull) when unchanged, re-verify when it changes. Also fixes a latent awk bug the tests caught: an `exit 0` in a main rule still runs END, so the old `END { exit 1 }` overrode a positive result — the running-cluster check now decides only in END. Tests: new scripts/tests/gpu-nvidia.bats — the three helpers plus the acceptance behaviors (already-nvidia -> no reconfigure/restart; not-nvidia -> reconfigure + restart; live cluster -> warn + k3d cluster start; cached smoke test -> no pull; first run -> records the signature). Closes #431 * fix(#431): bound GPU probes, surface restart failure, re-verify after reconfigure Bugbot round on the idempotent GPU work: - High: _docker_default_runtime_is_nvidia ran a bare `docker info` with no timeout — a wedged daemon would hang a headless re-run at the skip gate. Route it (and the k3d probe) through _bounded (timeout(1)/gtimeout(1)) with TB_PROBE_TIMEOUT. - Medium: after warning the cluster would restart, `k3d cluster start` discarded output and `|| true`'d failure, so a failed bring-up was reported as success. Capture output, and on failure warn with the k3d error + a manual-start hint. - Medium: the smoke-test cache keyed only on toolkit+driver signature, so a re-run that ACTUALLY reconfigured + restarted Docker could still skip `docker run --gpus all` and report a possibly-broken post-restart path as verified. Force the smoke test (bypass the cache) whenever the runtime was reconfigured this run. Tests: bounded-probe source guard; a failed cluster restart is surfaced (warn + manual hint); a reconfigure re-verifies even with a matching marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#431): bound the post-restart k3d cluster start (Bugbot r2) The `k3d cluster start` added to bring the cluster back after a Docker restart had no deadline; k3d defaults to --wait with timeout 0 (forever), so a slow post-restart bring-up could hang a headless re-run. Add --wait --timeout "${min}m" (TB_CLUSTER_START_TIMEOUT_MIN, default 5) so it aborts with a real k3d error — which the existing failure path then surfaces (warn + manual-start hint). Mirrors the bounded `cluster create` (#426). Source guard added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#431): don't strand a live cluster on an inconclusive probe; clear stale smoke marker Bugbot round 3: - Medium: a forced smoke re-verify that FAILS left the old .gpu-smoke-ok marker in place, so the next (reconfigured=0) run saw a matching signature and skipped the test, treating a failing stack as verified. Remove the marker on smoke failure. - Medium: _k3d_cluster_running returned "not running" both when the cluster was genuinely down AND when the bounded probe failed/timed out. On a wedged daemon (which also fails the nvidia check) that meant Docker was restarted WITHOUT a cluster start, silently leaving a live cluster down. Make it tri-state (0 running / 1 not running / 2 unknown); the caller now attempts recovery on running OR unknown, warning "Couldn't confirm … will try to bring it back". Tests: probe failure -> unknown (rc 2); unknown state still attempts recovery + warns; a failed forced smoke test clears the stale pass marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#431): make the GPU probes set -e safe (installer sources under set -euo) Bugbot round 4 — the idempotency helpers tripped the installer's `set -e`: - High: `_k3d_cluster_running; cr=$?` (and the internal `out=$(...); rc=$?`) are bare sequences, so a non-zero probe aborts the install before `systemctl restart docker` on a first-run GPU host (no cluster yet, probe returns 1) — and made the UNKNOWN(2) timeout path unreachable. Use `|| cr=$?` / `|| rc=$?` so the code is captured, not fatal. - Medium: `_gpu_stack_signature` ended on a failed `[[ -n … ]] && printf`, returning 1 when no versions are found; `gpu_sig="$(…)"` then aborted under set -e. Add `return 0` so an empty signature means "don't cache", not "abort". Tests: two set -e integration tests (source common.sh + gpu-nvidia.sh under `set -euo pipefail`) — empty signature and a failed cluster probe both complete without aborting. These exercise the exact context the unit tests (no set -e) missed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#431): bound the GPU signature probes too (Bugbot r5) _gpu_stack_signature shelled out to nvidia-ctk + nvidia-smi with no deadline, and it runs at the smoke-test skip gate on every re-run — a half-ready driver (installed without reboot) or a stuck device node could hang a headless re-run, the same class already bounded for docker info / k3d cluster list. Route both through _bounded "${TB_PROBE_TIMEOUT:-5}"; an empty signature on timeout already means "don't cache". Source guard extended to assert both signature probes are bounded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#431): fix _gpu_stack_signature tests under CI (timeout present) The two _gpu_stack_signature tests didn't mock `has`, so on a runner that HAS timeout(1) (CI) the newly-_bounded probes exec the real (absent) nvidia-ctk/nvidia-smi via `timeout` — which can't see shell-function mocks — and the "combines versions" assertion failed (it passed on macOS, which lacks timeout, so _bounded took its passthrough branch). Add `has() { return 1; }` to both tests so _bounded runs the function mocks, matching how the docker/k3d probe tests already suppress timeout. Verified with a fake `timeout` on PATH: 21/21 pass. Production is unaffected (real GPU hosts have the binaries); this was purely a test-mocking gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…acOS) (#509) A YAML single-quoted value escapes a quote by doubling it ('' -> '). The unescape used `${line//\'\'/\'}`, whose `\'` REPLACEMENT literal is bash-version dependent: bash 4/5 (Linux CI) yields the intended `'`, but bash 3.2 (the macOS system bash) keeps the backslash and produces `a\'b` — a corrupted clientPassword for any macOS user whose password contains a doubled quote. The bats test for this case has been failing on macOS while passing on the Linux CI leg for exactly this reason. Use a variable for the quote in the pattern + replacement (`local _sq="'"`), which expands to a bare quote on bash 3.2 and 4/5 alike. Existing test kept; now passes on both. No behavior change on Linux.
…installs (#511) * fix(#427): grant docker group on any daemon run; refuse sudo-wrapped installs Two compounding identity bugs on Linux: 1. The docker-group grant ran ONLY inside the fresh-Docker-install branch. On a box where Docker was already present and the user wasn't in the group, the else-branch printed "Docker" without granting, and the recovery path dead-ended at "Could not connect to Docker. Try logging out and back in…" — which couldn't help, because membership was never granted. Re-runs looped on the same message. Fix: after the install/else, ensure the invoking user is in the docker group whenever the daemon path is chosen, regardless of a fresh install. Skip if already a member (no redundant usermod); prepare-host stays exempt (only TB_PREPARE_USER is granted, later — least-privilege, #381). The existing sg-docker re-exec then activates the new membership in-session, so no dead-end loop. 2. Nothing was SUDO_USER-aware: `sudo bash install.sh` ran the WHOLE provision as root — usermod granted root (not the user), and ~/.tracebloc, ~/.kube/config, and the chmod-600 credential landed root-owned under /root, with no chown anywhere to undo it. The installer's model is to run as the daily user and elevate per-step (RFC-0002), so rather than a fragile ownership remap, refuse the sudo-wrapped full run early (before any file is created): refuse_sudo_wrapped_install errors when EUID 0 AND $SUDO_USER is a real (non-root) user. Exemptions: a genuine root login (no SUDO_USER); prepare-host (the admin path, already dispatched+exited in main()). Because we never run as root-with-SUDO_USER, every $HOME/$USER path in the tree stays correct with no remap. Also: honor TB_PREPARE_USER as the grant target in the main install (new _real_install_user helper; the #418 Windows peer). Tests: install_docker_engine grants on the pre-installed path + when only TB_PREPARE_USER differs, skips when already a member, never grants the admin in prepare-host, still grants on a fresh install; refuse_sudo_wrapped_install refuses sudo+SUDO_USER but allows root-login / sudo -i / non-root; _real_install_user. Closes #427 * fix(#427): grant the invoking user (not TB_PREPARE_USER); name TB_PREPARE_USER in the refuse hint Bugbot: - The main-install docker-group grant targeted _real_install_user (TB_PREPARE_USER when set), but socket access and the sg-docker re-exec key off $USER. When they differ (e.g. a leftover `export TB_PREPARE_USER=` from prepare-host), the invoking user never got membership and hit the same dead-end this PR fixes. Grant $USER directly — the sudo-wrapped run is already refused, so $USER is the real daily user — and keep TB_PREPARE_USER on the prepare-host path only (where it's granted). Removed the now-unused _real_install_user helper + its tests. - The sudo-refuse hint pointed admins at a BARE `prepare-host`, but run_prepare_host only grants when TB_PREPARE_USER is set — so that remedy prepares the daemon and grants nobody. Name it: `export TB_PREPARE_USER=<user> && … prepare-host`. Tests: the grant targets $USER even with a leftover TB_PREPARE_USER; the refuse hint includes TB_PREPARE_USER=<user>. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#427): refuse hint names a RESEARCHER placeholder, not the admin (Bugbot r2) The sudo-refuse prepare-host remedy filled TB_PREPARE_USER=${SUDO_USER}, but SUDO_USER is the ADMIN who ran sudo — the "setting up for someone else" case targets a DIFFERENT researcher. Following it would grant the admin docker-group access and leave the intended user locked out (the #377 least-privilege footgun). Use a <researcher-username> placeholder and say "not yourself". Test updated to require the placeholder and reject $SUDO_USER. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#427): key the sg-docker re-exec off _grant_user, consistent with the grant (reviewer) The grant resolved its target with a `${USER:-$(id -un)}` fallback, but the in-session sg-docker re-exec guard still keyed off bare `$USER`. In the exact USER-unset case the fallback exists for, the grant landed on `$(id -un)` while that guard saw an empty `$USER`, skipped the re-exec, and re-introduced the "log out and back in" dead-end. Hoist _grant_user to the top of install_docker_engine and use it in both places so they can't disagree. Guard test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…isn't writable (#512) #432's documented core — running the network-FS guard before the log dir is created in the FULL install — already shipped in #441 (early_data_dir_guard runs before setup_log_file; validate_config before it only validates, never mkdirs). This closes the remaining path #441 didn't cover: prepare-host calls setup_log_file BEFORE any guard, and setup_log_file did a bare `mkdir -p "$HOST_DATA_DIR"` + tee — so on an NFS home under sudo + root_squash (the exact scenario #432 names) it failed with a cryptic error before any friendly message. setup_log_file now chooses its path via _choose_log_file: HOST_DATA_DIR when it's creatable AND writable, else a temp file (the scope's "start logging in a temp location" option) — so no install path dies on a bare mkdir/tee. The full install's early_data_dir_guard still refuses a network DATA dir before this runs, unchanged. (Also: the temp template uses trailing X's — BSD mktemp on macOS rejects a suffix after XXXXXX.) Tests: _choose_log_file returns a path under a writable HOST_DATA_DIR, and falls back to a temp path (never a bare failure) when the dir is uncreatable/unwritable. Closes #432
…colima from RAM (#513) * fix(#428): enforce the macOS memory floor, clamp recommendations, size colima from RAM On macOS the memory picture was the Windows WSL2 story with weaker guardrails: - Floor unenforced: the post-Docker runtime recheck (_pf_recheck_runtime_mem, the one point the REAL VM size is known) only WARNed, so a Docker VM below the 5 GB floor proceeded and OOM-crashlooped the client. It now HARD-FAILS a sub-floor VM with the exact fix (Docker Desktop memory / COLIMA_MEMORY) on every OS; a between-floor-and- warn VM still only warns. - Recommendations exceeded physical RAM: "raise to 16 GB" on a ≤16 GB Mac is impossible. New _pf_clamp_mem_gb clamps every SHOWN figure (rec AND warn) to physical − PF_OS_RESERVE_GB (default 2); all _pf_memory / recheck hints use it. - Colima never sized: it was hard-coded --memory 6 (too big for a ≤8 GB Mac, never scaled up). New _macos_vm_mem_gb derives min(half of physical, clamped rec), floored at PF_MIN_MEM_GB, from hw.memsize — the single sizing helper the macOS VM paths share. setup-macos.sh's colima start uses it (COLIMA_MEMORY still overrides). Deferred (noted): writing Docker Desktop's settings.json memoryMiB with consent — the riskiest piece (mutates a user's Docker config) and not in the acceptance criteria; better as its own PR. Tests: _pf_clamp_mem_gb (clamp/headroom/unknown), _macos_vm_mem_gb (8->5 floor, 16->8, 64->16 cap, unknown->default), recheck sub-floor HARD FAIL vs between-floor warn, and a guard that colima memory is derived not hard-coded. Closes #428 * fix(#428): floor the memory clamp; correct colima resize command (Bugbot) - Clamped hints could undershoot the floor: _pf_clamp_mem_gb capped at physical − reserve with no lower bound, so a ~6 GB host got "raise to 4 GB" — below the 5 GB floor AND below the RAM it already has. Floor the clamp at PF_MIN_MEM_GB so a hint never recommends a sub-floor number (mirrors the PowerShell path's "at least min"). - Colima remedy didn't resize: the hard-fail hint said `COLIMA_MEMORY=… colima stop && colima start`, but colima doesn't read COLIMA_MEMORY and the env prefix applies only to `stop`, so `start` kept the old size. Use the real resize: `colima stop && colima start --memory <N>`. Also fixes a test bug: `_pf_clamp_mem_gb 16 ''` hit the ${2:-host} default and read real host RAM (passed on a big-RAM Mac, failed on the smaller CI runner) — the "can't clamp" case now tests 0 and a non-numeric string. Tests: never-undershoot-floor on a 6 GB host; colima-resize command guard; corrected unknown-physical test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#428): size colima above the floor + honest host-too-small message (Bugbot r2) Catch-22 my sizing introduced: _macos_vm_mem_gb sized colima to EXACTLY the floor (5 on an 8 GB Mac), but a guest's MemTotal runs a few hundred MiB below the configured VM size while the recheck allows only 64 MiB grace — so a fresh install started colima at the derived budget and then hard-failed at cluster-create on the size it had just chosen. And on ≤7 GB hosts the remedy repeated an unachievable size (no way forward). - _macos_vm_mem_gb now sizes ≥ PF_MIN_MEM_GB + 1 (headroom so the guest clears the recheck floor), but never over-commits the host (capped at physical − reserve). An 8 GB Mac -> 6 (clears the recheck); a too-small host gets less and the recheck stops it honestly. - _pf_recheck_runtime_mem: when physical − reserve < floor (host can't ever give the VM the floor), it now says "this Mac has N GB — too little … use a larger machine" instead of a resize remedy that repeats an impossible size. Mirrors the PowerShell host-too-small branch. Tests: 8 GB -> 6; 6 GB host capped to 4 (not over-committed); recheck host-too-small message with no colima-resize line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#428): widen the runtime-recheck grace to the guest-vs-configured overhead (reviewer) The recheck tolerated only 64 MiB below the floor, but a guest's MemTotal runs a few hundred MiB under its CONFIGURED size — so a Docker Desktop VM hand-set to exactly the documented 5 GB floor reported ~4.8 GB and hard-failed, making the effective floor a GB higher than we tell people (colima dodged it by sizing floor+1). Add PF_VM_MEM_GRACE_MIB (512) and use it in the recheck's floor comparison so a VM at the documented floor passes (warns), while a genuinely sub-floor VM (e.g. 4 GB) still hard-fails. Test: a ~4.8 GB guest warns, doesn't hard-fail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…floor (#516) Get-WslConfigMemoryGb writes the WSL2 memory budget into the daily user's .wslconfig during elevated provisioning. It did its own arithmetic -- physical RAM minus a PRIVATE 4 GB reserve, floored at 1 GB -- so it could persist a budget the client cannot run in, and it contradicted the installer's own advice in the same run. Measured before ($env:TB_PESTER="1"; . scripts/install-k8s.ps1): host writes advises (run / train) 8 4 6 / 6 <- below the 5 GB floor: guaranteed OOM crashloop 16 12 8 / 14 32 28 8 / 16 <- over-committed: Windows left 4 GB 6 2 4 / 4 <- doomed budget persisted as if intentional 4 1 2 / 2 8 GB is the important case: a perfectly viable host (8 - 2 GB OS reserve = 6, clear of the 5 GB floor) got memory=4GB while Show-MemoryStatus in the same run said "give Docker up to 6 GB". Rather than re-derive a floor here, the function now DELEGATES to Get-PfMemRecommendation -- the existing single-source helper the advice path uses. The written budget is therefore the advised budget by construction, not by two calculations agreeing: host writes advises (train) 7 memory=5GB 5 (= the floor; 5+2 is the stated practical minimum) 8 memory=6GB 6 (was 4 -> OOM) 16 memory=14GB 14 (was 12) 32 memory=16GB 16 (was 28; Windows keeps 16, and the client cannot 64 memory=16GB 16 use more than the recommended training budget) 6 skipped - 4 skipped - Reserve reconciled: the private ReserveGb=4 is gone and the PARAMETER is gone with it, so no caller can reintroduce the drift. $script:PfOsReserveGb (2) is now the only reserve -- which is what its own comment already claimed ("used to cap recommendations AND to reason about the achievable budget in one place, so the two can't drift", #417 reviewer). Three accessors (Get-PfOsReserveGb / Get-PfMinMemGb / Get-PfRecMemGb) give the printing and writing paths one read path for the same numbers; the reserve accessor fails closed (never 0, which would hand WSL2 the entire host). Host genuinely too small (physical - reserve < floor): the function returns 0 and the caller SKIPS the memory setting instead of persisting a budget known to OOM -- which would also bake the failure into the daily account for every later run and read as intentional to whoever debugs it next. It says so plainly instead ("about 7 GB physical is the practical minimum ... use a larger machine"), the same honest framing the macOS path landed in #513. Still warn-only: Set-DailyUserProvisioning is documented never to fail the install, and Windows memory preflight is warn-only throughout. No headroom fudge is needed on Windows, unlike colima in #513: only a 7 GB host lands exactly on the floor, 7 GB is precisely the practical minimum this code reports, and the Windows recheck warns rather than hard-failing -- so there is no catch-22 where the installer hard-fails on the size it just chose. Scope: Show-MemoryStatus and Get-PfMemRecommendation are deliberately untouched so this does not collide with the open #444 (which rewrites both). Verified forward-compatible -- with #444's floored recommender simulated, every host above returns an identical result, because the too-small gate tests the achievable ceiling rather than the recommender's output. Tests: the two assertions that pinned the old cap/floor are replaced by the new contract -- never below the floor, 8 GB -> 6 not 4, 0 (don't write) when the host can't reach the floor, never over-commits, caps at the training budget, PF_MIN_MEM_GB override honoured, no ReserveGb parameter, and a lock asserting written == advised across seven host sizes. Plus source guards that the caller gates the write on 0 and prints the honest too-small message. Gates: Invoke-Pester scripts/tests/ -> 377 passed / 0 failed; Invoke-ScriptAnalyzer (as CI scopes it) -> 0 errors; check-style.sh clean; check-drift.sh no drift; manifest.sha256 regenerated. Refs #418 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… adapters (#434) Records the strategy decision from the 2026-07 multi-environment installer sweep: single Linux bash core, thin macOS/Windows adapters (Windows adapter gated on the rootless-in-WSL validation, backend#1179), facts single-sourcing + CI parity gate + a real Windows e2e leg regardless of the gate outcome. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
bugbot run |
|
👋 Heads-up — Code review queue is at 35 / 30 Above the WIP limit. The team convention is to review existing PRs before opening new work. Open PRs currently in Code review (oldest first):
Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.) |
Get-WslConfigContent had no production caller. It was superseded by Add-WslMemorySetting, which merges a memory= line into EXISTING .wslconfig content instead of overwriting it -- so it preserves other tuning (processors, swap, ...). Set-DailyUserProvisioning calls Get-WslConfigMemoryGb + Add-WslMemorySetting; nothing calls the old whole-file builder. The only remaining reference was its own Pester test, which asserted the function worked rather than that anything used it -- so the test kept dead code alive. Removed both. Re-verified no caller across scripts/ and .github/ (incl. a case-insensitive sweep and a check for dynamic invocation) before removing. Regenerated scripts/manifest.sha256 for the R8 static-analysis gate. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: shujaat hasan <shujaat@tracebloc.io>
…uence (#514) * fix(#496): verify cgroup delegation is active, and tell the real consequence when it isn't _write_cgroup_delegation wrote the user@.service.d/delegate.conf drop-in + ran `systemctl daemon-reload`, then unconditionally printed "Delegated … A re-login may be needed." But daemon-reload re-reads unit files WITHOUT restarting the running user@$(id -u).service, so cpu/cpuset/io aren't in effect for that session — and the k3d node inherits the delegation state from when it's CREATED. So limit-bearing pods run unconstrained until a re-login AND a cluster recreate, while the install looks green. The old message understated this ("may be needed"). Fix (the issue's recommended Option 3 — verify, don't assume; not the session-killing `systemctl restart user@…`): - New _cgroup_controllers_active reads the live cgroup.controllers of the user slice (/sys/fs/cgroup/user.slice/user-$(id -u).slice/cgroup.controllers; overridable for tests) and is true only when cpu+cpuset+io are all delegated. - After the reload, _write_cgroup_delegation checks it: if active -> success ("active in this session"); if not -> a LOUD warn stating limits won't enforce until a re-login AND `k3d cluster delete <name>` + re-run. Tests: _cgroup_controllers_active (all-present / missing / unreadable), and _write_cgroup_delegation's active-vs-not report. Closes #496 * fix(#496): verify on every run + mode-aware remediation (Bugbot) - Silent re-run skipped verification: _cgroup_controllers_active only ran after a fresh drop-in write; the idempotent path (file already present) early-returned. So the re-run the hint tells operators to do took the silent fast path — and with lingering (the rootless path) a re-login often doesn't restart user@.service, leaving inactive delegation invisible. Restructured so the report/verify runs on EVERY invocation (fresh AND idempotent), and the wording now notes a reboot may be needed with linger. - Wrong prepare-host remediation: _write_cgroup_delegation is shared with run_prepare_host, but the warn told the admin to log out + `k3d cluster delete` — prepare-host creates no cluster, and the verify read the ADMIN's user slice, not the researcher's. Split out _report_cgroup_delegation, mode-aware: prepare-host just confirms the drop-in is written (takes effect at the researcher's next login); the full install verifies this session + gives the recreate remedy. Tests: re-run over an existing drop-in still verifies (no silent fast path); prepare-host mode uses researcher-login wording with no cluster-delete / no admin-slice judgement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#496): re-surface inactive delegation on the REAL call paths (#514 reviewer) The round-2 verify/report worked in _write_cgroup_delegation's own tests but not the actual wiring: 1. _ensure_cgroup_delegation (the only full-install caller) short-circuits at its OWN fast path before ever reaching _write_cgroup_delegation. On a 2nd+ run over a written-but-not-yet-active drop-in — the exact #496 case — it just logged "already present" and returned; the verify/warn never ran. Route the fast path through _report_cgroup_delegation (an unprivileged read, so the no-sudo property holds) so an inactive drop-in re-surfaces. 2. run_prepare_host resets TB_PREPARE_HOST_MODE right after install_docker_engine, before the cgroup write — so the report fell through to the full-install branch and printed "recreate the cluster" advice judged on the ADMIN's slice. Set the mode around the cgroup write (mirrors install_docker_engine) so it reports in prepare-host wording. Tests exercise the real callers now, not the helper in isolation: fast path re-surfaces an inactive drop-in (+ stays sudo-free), confirms an active one, and run_prepare_host reports researcher-login wording with no cluster-delete advice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#496): verify delegation on user@$UID.service, not the slice (#514 Bugbot High) _cgroup_controllers_active read user-$UID.slice/cgroup.controllers, but Delegate= on user@.service enables the controllers INSIDE user@$UID.service (the path runc/rootless-containers document). The slice node routinely lists cpu/io by default (DefaultCPUAccounting), so the full-install path could print "active in this session" while the user manager still lacked the delegation and limit-bearing pods ran unconstrained. Read the manager's node instead; split the default path into a unit-testable _cgroup_controllers_path. Also (reviewer): drop the sudo from _write_cgroup_delegation's idempotent cmp — the drop-in lives under /etc and is world-readable, so a plain cmp matches _ensure_cgroup_delegation's unprivileged grep and avoids a needless elevation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tmpdir symlink + host-CLI leak) (#443) * fix(installer): make the bats suite green on stock macOS (bash 3.2 + tmpdir symlink + host-CLI leak) Three independent reasons the suite failed on a stock macOS box but passed in CI: - bash 3.2 keeps the backslash in a `\'` REPLACEMENT literal, so "${TB_CLIENT_PASSWORD//\'/\'\'}" corrupts a quote-bearing password into a\'\'b inside the generated values file. Replace via a $_sq variable, which expands to a bare quote on 3.2 and 4/5 alike. (develop already landed the same fix for _extract_yaml_value; this is the sibling call site, which was still on the broken form.) - macOS puts BATS_TEST_TMPDIR under the /var -> /private/var symlink while validate_config resolves via `cd -P`, so the under-$HOME check failed spuriously. Resolve $HOME with `cd -P` like the neighbouring tilde tests. - the no-REF/BRANCH bootstrap tests exec'd the HOST's real `tracebloc` CLI (on PATH or re-prepended ~/.local/bin) and exited 0 through the already-installed bail-out, before reaching the gate under test. Add run_boot_hermetic (sandboxed HOME + PATH) for those cases. Also declares bats_require_minimum_version 1.5.0 for the `run -<code>` syntax, and uses `run -127` where 127 is the asserted outcome (silences BW01). Verified: 651/651 bats green on stock macOS bash 3.2 (GNU bash 3.2.57), and shellcheck --severity=error + bash -n + check-style all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(installer): centralize the YAML single-quote escaping + address Saqlain's review (#443) All five review points, verified before acting on each. 1. clientId was interpolated RAW into a double-quoted scalar (`clientId: "$TB_CLIENT_ID"`), so a `"` or `\` in the value would corrupt the generated values file — the same bug class this PR fixes for the password one line below, and unguarded (`_sanitize_credential` only strips paste and non-printable characters). Both credentials now go through the shared escaper into single-quoted scalars. verify_credentials gates IDs to UUIDs in practice, so this is hardening, not a live break. 2. `bats_require_minimum_version 1.5.0` was self-defeating. Per bats-core's changelog, `run -<N>` landed in 1.5.0 (2021-10-22) but `bats_require_minimum_version` itself only exists from 1.7.0 (2022-05-14) — so on a 1.5.x-1.6.x bats that line is an undefined command and kills the file before the guard can help. Raised to the real floor, 1.7.0. 3. The `[ "$status" -eq 127 ]` after `run -127` was dead code. Confirmed empirically: with a mismatched code bats fails AT the `run` line ("failed, expected exit code 127, got 3"), so nothing after it executes. Dropped, with a comment saying why. 4. The bash-3.2 quote idiom now lives in exactly one place — `_yaml_sq_escape` / `_yaml_sq_unescape`. Previously the escape and the unescape each carried their own copy of the rationale and a cross-reference to keep them in sync; the rule is now encoded once and both call sites just call it. 5. Tightened the `run_boot_hermetic` comment. The distinguishing condition is "no REF/BRANCH *and* no mock tracebloc of its own" — the bail-out tests at ~226/242/254 also pass no REF/BRANCH but each writes its own mock into $BIN, so they are hermetic by construction and correctly stay on plain `run_boot`. Tests: 7 new — both escaper directions, a round-trip over quote-heavy values ("a'b", "'", "''", "it's a 'test'"), clientId surviving a single-quote round-trip, a double quote no longer terminating the scalar early, and a guard that the generated file uses the escaper rather than raw interpolation. Five existing assertions on the GENERATED values file moved to the single-quoted form; the double-quoted inputs and mocked `helm get values` output are deliberately left as-is, since reading that form is the backward-compatibility path for values files written by older installers. Full bats suite 658/658 green on stock macOS bash 3.2 (GNU bash 3.2.57) — including `validate_config: valid config passes`, which this PR fixes. shellcheck --severity=error and --warning clean, bash -n, check-style, check-drift and gen-manifest --check all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…hare the good one (#518) _pf_storage_type told operators to set HOST_DATA_DIR="$HOME/.tracebloc", but on a network HOME that path is still NFS (the very problem), and validate_config rejects paths OUTSIDE $HOME — so the printed fix could never work. early_data_dir_guard (#441) already had a followable remedy for exactly this. Extract that remedy into a shared _pf_network_fs_remedy and use it in BOTH _pf_storage_type and early_data_dir_guard, so they can't drift: name the options that actually work (install as a user whose home is on a local disk, or TRACEBLOC_ALLOW_NETWORK_FS=1), and note datasets may stay on network storage via HOST_DATASET_DIR. Tests: _pf_storage_type NFS now prints the followable remedy and NOT the old ~/.tracebloc advice; a consistency test asserts both callers emit the same remedy. Closes #479
…n unachievable range (#444) * fix(installer): memory advice never drops below the client minimum; a too-small host says so (#417 residual) This PR's ORIGINAL scope is superseded. #483 ("report host RAM consistently + achievable memory advice (#417)") merged to develop on 2026-07-30 and closed #417, delivering the same Windows half by a different route: `Get-PfMemGb` returns host RAM only, `Get-PfRuntimeMemGb` is shown as its own labelled line, `Get-PfMemRecommendation` caps at host − 2 GB, and `Show-MemoryStatus` is the single copy shared by Step 1 and the post-Docker recheck. Re-landing this branch's `Get-PfHostMemGb` / `Get-PfMemTargets` / `Write-PfRuntimeMemStatus` would only rename what already works. What survives is the finding behind this branch's second commit, which is STILL LIVE on develop: `Get-PfMemRecommendation` floors at 1 GB, so on a small host it returns a number below the client's own minimum. Measured on develop: Show-MemoryStatus -HostGb 6 -BudgetGb 3 ⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM. Give Docker at least 5 GB (up to 4 GB): ... [wsl2] memory=4GB ... "at least 5 GB (up to 4 GB)" is an empty range, and the concrete value it tells the operator to write is below the 5 GB the same sentence demands — the warning cannot be cleared by following the advice. #483's Bugbot pass fixed this only for the host-RAM-unreadable case; a KNOWN small host still hits it. - `Get-PfMemRecommendation` now floors at PF_MIN_MEM_GB instead of 1, matching bash's `_pf_clamp_mem_gb` exactly so both installers advise the same on the same hardware. - `Show-MemoryStatus` treats a host that cannot reach the floor even with the OS reserve honoured (host − PF_OS_RESERVE_GB < PF_MIN_MEM_GB) as NOT a budget bottleneck, so it gets the honest "use a larger machine" line rather than a resize remedy it can never satisfy. This mirrors the bash recheck's host-too-small branch, and the sibling fix in #445. After: Show-MemoryStatus -HostGb 6 -BudgetGb 3 ⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM. This machine has 6 GB of RAM total; the client needs at least 5 GB. Free up memory or use a larger machine. Hosts that can reach the floor are unchanged (8 GB host still offers the resize; 15 GB → 13 and 16 GB → 14 recommendations are untouched). Tests: 7 new/updated — the floor is the client minimum not 1, a 6 GB host never yields a sub-floor number, PF_MIN_MEM_GB overrides the floor, an invariant sweep over hosts 1..24 GB, the too-small host gets "larger machine" with no memory=1-4GB value, and a host that CAN reach the floor still gets the resize hint. The former "floors at 1 GB" assertion is replaced — it pinned the bug. Pester 374 passed / 0 failed / 9 skipped; PSScriptAnalyzer 0 errors; check-style, check-drift and gen-manifest --check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(installer): name the OS reserve in the host-too-small hint (Bugbot #444) The previous commit routed 5-6 GB hosts into `Show-MemoryStatus`'s generic too-small hint, which compares total RAM to the floor and stops there: host=6 GB, budget=3 ⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM. This machine has 6 GB of RAM total; the client needs at least 5 GB. Free up memory or use a larger machine. 6 >= 5, so as written the operator is told they have enough and still need a bigger machine. The shortfall only adds up once the ~2 GB the OS needs is named — which is exactly what bash's `_pf_recheck_runtime_mem` already says. That hint predates this PR, but this PR is what made 5-6 GB hosts reach it, so it fixes it. host=6 GB, budget=3 # after ⚠ Memory: 6 GB (Docker's current share: 3 GB) - below the 5 GB the client needs; it will OOM. This machine has 6 GB of RAM total - too little for tracebloc: the client needs a 5 GB Docker budget and the OS needs ~2 GB, so about 7 GB physical is the practical minimum. Use a larger machine. Hosts that can reach the floor are untouched (8 GB still gets the resize remedy; 16 GB still gets the training recommendation), and a host below the floor outright keeps the plain copy. Tests: 3 new — the reserve and the practical minimum are both named, and the arithmetic is explained for every too-small host (4-6 GB). Pester 376 passed / 0 failed / 9 skipped; PSScriptAnalyzer 0 errors; parse, check-style, check-drift and gen-manifest --check all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(installer): the too-small-host guard also covers the training branch (Bugbot #444 r2) `hostTooSmall` was consulted only inside the below-floor branch, so a 5-6 GB host with Docker DOWN graded as "enough to run" and fell into the TRAINING branch, which printed a concrete budget to write: host=6 GB, budget=<none> ⚠ Memory: 6 GB - enough to run the client, but training (~8 GB/job) may OOM; 5 GB recommended to train locally. For local training, give Docker up to 5 GB: WSL2 backend - [wsl2] memory=5GB ... memory=5GB on a 6 GB machine leaves the OS 1 GB — a budget this same function calls unachievable two branches up. The previous commit's floor change is what lifted that number from 4 to 5 and made it reachable, so this closes the hole it opened rather than trading one inconsistency for another. Such a machine cannot be tuned into a training box at all, so the branch now says that instead of printing a number: host=6 GB, budget=<none> # after ⚠ Memory: 6 GB - enough to run the client, but too little to train locally (~8 GB/job). This machine has 6 GB of RAM total and the OS needs ~2 GB, so it cannot give Docker a training-sized budget. Run the client here and train on a larger machine. Machines that CAN reach the floor keep the actionable number (16 GB host with a 6 GB budget still gets "give Docker up to 14 GB" / memory=14GB), and the healthy paths are untouched. Tests: 4 new, including the invariant that closes this class for good — across every branch and every budget shape (host 1-6 GB x budget none/1-6), no branch may emit a concrete memory= value for a host that cannot reach the floor while keeping the OS reserve. Pester 379 passed / 0 failed / 9 skipped; PSScriptAnalyzer 0 errors; parse, check-style, check-drift, gen-manifest --check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… proceed (#520) * fix(installer): Windows was the one OS that let a sub-floor Docker VM proceed #513 decided a Docker VM below the client's memory floor must STOP the install rather than proceed and OOM-crashloop -- "proceeding is worse than the jarring stop the WARN path used to avoid" -- and implemented that in bash for every OS (_pf_recheck_runtime_mem -> error -> exit 1). The Windows installer never got it. Test-PreflightRuntimeMem just called Show-MemoryStatus, which is warn-only, so Windows printed "it will OOM" and then carried on and OOM-crashlooped. The platform this whole memory story (#417/#418/#428/#444/#516) is about was the one platform still shipping the crash. Enforcement now lives in Test-PreflightRuntimeMem, mirroring bash's split: Show-MemoryStatus stays purely presentational (its documented job -- and the function two PRs just contended over), the recheck grades then enforces. It runs as New-K3dCluster's FIRST statement, so exiting leaves no half-built cluster. The subtlety that makes this safe: Get-PfRuntimeMemGb floors to whole GB, and a guest reports a few hundred MiB BELOW its configured size, so a VM set to exactly the documented 5 GB floor reports ~4.8 and floors to 4. A bare `-lt 5` would have hard-failed a correctly configured machine -- the same trap #513's reviewer caught in bash, but worse here because flooring to whole GB discards up to a GB. So the gate compares MiB against floor - grace: - New Get-PfRuntimeMemMib: the same `docker info` value at MiB precision. - New Get-PfVmMemGraceMib (512, PF_VM_MEM_GRACE_MIB) -- the same constant and the same comparison bash uses, so both installers put the floor in the same place. - The recheck now reads the budget ONCE, in MiB, and derives GB from it, so the number printed and the number enforced on cannot disagree. Flooring (not rounding) is kept deliberately: Step-1 floors too, and #417 exists so the reported figure doesn't flip-flop between the two reads. Remedies stay honest and achievable, matching the copy the advice path already prints: a host that CAN reach the floor gets a resize target clamped to it (min(warn, physical - reserve) -- bash's clamped warn target); a host that cannot (physical - reserve < floor) gets the practical minimum and "run the client on a larger machine", never a resize that repeats an impossible size. A between-floor-and-warn budget still only warns -- it can run, just tightly. TRACEBLOC_SKIP_PREFLIGHT still overrides, and Err names it. Tests: the Describe that asserted warn-only is replaced by the new contract -- sub-floor 4 GB hard-fails; a floor-sized VM reporting 4800 MiB passes; the grace band is bounded on both sides (4607 fails, 4608 passes); daemon-silent is a no-op; between-floor-and-warn only warns; the rec is still capped at host RAM; big-host vs host-too-small remedies; host RAM unreadable still fails; the skip env overrides; and the budget is read exactly once. Plus a parity Describe that reads BOTH sources and asserts bash still hard-fails, Windows hard-fails too and is no longer warn-only, and both name the same grace constant -- so the next divergence fails a test instead of shipping. The old tests mocked Get-PfRuntimeMemGb, which this no longer calls; left as-is they would have passed while testing nothing, so they now mock the MiB reader. Mutation-tested rather than trusted: neutering the gate back to warn-only fails 4 enforcement tests; restoring passes all 11. Gates: Invoke-Pester scripts/tests/ -> 401 passed / 0 failed; Invoke-ScriptAnalyzer as CI scopes it -> 0 errors; check-style.sh clean; check-drift.sh no drift; manifest.sha256 regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(installer): make the grade and the gate share one boundary (Bugbot) Bugbot caught a self-contradiction I had reasoned about and wrongly accepted: the grade was computed from floor($mib / 1024), so a VM configured at exactly the 5 GB floor (reporting ~4800 MiB) became budget 4, and Show-MemoryStatus printed hard-floor "it will OOM" copy plus a resize hint -- for a machine the grace-aware gate immediately ACCEPTED. We told a correctly configured box it would crash and then carried on. That is precisely the "installer contradicts itself in the same run" pattern #418/#516 existed to remove; bash classifies that band warn-only. I had rejected rounding because Step-1 floors and #417 exists so the reported figure doesn't flip-flop. The fix avoids that trade-off entirely: fold the SAME grace in before flooring. $budget = floor(($mib + $grace) / 1024) - 4800 + 512 -> 5: reports the CONFIGURED size (what the user set and can change), grades in the warn band, gate passes. Consistent. - 4096 + 512 -> 4: still sub-floor, still "it will OOM", gate still fails. Consistent. Because the grade and the gate now pivot on the same constant, their boundaries are the same boundary -- (floor * 1024 - grace) MiB. There is no band that warns "will OOM" yet proceeds, and none that passes while being called sub-floor. The contradiction is impossible by construction, not merely absent at the values I happened to test. Tests: the floor-sized VM is asserted NOT to be told it will OOM and to report its configured 5 GB; plus a boundary-coincidence test sweeping 4096/4607/4608/ 4800/5120 that asserts at EVERY point the copy and the gate agree. Mutation-tested: reverting the grade to floor($mib / 1024) fails both new tests; restoring passes all 13. Gates: Invoke-Pester scripts/tests/ -> 403 passed / 0 failed; Invoke-ScriptAnalyzer as CI scopes it -> 0 errors; check-style.sh clean; check-drift.sh no drift; manifest.sha256 regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ange (#522) Three `producer | early-exiting-consumer` pipelines ran under `set -o pipefail`. When the consumer exits on its first match/line it closes the pipe, and once the producer has more than the ~64KB pipe buffer left to write it takes SIGPIPE and the pipeline exits 141 — a *success* case reported as failure. Measured on ubuntu-24.04 (bash 5.2.21 / GNU grep 3.11 / coreutils 9.4): 65,622 bytes is already enough. WHY this matters most in the chart guard: there the 141 lands on the `if !` branch, so a genuine `client/templates/**` edit is reported as "guard N/A" and the version-bump check is SKIPPED — the guard fails OPEN and waves through exactly the dark ship it was added to stop (PR #472 / the perIngestionTables flag block). Demonstrated end-to-end on a real repo: at 1,301 changed paths (58,723 bytes) with a template edit and NO version bump, the old body exits 0 "guard N/A"; the new one exits 1 and blocks. The mirror direction is broken too: a SIGPIPE on the `grep -qx client/Chart.yaml` MATCH short-circuits the `&&`, so a PR that DID bump the version is failed with a message that says it did not. Restructured rather than papered over with `|| true`, which would only convert a fail-open into a different fail-open: - chart-version-guard.yml — classify the changed-file list with a bash `read` loop + `case` (no pipe, no subprocess, so neither SIGPIPE nor a grep rc=2 can be mistaken for "no match"), and fail CLOSED with ::error:: on a missing base SHA or a failed `git diff`: "don't know" must never read as "nothing changed". - _pull_failure_detail — `head -n 3 <<< "$bad"`. With errexit live the old pipeline aborted the function AT that line, dropping the scoped pull event underneath it, i.e. the one actionable reason (x509 / blocked registry / auth). - _gpu_stack_signature — capture whole, take the first line with `%%$'\n'*`. This site was NOT reachable in practice (the trailing `|| true` already absorbed the 141), but that `|| true` swallowed every real failure code alike; `|| ..._out=""` states the actual contract (absent tool / timeout ⇒ empty ⇒ don't cache). Behaviour is unchanged below the buffer threshold: 669/669 bats tests pass, and _gpu_stack_signature is byte-identical across normal / multi-line / absent / timeout / 20k-line-chatty probe output. Verified on bash 3.2 (macOS floor) too. scripts/manifest.sha256 regenerated via scripts/gen-manifest.sh (R8). Co-authored-by: Claude <noreply@anthropic.com>
…fied path (not bare brew) (#521) macOS installed the CLI tools with bare `brew install kubectl/k3d/helm`, which floated to latest and SILENTLY ignored the K3D_VERSION/HELM_VERSION pins — so Macs ran different, chart-untested tool versions than the pinned Linux installs (and than the docs claim), with no checksum of our own. Route macOS through the SAME pinned, checksum-verified direct-download path as Linux. Both setup-*.sh are always sourced, so install_macos_cli_tools now calls the shared install_kubectl/install_k3d/install_helm (setup-linux.sh) after setting OS_DL=darwin and a macOS tools target (/usr/local/bin, on the default PATH on Intel + Apple Silicon). The fetchers are made OS-aware via ${OS_DL:-linux} — Linux (and every bats fetch test that leaves OS_DL unset) stays byte-identical. - common.sh: portable _verify_sha256 — GNU sha256sum on Linux, shasum -a 256 on macOS (which ships a BSD /sbin/sha256sum that lacks GNU --check). A `type -t` guard honors the bats mocks' sha256sum shell-function so the Linux fetch tests keep passing on macOS dev boxes. - Execute-gate (#411) is preserved: each shared installer ends in assert_tool_runs, so a broken/wrong-arch binary fails the "System tools" step loudly. Updated the drift-check contract to accept macOS delegating to the gated install_<tool>. - brew still delivers Docker Desktop / colima (install_docker_desktop) unchanged. Tests: new scripts/tests/setup-macos.bats (OS_DL=darwin fetch/verify for all three tools + _verify_sha256 portability + install_macos_cli_tools delegation, no bare brew); +2 check-drift self-tests for the delegation contract. shellcheck/style/drift clean; manifest regenerated. Closes #429 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit cf9e727. Configure here.
…h VZ/Rosetta (#524) * fix(#433): verify amd64 emulation on Apple Silicon + start colima with VZ/Rosetta The client images are amd64-only. On Apple Silicon the installer merely printed a note and proceeded, ASSUMING Docker Desktop's emulation works — so if "Use Rosetta for x86_64/amd64 emulation" is off (or colima lacks it), the images crash-loop with an exec-format error minutes later, with no preflight/setup catch. The headless colima path was worse: `colima start` passed no arch/Rosetta flags, so an Apple Silicon Mac got an arm64 VM running amd64 images under slow QEMU or not at all. - assert_amd64_emulation (setup-macos.sh): post-Docker smoke — force-run a tiny amd64 binary (`docker run --rm --platform linux/amd64 busybox:1.36 true`) once Docker is up, and HARD-FAIL naming the exact Docker Desktop setting + the colima remedy, so the problem is caught at setup, never as a crash-looping pod. Wired into install_macos right after Docker is confirmed ready. Intel Macs skip it (native amd64); TRACEBLOC_ALLOW_ARM64 is the escape hatch; image overridable via TB_AMD64_SMOKE_IMAGE. - _install_docker_colima: on Apple Silicon + macOS 13+ (VZ), start colima with `--vm-type vz --vz-rosetta` for Rosetta-accelerated amd64 (matches Docker Desktop's Rosetta setting); older macOS keeps the QEMU default. bash-3.2-safe: the arg vector is never empty. New _macos_supports_vz helper (TB_MACOS_VER-overridable). - _pf_arch (preflight.sh): the macOS note now NAMES the Rosetta setting and says the real check runs once Docker is up — instead of "assume it works". Tests: new scripts/tests/setup-macos-arch.bats (VZ detection, colima flag matrix, smoke pass/fail/skip/override) + a preflight.bats assertion on the named setting. Separate test file from setup-macos.bats to avoid a file-add clash with #429/#521. shellcheck/style/drift clean; manifest regenerated; preflight.bats (82) green. Closes #433 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#433): guard colima VZ flags on existing VMs + time-bound the amd64 smoke (Bugbot) Two Bugbot findings on the Apple Silicon work: 1. (High) colima refuses to change vmType on an EXISTING instance, so unconditionally appending --vm-type vz --vz-rosetta aborted `colima start` on a prior QEMU VM (from an earlier install or reboot) with a generic failure. Only request VZ+Rosetta on a FRESH start now (new _colima_instance_exists via `colima list --json`); a pre-existing VM starts as-is, and if its amd64 emulation is broken the post-Docker smoke already names the `colima delete && colima start --vm-type vz --vz-rosetta` recreate remedy. 2. (Medium) assert_amd64_emulation ran `docker run` via unbounded spin_cmd — a wedged daemon or stuck pull could hang a headless install forever. Switched to spin_cmd_bounded (TB_AMD64_SMOKE_TIMEOUT, default 120s); a 124 timeout falls through to the same remediation, per the installer's every-docker-call-is-bounded rule. Tests: +existing-VM colima test (no VZ flags), + bounded-smoke assertion; 11/11 in setup-macos-arch.bats. shellcheck/style/drift clean; manifest regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f772e62. Configure here.
|
Confirmed and fixed at source: #530 (against The premise holds —
#530 derives the guarded chart list from the release workflow's Resolving here: the fix is on |
The guard classified only `client/templates/**` and `client/values.yaml`, but release-helm-chart.yaml packages BOTH `./client` and `./ingestor` (lines 131-132) into one shared index.yaml (line 193). So an unbumped `ingestor/**` edit merged green — the exact dark ship the guard exists to stop (PR #472). Raised by Bugbot on the develop->staging promotion PR #519. Measured on develop: 6 of 9 commits touching ingestor chart content never bumped ingestor/Chart.yaml, whose version has been 0.2.0 since 2026-05-20. Because `helm package ./ingestor` passes no --version override, ingestor's published version IS that file, so those edits did not go nowhere — they overwrote an already-published version. ingestor-0.2.0.tgz was replaced 5x between 2026-05-20 and 2026-07-29 and `helm repo index --merge` refreshed the digest in place, so two installs of "0.2.0" months apart are not the same chart. Helm also caches by version, so an existing client may never pick the change up at all. - derive the guarded chart list from the release workflow's `helm package` lines instead of hardcoding it, so a third chart is guarded on day one - require a bump of the chart's OWN Chart.yaml (bumping client no longer satisfies an ingestor change) - add client/values.schema.json (packaged, and Helm validates user values against it at install time), plus charts/** and crds/** pre-emptively - report every unbumped chart in one run rather than one per push - fail closed when the chart list cannot be read, or when a packaged chart has no Chart.yaml Move the logic into scripts/chart-version-guard.sh so it can be tested: the new scripts/tests/chart-version-guard.bats (23 cases, real throwaway git repo, no stubbed git) runs under the required `Unit tests` check, which this workflow is not. 13 of the 23 fail against the previous inline logic; the 10 that pass on both are the client-side semantics, deliberately unchanged. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-stagingbranch (a mirror ofdevelop), so it never collides with a human PR. Merged only when the fr-gate is green.Note
Medium Risk
Chart default changes alter dev/stg ingestor spawn tags on upgrade (prod stays digest-pinned), and optional DB-cred/RBAC paths touch jobs-manager security when enabled; installer hard-fails and resume behavior affect edge provisioning reliability.
Overview
Automated develop → staging promotion carrying a broad set of merged work, centered on client Helm chart 1.9.9 and installer/CI hardening.
Helm chart bumps to 1.9.9 and changes how spawned ingestor images are chosen: empty
images.ingestor.tagnow resolves viachannelTagskeyed on a sharedtracebloc.clientEnvhelper (canonicaldev|stg|prodplusdevelopment/staging/productionaliases), so dev/stg edges float:dev/:stginstead of the0.7float. Prod digest pinning still uses the same normalized env. An opt-inperExperimentDbCredsflag wires jobs-manager env,TB_CREDMGR_PASSWORDsecret generation, and flag-gated SecretdeleteRBAC (default off, byte-identical when off).CI / repo hygiene adds a chart version guard on PRs (template/values changes must bump
Chart.yaml), Dependabot config for security-only GitHub Actions updates,.gitleaks-baseline.jsonwired into code-quality, and helm-ci multi-arch checks for every spawnable tag (tag+channelTags).Installers (bash + PowerShell) gain aligned memory floor enforcement on Docker/WSL budgets, Linux
sudofull-run refusal, Windows resume-after-reboot state + RunOnce, honester image-pull progress/failure messaging, NVIDIA Docker restart skip when already configured, cgroup delegation live verification, and macOS-friendly checksum verification for pinned tool downloads. Docs add RFC-CLIENT-0003 (installer architecture) and update SEAL-CHECK k3d egress substrate status.PR template cross-repo examples and checklist items (expand-then-contract,
client-runtimequalifiers) are refreshed.Reviewed by Cursor Bugbot for commit f772e62. Bugbot is set up for automated code reviews on this repo. Configure here.