Skip to content

fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8) - #111

Merged
saadqbal merged 2 commits into
developfrom
sec/rfc-0001-r8-mandatory-verify
Jun 25, 2026
Merged

fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)#111
saadqbal merged 2 commits into
developfrom
sec/rfc-0001-r8-mandatory-verify

Conversation

@saadqbal

@saadqbalsaadqbal commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Security-sensitive — DRAFT for human review. Part of RFC-0001 R8 (Phase-1 blocking security must-have), tracked as tracebloc/backend#889. Cross-repo companion PR lands in tracebloc/client (the curl|bash bootstrap).

The gap

scripts/install.sh verified SHA256, then verified the cosign signature only if cosign was on PATH — and silently skipped it otherwise (the default on a fresh box), printing (cosign not installed; SHA256 verified, signature skipped). SHA256SUMS is fetched over the same channel as the binary, so an on-path attacker who swaps the binary can swap the sums too. The signature was the only independent proof, and it was optional.

The fix

Signature verification is now mandatory on the default path:

  • if cosign isn't installed, bootstrap a pinned (v2.4.1), checksum-verified cosign and use it;
  • fail closed if cosign can be neither found nor bootstrapped, or if .sig/.cert aren't published — never fall back to the same-channel SHA256 alone, never print the old "signature skipped" line;
  • keep the SHA256 gate as the first layer;
  • one explicit, loud escape hatch for constrained environments: TRACEBLOC_ALLOW_UNVERIFIED=1.

Stays POSIX sh (verified with dash -n + shellcheck --shell=sh).

Tests / CI

  • New scripts/tests/install-verify.sh — a network-free harness (mocked curl/cosign, host cosign hidden via PATH) asserting: valid-sig installs, bad-sig aborts, cosign-absent fails closed, the signature skipped string is gone, opt-in degrades with a warning, SHA256 mismatch aborts. 6/6 pass locally.
  • New installer job in build.yml runs shellcheck + dash -n + the harness (the installer had no automated test before).

Decisions / open questions

  • cosign keyless (not minisign/gpg): no new signing secret; the binary release already uses it. See the client PR's docs/SUPPLY_CHAIN.md §3.
  • 386 / arm without cosign installed can't bootstrap (no official cosign build for those arches) → they fail closed, or pre-install cosign. Documented in the README note. Acceptable: fail-closed is the point.
  • The cosign-bootstrap trust root is TLS-to-GitHub + cosign's own checksums (you can't verify cosign's signature without cosign). Strictly better than the status quo; the stronger path (pre-install cosign / internal mirror) is documented.

Not verified

I could not run the installer end-to-end against a real GitHub release from this environment (no network to releases), so the happy path is exercised only via the mocked harness, not a live download.

🤖 Generated with Claude Code


Note

High Risk
Changes the default supply-chain trust model for the privileged curl|sh installer; regressions could block installs or weaken verification, though behavior is covered by the new harness and static checks.

Overview
scripts/install.sh no longer treats cosign as optional. After SHA256, signature verification is required: use cosign on PATH, or bootstrap pinned v2.4.1 with TLS 1.2 fetches and checksum verification against Sigstore’s published sums. If verification can’t run (no cosign/bootstrap, missing .sig/.cert, or failed verify-blob), the install exits instead of printing “signature skipped.” TRACEBLOC_ALLOW_UNVERIFIED=1 is the only downgrade and emits explicit warnings.

validate_tag runs on the resolved release tag before downloads, blocking /, .., and non-vX.Y.Z shapes so --version can’t be used for path traversal in release URLs.

CI and docs: a new installer job runs shellcheck, dash -n, and scripts/tests/install-verify.sh (mocked curl/cosign, no network). README documents mandatory verification and the opt-out env var.

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

