Skip to content

main: Enhance CLI security, authentication, and configuration features#148

Merged
saadqbal merged 23 commits into
mainfrom
develop
Jul 7, 2026
Merged

main: Enhance CLI security, authentication, and configuration features#148
saadqbal merged 23 commits into
mainfrom
develop

Conversation

@saadqbal

@saadqbal saadqbal commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Note

High Risk
Touches authentication, token lifecycle, client provisioning idempotency, and machine credential handling—mistakes could leak credentials, adopt the wrong cluster/account, or break unattended installs.

Overview
Delivers RFC-0001-aligned CLI and installer behavior: browser login and logout now use env-scoped profiles (fixing cross-env active-client clobber), server-side token revoke on logout, and auth status --check for scripts that must match the target backend env.

client create is reworked for silent installer flows: auto <firstname>-NN naming, optional location, cluster_id get-or-create plus R7 adopt-backfill when a client is already live in-cluster (avoids duplicate mints), stricter non-interactive guards so credentials are not printed accidentally, ~/.tracebloc/install-*.log traces with idempotent resume hints, and TRACEBLOC_CLIENT_ID written as the auth UUID username (not the numeric dashboard id). Adds client status with optional --wait for post-setup online checks.

The HTTP client sends a User-Agent version header, maps 426 to an upgrade error, and adds PATCH cluster_id / POST /auth/revoke helpers. Docs rename the primary verb to data ingest and document mandatory cosign on install. CI gains an installer job (shellcheck, dash parse, cosign fail-closed harness). The full RFC is added under docs/rfcs/0001-*.md as design-of-record.

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

