Skip to content

feat(installer): stop-and-check gate — recognize an existing install - #339

Merged
LukasWodka merged 2 commits into
developfrom
feat/installer-stop-and-check
Jul 13, 2026
Merged

feat(installer): stop-and-check gate — recognize an existing install#339
LukasWodka merged 2 commits into
developfrom
feat/installer-stop-and-check

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only "stop-and-check" gate to the installer so re-running it on an already-set-up machine no longer drags the user through full provisioning. On a verifiably healthy machine it prints one line and hands off to tracebloc (the home screen), then exits 0. Otherwise it falls through to today's 5-step flow, unchanged.

This is PR 2 of 2 (PR 1 = the status-aware tracebloc home screen, tracebloc/cli#244). Together they fix the "I just wanted to use the CLI / reinstall, and suddenly a whole new environment gets provisioned" problem.

Behavior

The gate runs after the banner, before the 5-step roadmap. assess_existing_install() classifies the machine (read-only, bounded, never fatal):

  • healthy — cluster running AND a tracebloc release present AND all core workloads Ready (mysql-client + jobs-manager + requests-proxy) AND the CLI present → prints ✓ Already set up on this machine — no need to run the installer again., then runs tracebloc (the home screen) and exit 0. Never re-provisions, never re-prompts login.
  • degraded — cluster stopped, a workload not Ready, or the CLI missing → prints an honest one-liner naming what's off ("your secure environment is stopped / still starting up / the CLI isn't installed"), then falls through to today's flow to reconcile.
  • fresh — no cluster, or no tracebloc release → today's 5-step install, unchanged.

--force / --reinstall (or TRACEBLOC_FORCE_REINSTALL=1) bypasses the gate entirely. Non-interactive (curl | bash): healthy still short-circuits (the hand-off is output-only, reads no stdin); degraded/fresh run the normal flow.

Safety (this is a public, cosign-verified installer)

  • assess is strictly read-onlyk3d cluster list, kubectl get, helm list (via the reused detect_installed_client), has. It never starts the cluster, runs helm upgrade, mints credentials, or writes anything, and it does not call the mutating _handle_existing_cluster.
  • No false "healthy." Every probe degrades to non-healthy on any error / timeout / missing tool / uncertainty — a machine that needs setup is never told "already set up." (Mutation-proven.)
  • Manifest integrity.scripts/lib/assess.sh is added to both bootstrap FILES arrays and manifest.sha256 is regenerated (gen-manifest.sh --check → up to date), so the cosign bootstrap still verifies; install-bootstrap.bats file-count lists updated (15 → 16).
  • The healthy path uses exit 0 (not exec) so the install_cleanup EXIT trap still runs — a verified no-op at gate time (before any credential / sudo-keepalive is set).
  • provision.sh untouched, to stay clear of the active release-train: staging -> main #838 provision rewrite.

Single source of truth for "ready"

Extracted the client-workload deployment list into _client_workload_deployments() (common.sh); both wait_for_client_ready (the post-install gate) and the assess readiness probe consume it, so the installer's definition of "ready" and the gate's can't drift.

Code review

Adversarial review before opening — both invariants (no false-healthy; non-mutating + manifest-consistent) confirmed and mutation-proven. Fixed in the second commit:

  • [should-fix] the readiness gate checked only jobs-manager; a machine with jobs-manager up but requests-proxy (training egress) or mysql-client down could be called "healthy" → now requires all three, via the shared helper.
  • [should-fix] an unguarded awk-pipe assignment that could abort the installer under set -e/pipefail (SIGPIPE) → guarded to match its jq sibling.

Deferred (fast-follow)

Surgical per-layer repair of a degraded machine (start-only / CLI-only / reconcile-only, and skipping re-login when already authenticated) — deliberately left out to keep this PR off provision.sh and clear of #838. Rebases cleanly once #838 lands.

Test plan

  • scripts/tests/assess.bats (31 tests): each probe (cluster-running jq + awk fallback; workload-ready per-deployment incl. requests-proxy / mysql-client down; CLI present); all classify branches; the hand-off (runs tracebloc, exit 0, fallback); and the main() branch (healthy → hand-off + exit 0; --force bypass; degraded/fresh → flow). Assertions are fail-loud (grep-backed + single-bracket status) to dodge the macOS bats [[ ]] blindspot; the healthy short-circuit and the all-three-workloads gate are mutation-verified.
  • shellcheck --severity=error clean; bash -n clean; gen-manifest.sh --check up to date.
  • Full suite: only 2 pre-existing macOS-local failures (reproduce on pristine develop, unrelated files) — Linux CI is authoritative for bats.

Checklist

  • Targets develop
  • Read-only assess; no false-healthy (mutation-proven)
  • Manifest regenerated + bootstrap FILES in sync
  • provision.sh untouched (release-train: staging -> main #838 coordination)
  • Tests added + mutation-checked
  • Human review (Bugbot + reviewer on Monday)

Pairs with tracebloc/cli#244. Hand-off degrades gracefully: shows today's home screen on the released CLI, the new status-aware one once cli#244 ships.


Note

Medium Risk
Changes behavior of the cosign-verified public installer entrypoint; a false healthy would skip needed setup, though the design biases toward fall-through on any doubt and adds extensive bats coverage.

Overview
Adds a read-only stop-and-check gate so re-running the installer on an already-healthy machine skips full provisioning and hands off to tracebloc instead.

New scripts/lib/assess.sh classifies the host as fresh, degraded, or healthy using bounded k3d/kubectl/helm probes (never mutating). Only healthy short-circuits: running cluster, tracebloc release, all core workloads ready, and CLI present. Uncertainty always falls through to the normal install flow—never a false “already set up.”

install-k8s.sh invokes the gate after the banner; --force / --reinstall and TRACEBLOC_FORCE_REINSTALL=1 bypass it. assess.sh is guarded when missing (stale bootstrap) like other optional libs.

_client_workload_deployments() in common.sh centralizes readiness (mysql-client, jobs-manager, requests-proxy); wait_for_client_ready and the assess gate both use it so “ready” cannot drift.

Bootstrap FILES, gen-manifest.sh, manifest.sha256, help text, and assess.bats / bootstrap tests updated for the new module.

Reviewed by Cursor Bugbot for commit 18b113a. Bugbot is set up for automated code reviews on this repo. Configure here.

LukasWodkaand others added 2 commits July 12, 2026 10:00
…ome screen
Re-running the installer on an already-set-up machine no longer drags the user
through full provisioning. A new read-only gate (scripts/lib/assess.sh) runs
after the banner and classifies the machine:
• healthy — cluster running AND a tracebloc release present AND jobs-manager
Ready AND the CLI present → print "Already set up on this
machine", hand off to `tracebloc` (the home screen), exit 0.
• degraded — cluster stopped / workload not Ready / CLI missing / any partial
state → print an honest one-liner, fall through to the normal
flow to reconcile.
• fresh — no cluster, or a cluster with no release → the normal flow.
assess is STRICTLY non-mutating and bounded: read-only `k3d cluster list`,
`helm list`/`get values` (reuses detect_installed_client), and a bounded
`kubectl get` (--request-timeout). On ANY uncertainty it degrades toward the
normal flow — never a false "healthy" that would skip a needed install. The
hand-off uses `exit 0` (not exec) so the EXIT-trap cleanup still runs; if the
CLI is somehow unresolvable it falls back to a status line and still exits 0.
--force / --reinstall (or TRACEBLOC_FORCE_REINSTALL=1) bypasses the gate. A
healthy machine still short-circuits under curl|bash (output only, no input).
Per-layer surgical repair of a degraded machine is a deliberate fast-follow so
this PR stays off provision.sh (clear of the active #838 work).
Wires the gate into main() (after print_banner, before the roadmap), adds
scripts/lib/assess.sh to the bootstrap FILES + gen-manifest, regenerates
scripts/manifest.sha256, and adds scripts/tests/assess.bats. New copy says
"secure environment", never "client". PR 2 of 2 (PR 1 = the cli home screen,
cli#244).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… awk branch
Code-review should-fix items on the stop-and-check gate:
1. _assess_cluster_servers_running: add `|| line=""` to the awk-branch
assignment, mirroring the jq branch. awk's `exit` closes the pipe, so under
`set -o pipefail` a SIGPIPE from k3d (141) — or any k3d failure — would
otherwise propagate non-zero out of the assignment and abort the installer.
2. "healthy" must match the installer's OWN definition of ready. The probe now
requires ALL the workloads wait_for_client_ready checks — mysql-client,
${ns}-jobs-manager, ${ns}-requests-proxy — not jobs-manager alone. Previously
a machine with jobs-manager up but requests-proxy (training egress) or
mysql-client down was classified healthy and short-circuited without
reconciling — the false-positive we designed against.
Single source of truth: the deployment set is extracted into
_client_workload_deployments (common.sh); both wait_for_client_ready
(summary.sh) and the assess gate consume it, so they can't drift. Renamed
_assess_jobs_manager_ready -> _assess_workload_ready (reason stays
`workload-not-ready`); any one workload not-Ready/absent -> degraded.
Tests: _assess_workload_ready now covers all-three-Ready plus each workload
individually down/absent; classify covers "one down -> degraded" via the real
probe and "all three Ready + CLI -> healthy". Mutation-checked: shrinking the
shared list to jobs-manager-only fails the mysql-client/requests-proxy/one-down
tests. Manifest regenerated (assess.sh, common.sh, summary.sh). provision.sh
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

👋 Heads-up — Code review queue is at 46 / 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.)

@aptracebloc

Copy link
Copy Markdown
Contributor

Verdict: Approve with nits — the gate is genuinely read-only, biases toward fall-through on any doubt, and is well tested; the only substantive item is a cheap defensive hardening of the healthy hand-off's stdin.

cc @LukasWodka@saadqbal

I reviewed scripts/lib/assess.sh, the main() wiring in install-k8s.sh, the shared _client_workload_deployments helper, and assess.bats, against the surrounding code (detect_installed_client, _cluster_exists, install_cleanup, the bootstrap install.sh). The core safety invariants hold: assess never mutates, never calls the mutating _handle_existing_cluster, has no delete/uninstall path, and every probe degrades to non-healthy on error/timeout/uncertainty. healthy is the only short-circuit and requires all four signals. Confirmed the exit 0 hand-off is safe because at gate time (after print_banner, before Step 3) install_cleanup is a no-op — SUDO_KEEPALIVE_PID and _PROVISION_CRED_FILE are both unset, so its two guarded branches don't fire.

🟡 Minor

  • assess.sh:253 — healthy hand-off runs tracebloc with the inherited curl-pipe stdin. Under curl … | bash, install.sh:370 runs bash install-k8s.sh "$@", so the child (and therefore the bare tracebloc in _assess_handoff) inherits the pipe as stdin. || true protects against a non-zero exit but not against a blocking read. The claim "the hand-off reads no stdin" is a property of cli#244, not of the currently-released CLI this path also runs (per the PR's own "degrades gracefully to today's home screen"). Cheap, CLI-independent hardening: tracebloc </dev/null || true. Then a healthy curl | bash re-run can never hang regardless of what bare tracebloc does on any CLI version.

  • install-k8s.sh:48-53 — the --force/--reinstallTB_FORCE_REINSTALL arg translation in main() is untested.assess.bats exercises the bypass by pre-setting TB_FORCE_REINSTALL=1 directly; nothing covers the argv scan that actually sets it. A rename/typo there (the one path a user hits with ./install-k8s.sh --force) would pass CI. The gate logic itself is well covered — this is the one wiring seam left unguarded. A small test sourcing the loop, or asserting --reinstall is honored, would close it.

⚪ Nits

  • assess.sh:236 "healthy" release signal can in principle match a non-tracebloc release.detect_installed_client treats any Helm release whose CHART column matches client-[0-9]and whose values contain a clientId as "ours". The clientId requirement makes a real-world collision very unlikely, and this is pre-existing shared logic (reused deliberately for consistency with the Installer mints a new client before the one-client-per-machine guard — orphans it on the backend #303 ownership guard), so no change requested — noting it only because the gate now also keys "already set up" off it.

  • assess.sh header says the module "writes nothing". Accurate about install/cluster state, but detect_installed_client (invoked from _assess_classify) does helm get values into a local temp file it owns and rms. Harmless and read-only w.r.t. the machine; the header wording is just slightly stronger than literal.

What's good

  • Read-only + fail-safe by construction. Every probe (_assess_cluster_servers_running, _assess_workload_ready, _assess_cli_present) returns non-healthy on any error, missing tool, or non-numeric output; _assess_classify requires positive certainty at each layer before reaching healthy. A machine that needs setup is never told "already set up".
  • The shell-robustness class of bug is handled correctly. The awk-pipe SIGPIPE guard (… | awk … } ' ) || line="", assess.sh:145) mirrors its jq sibling; the kubectl … || return 1 capture and the k3d … || running="0" capture all keep set -euo pipefail from aborting the installer on a subshell non-zero. The if [[ -f assess.sh ]]; then source; fi guard (not && source) and the declare -F assess_existing_install check in main() both correctly avoid tripping set -e on a stale bootstrap.
  • Single source of truth for "ready". Extracting _client_workload_deployments so both wait_for_client_ready and the assess probe consume it genuinely prevents drift, and requiring all three workloads (not just jobs-manager) is the right fix — the per-workload-down bats cases are mutation-real against the old probe.
  • exit 0 (not exec) preserving the EXIT trap is the correct call, and I verified the trap is a no-op at gate time.
  • Test coverage is thorough: every classify branch, both cluster-probe paths, per-workload readiness, CLI-in-~/.local/bin, the hand-off (incl. bare-invocation and unresolvable-CLI fallback), the --force bypass, and mutation guards on the short-circuit. Manifest + bootstrap FILES + file-count all updated in lockstep.

🤖 Generated with Claude Code

@aptraceblocaptracebloc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LukasWodkaApproved. The core invariants hold: the gate is read-only, every probe fails safe toward the normal flow, the set -euo pipefail patterns are correct, and the exit 0 hand-off is safe (detail in my comment above).

Caveats — follow-up, none blocking:

  • 🟡 the healthy hand-off runs bare tracebloc on the inherited curl-pipe stdin; || true guards a non-zero exit but not a blocking read — tracebloc </dev/null || true is a cheap hardening if you're already in the file, otherwise fine as a follow-up.
  • 🟡 the --force/--reinstallTB_FORCE_REINSTALL arg loop is untested (bats presets the var directly).
  • ⚪ "release present" could in principle match a non-tracebloc client-* helm release; header "writes nothing" is slightly stronger than literal (detect_installed_client writes a temp file).

This sits squarely in the R8 trust root (install.sh, install-k8s.sh, gen-manifest.sh, lib/*), so Asad's review is the intended backstop — but the gate isn't enforced, so you're clear to merge on my approval.

🤖 Generated with Claude Code

@LukasWodka
LukasWodka merged commit 3a99614 into developJul 13, 2026
35 checks passed
LukasWodka added a commit that referenced this pull request Jul 13, 2026
Resolves the scripts/manifest.sha256 conflict: both #340 and #339 (stop-and-check)
edited scripts/install-k8s.ps1, so the merged file's hash matched neither side.
Regenerated the manifest from the merged tree via scripts/gen-manifest.sh
(install-k8s.ps1 -> be42945b = the merged content's actual hash); `gen-manifest.sh
--check` passes. install-k8s.ps1 / install-k8s.Tests.ps1 auto-merged cleanly (both
the ConvertFrom-Json one-client guard and the stop-and-check gate present).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LukasWodka added a commit that referenced this pull request Jul 13, 2026
Resolves two conflicts from #339 (stop-and-check gate) landing on develop:
- scripts/install-k8s.sh: #341 carried a placeholder "SLOT for client#339"
comment + guarded no-op call; develop shipped the real gate. Took develop's
version (the real assess_existing_install gate with the TB_FORCE_REINSTALL
bypass), keeping #341's first-run-through roadmap around it. shellcheck +
bash -n clean.
- scripts/manifest.sha256: regenerated from the merged tree (gen-manifest.sh --check passes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saadqbal added a commit that referenced this pull request Jul 14, 2026
…digest pinning (#345)
* test(provision): pin the #303 pre-flight's grep contract with hidden `client list` (Refs #141) (#335)
The installer's #303 one-client-per-machine pre-flight
(`scripts/lib/provision.sh` `_account_owns_namespace`) shells out to
`tracebloc client list --plain` and greps the output for
`namespace=<ns>([[:space:]]|$)` to refuse a cross-account re-provision.
In the cli repo `client list` is now a HIDDEN cobra command — still
callable, but nothing here pinned that the pre-flight keeps classifying
the CLI's exact --plain output correctly, nor that it still passes
--plain. If either drifts the grep silently fails and #303 stops firing.
Add two focused unit tests of `_account_owns_namespace` (the consumer
half of the cli#141 contract; the producer half is pinned in the cli
repo at internal/cli/client_list_contract_test.go). The fixture mirrors,
field-for-field, what cli's runClientList prints under --plain:
- owned namespace → rc 0; absent → rc 1; a STRICT PREFIX of a real
namespace → rc 1 (pins the ([[:space:]]|$) anchor: an account must
not "own" acme-prod-0 just because it owns acme-prod-01);
- the pre-flight actually invokes the hidden list WITH --plain;
- an unreadable list is rc 2 (fall through to create), distinct from
rc 1 (refuse) — the distinction #303 branches on.
Assertions use `[ -eq ]` + `grep` (real exit codes, robust on bash 3.2);
Linux CI (standard-checks.yml → `bats scripts/tests/*.bats` on ubuntu)
is the authority. Mutation-verified: dropping the grep anchor or --plain
from provision.sh fails these tests.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(proxy): NO_PROXY covers the cloud metadata IP 169.254.169.254 (#337)
The auto-augmented NO_PROXY lists (chart tracebloc.proxyEnv helper,
bash _augment_no_proxy defaults, Windows Get-EffectiveNoProxy defaults)
omitted 169.254.169.254 — behind a corporate proxy, cloud metadata
lookups would be routed through the external proxy (broken IMDS access
and an unnecessary place for instance credentials to transit).
Add the IP to all three lists in lockstep, pin it in the bats + Pester
+ helm-unittest expectations, regenerate manifest.sha256 for the two
touched installer scripts, bump chart 1.9.3 -> 1.9.4.
Ref tracebloc/backend#803 (item J), tracebloc/client-runtime#120
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(installer): stop-and-check gate — recognize an existing install (#339)
* feat(installer): stop-and-check gate — hand a healthy re-run to the home screen
Re-running the installer on an already-set-up machine no longer drags the user
through full provisioning. A new read-only gate (scripts/lib/assess.sh) runs
after the banner and classifies the machine:
• healthy — cluster running AND a tracebloc release present AND jobs-manager
Ready AND the CLI present → print "Already set up on this
machine", hand off to `tracebloc` (the home screen), exit 0.
• degraded — cluster stopped / workload not Ready / CLI missing / any partial
state → print an honest one-liner, fall through to the normal
flow to reconcile.
• fresh — no cluster, or a cluster with no release → the normal flow.
assess is STRICTLY non-mutating and bounded: read-only `k3d cluster list`,
`helm list`/`get values` (reuses detect_installed_client), and a bounded
`kubectl get` (--request-timeout). On ANY uncertainty it degrades toward the
normal flow — never a false "healthy" that would skip a needed install. The
hand-off uses `exit 0` (not exec) so the EXIT-trap cleanup still runs; if the
CLI is somehow unresolvable it falls back to a status line and still exits 0.
--force / --reinstall (or TRACEBLOC_FORCE_REINSTALL=1) bypasses the gate. A
healthy machine still short-circuits under curl|bash (output only, no input).
Per-layer surgical repair of a degraded machine is a deliberate fast-follow so
this PR stays off provision.sh (clear of the active #838 work).
Wires the gate into main() (after print_banner, before the roadmap), adds
scripts/lib/assess.sh to the bootstrap FILES + gen-manifest, regenerates
scripts/manifest.sha256, and adds scripts/tests/assess.bats. New copy says
"secure environment", never "client". PR 2 of 2 (PR 1 = the cli home screen,
cli#244).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): assess "healthy" = ALL client workloads Ready + guard awk branch
Code-review should-fix items on the stop-and-check gate:
1. _assess_cluster_servers_running: add `|| line=""` to the awk-branch
assignment, mirroring the jq branch. awk's `exit` closes the pipe, so under
`set -o pipefail` a SIGPIPE from k3d (141) — or any k3d failure — would
otherwise propagate non-zero out of the assignment and abort the installer.
2. "healthy" must match the installer's OWN definition of ready. The probe now
requires ALL the workloads wait_for_client_ready checks — mysql-client,
${ns}-jobs-manager, ${ns}-requests-proxy — not jobs-manager alone. Previously
a machine with jobs-manager up but requests-proxy (training egress) or
mysql-client down was classified healthy and short-circuited without
reconciling — the false-positive we designed against.
Single source of truth: the deployment set is extracted into
_client_workload_deployments (common.sh); both wait_for_client_ready
(summary.sh) and the assess gate consume it, so they can't drift. Renamed
_assess_jobs_manager_ready -> _assess_workload_ready (reason stays
`workload-not-ready`); any one workload not-Ready/absent -> degraded.
Tests: _assess_workload_ready now covers all-three-Ready plus each workload
individually down/absent; classify covers "one down -> degraded" via the real
probe and "all three Ready + CLI -> healthy". Mutation-checked: shrinking the
shared list to jobs-manager-only fails the mysql-client/requests-proxy/one-down
tests. Manifest regenerated (assess.sh, common.sh, summary.sh). provision.sh
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): run the helm client install behind a spinner (no more silent ~10-15s) (#333)
* fix(installer): run the helm client install behind a spinner (no more silent ~10-15s)
Step 4/5 ran `helm upgrade --install` (and the in-place reconcile) blocking, with
all output redirected to the log, so the terminal sat frozen ~10-15s (render +
apply + image pull) with no feedback. Wrap both in the existing spin_cmd helper:
an animated spinner + message, output still streamed to $LOG_FILE, and on failure
the log tail to stderr + the same error-exit. Honours RFC-0002 §2 "progress on
every wait" (the principle already applied to the CLI's post-submit waits).
- bash -n + shellcheck clean; install-client-helm.bats green (33 tests).
- Failure path preserved (error-exit + full output in $LOG_FILE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(scripts): refresh integrity manifest for spinner change
gen-manifest.sh after the install-client-helm.sh spinner edit; static-analysis
CI gate requires the sha256 manifest to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(chart): prod-only ingestor digest pin (#1028 item 1) (#334)
* chore(chart): prod-only ingestor digest pin via values-prod overlay (#1028)
Prod deploys the ingestor image by immutable digest; dev/staging keep floating
:0.7 (imagePullPolicy=Always). Base values.yaml keeps digest empty; the new
client/values-prod.yaml sets only images.ingestor.digest and is layered at
prod-release time (-f values.yaml -f values-prod.yaml). Digest verified live
against ghcr.io (multi-arch index, amd64+arm64). Adds resolve-ingestor-digest.sh
to re-resolve/verify the digest at each cut instead of hand-typing it.
Refs tracebloc/backend#1028.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(digest): --write refuses single-arch pin + verifies sed landed
Multi-arch guard now inspects the resolved repo@digest and hard-fails under --write instead of only warning (a single-arch pin breaks arm64 and fails helm-ci ingestor-multiarch). --write also greps the overlay for the digest after sed and errors on a silent no-op instead of falsely reporting success. Bugbot findings on client#334.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(digest): read chart tag without yq; fail loudly, never hardcode 0.7
The no-arg TAG default silently fell back to a hardcoded 0.7 when yq was absent, so a prod cut after the chart tag moved could --write a stale/wrong digest while appearing to follow the chart. Reads images.ingestor.tag via yq when present, else a portable awk parse scoped to the images:->ingestor: block; if neither works it errors and exits non-zero. Bugbot follow-up on client#334.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(digest): tolerate pipefail in the multi-arch platforms capture
Under set -o pipefail, a failed inspect or a grep -v that filters every line exits non-zero and aborted the whole script even after the digest resolved. Add || true; an empty platforms then trips multiarch=0 so the guard still fires (ERROR under --write, WARNING otherwise). Bugbot round-3 on client#334.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(security): update §8.3 — backend tokens now bounded + revocable (#302) (#342)
§8.3 claimed "Backend tokens never expire"; that finding is now mitigated
for interactive DS web sessions via a bounded, revocable 30-day
ClientAccessToken (backend#933 + frontend-app#575, shipped through
backend#590). Document the mitigation, the intentional carve-out for edge
devices / bots (which keep the legacy long-lived DRF Token as
non-interactive service credentials, by design), and the residual
JS-readable-storage risk tracked as the SEC-06 follow-up in tracebloc/backend.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): Windows one-client guard reads clientId via ConvertFrom-Json (#200) (#340)
The one-client-per-machine guard regex-scraped `helm get values` YAML and
only matched a DOUBLE-QUOTED clientId. Helm re-serializes values on `get`,
so typical clientIds come back unquoted -- the guard silently matched
nothing and a re-install could re-point the machine to a different Client
ID, defeating the protection from #192 (Bugbot finding on #199).
Read the values as JSON instead (`helm get values <rel> -n <ns> -o json |
ConvertFrom-Json` -> `.clientId`), which sidesteps YAML quoting entirely
and matches the adjacent comment that already claimed ConvertFrom-Json.
A release with no user values (literal `null`), a missing clientId key, or
unparsable output from one release is skipped without aborting the scan of
the remaining releases.
Tests: existing guard mocks now serve real-helm-shaped output (JSON for
`-o json`, unquoted YAML otherwise), plus new cases for unquoted / single-
quoted / double-quoted YAML views, a null-values release mid-scan, and
values without a clientId key. Pester (Linux container, lts-7.4): 95
passed, 0 failed, 6 skipped ($IsWindows skips).
scripts/manifest.sha256 regenerated (install-k8s.ps1 line only) -- the
file is on the signed-manifest surface; gen-manifest.sh --check passes.
Closes#200
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs(supply-chain): document the Windows bootstrap (install.ps1) (#336)
* docs(supply-chain): document the Windows bootstrap (install.ps1)
SUPPLY_CHAIN.md scoped itself to install.sh with zero Windows mentions,
while the R8 Windows leg (PR #299, released v1.8.5) has shipped the same
guarantee for install.ps1. Add the Windows section, the install.ps1 release
asset to the asset table, and a PowerShell verify-by-hand note.
Closestracebloc/backend#957
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(supply-chain): correct the raw-<TAG> Windows bootstrap (fails closed w/o $env:REF)
Arturo's review (client#336): the §7 parenthetical told customers they could
bootstrap install.ps1 from a pinned <TAG> raw URL, but the committed tag tree
still ships the __TRACEBLOC_RELEASE_REF__ placeholder (only the release asset is
stamped), so that path refuses to run unless $env:REF is set. Documented the
$env:REF requirement + why, instead of implying the raw <TAG> URL works as-is —
aligned with §1's $env:REF description and the bash side (which uses raw-<TAG>
only for hand-verifying a sub-script against the manifest, not for bootstrap).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(installer): first-time install run-through (1. Downloading / 2. Installing a–f) + healthy re-run bailout (#341)
* feat(installer): first-run banner/roadmap + step_header + count_bar helpers
- print_banner: new title 'Setting up tracebloc on your machine · <version>'
(tracebloc bold-cyan) + rule; TB_VERSION from TRACEBLOC_INSTALL_REF; skips
when the bootstrap already drew it (TRACEBLOC_BANNER_SHOWN).
- print_roadmap: the '2. Installing' a–f plan.
- step_header: bold gerund running headers for steps a–f.
- count_bar: honest N-of-M render helper for multi-image pulls.
- preflight_sudo: step-b password intro copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): bootstrap early-bailout + "1. Downloading" run-through
- Early bailout: if the tracebloc CLI is present and `tracebloc doctor` reports
healthy (bounded, exit-code gated), print the healthy line and exec the home
screen — skipping the download entirely. Skipped on --force/--reinstall,
TRACEBLOC_FORCE_REINSTALL, the dev/unverified path, or an explicit REF/BRANCH.
- Draw the first-run banner here and export TRACEBLOC_BANNER_SHOWN (so
install-k8s.sh does not draw a second) + TRACEBLOC_INSTALL_REF.
- "1. Downloading" copy: Installer downloaded — N files / Verifying it's
authentic (cosign)… / Signature verified / All N files intact — nothing was
altered. Local colour palette (common.sh not yet sourced).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step a — collapsed hardware summary + connectivity spinner
- run_preflight renders the run-through's step-a view: one hardware line
('arch · N CPU cores · N GB memory · N GB free disk'), a connectivity spinner
+ combined 'Connected: …' line, and a 'Local storage (~/.tracebloc)' line.
- PF_QUIET_SUCCESS suppresses the per-check ✔ lines only inside run_preflight
(folded into the summary); called directly (bats), the checks still print their
✔/info lines, so the unit contracts hold. Warnings + hard-fails always print.
- Connectivity probes stay in the foreground (PF_HARD_FAIL propagation) with a
no-sleep per-host spinner frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step b — Docker %-bar copy + 'ready' success lines
- Real %-by-bytes bar for the Docker Desktop .dmg (single-file curl via
download_with_progress); fresh-Mac intro copy on the label.
- 'Docker ready' and 'System tools ready (k3d, helm, kubectl)' to match the
run-through. Linux (Docker Engine) copy left untouched — a deliberate follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step c — spinner around the (silent) k3d create
- _create_new_cluster wraps the 1-2 min k3d create in a spinner ('Creating your
secure environment…'), the real fix for the long silent gap; spin() waits for
the backgrounded create so exit-code capture + proxy-config cleanup are intact.
- Runtime intro copy; terminology 'compute environment' → 'secure environment'.
- _wait_for_api owns the single 'Secure environment ready' (API confirmed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step d — device-flow sign-in copy + "Registered as"
- Reworked sign-in copy: "open the link on this or any device and enter the
code" (print-only; the CLI prints the URL/code/wait — no auto-open claim).
- Success line → Registered as "<slug>" (the minted namespace = dashboard name).
- CLI install call removed (now step b); keeps the has-tracebloc FATAL guard.
- KEPT the interim name/location prompt (deployed CLI still hard-requires --name
without a TTY, backend#992) + comment: remove when cli#137 ships.
- No internal step header (main() prints "d) Registering this machine").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step b — "tracebloc CLI ready" on the fully-clean path
Fully-clean verdict (usable now + in new terminals) → "tracebloc CLI ready …
verified on your PATH", matching the run-through's Docker/System-tools/CLI
"ready" pattern. Edge-case lines stay "installed" (installed but not yet usable
here). Test assertion updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step e — services count-bar + "tracebloc installed"
- Services intro copy (training runner / data manager / live monitor / local
database; runs on your machine, data never leaves).
- _download_services_progress: honest N-of-M count bar as service images pull
(imageID-populated count), bounded + non-fatal; guarded by
TB_NO_SERVICE_PROGRESS (set in the bats setup so the mocked-kubectl poll cannot
hang). Never a fabricated aggregate %.
- Success line "Connected to tracebloc" → "tracebloc installed" (step f/summary
owns "Connected"); dropped the internal step 4/5 headers (main owns a–f).
- Tests: retarget the 3 assertions + guard the poller.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): step f — rich connected summary matching the run-through
- connected summary: "✔ Connected to tracebloc"; Environment/Version/Mode block;
"live 🟢" + dashboard; NON-dim "What's next" with 3 numbered steps
(tb data ingest / my-use-cases / invite collaborators); prominent
"Run tracebloc to get started."; dim footer (Logs · Data) with the reboot tip
as the LAST dim line. Dropped the green ━━━ border.
- Terminology: "secure environment"; trust claim "never leaves this machine".
- _reboot_note stays OS-guarded (Linux: restarts automatically; macOS: open
Docker Desktop) — Linux not regressed.
- wait_for_client_ready intro reframed as step-f "Connecting…".
- Tests updated to the new copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(installer): main() reorg to the six-step run-through (a–f) + gate slot
- main() now: banner → [client#339 gate SLOT] → roadmap → a) Check your machine →
b) Install what tracebloc needs → c) Create your secure environment → d)
Register this machine → e) Install tracebloc → f) Connect to the tracebloc
network, each with a step_header + trailing blank-line pair.
- Step b now owns prerequisites AND the tracebloc CLI (moved out of provisioning;
step d needs it to sign in).
- Gate slot: guarded NO-OP call to assess_existing_install, clearly commented as
client#339's — logic NOT implemented here; positioned after banner/before
roadmap so it reconciles cleanly with that branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(installer): regenerate manifest.sha256 for the first-run UX changes
Content hashes for the 9 edited sub-scripts (common/preflight/setup-macos/
cluster/provision/install-cli/install-client-helm/summary/install-k8s). No files
added or renamed, so install.sh's FILES array is unchanged; gen-manifest.sh
--check passes (both bootstrap-sync checks green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(installer): cover the first-run UX logic (Linux CI is the authority)
- common.bats: count_bar (render/clamp/bad-input/divide-by-zero), step_header,
print_roadmap (a–f plan), print_banner (version + bootstrap-suppression).
- preflight.bats: _pf_hw_summary_line, connectivity combined 'Connected:' line,
run_preflight collapsed step-a view (per-check ✔ lines folded away).
- install-client-helm.bats: _download_services_progress guards (TB_NO_SERVICE_
PROGRESS / no-kubectl / empty-ns) so the poller can never hang the suite.
- install-bootstrap.bats: early bailout — healthy doctor execs home screen (no
download), unhealthy does not bail, --force skips the bailout.
Not run locally (macOS [[ ]] blindspot + spin/sleep/read block without a TTY);
assertions written fail-loud against the exact emitted copy for Linux CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(installer): retarget two assertions to the first-run copy
Linux CI caught two stale assertions the local checks couldn't (macOS
[[ ]] blindspot):
- install-bootstrap happy path grepped the old "installer files
verified" line, now "All N files intact — nothing was altered".
- _pf_storage_type local-fs test grepped the fstype (ext4), which the
first-run redesign moved into the log; the visible line is now the
clean "Local storage (…)". Assert that instead.
Both tests keep their original intent; no code behavior changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): address Bugbot #341 — real tracebloc cmd, ~/.local/bin bailout probe, exec fallthrough
- summary.sh "What's next" said `tb data ingest`, but there is no `tb` on PATH
(the binary is `tracebloc`; the CTA two lines below already says `tracebloc`)
→ use `tracebloc data ingest`.
- install.sh bailout probed `command -v tracebloc` before any PATH prepend, so a
healthy CLI in ~/.local/bin (the installer's fallback dir, not on a fresh
curl|bash PATH) was missed → forced a needless full re-download. Prepend
~/.local/bin first, mirroring provision_client.
- install.sh bailout `exec tracebloc || true; exit 0` silently exited 0 if exec
failed (bad interpreter / missing exec bit) — healthy machine, but no home
screen and no install. Dropped `|| true`; if exec returns (i.e. it failed),
print an actionable line and exit 1.
Regenerated scripts/manifest.sha256 (gen-manifest.sh --check passes); retargeted
the summary.bats assertion to the corrected command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): redirect bailout CLI stdin away from the curl|bash pipe (Bugbot #341)
Bugbot HIGH (learned rule): under `curl … | bash` the healthy-setup bailout ran
`tracebloc doctor` and `exec tracebloc` without redirecting stdin, so the child
CLI inherited the install pipe as stdin — a CLI that reads stdin could block the
bailout or consume the pipe instead of behaving non-interactively.
- `tracebloc doctor` (bounded health probe): stdin ← /dev/null (non-interactive).
- `exec tracebloc` (interactive home-screen hand-off): stdin ← /dev/tty (the
user's real terminal, not the pipe); the exec-failure fallthrough (incl. no
controlling terminal to open /dev/tty) keeps the existing "couldn't launch →
exit 1" path.
bash -n + shellcheck clean. install.sh is the bootstrap (not in the signed
manifest), so no manifest change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): probe /dev/tty openability before the bailout exec (fixes#341 CI)
c19b51d's `exec tracebloc </dev/tty` is unconditional, but a readable /dev/tty
device node with no controlling terminal (CI, detached sessions) still fails to
OPEN — the redirect then aborts the exec, the healthy bailout prints "could not
launch" + exit 1, and every job that runs install.sh (bats, unit, prereqs,
path-persist) goes red. Probe openability first: `</dev/tty` when it opens, else
`</dev/null` — still always execs (never the install pipe), so the hand-off and
its bats coverage hold.
install.sh isn't a hashed sub-script (manifest unchanged). bats install-bootstrap
green; shellcheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Merge pull request #344 from tracebloc/test/290-check-drift-cli-invariants
test(drift): pin the CLI-assumed chart contract in check-drift.sh
* test(e2e-proxy): deflake §4 — retry the squid Service-readiness race + raw log on failure (#346)
§4 (the #119 ingestion-egress guard) flaked red on develop (run 29255451968); a
re-run of the SAME commit (run 29340987746) passed — a transient, not a #341
regression (#341 never touched this test). Cause: the app pod's section-A curl
reaches the in-cluster squid via its Service, but `kubectl rollout status` gates
only on the squid pod being Ready, not on its Service endpoints being programmed
in kube-proxy. In that window a brand-new pod's first CONNECT to the ClusterIP is
refused, and curl gave up before attempting the tunnel (no "Establish HTTP proxy
tunnel" line) — so the assertion false-failed.
- Add `--retry 5 --retry-connrefused --retry-delay 2` (-m 30) to section A's curl:
it rides out the transient connection-refused. A genuine #119 regression still
fails all retries, so the guard keeps its teeth.
- On the (A) assertion failure, dump the RAW egress-app log + squid pod/endpoints
state. This flake surfaced only the filtered B line and a bare "did NOT tunnel";
the raw dump makes any future failure debuggable instead of opaque.
bash -n + shellcheck clean. (The k3d e2e can't run locally; CI is the authority.)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): resolve Bugbot findings on #345 (tty handoff, pinned-ref reinstall, jq split) (#347)
* fix(installer): resolve Bugbot findings on #345 (tty handoff, pinned-ref reinstall, jq split)
Three Cursor Bugbot findings on the develop→main PR:
- High — assess.sh `_assess_handoff` ran a bare `tracebloc` with no stdin
redirect. Under `curl | bash` that inherits the install pipe, so the
interactive home-screen handoff could consume script bytes or block (same
class as #341). Redirect </dev/tty when openable, else </dev/null — mirrors
the bootstrap hand-off in install.sh; still no `exec` so the EXIT trap runs.
- High — install.sh skipped its healthy early-bailout on a pinned
REF/BRANCH/unverified request but never told install-k8s.sh's own
stop-and-check gate. A pinned-ref re-run would download the new installer and
then short-circuit to the home screen, doing nothing. Export TB_FORCE_REINSTALL
whenever the bailout is skipped so the downstream gate runs the full flow.
- Low — assess.sh `_assess_cluster_servers_running` reintroduced a jq/awk
bifurcation; jq is not a guaranteed installer prerequisite (Bugbot #284).
Collapse to the single jq-free awk path (the SERVERS column read already there).
Adds bootstrap tests asserting the reinstall intent reaches install-k8s.sh; test
suites green (assess 31/31, install-bootstrap 15/15).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(supply-chain): regenerate manifest.sha256 for assess.sh change
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): give the healthy-machine hand-off a real terminal on stdout/stderr (#348)
Bugbot (Medium) follow-up to #347: main() runs setup_log_file
(`exec > >(tee …) 2>&1`) BEFORE the stop-and-check gate, so by the time
_assess_handoff runs the shell's stdout/stderr are a pipe to tee, not the
terminal. The earlier fix redirected only stdin, so the interactive `tracebloc`
home screen still rendered onto the tee pipe instead of a tty (the bootstrap
bailout in install.sh avoids this only because it hands off before any tee
redirect).
Point all three streams at the terminal ($TB_TTY, /dev/tty) when it's openable —
bypassing tee for the interactive screen, exactly as the bootstrap does — else
fall back to </dev/null and leave stdout/stderr on the pipe (non-interactive/CI).
Adopt provision.sh's TB_TTY indirection so the redirect is testable; assess.bats
now proves the home screen lands on the terminal (not the pipe) on the openable
path and falls back cleanly otherwise. Manifest regenerated. assess.bats 32/32.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): assess gate checks cluster is running before the Helm probe (#349)
* fix(installer): check cluster is running before the Helm probe in the assess gate
Bugbot (High) follow-up: _assess_classify called detect_installed_client
(unbounded `helm list -A` / `helm get values`) BEFORE the cluster-servers-running
check. On a stopped cluster (post-reboot or manual stop) the k8s API is down, so
Helm hangs — violating the gate's "bounded, never-hang" contract — and, when it
finally fails, the machine is mislabeled fresh/cluster-no-release, making the
cluster-stopped path effectively unreachable on real re-runs.
Reorder so the cheap read-only k3d servers-running probe runs first: a stopped
cluster short-circuits to degraded/cluster-stopped without ever touching Helm,
and detect_installed_client only ever runs against a live API. Adds an ordering
guard test asserting the Helm probe is not invoked on a stopped cluster.
Manifest regenerated. assess.bats 33/33.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(e2e-proxy): deflake §4 — retry the app-pod proxy-DNS resolution race
The §4 app-pod curl used --retry-connrefused, which retries a refused CONNECT
(Service endpoints not yet programmed) but NOT a DNS failure. Under CI load the
squid Service name isn't yet in the new pod's resolver when curl runs, so it
fails with "Could not resolve proxy" (curl exit 5) — which curl does not retry on
its own — and §4 flaked red (PR #349 run 29345383969) even though the squid pod
and endpoints were up.
Add --retry-all-errors (covers the DNS-resolution error too) and bump to
--retry 8. A genuine #119 regression still fails all retries, so the guard keeps
its teeth. Test-only; not part of the installer supply-chain manifest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): fail closed on unreadable one-client guard values; fix bootstrap spinner on bash 3.2 (#350)
Two Bugbot findings on #345:
- Medium (install-k8s.ps1): the Windows one-client-per-machine guard treated an
unreadable `helm get values` (fetch failure OR unparsable JSON) for a
client-chart release the same as "no client here" — so if the ONLY installed
client's values couldn't be read, $existingId stayed empty and the install
proceeded, silently overwriting a client it couldn't identify (fail-open).
Now record any client release whose clientId we can't read and fail CLOSED:
refuse with an actionable message rather than overwrite an unknown client.
A parsed release with no clientId (literal `null`) is unchanged — still not a
match. Adds Pester coverage for both unreadable paths.
- Low (install.sh): the early-bailout spinner stored 3-byte braille frames in a
single string and indexed `${frames:i:1}` / `${#frames}` — byte-based on macOS
system bash 3.2, so glyphs sliced mid-byte and rendered as garbage. Switch to
an array of glyphs indexed by element (matches spin() in lib/common.sh), which
is char-correct on 3.2.
Manifest regenerated (install-k8s.ps1 hash). install.sh shellcheck clean, .ps1
parses cleanly.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): bound the services progress-bar kubectl calls with --request-timeout
Bugbot (Medium): _download_services_progress claims to be bounded/non-blocking,
but its two `kubectl get pods` calls omitted --request-timeout. The
TB_PULL_TIMEOUT deadline is only checked BETWEEN iterations, so a wedged/
unreachable API makes kubectl block indefinitely — freezing step e's progress
bar before the authoritative readiness gate in step f ever runs.
Add --request-timeout (default 5s, overridable via TB_PROGRESS_KUBECTL_TIMEOUT)
to both calls, mirroring assess.sh's bounded probe — a call that can't reach the
API now returns quickly, the loop re-checks the deadline, and step e degrades to
the honest "still downloading in the background" line instead of hanging. Manifest
regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): proactive hardening sweep (bounded probes, tty handoffs, fail-closed guards) (#352)
* fix(installer): bound the services progress-bar kubectl calls with --request-timeout
Bugbot (Medium): _download_services_progress claims to be bounded/non-blocking,
but its two `kubectl get pods` calls omitted --request-timeout. The
TB_PULL_TIMEOUT deadline is only checked BETWEEN iterations, so a wedged/
unreachable API makes kubectl block indefinitely — freezing step e's progress
bar before the authoritative readiness gate in step f ever runs.
Add --request-timeout (default 5s, overridable via TB_PROGRESS_KUBECTL_TIMEOUT)
to both calls, mirroring assess.sh's bounded probe — a call that can't reach the
API now returns quickly, the loop re-checks the deadline, and step e degrades to
the honest "still downloading in the background" line instead of hanging. Manifest
regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): proactive hardening sweep — bounded probes, tty handoffs, fail-closed guards
A comprehensive audit of the installer (bash + PowerShell) for the failure
classes Bugbot has been surfacing one at a time on #345, fixing the genuine
instances in one pass.
Bounded probes (no more indefinite hangs against a wedged/unreachable API):
- cluster.sh: `kubectl cluster-info` in the _wait_for_api gate (the 60s cap was
only re-checked between iterations — a single call could block forever).
- summary.sh: cluster-info / get nodes / get pods / logs on the summary +
not-ready diagnostic paths.
- diagnose.sh: every kubectl call in the support-bundle path (`set +e` stops
aborts but NOT hangs); helm calls now run only behind a bounded cluster-info
probe (helm has no --request-timeout).
- gpu-plugins.sh: `kubectl get nodes` in the verify_gpu poll loop.
- common.sh download_with_progress: HEAD probe (-m) + the backgrounded curl
(--connect-timeout + --speed-limit/--speed-time stall abort — it was monitored
only by `kill -0`, no deadline, no kill).
- install.sh: every bootstrap fetch (--connect-timeout/--max-time; retry already
present, so a stall becomes retriable).
- install-cli.sh: the CLI-installer download (a stall now falls to "install
later" instead of hanging the step).
curl|bash tty handoffs (stdin is the install pipe; stdout/stderr are the tee):
- provision.sh: `tracebloc login` (the credential-mint device flow) now gets the
real terminal on all three streams when openable, else </dev/null — same idiom
as assess.sh's hand-off.
- gpu-nvidia.sh: the reboot `read` read from the EOF pipe with no `|| true`,
aborting the whole installer under `set -e` right after a successful driver
install; now reads /dev/tty, no-tty => no reboot.
- setup-macos.sh: the Docker-arch replace prompt read the pipe (empty answer =>
meaningless confirm); now reads /dev/tty.
Fail-closed guards (a failed check must not read as "safe to proceed"):
- install-client-helm.sh detect_installed_client now reports
INSTALLED_CLIENT_UNKNOWN=1 when `helm list` FAILS (vs genuinely no releases),
and the one-client guard refuses rather than risk overwriting a client it
couldn't enumerate. +bats coverage.
- install-k8s.ps1 one-client guard: same fix at the `helm list` level (non-zero
exit OR non-JSON now fails closed, matching the per-release fix from #350).
+Pester coverage.
set -e footgun:
- common.sh _chart_version: trailing `|| true` so a no-match `grep` (no client
release) can't abort callers that assign it under `set -e`.
PowerShell 5.1 portability:
- install-k8s.ps1: the WSL-path build used a scriptblock `-replace` (PS 6.1+);
under Windows PowerShell 5.1 (the bootstrap target) the drive letter wasn't
lowercased -> malformed path -> 180s NCT-install timeout. Now -match/$Matches.
Deliberately NOT changed (documented):
- k3d `cluster create --wait` has no --timeout in EITHER bash or PowerShell — at
parity, and the known --wait hang cause (proxy misroute) is already mitigated
by the proxy config. Not changing critical-path create behavior speculatively.
- The dataset-mount check's inspect-failure no-op is a documented, tested design
choice (cluster.bats) — left as-is.
- Long tail of Linux-GPU-only driver-download curls (gpu-nvidia/gpu-amd,
setup-linux) and misc PowerShell -TimeoutSec: LOW severity, retry-wrapped;
left for a follow-up to keep this reviewable.
All bash suites green (assess 33, install-client-helm 54, cluster 27, provision,
summary, install-cli, bootstrap, preflight); shellcheck --severity=error clean;
install-k8s.ps1 parses cleanly; manifest regenerated. Two pre-existing local-env
bats failures (_extract_yaml_value '' escape; validate_config) are unrelated and
also fail on develop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(installer): fail closed on unknown helm state in the #303 pre-provision guard
Bugbot (High) on the sweep: the sweep taught detect_installed_client to signal
INSTALLED_CLIENT_UNKNOWN=1 on a failed `helm list` and wired it into the Helm-step
one-client guard — but provision_client's #303 pre-flight still only checked
INSTALLED_CLIENT_NS. A failed enumeration leaves both globals empty, so
provisioning continued to `client create` and could register a dashboard client
the later Helm guard then refuses to install — the exact orphan the pre-flight
exists to prevent.
Fail closed on INSTALLED_CLIENT_UNKNOWN right after detect_installed_client, before
any mint — same signal the Helm-step guard keys on. +bats coverage (unknown state
refuses before `client create`, no orphan).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Merge pull request #353 from tracebloc/fix/bugbot-345-detect-values-failopen
fix(installer): bash guard fails closed on unreadable client values (PS parity)
---------
Co-authored-by: lukasWuttke <54042461+LukasWodka@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Arturo Peroni <arturo@tracebloc.io>
@LukasWodka
LukasWodka deleted the feat/installer-stop-and-check branch August 14, 2026 13:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@LukasWodka@aptracebloc@saadqbal