Skip to content

release: promote develop → main (v0.4.0 — auth + client provisioning, cluster doctor) (#106) - #107

Merged
saadqbal merged 15 commits into
mainfrom
develop
Jun 24, 2026
Merged

release: promote develop → main (v0.4.0 — auth + client provisioning, cluster doctor) (#106)#107
saadqbal merged 15 commits into
mainfrom
develop

Conversation

@saadqbal

@saadqbalsaadqbal commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Promotes developmain to cut v0.4.0. Tracking: #106.

develop is a strict superset of main (main-ahead commits are prior promotion merges only); CI on develop HEAD (722d2cf) is green.

What ships (12 commits since v0.3.1)

Interim state (additive / opt-in — not blockers)

client create still prints the credential by default; --credential-file is the building block for the installer reorder (#838, not yet built). Location auto-detect (#93) + RFC (#55) still draft.

After merge

Tag v0.4.0 on mainrelease.yml builds multiarch + cosign-signs + publishes.

🤖 Generated with Claude Code


Note

Medium Risk
Large feature surface (auth tokens, credential files, backend provisioning) plus behavioral changes to dataset rm teardown; well covered by tests but touches security-sensitive paths and in-cluster destructive ops.

Overview
v0.4.0 is a develop→main promotion that ships RFC-0001-style browser auth and machine provisioning, a cluster health command, and several correctness fixes around datasets and CI.

Auth & clients: New login / logout / auth status use OAuth device flow (RFC 8628) against the public backend API, persisting tokens in ~/.tracebloc (0600). client create, list, and use provision edge clients with optional cluster-id anchoring (kube-system UID) for idempotent re-runs, --credential-file for installer integration, and clearer handling of 403/409. Supporting packages add internal/api, internal/config, internal/slug (Python parity), and cluster.ClusterID.

Cluster doctor:tracebloc cluster doctor runs read-only checks (release discovery, pods, PVC, proxy wiring, backend egress from the CLI host, requests-proxy, node fit, image pull secrets) with ✔/⚠/✖ output and remedies.

Dataset / push: Task categories move to a single CategorySpec registry synced with ingest.v1.json (new categories, target_size width×height docs). dataset push gates on registry support with accurate messages for known-but-unsupported categories. dataset rm teardown deletes PVC files via a short-lived stage-identity pod (uid/fsGroup 65532) instead of exec into jobs-manager, fixing permission failures on staged files (#259).

CI: Workflows gain concurrency (cancel in-flight PR runs) and job timeouts; a public PII gate caller workflow is added.

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

LukasWodkaand others added 12 commits June 17, 2026 18:34
* ci(security): add public-repo PII gate caller
Blocks PRs that leak customer/partner names or secrets in title/body/commits.
Calls the reusable gate in tracebloc/.github. Inactive until the org
PII_DENYLIST secret is set (warns, doesn't block, until then).
* chore(schema): sync ingest.v1.json from data-ingestors master
The vendored copy at internal/schema/ingest.v1.json had drifted from
upstream, failing the `scripts/sync-schema.sh --check` CI gate on every
PR. Upstream replaced `instance_segmentation` with `token_classification`
across the category enums, updated the texts/resolution descriptions
([width, height] order + PIL note), and added the token_classification
`texts` and masked_language_modeling no-`label` conditional rules.
Regenerated via `scripts/sync-schema.sh`; `--check` is now clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: shujaat hasan <shujaathasan@shujaats-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a per-ref concurrency group (cancels superseded PR runs only;
push/tag/schedule never cancelled) and timeout-minutes to every job, so
stale PR pushes stop wasting runner time and hung steps (kind boot, cosign)
can't run to the 6h default. No change to job behavior.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
chore(schema): re-sync vendored ingest.v1.json from data-ingestors master
…manager (#259) (#78)
`tracebloc dataset rm` dropped the table but failed to delete the dataset's
staging files on the shared PVC:
rm: cannot remove '/data/shared/.tracebloc-staging/<t>/labels.csv': Permission denied
Root cause: the staging files are written by the CLI's ephemeral stage pod as
uid 65532 (+ fsGroup 65532), but the teardown exec'd `rm` inside the long-lived
jobs-manager pod, which runs as a different non-root uid with no shared fsGroup.
A non-65532 uid cannot delete 65532-owned files in a non-group-writable dir, so
the rm hit EACCES and left orphans. The "re-run to clean up" advice was a dead
end — the same permission error every time.
Fix: run the teardown `rm` from a short-lived pod that mirrors the stage pod's
identity (uid 65532 + fsGroup 65532, shared PVC mounted), reusing the existing
BuildStagePodSpec / CreateStagePod / WaitForStagePodReady / DeleteStagePod
machinery. That pod OWNS the staging files it deletes, so it works by ownership
on hostPath (where fsGroup is a no-op, kubernetes/kubernetes#138411) and CSI
alike. Fully fixes tabular datasets (no sidecar files) on every volume type.
Also:
- Teardown now takes an injectable Executor (matching push.Stage), enabling a
regression test that pins "rm runs in a uid-65532 stage pod, not jobs-manager".
- dataset_rm: drop the misleading "re-run completes the cleanup" claim; the table
DROP is idempotent, and if file removal keeps failing, point to node-side cleanup.
Refs #259. The image/sidecar case (ingestor's /data/shared/<table> written as
uid 65534) on hostPath still needs the documented complement (ingestor fsGroup +
group-writable DEST_PATH in client-runtime/data-ingestors).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Asad Iqbal <asad.dsoft@gmail.com>
#89)
* feat(#88): tracebloc cluster doctor — live-cluster health checks (WS3)
Adds `tracebloc cluster doctor`, a read-only health sweep of a running
tracebloc client cluster that prints ✔/⚠/✖ per check with a remedy — so a
customer can diagnose "why isn't my experiment running?" without tracebloc
shelling into their cluster (epic client-runtime#116, WS3).
Sibling of `cluster info` (which the code's own comment anticipated); reuses
its kubeconfig/context/namespace flags + cluster.Load / NewClientset /
DiscoverParentRelease, the ui.Printer status vocabulary, and exitError.
Lean MVP — 6 checks:
- cluster reachable (parent client release discovered)
- pod health (crash-loops / long-Pending — local complement to #117)
- dataset volume (shared PVC Bound, via cluster.DiscoverSharedPVC)
- proxy configuration (in-cluster requests/egress proxy wiring)
- backend egress (host-side, proxy-aware probe; in-cluster probe = follow-up)
- Service Bus egress (requests-proxy readiness — the experiments-queue broker)
internal/doctor is a standalone package with injectable network probes,
82% covered via client-go's fake clientset. Every check is independent and
best-effort (one failure never hides the others); the worst status sets the
exit code (0 ok/warn, 2 failures, 3 kubeconfig).
Out of scope (already shipped / follow-up): support-bundle ships as the
installer's `--diagnose`; node-resources-vs-job-request and image-pullability
are the broader cut.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#88): don't flag Succeeded/recovered pods as crash-looping (Bugbot)
podCrashLooping flagged any pod with RestartCount>=3 — including Succeeded
job pods that retried before completing, and Running pods that recovered
after past restarts — producing a false ✖ when nothing is actually unhealthy.
Guard terminal phases (Succeeded/Failed) and require the container to not be
currently running, mirroring the controller's recovered-container fix
(client-runtime#117). Adds regression tests for both false-positive cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#88): detect init-container crash-loops + nil-release prefixed deploys (Bugbot)
Two Medium Bugbot findings on the previous commit:
- podCrashLooping ignored InitContainerStatuses, so an init container stuck in
CrashLoopBackOff read as a Pending warning instead of a failure even though
the pod cannot start. It now checks init + app containers, and detects only
active CrashLoopBackOff — dropping the RestartCount heuristic entirely, since
that was the source of the earlier Succeeded/recovered-pod false positives.
- requestsProxyNames/jobsManagerNames only probed unprefixed names when the
release was nil (e.g. DiscoverParentRelease errored on multiple releases),
falsely reporting missing wiring even though <release>-requests-proxy exists.
Added findDeployment: exact-name Get, then a namespace List + name-suffix
fallback that resolves the prefixed name without knowing the release.
Adds regression tests: init-crash-loop, nil-release-finds-prefixed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs/polish(#88): address Arturo's post-approval review nits
- Tunables comment: they're conservative package consts, not vars; point at
Options (like HTTPProbe) for any future runtime tuning.
- checkProxy WARN: note that a REQUESTS_PROXY_URL set via a configMap/secret
ref reads as empty here (jobsManagerEnv reads only literal env).
- httpProbe: a successful connection means reachable — discard the body-close
error rather than reporting it as "unreachable".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#88): suffix fallback must not pick across multiple releases (Bugbot)
findDeployment's suffix fallback (added for the nil-release case) picked the
first suffix-matching deployment, so in a namespace running multiple parent
releases, jobsManagerEnv and checkRequestsProxy could resolve to different
releases in a single run — presenting mixed data as fact.
Resolve the fallback only when exactly one deployment carries the suffix;
with more than one (the multi-release case DiscoverParentRelease already
refuses to disambiguate) return nil and let the check report can't-determine.
The single-release nil-discovery case still resolves.
Adds TestCheckRequestsProxy_NilReleaseAmbiguous.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#88): tie deployment lookup to the discovered release (Bugbot)
When a release was discovered, findDeployment's fallbacks could still match a
DIFFERENT release's component (or a stray bare one), so the Service Bus check
went green on the wrong requests-proxy while the discovered release's was
missing.
findDeployment now takes the release directly. When it's known, it accepts only
"<release>-<suffix>" or a bare "<suffix>" whose app.kubernetes.io/instance label
ties it to that release — never another release's, never an unattributable bare
one. The release-unknown path keeps the exactly-one-suffix-match rule (returns
nil on >1, so checks report can't-determine rather than guess).
Folds the jobsManagerNames/requestsProxyNames candidate builders into
findDeployment. Adds tests: other-release-ignored, bare-name-tied-by-label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…patch (#74) (#80)
The CLI enumerated task categories in four hand-maintained places that had
drifted: the `--category` help listed 5 of 9 (#74), the push accept-gate
hand-listed the supported set twice, the interactive picker kept its own
list, and internal/push/category.go held four separate family maps.
Consolidate into one CategorySpec registry (internal/push/category.go):
each category's family, label, regression-class flag, and CLI-support
status lives in one ordered table. The family predicates
(IsImage/IsTabular/IsText/IsRegressionClass), the `--category` help, the
gate's "Supported:" lists, and the interactive picker now all derive from
it, so the enumerations can't drift apart again. The help now lists all 9
supported categories.
Behaviour-preserving: the gate accepts/rejects exactly the same set
(IsCLISupported == the prior nine-category condition); only the help text
and the now-registry-derived error messages change. semantic_/
instance_segmentation stay known-but-unsupported, each with a per-category
UnsupportedNote.
Adds a registry parity + predicate-derivation test (the anti-drift guard).
First phase of the CLI ingestion consolidation epic (backend#828).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Shujaat Hasan <shujaat@tracebloc.io>
…up) (#91)
* feat(#90): cluster doctor — node-fit + image-pull checks (WS3 follow-up)
Two read-only checks added to `tracebloc cluster doctor` (follow-up to #89):
- Node capacity: parses the resource requests jobs-manager stamps on spawned
training jobs (RESOURCE_REQUESTS / GPU_REQUESTS env) and checks at least one
Ready node can fit them — the "Pending forever, no node big enough" class.
GPU is soft: a hard ✖ only on cpu/mem, and a ⚠ when a GPU is requested but no
node exposes it (jobs-manager has a GPU->CPU fallback).
- Image pull secret: when jobs-manager references a registry pull secret,
verifies it exists and is a well-formed dockerconfigjson so private-image
pulls don't ImagePullBackOff.
Both read-only/best-effort, tested with client-go's fake clientset. The
in-cluster egress probe (the third deferred check on #90) is intentionally a
separate PR — it needs a port-forward/exec mechanism, not this read-only pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#90): node-fit must require cpu+mem+GPU on ONE node (Bugbot)
checkNodeFit set cpuMemFits and gpuFits independently, so they could come from
different nodes — reporting OK even when no single node had cpu+memory+GPU
together (a GPU job would then stay Pending). It now evaluates each node as a
whole: cpuMemFits (any node) drives the hard fail; fullFits (one node with
cpu+mem AND the GPU) drives the ok/warn split. Adds regression tests for the
cross-node and single-node-fits cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d client (cli#83) (#85)
* feat(cli): auth scaffold — login/logout/auth status + config + backend client (cli#83)
RFC-0001 (backend#830) Phase-1 CLI side, scaffolded ahead of the backend
device-grant so it activates the moment backend#835 ships.
- internal/config: ~/.tracebloc config store (0600, atomic write) — backend
env + user token + active client. Fully functional + unit-tested.
- internal/api: backend REST client. CLIENT_ENV -> {dev,stg,prod} base URL
(matches the installer's _backend_url); proxy + CA aware (honors
HTTP(S)_PROXY / NO_PROXY + the system cert pool, for corporate-proxy
networks); the RFC 8628 device-flow methods (RequestDeviceCode + PollToken
with the authorization_pending / slow_down / expired_token / access_denied
states). Unit-tested via httptest.
- internal/cli: `tracebloc login` (device flow — show URL + code, poll, store
the token), `logout`, `auth status`. `client create/list/use` are stubbed
(cli#84 — they need the user token from login + provisioning backend#836).
login calls /device/code + /device/token, which land in backend#835; until
then it reports that the backend doesn't support browser sign-in yet. Builds,
gofmt-clean, unit-tested (config round-trip + 0600 mode; api URL map +
device-flow poll states); `tracebloc --help` lists the new verbs.
Part of cli#83 / backend#830 (end-to-end login activates with backend#835).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): authenticate with Bearer + verify token on login (cli#83) (#86)
* feat(cli): authenticate with Bearer + verify token on login (cli#83)
Completes `tracebloc login` against the now-built device-grant endpoints
(backend#846). The token the flow issues is a ClientAccessToken, which the
backend authenticates as `Authorization: Bearer` (ClientAccessTokenAuthentication,
backend#835) — not the legacy DRF `Token` scheme the client was sending on
authenticated requests, which would have failed to authenticate a logged-in token.
- internal/api: authenticated requests now send `Bearer <token>` (was `Token`);
add get() + WhoAmI() (GET /userinfo/) to confirm the token + fetch the account.
- login now verifies the freshly-issued token (best-effort) and stores/shows the
account ("Signed in as you@co.com"); a failed lookup never fails a valid sign-in.
- tests: WhoAmI sends Bearer + parses the identity; a 401 surfaces as an APIError.
The device-flow contract (paths/fields/error codes) was already aligned with
backend#846 — verified, unchanged. Stacked on cli#85 (auth scaffold).
go build/vet/test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): cover the login command end-to-end + add test seams (cli#83)
internal/api was unit-tested, but the login / logout / auth status COMMANDS
weren't. Adds auth_test.go driving the full device-flow command against an
httptest backend whose shapes match backend#846 — so it also guards the
CLI<->backend contract that the Token->Bearer fix corrected:
- login: device_code -> authorization_pending -> token -> WhoAmI(Bearer) ->
"Signed in as ..." with config persisted; the 404 "unsupported backend" gate
(asserts no token is stored); access_denied.
- logout clears the token; auth status (signed-in + not-signed-in).
Two unexported test seams in auth.go — newAPIClient (point at an httptest
server) and pollAfter (fire the poll immediately) — since the flow otherwise
makes real HTTP calls on a timer. go build / vet / test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): client create / list / use commands (#84)
RFC-0001 P1. Adds the `client` subtree the login flow hands off to:
provision a tracebloc client for this machine, list the account's clients,
and attach this machine to an existing one.
- internal/slug: Go port of RFC-0001 Appendix B (backend common/utils/slug.py)
— DNS-1123 slugify (NFKD via x/text) + collision suffix + empty-slug guard,
kept in lock-step with the backend that validates the result.
- internal/api: CreateClient / ListClients / ListClientAdmins against
/edge-device/, Bearer-authed (backend#836).
- internal/cli/client.go: create (--name / --location / --yes), list (ls),
use <id>. The derived namespace is shown for confirmation; location is
required (never silent-empty); a 403 surfaces the ask-an-admin path
(backend#836); the generated machine credential is printed once.
Location auto-detect (cloud-metadata / GeoIP suggested default) is a
fast-follow — this PR takes --location or prompts for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): paginate client list + gather-then-review + parity/interactive tests
Self-review follow-up on the client commands:
- api: ListClients now follows DRF `next` to the end (was page-1 only), so
`list`, `use <id>`, and create-time collision detection see every client in
the account, not just the first page.
- cli: `create` gathers name + location first, then shows one review + a single
confirm (was confirm-mid-flow) — matches the dataset-push interactive flow.
- tests: committed slug golden-parity test (24 pairs verified byte-identical
against the Python slugify_dns1123, incl. NFKD ligatures/fractions/roman/
fullwidth); interactive create + cancel via the prompter seam; paginated
list; collision-suffix end-to-end.
- slug: doc-note the redundant dash-collapse (mirrors slug.py) and the
""/None fallback divergence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t check) (#103)
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 `causal_language_modeling` task
category (data-ingestors#805: the enum + a self-supervised "requires texts, not
label" conditional).
- Re-vendored the schema (sync-schema.sh) → --check now passes.
- Registered the new category in internal/push/category.go as recognize-but-not-yet-
CLI-supported (CLISupported:false + UnsupportedNote): the CLI's discover/build for
its raw-.txt / prompt\tcompletion `texts` layout isn't implemented, so push cleanly
reports it as pending rather than leaving a schema<->registry gap (the cli#74 drift
class the registry exists to prevent). Updated the parity test.
Scope: schema re-vendor + the registry recognition only. Full CLI push support for
causal_language_modeling (discover/build) is a follow-up feature, not this PR.
go build/vet/test ./... green; drift check green.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-create + 409 (#84) (#102)
* feat(cli): client create reads the cluster anchor — idempotent get-or-create + 409 (#84)
RFC-0001 §7.2 / backend#883: `client create` now reads the cluster's kube-system UID
and sends it as cluster_id, so the backend does get-or-create keyed on it.
- Reads the anchor via a new cluster.ClusterID (kube-system namespace UID) behind
--kubeconfig/--context flags. Best-effort + never-silent: if the cluster isn't
reachable it provisions WITHOUT an anchor (a plain mint) and says so.
- api.CreateClient returns adopted (HTTP 200) vs minted (201): an idempotent re-run
on the same cluster adopts the existing client (no new credential printed) instead
of duplicating; a 409 → a clear "registered to a different account" (cluster_conflict).
- Adds api.BackfillClusterID (PATCH /edge-device/<id>/) for the adopt-backfill path
(the installer #838 orchestrates the full R7 flow).
Scope: anchor + idempotency only. never-show (writing the credential into the cluster
secret) and the R7 in-cluster TB_CLIENT_ID backfill orchestration land with the
installer reorder (#838); the mint-time credential print stays as the interim.
Tests: cluster.clusterIDFrom (fake clientset); api CreateClient mint/adopt/409 +
BackfillClusterID; cli create anchor-mint / adopt-idempotent / 409 / no-cluster-warns.
go build/vet/test ./... green (Go 1.26).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): bound the anchor read + make adopt save-failure non-fatal
Two review fixes folded into the create-anchor work (#84):
- cluster.ClusterID: cap the best-effort kube-system read with an 8s
rest.Config timeout. A kubeconfig pointing at an unreachable API
server would otherwise hang the GET for the OS TCP timeout; now
`client create` degrades to a non-anchored mint promptly instead of
stalling before the review prompt.
- cli client create: on an idempotent adopt, print the result before
saving the active-client pointer and treat a save failure as a hint
(mirroring the mint path), so a config-save error can't bury the
"adopted it" message or the recovery hint.
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>
… the installer (#84) (#104)
* feat(cli): client create --credential-file — write the machine credential for the installer (#84)
Adds `tracebloc client create --credential-file PATH`: instead of printing the
minted credential, write it to PATH (mode 0600) as a sourceable env file the
installer reorder (#838) consumes — the secret never hits the terminal (RFC §9
"secure by invisibility" / never-show, deferred here from cli#102).
- Mint (201): writes TRACEBLOC_CLIENT_ID + TRACEBLOC_CLIENT_PASSWORD + TB_NAMESPACE
(0600) and suppresses the stdout credential print. Write failure is fatal (the
credential is the only copy).
- Adopt (200): writes TRACEBLOC_CLIENT_ID + TB_NAMESPACE + TRACEBLOC_CLIENT_ADOPTED=1
(no password — the existing one stands, write-only on the backend); the installer
reconciles the existing release rather than expecting a fresh credential.
- Without the flag: behaviour unchanged (the interim credential print).
Unblocks the #838 installer reorder (login -> create -> feed the chart). Tests cover
mint (0600 + sourceable + never-printed) and adopt (id+ns+marker, no password).
go build/vet/test ./... green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): force 0600 on credential file via temp+rename (#84)
writeClientCredential used os.WriteFile(path, ..., 0o600), but WriteFile
only applies its perm bits when it *creates* the file — over a pre-existing
target it truncates and writes WITHOUT changing the mode. So a stale file, or
one an attacker pre-creates world-readable, at --credential-file would receive
the minted password (the only copy) at its old, possibly 0644 mode — silently
breaking the flag's own 0600/never-show contract (RFC §9). Verified: a 0644
target stays 0644 after the write.
Write to a 0600 temp file in the target dir and atomically rename over the
path instead. CreateTemp is 0600 by construction, so the guarantee holds
unconditionally; rename is atomic (no half-written credential) and the final
write never follows a symlink planted at the target.
Tests:
- preexisting-perms: a 0644 target ends up 0600 (locks in the fix).
- write-fail-fatal: an unwritable target surfaces an error, never a silent
drop (the credential is the only copy).
- mint never-show: also assert the password VALUE is absent from stdout, not
just the literal "password"/"Machine credential" strings.
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 threadinternal/api/client.go Outdated
Comment threadinternal/cli/dataset.go
* fix(api): error on unparseable pagination next, don't silently truncate (#106)
ListClients followed DRF `next` via nextPath, which returned "" for BOTH an
empty link (end of pages) and an unparseable one — so a non-empty `next` the
server sends that url.Parse rejects silently ended the loop, and ListClients
returned only the pages seen so far with a nil error. list / `use` /
namespace-collision checks would then miss clients with no signal.
nextPath now returns (string, error): "" + nil for an empty link, an error for
a non-empty link that won't parse. Trigger is unlikely (DRF emits well-formed
URLs, url.Parse is lenient), but the failure mode — silent partial list — is
the wrong one for a correctness-sensitive call.
Tests: pagination still followed end-to-end (page 1 → 2 → done); an
unparseable next link is now a hard error, not a truncation.
Bugbot: 8dadb5c2-804a-48ed-bc81-eb14e6317be1 (v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): route all known-but-unsupported categories to the pending note (#106)
The dataset-push category gate only special-cased known-but-unsupported *image*
categories (`case push.IsImage`), so a registry-known non-image category that
isn't CLI-supported yet — `causal_language_modeling` (FamilyText,
CLISupported:false, with a real UnsupportedNote) — fell to the default branch
and was reported as "isn't a recognized task category". It IS recognized; it's
pending support.
Swap the gate to `case push.IsKnown`: supported categories are already caught
by the prior case, so IsKnown here means known-but-unsupported (image or text),
all routed through the registry's per-category pending-support note. The default
branch is left for genuinely unknown/typo'd categories. Message-only (exit code
was already 2).
Test: causal_language_modeling now gets the pending-support note, not the
unrecognized-category message. (The existing exit-2 test didn't assert the
message, which is how this slipped through.)
Bugbot: 16f5b945-5d67-4201-8bc6-1f6baf633672 (v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Comment threadinternal/cli/auth.go
Comment threadinternal/push/category.go
#109)
* fix(cli): clear active_client_id on logout (#106)
logout cleared the token and email but left active_client_id in
~/.tracebloc/config.json. Since that pointer is account-scoped, a later
`login` as a different user inherited the stale id — `auth status` and
`client list` would surface the previous account's active client until the
user ran `client create`/`client use` again.
Clear it alongside the token/email so logout fully drops local session state.
Bugbot: "Stale active client after logout" (Medium, v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(push): add token_classification to the registry + pin schema parity (#106)
The re-vendored ingest.v1.json (#103) accepts token_classification, but the
category registry didn't list it — so `dataset push --category=token_classification`
hit the "isn't a recognized task category" path despite being schema-valid (the
same misrouting just fixed for causal_language_modeling). Added it as a known,
not-yet-CLI-supported FamilyText category with a pending-support note (the safe
default — it was never pushable, so this only improves the message; flip to
supported when the texts/token-label staging lands).
Root-cause guard: the existing registry tests only pinned the registry against
a hand-written list, which stayed self-consistent while drifting from the
schema. Added TestRegistryCoversSchemaCategories — parses the embedded schema's
category enum and asserts every entry is registry-known — so any future
schema-only category is caught here, not in the next review pass. (The reverse,
a registry-only known-unsupported alias like instance_segmentation, is allowed:
it's gated out before schema validation.)
Bugbot: "Missing token_classification registry entry" (Medium, v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

@cursorcursorBot 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 2 potential issues.

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 3881746. Configure here.

Comment threadinternal/cli/auth.go Outdated
Comment threadinternal/doctor/doctor.go
…es (#106) (#110)
* fix(cli): back off device-flow poll by 5s on slow_down (RFC 8628) (#106)
On `slow_down`, runLogin bumped the poll interval by 1s (`interval++`). RFC 8628
§3.5 requires increasing it by 5s for that and all subsequent polls, so the CLI
kept polling too aggressively after the server asked it to back off.
Test captures the durations handed to the pollAfter seam: post-slow_down wait is
now 10s (5+5), not 6s.
Bugbot: "Device flow slow_down interval" (Medium, v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(doctor): resolve CLIENT_ENV case-insensitively in backendHost (#106)
backendHost switched on CLIENT_ENV case-sensitively, but the API client
(api.ResolveEnv/BaseURL) lowercases env values. A non-lowercase CLIENT_ENV on
the edge box (e.g. "DEV") fell through to the prod default, so `cluster doctor`
probed api.tracebloc.io even when the cluster targeted dev/stg. Normalize with
ToLower+TrimSpace before the switch.
Test extends TestBackendHost with "DEV"/"Stg"/" dev " cases.
Bugbot: "Doctor backend env casing" (Low, v0.4.0 RC, #107)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(api): guard the bare-array list decode to the first page (#106)
ListClients attempted the unpaginated bare-array decode on every iteration. A
bare array is only valid as the sole response (a paginated chain is a
{next,results} object on every page), so a stray bare body mid-chain could
silently end the loop and drop earlier pages. Guard the bare decode to pageNum 0.
Test: a bare-array response still returns the full list.
Found in the proactive RC review (low; latent — DRF doesn't mix shapes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): exclude this cluster's own client from create collision check (#106)
`client create` built the namespace-collision set from ALL clients, including the
one already anchored to this cluster. On an idempotent re-run with the same
--name, that bumped the derived slug (lab-one → lab-one-2) and showed it in the
review — but the backend adopts on cluster_id and returns the original namespace,
so the review contradicted the actual outcome. Skip the client whose cluster_id
matches this cluster's anchor.
Test: a re-run review no longer shows a bumped namespace.
Found in the proactive RC review (low; cosmetic — backend state was correct).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbal
saadqbal merged commit 57805b2 into mainJun 24, 2026
28 of 29 checks passed
saadqbal added a commit that referenced this pull request Jul 3, 2026
…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>
saadqbal added a commit that referenced this pull request Jul 3, 2026
…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>
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.

2 participants

@saadqbal@LukasWodka