saadqbal and others added 18 commits June 25, 2026 16:28
fix(security): mandatory / fail-closed cosign verification in the CLI installer (RFC-0001 R8)
…ds (#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>
…ze-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>
…red (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 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>
…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>
…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>
…tall 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>
…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>
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>
…OC_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>
…ecord (#55)

* docs: add DRAFT RFC 0001 — browser auth & client provisioning

Design epic for replacing copy-pasted Client ID + password onboarding
with a device-flow (RFC 8628) browser sign-in + auto-provisioning.

Refs #54.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): derive-once-freeze namespace + soft-required location

- §6.6: derive namespace slug from display name ONCE then freeze (k8s
  namespaces are immutable); collision-suffix + empty-slug guard +
  --namespace override; backfill leaves existing slugs untouched.
- §6.7: location is soft-required (required but pre-filled); never accept
  a silent empty (reads as carbon-free); explicit "set later" path; keep
  DB blank=True for back-compat, enforce at UX layer.
- Appendix B: name→slug reference algorithm + prototype validation table
  + manage.py query to validate against production namespaces.

Refs #54.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): rev 2 — lead with client lifecycle; settle setup/credential/handle decisions

Refresh the RFC against the current code and the cross-repo review on
backend#830. The auth handshake turned out to be the easy half; the
design now leads with the client lifecycle on a machine, which is where
the real bugs are.

- New §0: settle three product decisions — silent/auto setup (zero
  prompts: name=hostname, location=auto-detect, surfaced not asked); the
  machine credential is never shown (written to the cluster secret 0600,
  never to stdout/scrollback/~/.tracebloc); clients are referred to by
  slug + arrow-key picker, never the UUID/username/password.
- New §3.2: two operational contexts (account vs client) + command map.
- New §7: client-lifecycle loopholes and their resolutions — idempotent
  create + machine→client anchor, selected-vs-connected, guarded
  delete, cross-account pointer scoping (fixes logout leaving the active
  client set), orphan resume, auth/expiry, manage-by-name + rotation.
- Refresh §4 "what exists" to today: auth scaffold merged (cli#83),
  client commands in flight (cli#84/#92), dataset commands target a
  cluster via kubeconfig flags and never read the active pointer (§4.6).
- §12 records the backend#830 resolutions of the old §11 open questions
  (air-gap out, namespace name→slug→both, location future-only, RBAC
  read/write split, multi-client free / re-parenting deferred, reuse web
  IdP). Two product calls flagged for owner confirmation.
- Rewrite §8 UX (zero-prompt flows) and §9 security (credential never
  shown; where it lives; rotation = delete + recreate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): cluster is the idempotency anchor (1:1 client↔cluster)

A client attaches 1:1 to a cluster, so client identity is per-cluster.
`client create` becomes get-or-create keyed on the cluster identity
(proposed: kube-system namespace UID), which is readable before install —
closing the config-lost / pre-install orphan gap a machine-id key
couldn't.

- §3.1: state the 1:1 client↔cluster invariant up front.
- §6.3: new backend cluster_id field (unique=True) + get-or-create-by-
  cluster on POST /edge-device/.
- §7.2: rewrite around the cluster anchor; demote the -2/-3 suffix to
  cross-cluster-only — kills the same-cluster duplicate the current PR #92
  derive would mint on re-run.
- §7.9: orphan recovery keyed on cluster_id; password-reset fallback when
  the credential was lost before Helm consumed it.
- §6.4 / §6.6 / §8.2 / §12-Q5 / §13: align everything to
  one-client-per-cluster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): add §14 Risks & dependencies; reframe §11 around the critical path

Capture the bottlenecks around the flow (not the in-flow bugs of §7):
- R1 critical path crosses backend#835 + backend#836 + an unowned
  frontend /activate page; the CLI is gated on them, not the reverse.
- R2 the user token (account-scoped, long-lived, 0600 on every box,
  logout is local-only) is the real blast radius — not the machine
  credential D2 hid.
- R3 the cluster anchor needs k8s up at create time, and the kube-system
  UID changes on a cluster rebuild.
- R4 the namespace-uniqueness migration can hit k8s namespace
  immutability — destroy+rebuild, not rename.
- R5 fleet provisioning is a thundering herd on the unique constraint.

§11 reframed to show the backend + frontend → CLI dependency order.
Appendix A: replace the slug-check sketch with a complete READ-ONLY
collision check that reports the (account, namespace) duplicates that
would block backend#863 — runnable against staging now (R4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): fold in review — cross-account, fleet backfill, logout revoke + polish

Address the code-grounded review's blocking + polish findings.

Blocking:
- R6 (new): account-scope get-or-create — a cluster_id bound to another
  account is a 409, never a silent adoption (the kube-system UID isn't a
  secret) (§6.3/§7.2).
- R7 (new): the existing fleet has null cluster_id, so a naive re-run would
  double-provision and orphan the live client. Backfill cluster_id via the
  heartbeat, plus a §7.2 step-2a "never mint over a live in-namespace
  release" guard for the pre-backfill window (§4.5/§10).
- Heartbeat must report cluster_id (§4.5) — powers the connected check and
  backfills R7.

Correctness/security:
- logout revokes server-side now via backend#845, not Phase 2 (§7.5/§9/R2).
- Orphan password-reset gated on heartbeat recency, not just "no values
  file" — a once-connected client still has a running pod (§7.9).
- client create operates against an already-reachable cluster (§6.2); add a
  reaper/teardown hook for rebuild orphans (R3).

Clarity:
- Fix the Appendix A "authoritative" contradiction: cluster_id is
  authoritative for idempotency; namespace-unique is cosmetic dedup (§12).
- Rename is cosmetic; the handle stays the frozen slug (§7.1/§8.1).
- §9: device-code phishing mitigations + etcd-at-rest note.
- §0 + rev note (Rev 3); §12 marks cluster-id + Q1 + Q5 confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): ground R4/§4.2/§6.6 in backend code (namespace is unguarded today)

Verified against the backend tree: namespace is stored client-reported and
verbatim (common/utils/edge_device_utils.py) — no slug derivation, no format
validation, no uniqueness anywhere (the only slugify is for Competition titles).

- §4.2: state that the heartbeat stores client_info.namespace as-is.
- §6.6: callout that the slug rule, set-at-create, and the §6.3 constraint are
  all net-new; the slug rule lives only in the CLI + Appendix A, not the backend.
- R4: collisions are *unprevented* today, not merely possible. The code already
  proves they're possible; only the data shows whether any exist — so the
  collision check is the implementer's pre-migration step, not an RFC blocker
  (no staging access needed to finalize the RFC).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): hardening pass — supply-chain, audit, multi-env, version, uninstall

Second adversarial review (independent security + product reviewers + own pass)
against the five goals. The auth/idempotency core held; the gaps clustered in
bootstrap trust, compliance, and the operational failure tail:

- §9: bootstrap supply-chain (R8), audit trail (R9), machine-credential revoke in
  phase 1, authenticated cluster_id claims, explicit tenancy boundary.
- §14: R8 supply-chain, R9 audit, R10 multi-env config clobber (a confirmed bug —
  login --env strands the old env's ActiveClientID), R11 version negotiation, R12
  uninstall/offboarding; + watch-items (FL threat model, data-residency, emoji
  glyphs, --token re-apply).
- §8.5: silent-failure flow — --verbose, persistent install log, resume command,
  cluster doctor auth/config check, streamed rollout progress.
- §7.2: adopt keeps the existing namespace (never re-derive from hostname).
- §7.4: refuse delete on a running experiment, not just an advisory heartbeat.
- §7.5: scope the active client to env, not just account (the R10 fix).
- §11 / §13: phases + cross-repo work breakdown updated (cli / backend / installer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): add Appendix C — implementation-grade API & data contracts

Pin the cross-repo wire contracts so the RFC can be built from directly, without a
separate SDD. Shapes are grounded in the shipped CLI client (internal/api/client.go)
— the backend must match them or reconcile deliberately; [NEW] marks net-new work.

- C.1 conventions: env base URLs, Bearer auth, CLI version header (R11).
- C.2 device grant (backend#835): /device/code, /device/token error model,
  /userinfo/, /activate.
- C.3 provisioning (backend#836): /edge-device/ get-or-create with cluster_id —
  201 mint / 200 adopt / 409 cross-account; ProvisionedClient; list / admins / delete.
- C.4 heartbeat cluster_id + authenticity rule (R7 / §9).
- C.5 audit event schema (R9).  C.6 token revoke (backend#845).
- C.7 data model: cluster_id field + global-unique + the namespace constraint, with
  the strict migration order (R4 / R7).  C.8 env-scoped config v2 (R10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): lock data-verb naming (ingest, not push) + tie flows to the #877–880 trackers (#96)

Folds in two decisions made with Lukas (2026-06-23) that postdate Rev 3:

- Naming: `dataset push|list|rm` → `data ingest|list|delete`. **ingest, not push**
  (data is loaded into the client's own on-prem cluster and never leaves it; "push"
  implies egress to a remote and undermines the core trust message). **delete, not
  rm** (spelled-out, consistent with `client delete`). `dataset`/`push`/`rm` kept as
  hidden aliases for one deprecation cycle. Swapped across §3.2/§4.6/§6.2/§6.4/§7.3/§13
  + a rationale note in §6.2.
- §8: framed the drafted flows as the four acceptance families (#877–880) under the
  2-phase shape (one human gate → unattended idempotent convergence) + the 7 design
  principles.

Additive only — does NOT touch the cluster_id anchor design. Two round-2 review
residuals remain for Rev 4: the heartbeat `cluster_id` backfill names a carrier
(jobs-manager) that lacks RBAC to read the kube-system UID, and §7.2 step-2a adopts
the in-cluster credential before the cross-account check (a 409 bypass).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(rfc-0001): Rev 6 — close the two anchor residuals flagged on #96

Carrier decision: cluster_id is set/backfilled by the CLI/installer (the
kubeconfig-holder that can read the kube-system UID), NOT the heartbeat — whose
sender (jobs-manager) has no `namespaces` RBAC and can't read it.

- Residual 1 (carrier): §4.5 / §6.3 / §10 / R7 / C.4 / C.7 — the heartbeat no longer
  carries cluster_id; the CLI PATCHes it on adopt (new C.3 PATCH /edge-device/<id>/),
  with the §7.2 step-2a live-release guard covering the pre-backfill window. §9: since
  the authenticated CLI sets it from the real UID, the self-report spoofing surface
  is gone.
- Residual 2 (ordering): §7.2 — the account-scoped backend check now gates ADOPTION
  itself, so reading a live TB_CLIENT_ID off the cluster can't bypass the
  cross-account 409 (previously step-2a adopted before the check).
- §13 + Appendix C updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): retag §6.3 + C.3 cluster_id anchor to its own ticket (backend#883) (#97)

The cluster_id anchor (field + get-or-create + cross-account 409 + adopt-
backfill) was attributed to backend#836 in §6.3 and C.3, but #836/#862 ship
only namespace validation + per-action RBAC. Split the anchor out to its own
ticket so the critical-path lynchpin is tracked:

- §6.3: retag the cluster_id sub-bullet to backend#883 (split out of #836).
- C.3: heading now credits #836 (namespace + RBAC) and #883 (the [NEW]
  cluster_id items) separately, so the doc no longer self-contradicts §6.3.
- Ref-links for backend#862 (PR) and backend#883 (issue).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(rfc-0001): Rev 7 — correct the `logout` server-side-revoke overclaim (FR finding)

FR'ing the connect/install flow on dev (#877) confirmed `tracebloc logout` is
**local-only** today: it clears the local token but a copied/leaked token still
authenticates afterward. The RFC claimed "logout revokes server-side (backend#845)"
as shipped fact across §6.3, §7.5, §9, §13, and Appendix C.6 — that overstated it.

Reframed consistently: server-side revoke is **pending the `POST /auth/revoke`
endpoint (backend#887, not built) + a CLI `logout`→revoke call**; backend#845
shipped only the underlying `revoke()` primitive. Added a Rev 7 changelog note.

Evidence + tracking: backend#887 (endpoint) carries the FR evidence; the earlier
cli#55 resolution-map reply is corrected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rfc-0001): mark ACCEPTED — implemented in v0.4.0 (close out the draft)

The design shipped in CLI v0.4.0 (#107) and epic #54 is closed. Flip the
status header from DRAFT to ACCEPTED and reconcile it with what actually
landed, so the RFC can merge as the design-of-record rather than sit as a
perpetual draft. Rev history retained as the convergence record.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: lukasWuttke <54042461+LukasWodka@users.noreply.github.com>
…n't mint an orphan (#132)

On an existing-fleet box a live tracebloc client whose backend cluster_id is
still null (installed before the anchor shipped) was re-provisioned by
`client create` as a fresh mint: the freshly-read kube-system UID matched no
client, so the backend minted a new one and stamped the anchor on it, stranding
the live client. This is the RFC-0001 §7.2 / R7 case the create flow explicitly
deferred; it's the root cause behind the installer band-aid in
tracebloc/client#303 (PR #305).

The backend already ships everything (backend#883/#893): cluster_id get-or-create
and a PATCH adopt-backfill. This wires the CLI half:

- api: PatchClientClusterID → PATCH /edge-device/{id}/ {cluster_id}; 409/403
  surface as typed APIError.
- cluster: DiscoverInClusterClient(ID) reads the live CLIENT_ID from the chart's
  <release>-secrets Secret, located by the chart labels in the jobs-manager
  namespace (best-effort, time-bounded like ClusterID).
- client create: before minting, if a client is live on this cluster and the
  anchor is readable, confirm the signed-in account owns it (by UUID username),
  PATCH-backfill the anchor onto it, and adopt it — no mint. A live client not in
  the account is refused (cross-account, R6); ownership that can't be verified
  (list failed) fails closed rather than mint over it.

Fresh machines and already-anchored re-runs are unchanged.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ve client (§7.3) (#130)

* refactor(cli): extract resolveClusterTarget for the data commands (cli#127)

data ingest / list / delete each repeated the same
Load -> NewClientset -> DiscoverParentRelease (-> DiscoverSharedPVC)
sequence and its exit-code contract (3 for kubeconfig/clientset, 4 for a
missing release/PVC). Collapse it into one helper in clustertarget.go.

Behavior-preserving; `cluster doctor` is deliberately not a caller (it has
a different 2/3-escalation exit contract). This is the resolveClusterTarget
task from cli#127, landed here as the base for the cli#128 §7.3 binding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli#128): bind data commands to the active client's cluster (§7.3)

RFC-0001 §7.3: "selected" (a local pointer) was not "connected" (a
reachable cluster). `client use` set only a pointer, and the data commands
ignored it — they targeted whatever the current kubeconfig context pointed
at, risking a silent wrong-target on a multi-cluster / remote host.

- Cache the active client's namespace + display name in the env profile at
  `client use` / `create` time (new setActiveClient), so the data commands
  can bind without a backend round-trip (they run cluster-local, maybe
  offline).
- data ingest/list/delete default -n to the active client's namespace when
  the user gave neither --namespace nor --context (bindActiveClientNamespace).
  No active client → unchanged current-context behavior (backward compatible).
- A "no release in namespace" miss on a bound namespace is rewritten to the
  §7.3 "active client runs on another machine" guidance (typed
  noParentReleaseError), while a PVC-missing failure passes through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli#128): show selected-vs-connected state in `client list` (§7.3)

`client list` marked only the active (selected) client and showed no
connection state, so a stale pointer to a client whose cluster is gone was
invisible. Add a state column (online/offline/pending) sourced from the
backend EdgeDevice.status code — the "connected somewhere" signal per
RFC-0001 C.4 — alongside the existing "(active — this machine)" marker, and
a hint separating selected (local pointer) from connected (backend
heartbeat).

Plain words, not flag/emoji glyphs, to avoid the CI/Windows-console
mojibake called out as a §12 watch-item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli#128): only rewrite a genuine no-release miss as "runs elsewhere" (Bugbot)

The §7.3 binding wrapped every DiscoverParentRelease failure in
noParentReleaseError, so `explain` rewrote API/RBAC list errors and
ambiguous multiple-release matches as "the active client runs on another
machine" — hiding the real diagnostic.

Add a cluster.ErrNoParentRelease sentinel on the true not-found case and
gate the wrapping (and thus the rewrite) on errors.Is; other discovery
failures keep their own message and exit 4. Tests assert the sentinel
matches not-found and not the multiple-release case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ings (#135)

Several user-facing printed and help strings still named the old
`dataset push` command, which was renamed to `data ingest` (old name
kept only as a deprecated alias). Update the strings a customer actually
sees to the canonical verbs:

- cluster info/doctor success line: "Ready for `tracebloc data ingest`."
- `cluster` Long help: next `data ingest` will target …
- `cluster info` Long help: jobs-manager Service the next data ingest
- `ingest validate` Long help: pre-flight before `tracebloc data ingest`

Left internal code comments (developer-facing, not printed) and the
alias-resolution tests untouched. `data_list.go`'s "emit the dataset
list as JSON" is descriptive ("the list of datasets"), not a command
reference, so it stays as-is.

Separate from the install-time messaging fixed in #134.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ut (#134)

The self-installer's "First steps" pointed at the old `dataset push` name
(kept only as a deprecated alias). Point it at the canonical `data ingest`
on both the shell and PowerShell installers, and fix a grammar nit.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): location auto-detect for client create (#84)

The deferred fast-follow from cli#92: pre-fill `client create`'s location
prompt with a detected electricityMaps zone (backend ZONE_CHOICES), so a
cloud-hosted client doesn't have to look up its own zone.

- internal/geo: Detect() probes cloud instance metadata first (AWS IMDSv2/v1,
  GCP, Azure — concurrently under one short deadline, first wins → high
  confidence), then Cloudflare IP geolocation (low confidence, flagged). Returns
  an ISO country code, always a valid top-level zone; cloud regions map via a
  curated AWS/GCP/Azure table, and an unmapped region falls through to GeoIP for
  a valid zone rather than suggest something the backend would reject.
- client create: the detected zone pre-fills the prompt default (the user
  confirms with Enter or overrides) — still never silent, never empty. A
  detectZone seam keeps the command tests hermetic.
- Best-effort: offline / egress-restricted / bare metal → empty default (the
  prior behavior). Only runs interactively when --location is omitted.

Tests: geo per-provider + GeoIP fallback + unmapped-region fallthrough +
nothing-detected + region→country table; cli accepts-detected-zone end-to-end.
go build/vet/test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli#84): snapshot geo endpoint bases to kill a -race data race

probeCloud returns on the first probe to report a region and leaves the
losing goroutines running to their deadline. They read the package-level
endpoint vars (awsIMDSBase/gcpMetaBase/azureIMDSBase) directly, so a
test's t.Cleanup — which restores those vars — races the still-running
goroutines under `go test -race` (TestDetect_* flaked in CI).

Snapshot the bases into locals synchronously, before spawning the
goroutines, and pass each into its detectX; the probes now touch only
captured locals, never the mutable globals. First-wins latency is
unchanged. `go test -race -count=30 ./internal/geo/...` clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Asad Iqbal <asad.dsoft@gmail.com>
…uch surface, ship a `tb` alias (#142)

* feat(cli): auto-discover the client's namespace + first-touch surface rewrite

Fixes the out-of-box dead end: on a standard install the client lives in
its slug namespace, but data/cluster commands searched only the
kubeconfig default ("default") and errored — telling customers to run
helm install, the exact thing the CLI promises they never do.

Namespace discovery (§7.3):
- New cluster.FindClientNamespaces: cluster-wide scan for jobs-manager
  Deployments (same selector as single-namespace discovery).
- discoverRelease fallback in the shared resolve seam: a miss in the
  DEFAULT namespace scans; exactly one client → visible-note retarget
  (never silent), several → named + pick via --namespace/client use,
  none/RBAC-denied → the original error stands.
- §7.5 guard: the scan NEVER engages for an explicit --namespace/
  --context or an active-client binding miss (which keeps the §7.3
  "runs elsewhere" message) — pinned by activeClientBinding.allowScan()
  + table test. cluster info now binds the active client exactly like
  the data commands, so the pre-flight targets what data ingest will.

Surface rewrite:
- Root: tagline/Long/home screen rewritten around the real scope (two
  contexts: your account / this machine's client); login leads Get
  started; protocol jargon + internal issue links removed.
- ingest/data-ingest collision: validate moved to `data validate`;
  top-level `ingest` is a hidden deprecated alias, and a bare path
  there redirects: did you mean `tracebloc data ingest <path>`.
- No-client error copy: --namespace / installer one-liner (https) /
  cluster doctor — never helm. Sentinel renamed to "no tracebloc
  client found"; doctor no longer tells you to run doctor.
- Noun sweep: parent (client) release → client; vendors → other
  collaborators; PVC jargon out of Shorts/flag help; README command
  names updated.

Verified live on a real cluster (client in a non-default namespace):
data list / cluster info auto-find it; explicit -n default gets the
actionable error. go build/vet/test green; new tests cover the scan
(single/multi/none/explicit), the alias moves, and the allowScan
contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(installer): ship a `tb` short alias next to the binary

The kubectl→k pattern for the most-typed word in the product: the shell
installer symlinks $PREFIX/tb → tracebloc (skipped if an unrelated tb
already sits there); the PowerShell installer drops a tb.cmd shim. Both
announce the alias in the install output + README.

Requested by Lukas (2026-07-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(installer): tb alias is best-effort; harness covers it

The verification harness runs install.sh under a restricted PATH that
didn't include ln → rc=127 on the happy path. Two-sided fix: the alias
block now degrades silently when ln is unavailable or fails (an alias
must never fail an install), and the harness whitelists ln+readlink and
asserts the tb symlink exists and points at the binary on the happy
path (12 checks now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): cluster info shows the retargeted namespace; tb alias best-effort on Windows

Two review follow-ups on this PR:

- cluster info printed the "namespace" field before the cluster-wide
  fallback scan retargets resolved.Namespace, so when the scan finds the
  client in its slug namespace the pre-flight reported "default" while the
  Client-install section and ingestor-SA path below showed the real
  namespace. Print the field after the retarget — the command's whole job
  is to report what the next `data ingest` will target.

- install.ps1's tb alias wasn't actually best-effort: under
  $ErrorActionPreference='Stop' a Set-Content failure aborted the whole
  install (the third commit only hardened the Unix side). Wrap it in
  try/catch so a write failure just skips the alias. Also tighten the
  "is this tb.cmd ours?" guard from a loose "*tracebloc.exe*" substring
  (which would clobber any unrelated tb.cmd merely mentioning the name)
  to an exact match on our quoted binary path — matching install.sh's
  strict readlink-target equality.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Asad Iqbal <asad.dsoft@gmail.com>
Comment thread internal/cli/client.go
…ional (#137) (#144)

* feat(cli): auto-name clients <firstname>-NN and make --location optional (#137)

Zero flags, zero prompts on the installer's `client create` path (installer
UX v2 spec):

- Auto-name: when neither --name nor TRACEBLOC_CLIENT_NAME is given, derive
  <slug(first_name)>-NN from the signed-in identity (fallback: email
  local-part, then "client"). NN is the next free two-digit number across the
  account's existing client names AND namespaces, so a second machine lands on
  lukas-02 rather than a slug -2 bump, and the name passes through slug.Derive
  unchanged (name = namespace by construction). No name prompt, ever.
  first_name is parsed from /userinfo/ into api.Identity and persisted in the
  profile at login.
- Optional location: no required-flag error, no prompt. CreateClientRequest
  .Location is now json:"location,omitempty", so with no --location the CLI
  sends nothing and the backend records the client with no location
  (backend#993) instead of a silent default. --name/--location still honored
  verbatim; internal/geo stays in the tree for opt-in enrichment but no longer
  runs on the silent path (the detectZone seam and the now-dead
  errMissingFlag/validateNonEmpty helpers are removed).
- Noun pass: the adopt message now says "This machine is already registered
  as client ..." (machine = hardware, client = tracebloc entity).
- Docs: RFC-0001 Rev 8 amendment — §6.6/§6.7/§7.7 + the D1 row note the
  <firstname>-NN and optional-location deviation from the hostname-derived
  design (decided by Lukas 2026-07-06), retaining the original as
  design-of-record.

Tests: auto-name (no-location, numbering, email fallback), flags-honored,
reworked interactive/cancel and resume-command tests to the zero-prompt flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): address review — restore silent-path safety rails (#137)

From Lukas's deep review on PR #144. The zero-prompt design had removed
several guards; putting them back:

- Non-interactive mint now fails closed: a bare `client create` with no TTY,
  no --yes, and no --credential-file would have minted silently and printed
  the machine credential to stdout. Require --yes (consent) or
  --credential-file (keeps the secret off stdout); the installer passes both.
- Auto-naming fails closed on a client-list error instead of numbering against
  an empty list and minting a deterministic `<base>-01` duplicate (name AND
  namespace collide, no server-side uniqueness). An explicit --name still
  tolerates a list blip.
- login clears the profile's email/first_name before the best-effort WhoAmI,
  so a re-login as a different user on a shared box can't inherit the previous
  user's identity (and auto-name the new client after them) when WhoAmI fails.
- Auto-name caps <base>-NN at the 63-char DNS label so a long first_name keeps
  name == namespace instead of reintroducing the slug -2 bump.
- --help/Short no longer claim location is "prompted"; wire
  TRACEBLOC_CLIENT_NAME / TRACEBLOC_CLIENT_LOCATION as real flag defaults so
  the documented env-var override is true for a direct CLI call, not just via
  the installer.
- Revert the adopt message to "This cluster is already registered…": adopt is
  keyed on the cluster anchor and can run against a remote cluster, and the
  sibling resume/hint lines already say "cluster" (consistency + accuracy).

Test hygiene: drop the now-dead prompter answer maps; guard signInAs against
writing to a real ~/.tracebloc. New regression tests for each of the above.

Kept internal/geo (review #7): cli#137 explicitly parks it for opt-in
--location enrichment, and PR #95 (location validation) builds on it — deleting
now would churn a package that lands a caller shortly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(cli): remove internal/geo — location auto-detect is gone, not parked (#137, review #7)

Per Lukas's review #7: the zero-prompt location decision removed detectZone,
geo's only caller, so the package (geo.go + regions.go + tests) was dead. Rather
than park it "for later" behind a comment, delete it — git remembers, and the
product direction is that location is optional and validated by the backend (a
bad --location surfaces as its real create error), not auto-detected or
CLI-validated. Updates the RFC Rev 8 callouts + the create comment to match.

Supersedes PR #95 (CLI-side --location zone validation), which is closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): address Bugbot — 426 upgrade signal + slug-aware auto-name collisions (#137)

- Auto-name list-failure path: a 426 (UpgradeRequiredError) now surfaces the
  upgrade message verbatim instead of the "couldn't reach the backend — retry"
  framing, which is wrong for a too-old CLI (retrying never helps). Non-426
  list errors keep the retry/pass-`--name` guidance. (Bugbot #144-A)
- autoClientName reserves each existing client's handle in BOTH raw and
  slugified form, so a legacy client stored as "Lukas 01" (blank namespace)
  still blocks the derived handle "lukas-01". (Bugbot #144-B)

Regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): consent guard must not block idempotent adopt (#137, Bugbot follow-up)

The non-interactive consent guard sat before CreateClient, which can return
HTTP 200 and adopt an already-anchored client without printing any credential.
Skip the guard when this cluster is already anchored to a client in the account
(willAdopt) — a non-interactive re-run then adopts as before instead of erroring
"refusing to provision". A genuine fresh mint still requires --yes or
--credential-file. Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): re-run reuses the anchored client's name, not a fresh handle (#137, Bugbot follow-up)

On a re-run without --name against an already-anchored cluster, auto-naming
picked the next free handle (lukas-02) even though the backend adopts the
anchored client by cluster_id — so the review/confirm and POST body described a
client that was never created. Reuse the anchored client's existing name in that
case (new anchoredClient helper); fresh clusters still auto-name as before.
Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): honest message when the consent guard fires on a list failure (#137, Bugbot follow-up)

When the non-interactive consent guard trips because ListClients failed
(willAdopt unknown), the error now names the real cause — the client list
couldn't be read, so we can't tell a fresh mint from an idempotent adopt — and
notes that a retry once the backend is reachable adopts an existing client
without any flag. Still fail-closed (refusing beats an unconsented credential
print), just no longer implying --yes is the only path. Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread internal/cli/client.go
LukasWodka and others added 2 commits July 7, 2026 15:26
…n handling (#149)

* feat(data ingest): destination-table guard (--overwrite) + honest extension handling (#145)

Two fixes for the same failure class — the customer uploads their whole
dataset and only then learns it was doomed:

cli#70 (P4-lite) — table-exists guard:
- One cheap read (the data list query) after cluster discovery, BEFORE
  staging. Existing table without --overwrite → exit 6 (new, documented)
  with the full remedy; --overwrite replaces it via the exact teardown
  data delete uses. The check fails OPEN with a visible note (the
  in-cluster duplicate check still backstops).
- Teardown acts on the MATCHED name, not the flag's casing (Linux MySQL
  + PVC paths are case-sensitive; acting on the flag spelling could
  silently no-op the DROP/rm and claim success).
- --overwrite + --idempotency-key is refused outright: a replayed
  submit attaches to the PREVIOUS run after the teardown deleted the
  data — false success + data loss. (Adversarial-review catch.)
- Honest partial-failure copy: a half-finished replace names
  `data delete` as the primary recovery — a plain re-run would pass
  the DB-backed guard and hit the leftover files after a full upload.
- The teardown pod honors --stage-pod-image (air-gapped registries).

cli#68 — extension detection/emission:
- .webp removed from the accept-set: the ingestor's FileExtension enum
  + the ingest.v1 schema allow only .jpg/.jpeg/.png for images, and
  FileTypeValidator RAISES on webp — accepting it locally guaranteed an
  in-cluster failure after the full upload. (The old comment claiming
  chart support was itself the cli#68 drift.)
- The single shared extension is detected, shown in the summary
  ("3 files (.png)"), and emitted as spec.file_options.extension so
  the cluster validates the type that was actually staged — previously
  it checked its .jpeg convention default and rejected .jpg/.png
  datasets after upload.
- Mixed types fail locally with counts (exit 3); an all-unsupported
  dataset names what was found vs accepted.

Cross-repo traced against data-ingestors (conventions merge, per-
category validator factories, DuplicateValidator) and live-verified on
a real cluster: PNG detection, guard on an existing table (exit 6),
--overwrite dry-run creates nothing, mixed extensions refused, combo
flag refusal. go build/vet/test green; new tests cover the guard seam
(matched-name contract, fail-open), extension detection, spec emission
+ schema validation (keypoint top-level fields pinned), and the summary
rendering.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(data ingest): preflight parity — the dry-run's promise becomes a checked contract (#150)

* feat(data ingest): preflight parity — the dry-run's promise becomes a checked contract

backend#828 P3; closes cli#69, cli#71, cli#72, cli#73.

Every local check now previews a NAMED data-ingestors validator with
matching semantics, so 'preflight passed' means the in-cluster
validation passes too — failures land BEFORE the upload, not after.

New previews (internal/push/preflight.go, each cites its source rule):
- label column exists (LabelColumnValidator: exact, then
  case-insensitive+trimmed — never stricter than the cluster) (#69)
- BOM: tabular rejected pre-upload (the in-cluster stdlib schema probe
  falsely rejects BOM'd CSVs — data-ingestors#338); image/text BOM
  accepted+stripped, matching the pandas paths (#71)
- every image decoded (header-only, cheap): zero-byte, corrupt,
  resolution vs target — plus the labels↔images cross-check with the
  ingestor's _has_extension naming semantics (dotted stems!) (#72)
- duplicate headers (stripped, case-SENSITIVE like the probe), zero
  data rows, --schema columns ⊆ header, CSV encoding gate
  (check_csv_encoding preview: UTF-8 + no NUL) (#73 + gaps found)
- label diversity (LabelDiversityValidator: >=2 classes; NA-sentinel
  drop + numeric collapse for schema-typed tabular labels; empty
  string IS a class for image/text) — discovered BY the harness's
  first run, was in no ticket
- object_detection images↔annotations stem pairing
  (FilePairingValidator preview)

FIXES A PRE-EXISTING SHIP-BLOCKER found by the adversarial pass:
spec.go swapped target_size to [H,W] on emit (mistaken review note) —
but the schema + ImageResolutionValidator compare PIL's (W,H) verbatim,
so EVERY non-square dataset failed in-cluster post-upload. Emission is
now [W,H]; the parity pair imgc-nonsquare / imgc-nonsquare-swapped pins
the orientation end-to-end against the real validator.

THE PARITY HARNESS (the durable part):
- internal/push/testdata/parity: 23 fixture cases + goldens.json
  GENERATED from the real Python validators
  (scripts/gen-validator-goldens.py; scripts/sync-validator-goldens.sh
  --check for drift, verdict-level)
- parity_golden_test.go asserts the PRODUCTION dispatch
  (push.PreflightDataset — shared by runDataIngest and the test, so
  the two cannot drift) reaches the manifest's verdict per case, and
  that committed goldens match the manifest — an ingestor rule change
  fails the test until consciously reconciled
- deliberate divergences (read-/transfer-time failures the ingestor's
  preflight can't see but the CLI previews) are explicit manifest
  notes, never silent

Verified: 23/23 parity green; full go test green; live dry-runs on a
real cluster (missing file, bad label column, single-class, latin-1,
non-square 320x200 accept). Adversarial review (2 lenses, high
effort): all findings folded incl. the [W,H] bug, value-semantics
divergences in diversity/cross-check, the encoding gate, de-masked
fixtures, the shared dispatch, and a vacuous kubeconfig test revived
with decodable fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(preflight): label diversity respects the label's SQL type (#152)

CheckLabelDiversity collapsed numeric-looking labels ("1"/"1.0") for
every tabular_classification dataset. The in-cluster LabelDiversityValidator
pins dtype=str for string-family schema types (VARCHAR/CHAR/TEXT/STRING),
so those labels stay distinct — only numeric types get pandas numeric
inference (data-ingestors #252). A user-declared VARCHAR label with
numeric-looking classes was wrongly rejected at preflight.

Derive the drop-NA and collapse-numeric flags from the label's declared
schema type at the dispatch site; keep image/text (untyped) unchanged.
Adds leaf + dispatch tests. This aligns the Go side with the golden
generator, which already types columns as VARCHAR.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>

* fix(preflight): check deferred file Close (errcheck)

preflight.go used bare `defer f.Close()`, which the required Lint job's
`errcheck ./...` rejects on develop. Match the repo convention used in
detect.go / tabular.go / stream.go: `defer func() { _ = f.Close() }()`.
These are read-only opens for validation, so dropping the close error is
intentional. Slipped through earlier because the stacked merges never hit
the full Lint-on-develop gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Asad Iqbal (Saadi) <asad.dsoft@gmail.com>
…tus --wait` (#138) (#146)

* feat(cli): login progress spinner, auth status --check, client status --wait (#138)

Three installer-facing contract changes so the approved installer copy is
renderable and honest (installer UX v2 spec):

- login progress: extract the RFC 8628 poll loop into pollForToken, wrapped in
  a live ui.Spinner (braille frame + mm:ss elapsed + "(Ctrl-C to cancel)").
  TTY-gated — piped/--plain/NO_COLOR output prints one static line, no \r/ANSI.
  Sign-in fields relabelled open:/code: → Open/Enter (imperative ui.Action
  rows), and "Token saved…" demoted to a dim verbose-only detail so the happy
  path is just "✔ Signed in as …".
- auth status --check: a machine-readable session probe. Exit 0 only when a
  token is present AND the backend accepts it (live WhoAmI); exit 1 (silent,
  nil-inner exitError) when signed out or the token is rejected. --verbose
  narrates the verdict. Installer's session-probe step stops grepping prose.
- client status [--wait] [--timeout]: reports the backend's view of this
  machine's active client (online/offline/pending). --wait polls the same
  source `client list` renders until online (exit 0) or timeout (non-zero, with
  a plain-language last-state line) — the honest 🟢 Online signal (§8.5),
  replacing the local-rollout-only inference.

New ui: Spinner (animate-vs-static contract) + Action rows. Poll/interval go
through the existing pollAfter seam + a new clientStatusPollInterval var so
tests drive many iterations with no real delay. Tests cover all three
(including a race-free direct draw test and the timeout/last-state path); full
suite green under -race, errcheck/goimports/gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): address Bugbot — 426 fails fast with an upgrade signal (#138)

Two spots swallowed HTTP 426 (UpgradeRequiredError, "CLI too old") as if it were
an ordinary failure; both now surface the upgrade instruction:

- `auth status --check`: a 426 from WhoAmI is surfaced non-silently (prints even
  without --verbose) instead of exiting 1 like a rejected token with "re-login"
  advice, which wouldn't help. (Bugbot #146-C)
- `client status --wait`: a 426 from the poll fails fast with the upgrade signal
  instead of retrying until timeout and reporting a generic "unreachable" wait
  timeout. (Bugbot #146-D)

Regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): client status --wait — fail fast on missing client, honest timeout (#138, Bugbot follow-up)

- --wait now fails fast when the active client isn't in the account (deleted /
  wrong account), matching the one-shot path, instead of polling to the timeout.
  (Bugbot #146-E)
- The timeout message surfaces the last real list error when every check failed,
  instead of a bare "(last state: unreachable)" that hid persistent API/network
  failures behind the missing-client wording. (Bugbot #146-F)

Loop restructured into a single switch (426 fail-fast / transient-remember /
missing / online); regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): address Lukas review — env-aware --check + terminal-state --wait + polish (#138)

- auth status --check now resolves its target env like login (--env, then
  $CLIENT_ENV, then prod) and requires it to match the signed-in CurrentEnv;
  otherwise it exits 1 so the installer runs the login that switches env. Fixes
  the cross-env hole where a stale prod session made --check pass while the
  installer targeted dev, provisioning into prod. Added an --env flag. The probe
  now reuses authedClient() instead of re-implementing it. (findings 1, 7)
- client status --wait treats 401/403 (revoked/expired token) as terminal —
  fail fast pointing at sign-in rather than polling to timeout. (finding 2;
  426 + missing-client were already terminal from the Bugbot round)
- The animated spinner falls back to the static line on Windows: its raw
  \r\033[K + SGR redraw bypasses fatih/color's Windows VT-enable path and would
  render as escape garbage on legacy consoles. (finding 3)
- Reject --timeout without --wait instead of silently ignoring it. (finding 4)
- client status --help/Long no longer leaks "(RFC-0001 §8.5)" or
  installer-implementation narration. (finding 5)
- Failure copy points at tracebloc verbs (`client list`, `cluster doctor`,
  re-run installer) instead of speculation / implicit kubectl. (finding 6)
- findClientByID helper replaces the hand-rolled active-client match in the two
  find-loops; clientStatusPollInterval is now a const (tests inject via
  pollAfter). (finding 8)

Deferred finding 4a (adding --check to client status) — the probe already exists
via --wait --timeout; not expanding the surface without a call. Regression tests
for the env mismatch, 401 fail-fast, and --timeout guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): clear stale transient error after a successful --wait poll (#138, Bugbot)

In client status --wait, a transient ListClients failure set lastErr, but a later
successful poll showing offline/pending never cleared it — so a timeout reported
"the last status check failed: <old error>" instead of the real last state. Add a
default switch case (successful poll, not yet online) that resets lastErr, so the
timeout message reflects what actually happened last. Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): holistic hardening of client status --wait + auth check (#138)

Comprehensive pass (independent review + Bugbot) to stop the one-at-a-time drip,
redesigning the --wait loop to be correct-by-construction:

- --timeout is now a real ceiling: the sleep is clamped to the remaining budget
  and the timeout is decided post-poll, so total runtime can't overshoot by a
  whole poll cycle. A poll begun within budget that returns online is still
  honored. (Bugbot "wait ignores elapsed timeout")
- defer sp.Stop() as a leak-proof net so no return path can leak the spinner
  goroutine; the online path still Stops explicitly before printing its ✔.
- Ctrl-C during --wait (and login's poll) exits quietly via a silent exit 130
  instead of printing "Error: context canceled".
- auth status --check --verbose distinguishes a rejected token (401/403 → run
  login) from an unreachable/erroring backend ("couldn't verify your session"),
  so an outage isn't mislabelled a bad credential.
- Timeout message uses a hoisted lastState so the last real state (offline/
  pending) survives the restructure; stale-error clearing preserved.

Deliberately kept the fail-fast set at 401/403 (+426): 429 and 5xx are retryable,
so "all 4xx terminal" would regress rate-limit handling. Regression tests for the
silent Ctrl-C and the verbose unreachable-vs-rejected copy; full suite green under
-race, errcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread internal/cli/client.go
…] emit (#153)

The TargetSize field NOTE still described the old #25/[H,W] behavior
("EMITTED as [height, width] ... buildImage does the swap"), which #147
(landed via #149) deliberately reverted. buildImage now emits
[TargetSize[0], TargetSize[1]] = [width, height] with no swap — the order
ingest.v1.json documents ("matches PIL.Image.size and what
ImageResolutionValidator expects") and the order image_validator.py
compares against verbatim.

Rewrite the NOTE to state stored-and-emitted [width, height], no swap,
and warn against re-introducing the swap (which failed every non-square
dataset). Comment-only; rolls up under #145/#147.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit efa84f4. Configure here.

Comment thread internal/cli/client.go
…ails (#158) (#159)

The RFC-0001 §7.2 adopt-backfill ran only inside `if clusterID != ""`, so a
kube-system UID read that failed for a reason OTHER than unreachability (RBAC on
namespaces, a transient API error) skipped live-client detection entirely and
minted a duplicate — orphaning the live install. Worst under --yes.

adoptLiveInClusterClient already detects the live client (readInClusterClient)
independently of the UID; only the anchor backfill needs it. So run adopt
unconditionally and, when clusterID == "" but a live owned client is found, adopt
it as-is (no backfill Patch, no anchor-mismatch check) and warn that the
idempotency anchor was left unstamped. A genuinely-unreachable cluster fails
detection too and still falls through to a non-anchored mint — unchanged.

Adds a regression test (UID read fails + live owned client ⇒ pure adopt, no mint,
no PATCH) and stubs readInClusterClient in the cancel test that bypasses
withClientBackend (adopt now always calls it).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbal saadqbal merged commit 9de9e0a into main Jul 7, 2026
14 checks passed
Sign up for free to 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