…l-closed (R8)
The installer's cosign check was SKIPPED when cosign was absent — the default
on a fresh box — silently degrading to a SHA256 fetched over the same channel
as the binary, which an on-path attacker also controls. The most privileged
delivery step was the least verified (RFC-0001 R8, backend#889).
scripts/install.sh now:
- requires a signature on the default path; if cosign isn't on PATH it
bootstraps a pinned (v2.4.1), checksum-verified cosign and uses that;
- FAILS CLOSED if cosign can be neither found nor bootstrapped, and if the
.sig/.cert aren't published — never falls back to the same-channel SHA256
alone, never prints the old "signature skipped" line;
- keeps the SHA256 gate as the first layer (factored into sha256_of);
- offers one explicit, loud escape: TRACEBLOC_ALLOW_UNVERIFIED=1, for a
genuinely constrained environment.
Stays POSIX sh (verified with dash -n + shellcheck --shell=sh). Adds
scripts/tests/install-verify.sh — a network-free harness (mocked curl/cosign)
asserting the fail-closed paths — and wires shellcheck + the harness into a new
`installer` job in build.yml (the installer had no automated test before).
Cross-repo companion: tracebloc/client pins the curl|bash bootstrap to an
immutable tag + verifies sub-scripts against a cosign-signed manifest.
Refs: backend#889 (RFC-0001 §9, §14 R8, §13)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbalsaadqbal self-assigned this Jun 25, 2026
@saadqbal

Copy link
Copy Markdown
CollaboratorAuthor

Independent security review — should-fix

Confirmed the mandatory / fail-closed cosign property holds — the old "signature skipped" branch is gone and the fail-closed paths verified (harness 6/6).

  • 🟠 Should-fixresolve_tag (scripts/install.sh ~L188-190) returns --version/RELEASE_VERSION verbatim into BASE_URL=.../releases/download/${TAG}. Lower impact than the client case (the binary still must pass SHA256 and the pinned-identity cosign verify → fails closed), but validate TAG against ^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.]+)?$ + reject */*/*..*.
  • 🟡 Nit — the cosign-bootstrap curls miss the --tlsv1.2 floor the rest of the codebase uses; add for consistency.

Fixes are being applied to this branch now.

🤖 Reviewed by Claude Code

…or (R8 review)
Security-review follow-ups on PR #111 (RFC-0001 R8, backend#889), applied
before merge:
SHOULD-FIX — tag-traversal gap (scripts/install.sh):
`--version` / RELEASE_VERSION is returned by resolve_tag verbatim and flows
into BASE_URL=.../releases/download/${TAG}, so an unvalidated value like
`v1.2.3-../../heads/main` would let curl collapse the `..` and fetch from a
path other than the intended release. Add validate_tag() after resolve_tag:
reject any `/` or `..` (case glob) and require the same release-tag shape as
the client bootstrap (^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.]+)?$, via POSIX
`grep -Eq`). Extend install-verify.sh with four cases: traversal --version and
bare-slash --version rejected before any download, a malformed tag rejected,
and a well-formed --version still installs (validator isn't over-tight).
NIT — TLS floor (scripts/install.sh):
The two ensure_cosign bootstrap curls used bare `curl -fsSL`; add `--tlsv1.2`
to match the client installer's curls so we never negotiate below TLS 1.2 to
pull the verifier we then trust.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbal
saadqbal marked this pull request as ready for review June 25, 2026 10:35
@saadqbal
saadqbal merged commit 8ca3dda into developJun 25, 2026
20 checks passed
@saadqbal
saadqbal deleted the sec/rfc-0001-r8-mandatory-verify branch June 25, 2026 11:31
saadqbal added a commit that referenced this pull request Jul 1, 2026
* Merge pull request #111 from tracebloc/sec/rfc-0001-r8-mandatory-verify
fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)
* fix: validate COSIGN_VERSION and enforce TLS 1.2 on installer downloads (#114)
Bugbot (promotion PR #113), two findings in scripts/install.sh:
- Unvalidated cosign version in URL: COSIGN_VERSION (env-overridable) was
interpolated into the Sigstore download URL without the semver/path-traversal
gate applied to RELEASE_VERSION. Generalized validate_tag into
validate_version_tag and apply it to COSIGN_VERSION before building the URL.
(scripts/install.sh:45)
- Signature download curls omit TLS: the .sig/.cert fetches (and the binary,
SHA256SUMS, and latest-redirect curls) lacked --tlsv1.2, unlike the new cosign
bootstrap fetches. Add --tlsv1.2 to every security-sensitive download.
(scripts/install.sh:335)
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(schema): re-vendor ingest.v1.json — register seq2seq as recognize-but-unsupported (#118)
The CI "Schema drift check" (scripts/sync-schema.sh --check) was failing on
develop itself and therefore every PR — the vendored
internal/schema/ingest.v1.json had drifted from data-ingestors master, which
added the `seq2seq` task category (enum entry + an if/then requiring `texts`,
membership in the self-supervised text group, and an updated `texts`
description). The drift is purely additive — it does not change validation for
any already-supported category.
- Re-vendored the schema (sync-schema.sh) → --check now passes.
- Registered seq2seq in internal/push/category.go as recognize-but-not-yet-
CLI-supported (CLISupported:false + UnsupportedNote), Family text: the CLI's
discover/build for its raw-.txt / source\ttarget `texts` layout isn't
implemented, so push reports it as pending rather than leaving a
schema<->registry gap (the cli#74 drift class that TestRegistryCoversSchema-
Categories pins). Updated the parity tests.
Mirrors #103, which did exactly this for causal_language_modeling. Full
seq2seq push support (discover/build, flip CLISupported) is a follow-up
feature — the sibling of cli#105.
go build / vet / test ./... green; drift check green.
Closes#117
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): send User-Agent version header + handle 426 Upgrade Required (cli#98) (#115)
RFC-0001 §13 / §14 R11 / Appendix C.1 — the CLI half of the
minimum-supported-CLI-version handshake (server half: backend#888, shipped
in backend#916).
- Every backend request now carries
`User-Agent: tracebloc-cli/<ver> (<os>/<arch>)`, injected by a transport
wrapper on the shared internal/api client so login + provisioning are both
covered without threading the version through each command. The version is
the ldflags build info, recorded once via api.SetUserAgent in NewRootCmd; a
build with no version reports "dev" (unparseable server-side → fails open).
- A 426 from any endpoint is detected centrally in post/get and surfaced as a
typed *UpgradeRequiredError carrying min_version, so every command degrades
to the same actionable "your CLI is too old — upgrade to >= X" message and a
clean non-zero exit (no stack trace), in both interactive and --plain modes.
Tests: UA on the wire, dev fallback, 426 → UpgradeRequiredError (GET + POST),
unparseable-426 body, message actionability.
Closes#98.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): logout revokes the token server-side (cli#112) (#116)
* feat(auth): logout revokes the token server-side via POST /auth/revoke (cli#112)
RFC-0001 §7.5 / R2 — the CLI half of the logout-revoke gap (server endpoint:
backend#887, shipped in backend#903). Until now `logout` was local-only: it
cleared ~/.tracebloc but a copied/leaked token kept authenticating (confirmed
in the 2026-06-25 connect-flow FR).
- New api.Client.RevokeToken → POST /auth/revoke (Bearer in, 204 out,
idempotent); a non-2xx surfaces as *APIError.
- logout now revokes server-side before clearing local state. Best-effort by
contract: on failure (offline / already-revoked / 5xx) it logs a hint and
still clears local state — the user must always be able to log out locally.
Tests: RevokeToken 204→nil (Bearer + POST on the wire) and 5xx→APIError;
logout calls revoke (now routed at a stub — it was about to hit real prod)
and still clears local state when the revoke fails.
Closes#112.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): address Bugbot on logout revoke — env resolution + save ordering (cli#112)
Two Medium findings on the revoke wiring:
- Resolve the revoke env consistently with authedClient via a new shared
sessionEnv(cfg) helper (saved env → $CLIENT_ENV → prod), instead of
hardcoding prod for an empty cfg.Env — which could revoke against the wrong
host and leave the real session token valid after sign-out.
- Clear + persist local state BEFORE the revoke call, so a failed Save can't
leave a token that's already been revoked server-side on disk as a broken
"signed in" state. Revoke stays best-effort, after the local clear.
Test: TestLogout_RevokesAgainstSessionEnv pins empty cfg.Env → $CLIENT_ENV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): rename dataset → data (ingest/list/delete; keep aliases) (cli#99) (#119)
Rename the `dataset` command group to `data` and its verbs:
- `push` → `ingest` (canonical: `tracebloc data ingest <path>`)
- `rm` → `delete` (canonical: `tracebloc data delete <table>`)
- `list` stays `list`
Deprecated aliases retained for one cycle via cobra's Aliases field:
- `data` has `Aliases: []string{"dataset"}`
- `ingest` has `Aliases: []string{"push"}`
- `delete` has `Aliases: []string{"rm"}`
All user-facing text updated (Short, Long, Banner, home screen, examples).
Internal Go symbols renamed accordingly (newDataCmd, newDataIngestCmd,
newDataDeleteCmd, runDataIngestArgs, runDataIngest, runDataDelete, etc.).
Tests updated to use canonical names; alias-resolution tests added.
No behaviour change beyond naming; ingestion logic and cli#67–#77 fixes
are untouched. The separate top-level `ingest validate` command is
unmodified.
Closes#99
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): env-scoped config v2 — per-env profiles fix the cross-env clobber (cli#100) (#120)
RFC-0001 §7.5 / §14 R10 / Appendix C.8. The v1 ~/.tracebloc/config.json held a
single flat {env,token,active_client_id}, so `login --env X` overwrote env+token
but stranded the previous env's active_client_id — a dev/stg/prod user could
silently target the wrong client.
- Config is now v2: { version, current_env, profiles: { <env>: {email, token,
expires_at, active_client_id} } }. One active_client_id PER env.
- `login --env X` switches current_env and writes X's profile via Profile(env)
(which returns X's existing profile), so a re-login preserves its
active_client_id instead of clobbering it; other envs are untouched.
- v1 files auto-migrate to v2 on first read (the single record wrapped under
profiles[env]); the first Save rewrites the file as v2. No data loss.
- `logout` clears only the current env's profile; other envs untouched. (Still
revokes server-side, best-effort — from cli#112.)
- File stays 0600.
Call sites (login / logout / auth status / client create·list·use) now read and
write via cfg.Current() / Profile(env) / CurrentEnv. expires_at is persisted per
profile and shown by `auth status` when present (login doesn't capture it yet).
Tests: v1→v2 migration (+ empty-env→prod), the dev→prod→dev no-clobber (R10)
guarantee, SignedIn semantics, 0600, and every login/logout/auth/client path on v2.
Closes#100.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101) (#121)
* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101)
RFC-0001 §8.5 — make the zero-prompt connect flow's FAILURE path stand on its
own (the installer-side streaming stays with backend#838, per the ticket).
- `--verbose` / $TRACEBLOC_LOG_LEVEL: a root persistent flag that streams the
device-flow → provision detail via a new verbose-gated ui.Detailf. Default
output stays quiet (~the usual handful of ✔ lines).
- `cluster doctor` now runs an "Auth & config" section FIRST — before the
cluster checks, so it works even when no cluster is reachable (the
failed-provision case): signed in? which env + account? active client set?
plus a live token check (WhoAmI) — 401 → ✖ re-login, network error → ⚠.
Its status folds into the overall verdict.
- `client create` failure prints the exact, idempotent resume command (§7.2)
+ a `tracebloc cluster doctor` pointer, so a broken headless connect isn't a
dead end.
- Every `client create` run writes ~/.tracebloc/install-<ts>.log (0600) — a
full trace on disk even when the terminal stayed quiet.
Tests: ui Detailf gating; doctor auth (not-signed-in / valid / 401 /
no-active-client); verbose-streams vs quiet-default; provision failure →
resume command + install log.
Closes#101.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't log a cancelled provision as "done" (cli#101, Bugbot)
The success-path defer blanket-logged "done" on err==nil, so declining the
confirm prompt (which returns nil after "Cancelled.") recorded a user abort as a
successful run in install-<ts>.log — misleading for support / post-mortems.
Log the terminal outcome at each branch instead (minted / adopted / cancelled);
the defer now only handles the failure case.
Test: TestClientCreate_CancelLogsCancelledNotDone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): resume command includes prompted name/location, not just flags (cli#101, Bugbot)
resumeCommand(opts) read opts, which carries only the flag values — so a failed
interactive provision (name/location typed at a prompt) printed a resume command
missing --name/--location, defeating the copy-paste-to-retry goal. Write the
resolved values back into opts after gathering them, so the defer's resumeCommand
reflects what the user actually entered.
Test: TestClientCreate_ResumeCommandIncludesPromptedValues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): doctor folds auth into the kubeconfig-exit code; install log advertises no path on open failure (cli#101, Bugbot)
Two more Bugbot findings:
- `cluster doctor` returned exit 3 on a kubeconfig load / clientset failure even
when the auth section had failed (e.g. a 401), so automation could read a bad
token as a kubeconfig-only problem. A failed auth section now escalates the
exit to 2; exit 3 is kept for the auth-OK case (the documented contract).
- newInstallLog returned the intended path even when opening the file failed, so
the failure hint could print "Full log:" for a file that was never written. It
now returns an empty path, and the caller's guard skips the hint.
Tests: doctor kubeconfig-fail → exit 2 (auth fail) / exit 3 (auth OK); install
log returns an empty path when the file can't be opened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): cluster doctor treats a 426 as a hard upgrade failure, not a transient warning (Bugbot/#113) (#122)
doctor's live token check (WhoAmI) treated every non-401 error as a transient
"couldn't verify — check your network" warning. A 426 (the server enforces a
newer CLI, surfaced as *api.UpgradeRequiredError by the #98 handling) is a
definite, actionable problem — now reported as a hard ✖ "this CLI is too old —
upgrade to >= X" failure with the min-version message.
Test: TestRunAuthChecks_426IsHardFailure.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't misread a v2-shaped config as v1 (Bugbot/#113) (#123)
Load routed any config with version < 2 to migrateV1, which reads only the flat
v1 fields — so a v2-shaped file with a missing/wrong version would be migrated to
an empty record, silently dropping its profiles. migrateV1 now fires only for a
genuine v1 record (old version AND no `profiles` object); a profiles-bearing file
is always parsed as v2.
Test: TestLoadV2ShapedWithoutVersion_NotMigrated.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
Co-authored-by: shujaat_tracebloc <153823837+shujaatTracebloc@users.noreply.github.com>
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
saadqbal added a commit that referenced this pull request Jul 1, 2026
* Merge pull request #111 from tracebloc/sec/rfc-0001-r8-mandatory-verify
fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)
* fix: validate COSIGN_VERSION and enforce TLS 1.2 on installer downloads (#114)
Bugbot (promotion PR #113), two findings in scripts/install.sh:
- Unvalidated cosign version in URL: COSIGN_VERSION (env-overridable) was
interpolated into the Sigstore download URL without the semver/path-traversal
gate applied to RELEASE_VERSION. Generalized validate_tag into
validate_version_tag and apply it to COSIGN_VERSION before building the URL.
(scripts/install.sh:45)
- Signature download curls omit TLS: the .sig/.cert fetches (and the binary,
SHA256SUMS, and latest-redirect curls) lacked --tlsv1.2, unlike the new cosign
bootstrap fetches. Add --tlsv1.2 to every security-sensitive download.
(scripts/install.sh:335)
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(schema): re-vendor ingest.v1.json — register seq2seq as recognize-but-unsupported (#118)
The CI "Schema drift check" (scripts/sync-schema.sh --check) was failing on
develop itself and therefore every PR — the vendored
internal/schema/ingest.v1.json had drifted from data-ingestors master, which
added the `seq2seq` task category (enum entry + an if/then requiring `texts`,
membership in the self-supervised text group, and an updated `texts`
description). The drift is purely additive — it does not change validation for
any already-supported category.
- Re-vendored the schema (sync-schema.sh) → --check now passes.
- Registered seq2seq in internal/push/category.go as recognize-but-not-yet-
CLI-supported (CLISupported:false + UnsupportedNote), Family text: the CLI's
discover/build for its raw-.txt / source\ttarget `texts` layout isn't
implemented, so push reports it as pending rather than leaving a
schema<->registry gap (the cli#74 drift class that TestRegistryCoversSchema-
Categories pins). Updated the parity tests.
Mirrors #103, which did exactly this for causal_language_modeling. Full
seq2seq push support (discover/build, flip CLISupported) is a follow-up
feature — the sibling of cli#105.
go build / vet / test ./... green; drift check green.
Closes#117
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): send User-Agent version header + handle 426 Upgrade Required (cli#98) (#115)
RFC-0001 §13 / §14 R11 / Appendix C.1 — the CLI half of the
minimum-supported-CLI-version handshake (server half: backend#888, shipped
in backend#916).
- Every backend request now carries
`User-Agent: tracebloc-cli/<ver> (<os>/<arch>)`, injected by a transport
wrapper on the shared internal/api client so login + provisioning are both
covered without threading the version through each command. The version is
the ldflags build info, recorded once via api.SetUserAgent in NewRootCmd; a
build with no version reports "dev" (unparseable server-side → fails open).
- A 426 from any endpoint is detected centrally in post/get and surfaced as a
typed *UpgradeRequiredError carrying min_version, so every command degrades
to the same actionable "your CLI is too old — upgrade to >= X" message and a
clean non-zero exit (no stack trace), in both interactive and --plain modes.
Tests: UA on the wire, dev fallback, 426 → UpgradeRequiredError (GET + POST),
unparseable-426 body, message actionability.
Closes#98.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(auth): logout revokes the token server-side (cli#112) (#116)
* feat(auth): logout revokes the token server-side via POST /auth/revoke (cli#112)
RFC-0001 §7.5 / R2 — the CLI half of the logout-revoke gap (server endpoint:
backend#887, shipped in backend#903). Until now `logout` was local-only: it
cleared ~/.tracebloc but a copied/leaked token kept authenticating (confirmed
in the 2026-06-25 connect-flow FR).
- New api.Client.RevokeToken → POST /auth/revoke (Bearer in, 204 out,
idempotent); a non-2xx surfaces as *APIError.
- logout now revokes server-side before clearing local state. Best-effort by
contract: on failure (offline / already-revoked / 5xx) it logs a hint and
still clears local state — the user must always be able to log out locally.
Tests: RevokeToken 204→nil (Bearer + POST on the wire) and 5xx→APIError;
logout calls revoke (now routed at a stub — it was about to hit real prod)
and still clears local state when the revoke fails.
Closes#112.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(auth): address Bugbot on logout revoke — env resolution + save ordering (cli#112)
Two Medium findings on the revoke wiring:
- Resolve the revoke env consistently with authedClient via a new shared
sessionEnv(cfg) helper (saved env → $CLIENT_ENV → prod), instead of
hardcoding prod for an empty cfg.Env — which could revoke against the wrong
host and leave the real session token valid after sign-out.
- Clear + persist local state BEFORE the revoke call, so a failed Save can't
leave a token that's already been revoked server-side on disk as a broken
"signed in" state. Revoke stays best-effort, after the local clear.
Test: TestLogout_RevokesAgainstSessionEnv pins empty cfg.Env → $CLIENT_ENV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): rename dataset → data (ingest/list/delete; keep aliases) (cli#99) (#119)
Rename the `dataset` command group to `data` and its verbs:
- `push` → `ingest` (canonical: `tracebloc data ingest <path>`)
- `rm` → `delete` (canonical: `tracebloc data delete <table>`)
- `list` stays `list`
Deprecated aliases retained for one cycle via cobra's Aliases field:
- `data` has `Aliases: []string{"dataset"}`
- `ingest` has `Aliases: []string{"push"}`
- `delete` has `Aliases: []string{"rm"}`
All user-facing text updated (Short, Long, Banner, home screen, examples).
Internal Go symbols renamed accordingly (newDataCmd, newDataIngestCmd,
newDataDeleteCmd, runDataIngestArgs, runDataIngest, runDataDelete, etc.).
Tests updated to use canonical names; alias-resolution tests added.
No behaviour change beyond naming; ingestion logic and cli#67–#77 fixes
are untouched. The separate top-level `ingest validate` command is
unmodified.
Closes#99
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): env-scoped config v2 — per-env profiles fix the cross-env clobber (cli#100) (#120)
RFC-0001 §7.5 / §14 R10 / Appendix C.8. The v1 ~/.tracebloc/config.json held a
single flat {env,token,active_client_id}, so `login --env X` overwrote env+token
but stranded the previous env's active_client_id — a dev/stg/prod user could
silently target the wrong client.
- Config is now v2: { version, current_env, profiles: { <env>: {email, token,
expires_at, active_client_id} } }. One active_client_id PER env.
- `login --env X` switches current_env and writes X's profile via Profile(env)
(which returns X's existing profile), so a re-login preserves its
active_client_id instead of clobbering it; other envs are untouched.
- v1 files auto-migrate to v2 on first read (the single record wrapped under
profiles[env]); the first Save rewrites the file as v2. No data loss.
- `logout` clears only the current env's profile; other envs untouched. (Still
revokes server-side, best-effort — from cli#112.)
- File stays 0600.
Call sites (login / logout / auth status / client create·list·use) now read and
write via cfg.Current() / Profile(env) / CurrentEnv. expires_at is persisted per
profile and shown by `auth status` when present (login doesn't capture it yet).
Tests: v1→v2 migration (+ empty-env→prod), the dev→prod→dev no-clobber (R10)
guarantee, SignedIn semantics, 0600, and every login/logout/auth/client path on v2.
Closes#100.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101) (#121)
* feat(cli): --verbose + cluster doctor auth checks + resume hint & install log (cli#101)
RFC-0001 §8.5 — make the zero-prompt connect flow's FAILURE path stand on its
own (the installer-side streaming stays with backend#838, per the ticket).
- `--verbose` / $TRACEBLOC_LOG_LEVEL: a root persistent flag that streams the
device-flow → provision detail via a new verbose-gated ui.Detailf. Default
output stays quiet (~the usual handful of ✔ lines).
- `cluster doctor` now runs an "Auth & config" section FIRST — before the
cluster checks, so it works even when no cluster is reachable (the
failed-provision case): signed in? which env + account? active client set?
plus a live token check (WhoAmI) — 401 → ✖ re-login, network error → ⚠.
Its status folds into the overall verdict.
- `client create` failure prints the exact, idempotent resume command (§7.2)
+ a `tracebloc cluster doctor` pointer, so a broken headless connect isn't a
dead end.
- Every `client create` run writes ~/.tracebloc/install-<ts>.log (0600) — a
full trace on disk even when the terminal stayed quiet.
Tests: ui Detailf gating; doctor auth (not-signed-in / valid / 401 /
no-active-client); verbose-streams vs quiet-default; provision failure →
resume command + install log.
Closes#101.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't log a cancelled provision as "done" (cli#101, Bugbot)
The success-path defer blanket-logged "done" on err==nil, so declining the
confirm prompt (which returns nil after "Cancelled.") recorded a user abort as a
successful run in install-<ts>.log — misleading for support / post-mortems.
Log the terminal outcome at each branch instead (minted / adopted / cancelled);
the defer now only handles the failure case.
Test: TestClientCreate_CancelLogsCancelledNotDone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): resume command includes prompted name/location, not just flags (cli#101, Bugbot)
resumeCommand(opts) read opts, which carries only the flag values — so a failed
interactive provision (name/location typed at a prompt) printed a resume command
missing --name/--location, defeating the copy-paste-to-retry goal. Write the
resolved values back into opts after gathering them, so the defer's resumeCommand
reflects what the user actually entered.
Test: TestClientCreate_ResumeCommandIncludesPromptedValues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): doctor folds auth into the kubeconfig-exit code; install log advertises no path on open failure (cli#101, Bugbot)
Two more Bugbot findings:
- `cluster doctor` returned exit 3 on a kubeconfig load / clientset failure even
when the auth section had failed (e.g. a 401), so automation could read a bad
token as a kubeconfig-only problem. A failed auth section now escalates the
exit to 2; exit 3 is kept for the auth-OK case (the documented contract).
- newInstallLog returned the intended path even when opening the file failed, so
the failure hint could print "Full log:" for a file that was never written. It
now returns an empty path, and the caller's guard skips the hint.
Tests: doctor kubeconfig-fail → exit 2 (auth fail) / exit 3 (auth OK); install
log returns an empty path when the file can't be opened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): cluster doctor treats a 426 as a hard upgrade failure, not a transient warning (Bugbot/#113) (#122)
doctor's live token check (WhoAmI) treated every non-401 error as a transient
"couldn't verify — check your network" warning. A 426 (the server enforces a
newer CLI, surfaced as *api.UpgradeRequiredError by the #98 handling) is a
definite, actionable problem — now reported as a hard ✖ "this CLI is too old —
upgrade to >= X" failure with the min-version message.
Test: TestRunAuthChecks_426IsHardFailure.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't misread a v2-shaped config as v1 (Bugbot/#113) (#123)
Load routed any config with version < 2 to migrateV1, which reads only the flat
v1 fields — so a v2-shaped file with a missing/wrong version would be migrated to
an empty record, silently dropping its profiles. migrateV1 now fires only for a
genuine v1 record (old version AND no `profiles` object); a profiles-bearing file
is always parsed as v2.
Test: TestLoadV2ShapedWithoutVersion_NotMigrated.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli#125): write the UUID username (not the numeric id) as TRACEBLOC_CLIENT_ID (#126)
`client create` wrote strconv.Itoa(pc.ID) — the numeric dashboard id — as
TRACEBLOC_CLIENT_ID in both credential-file paths (mint + adopt) and in the
printed credential. But TRACEBLOC_CLIENT_ID is the *auth username*: it flows
cred → helm clientId → secret CLIENT_ID → pod env → controller.py
os.getenv("CLIENT_ID"), which is POSTed to api-token-auth as the login
username. The backend authenticates an EdgeDevice by its UUID username
(str(uuid4)), never the numeric id, so a freshly-provisioned client always
failed auth ("Unable to log in with provided credentials") and crash-looped.
Write pc.Username instead. The numeric id stays display-only (shown as
"dashboard id"). Both credential-file tests asserted the buggy numeric id and
are updated to require the username; the print-path test now asserts the
username is shown as the client id.
This is what client PR #293's skip-verify masked — verify_credentials was a
correct canary. #293 will be reverted once this ships (v0.5.1).
Refs: RFC-0001 backend#830, installer credential handoff #838.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
Co-authored-by: shujaat_tracebloc <153823837+shujaatTracebloc@users.noreply.github.com>
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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

@saadqbal@aptracebloc@LukasWodka