Skip to content

docs(rfc-0001): CLI auth & client provisioning — accepted design-of-record#55

Merged
saadqbal merged 14 commits into
developfrom
docs/rfc-0001-cli-auth-provisioning
Jul 3, 2026
Merged

docs(rfc-0001): CLI auth & client provisioning — accepted design-of-record#55
saadqbal merged 14 commits into
developfrom
docs/rfc-0001-cli-auth-provisioning

Conversation

@saadqbal

@saadqbal saadqbal commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Merges RFC 0001 into develop as the accepted design-of-record for CLI auth & one-command client provisioning.

Tracking epic: #54 (closed).

Status: ACCEPTED — implemented

This started as a draft for discussion. The design it proposed has since been built and shipped in CLI v0.4.0 (#107), so this PR closes it out: the status header is flipped from DRAFT → ACCEPTED and reconciled with what actually landed. The branch has been rebased onto current develop.

What shipped against this design:

Not in this PR (Phase 2, still tracked)

Server-side token revoke (backend#887), short-lived auto-refreshing tokens, client rotate, atomic fleet enrollment — see §11. The revision history in the doc is retained as the record of how the design converged.

🤖 Generated with Claude Code


Note

Low Risk
Documentation-only addition with no runtime or config changes; main caveat is readers may treat pending Phase 2 items (e.g. server-side revoke) as already shipped unless they read the status header.

Overview
Adds docs/rfcs/0001-cli-auth-and-client-provisioning.md (~1,266 lines) as the accepted design-of-record for browser-based CLI auth and one-command client provisioning, marked implemented in CLI v0.4.0 with Phase 2 items called out explicitly.

The document replaces copy-paste Client ID/password onboarding with RFC 8628 device flow plus silent client create, and centers the hard problems on client lifecycle: kube-system UID as cluster_id for idempotent get-or-create, slug-based use/delete, selected vs connected, and installer reorder (auth/provision before Helm). It also pins API/data contracts (Appendix C), migration risks (R1–R12), and reconciles Rev 6–7 accuracy (CLI/installer—not heartbeat—sets cluster_id; logout is local-only until POST /auth/revoke / backend#887).

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

@saadqbal saadqbal self-assigned this Jun 5, 2026
@LukasWodka

Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 16 / 8

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

@LukasWodka

Copy link
Copy Markdown
Contributor

Stood up a cross-repo tracking epic so this RFC is tracked as the leading CLI plan: tracebloc/backend#830 (North Stars, Epic, owner @saadqbal). It carries the phased plan (Phase 0/1/2), the cross-repo work breakdown (backend / cli / client), and the open questions for the discussion. This RFC doc stays the source of truth — the epic is just the tracker.

@LukasWodka

Copy link
Copy Markdown
Contributor

Worked the §11 open questions to resolution with a code-grounded sweep (backend / client / cli) and laid out the full cross-repo plan + device-grant design in the tracking epic tracebloc/backend#830. Headlines (most are dictated by the code, not opinion):

  • Q1 air-gap → out of scope. Preflight hard-fails on no egress; data ingest: silent wait after submit, undisclosed 1h watch cap, and k8s jargon in the run output #172 is corporate-proxy support, not air-gap. Real segment = egress-restricted-but-online (must work behind a TLS-inspecting proxy).
  • Q2 namespace → name→slug→BOTH EdgeDevice.namespace + TB_NAMESPACE. The heartbeat overwrites namespace every ping, so the provisioned slug must equal the install-time namespace or it drifts.
  • Q3 location change → future-only, already clean. gCO₂ is a frozen per-experiment snapshot — never re-derived from current location.
  • Q4 RBAC → split read from write + "pick existing / ask admin" (today one perm gates both list and create → bare 403).
  • Q5 → multi-client per account/host is free; re-parenting deferred (the viewset force-stamps account to the requester).
  • Q6 IdP → reuse the existing web login as-is. Auth is token-based; Google/GitHub already mint the same token, so /activate is just a token-authed endpoint binding to request.user — no new IdP wiring.

Two need your product nod — Q1 (air-gap out) and Q5 (re-parenting deferred); the rest follow from the code. Full detail, the DeviceGrant design, and the phased per-repo breakdown are in backend#830.

@LukasWodka

Copy link
Copy Markdown
Contributor

Review — aligned on the model, +1 on the cluster_id anchor; gaps to close before final

Read Rev 2 end-to-end against an independent user-flow teardown: the two-context model, the nine lifecycle loopholes, and their resolutions all match. Appendix A's slug rule is byte-identical to what's already shipped + parity-tested in cli/internal/slug. Strong shape.

Decisions locked (Lukas):

  • cluster-id = the kube-system namespace UID — go with it (closes the §12 open item).
  • Q1 (air-gap out of scope) + Q5 (one-client-per-cluster, re-parenting deferred) — confirmed (already worked on backend#830); recording the owner nod here.

Adopting the three refinements over our draft spec: the cluster_id anchor (stronger than a post-install Helm-clientId), location-unset-not-0, slug-as-handle.

Now the part that doesn't feel final yet — issues by priority.

🔴 High — correctness / security

  1. cluster_id uniqueness scope + cross-account. §6.3/§7.2 make cluster_id unique with get-or-create on it, but the kube-system UID is not a secret (anyone with cluster access reads it). If it's globally unique and get-or-create blindly returns the match, re-targeting a cluster from a different account would silently adopt the first account's client. Spec it: globally unique, but get-or-create returns the client only if it's in the requester's account — a cluster bound to another account is an explicit conflict, never a silent adoption.

  2. Existing installs have no cluster_id → re-run double-provisions. The anchor is net-new, so every current client has a null cluster_id. get-or-create-by-cluster won't match them, so the next installer re-run on an already-connected box mints a duplicate and orphans the live client. This needs an explicit backfill before it can ship (ties to Promote kubeconfig flags (--kubeconfig/--context/--namespace) to persistent on root #3) — otherwise every existing customer double-provisions on their next upgrade.

  3. Heartbeat must carry cluster_id. The "connected" check (§7.3 / §12 = cluster-id match + heartbeat recency) needs the runtime to report cluster_id, but §4.5 has the heartbeat reporting only namespace. Add cluster_id to the heartbeat contract — it both powers the connected-state check and backfills Phase 2: kubeconfig discovery + parent release detection + SA token #2 for live clients.

🟠 Medium — edge cases

  1. Cluster rebuild orphans the old client. The documented "switch client" path (k3d cluster delete → reinstall) produces a new kube-system UID → new cluster_id → a freshly minted client, leaving the old EdgeDevice orphaned forever (its cluster_id never returns). Needs a reaper or a teardown hook, or orphans accumulate per rebuild.

  2. Orphan password-reset assumption (§7.9). "An orphan never connected, so resetting its password is safe" doesn't hold for a client that connected once then lost its local values file — the reset would break its running pod. Gate the reset on heartbeat recency (offline / truly-never-connected), not just "no values file."

  3. client create requires a live cluster (ordering). The anchor is only readable once a cluster exists, so create can't precede cluster bootstrap. State that create operates against an existing reachable cluster (clear error if none) and who bootstraps it (installer vs a cluster verb) — §8.1 implies it, but the command contract (§6.2) doesn't.

  4. logout should revoke server-side. §6.2/§7.5 clear the local token, but the ClientAccessToken stays valid for 30 days — a leaked/copied token survives logout. Have logout call the backend revoke (backend#845 already supports it); otherwise "logout" is a false sense of security.

🟡 Low — clarity / polish

  1. Rename vs the frozen handle. The handle is the frozen slug (§7.1), but §8.1 offers "rename later." Rename changes first_name (display), not the slug — so the handle the user types stays the old slug, mismatching the new name. Say explicitly that rename is cosmetic; the handle remains the frozen slug.

  2. Reconcile the "authoritative" framing. Appendix A calls the namespace UniqueConstraint "authoritative"; §7.2 makes cluster_id the real one-per-cluster guard. With cluster-id confirmed, cluster_id is authoritative for idempotency and namespace-unique is cosmetic cross-cluster dedup (demotes backend#863 — noting it there).

  3. Device-code phishing (§9). For the regulated on-prem sell, spell the RFC 8628 mitigations out a bit more: bind user_code → account, short TTL (impl has 10 min), rate-limit, and an approval page that names the device + "only approve if you started this."

  4. Credential at-rest footprint. §9 covers the host values.yaml (0600); also note the credential lands base64 in the k8s / Helm release Secret (etcd). Fine for single-tenant on-prem — worth stating as a conscious call.

  5. Picker at fleet scale. The arrow-key picker is great for a handful of clients; an account with dozens needs filter/search (or the phase-2 enrollment-key path). Note the N-scaling.


#1#3 are the ones I'd treat as blocking — together they're the difference between "idempotent" working for new boxes only vs. the existing fleet too. Happy to draft the cluster_id field + heartbeat + backfill on the backend side if useful.

@saadqbal

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — all of it is folded into the RFC in 58bdd06 (rev 3). Map of where each point landed:

🔴 Blocking

  • Cross-account adoption → §6.3 + §7.2 (R6): account-scoped get-or-create; a cluster_id bound to another account is a 409, never a silent adoption.
  • Existing fleet has null cluster_id → §10 backfill (R7) + a §7.2 step-2a "never mint over a live in-namespace release" guard for the pre-backfill window. This was the real hole — idempotency now covers the existing fleet, not just new boxes.
  • Heartbeat must carry cluster_id → §4.5 + §6.3 + §10 (powers the connected check and backfills live clients).

🟠 Medium

  • Rebuild orphans → R3 (reaper / teardown hook).
  • Orphan password-reset → §7.9, now gated on heartbeat recency, not "no values file".
  • create needs a live cluster → stated in the §6.2 command contract.
  • logout server-revoke → §7.5 + §9 (R2), now done at logout rather than deferred.

🟡 Polish

  • Rename is cosmetic / handle stays the frozen slug → §7.1 + §8.1.
  • The "authoritative" contradiction → fixed: cluster_id is authoritative for idempotency; the namespace UniqueConstraint is demoted to cosmetic cross-cluster dedup (Appendix A + §12).
  • Device-code phishing detail + etcd-at-rest note → §9. Picker at scale → §7.1.

Decisions recorded: cluster-id = kube-system UID, Q1 (air-gap out) and Q5 (one-client-per-cluster, re-parenting deferred) all confirmed (§0 / §12).

The remaining blocking work is backend — the cluster_id field + account-scoped get-or-create, the heartbeat cluster_id + existing-fleet backfill, and wiring logout to the revoke endpoint — reflected in the §13 breakdown. Happy to take you up on the offer to draft those on the backend side.

@LukasWodka

Copy link
Copy Markdown
Contributor

Review round 2 — Rev 3 is a strong revision; one foundational soft spot left (the cluster_id anchor)

Ran a multi-perspective pass (security / UX / correctness / SRE / completeness) over the full Rev 3 + adversarial verification of every finding against the actual code (22 verified real, 10 misreads filtered). The round-1 blockers (R6/R7/R2) are genuinely mechanized now, not just named — nice. The findings below all converge on one place: the cluster_id anchor. The concept (idempotency keyed on cluster identity) is right; the mechanism as specified doesn't hold yet. Everything else is second-order.

🔴 The anchor cluster — recommend a design pass before Phase 1

These interlock (one problem, several facets), each grounded in the code this will ship against:

  1. Wrong trust model (root cause). The anchor's premise is "the kube-system UID is not a secret", yet cluster_id is self-reported by the client over the machine-credential-authed heartbeat, and common/utils/edge_device_utils.py:113 already trusts self-reported client_info verbatim (edge_device.namespace = client_info.get('namespace', ...), no residency check). So a compromised box — or anyone who read the non-secret UID off a shared cluster — can assert any cluster_id, poisoning the binding so a victim's next create adopts the attacker's client. A self-asserted, non-secret, every-ping-mutable value can't be a cross-account security boundary.

  2. The backfill (R7) is mechanically impossible as written. It backfills cluster_id "from the heartbeat", but the heartbeat sender (jobs-manager, send_heartbeat) reads its namespace from its own pod mount and its ClusterRole (client/client/templates/rbac.yaml) grants jobs/pods/secrets but not namespaces — it can't GET kube-system to read the UID. The only component that can (the resource-monitor DaemonSet, resource-monitor-rbac.yaml) doesn't send client_info and is disableable. The named carrier can't carry it — on exactly the existing fleet R7 must protect.

  3. §7.2 step-2a bypasses R6 and discloses credentials cross-account. Step-2a adopts the in-cluster TB_CLIENT_ID client-side, before any backend call. So account B pointed at account A's cluster reads A's machine credential straight out of the Secret and adopts it — the server-side 409 is never reached. That's cross-account credential disclosure, not a duplicate-client nuisance. (And the 409 is structurally unreachable even on the mint path: edge_device_views.py:17-19 hard-filters every query to request.user.account and :23 force-stamps the account, so account B can't even see A's row — get-or-create falls through and mints/500s instead of returning 409.)

  4. unique=True (global) contradicts the account-scoped 409. Specified literally as global-unique (§6.3 line 287, §13 line 716), but the 409 contract needs account scoping. Global-unique → the second account's INSERT is a DB IntegrityError/500, never the documented 409. → UniqueConstraint(account, cluster_id) + an app-level 409 check.

  5. step-2a's guard is keyed on the unstable namespace. §4.2 notes namespace isn't set at provisioning today — legacy clients' namespace wasn't installer-chosen, so a re-run's derived slug may not match the live release's namespace → the "never mint over a live release" guard misses → duplicate. The fleet-wide safety net keys on the one value that isn't stable for that fleet. (Fix: probe by Helm/Deployment label like DiscoverParentRelease, not by guessed namespace.)

  6. 1:1 client↔cluster contradicts the shipped chart. client/client/values.yaml (~L372–375) explicitly supports multiple tracebloc namespaces per cluster (BYO) — they share one kube-system UID, so unique(cluster_id) would merge or hard-collide them. §3.1/Q5 and the chart disagree on the product's core topology. → either anchor on (cluster_id, namespace), or declare BYO multi-namespace unsupported and reverse that values.yaml decision.

Suggested fix shape: cluster_id is an idempotency hint, not a security boundary. Bind it authoritatively at create under the user token (account-scoped); treat the heartbeat value as write-once advisory (reject divergent, flag for the reaper). Constraint = UniqueConstraint(account, cluster_id) (or (cluster_id, namespace) for BYO) + app-level 409; route 2a/2b/3 through one server-side get-or-create so the client-side adopt can't bypass the cross-account check. Pick the backfill carrier explicitly (and state it's impossible under clusterScope:false).

🟠 Second-order findings (RFC otherwise solid)

  • UX: /activate failure paths (tab closed / code expired / wrong account) unspecified — the Phase 1: embed ingest.v1.json + tracebloc ingest validate #1 beginner stuck-state. §8.1 never renders the 409/403 mid-install outcomes. Picker empty/single/remote-only states unspecified, and it invites selecting a client §7.3 then refuses. client list (§8.4) shows slug-only → "rename later" is invisible + handle coherence silently breaks. Silent wrong-account enrollment in the zero-prompt flow has no course-correct.
  • Correctness: §7.9 concurrent-mint "loser adopts the winner" has no credential to adopt (backend stores only the hash) — a dead end; define the loser's path (server mints-and-returns once, loser gets a deterministic 409→resume). The orphan password-reset gated on "heartbeat recency" is itself a TOCTOU — needs a compare-and-set fence or an explicit operator rotate.
  • Operability / security: cluster-rebuild orphans are unbounded live, un-revoked machine credentials (delete is the only revocation) — promote the reaper from watch-item to a security requirement; it also has no signal to distinguish a rebuild-orphan from a recoverable interrupted-install orphan.
  • Contracts to pin: the device-token lifetime/scope/"refresh" is self-contradictory across §2/§4.3/R2/§11; the heartbeat cluster_id field contract (name, first-send, namespace-overwrite interaction) and the /activate user_coderequest.user binding are left to the implementer.

✅ Solid

Two-identity model, the §7 lifecycle resolutions, the auth/device flow, dual-mode, and the §14 risk framing are all strong — and the round-1 blockers are closed. This is a mature RFC with one concentrated area to rework.


Net: the cluster_id anchor needs a focused pass (trust model → backfill carrier → constraint shape → BYO topology); the rest is buildable. Happy to draft the anchor-redesign as a concrete starting point if useful, @saadqbal — over to you.

Findings from a 5-lens review with per-finding adversarial verification against the backend/runtime/chart code; file:line refs above are the canonical repos.

@saadqbal

Copy link
Copy Markdown
Collaborator Author

Re: your Rev 3 round-2 review — the cluster_id anchor design pass.

Did the focused anchor pass you recommended; it landed across Rev 4→6 (58bdd06ab0ee88) and shipped on the backend. No new diff in this PR because it's already folded in — mapping each finding to where it closed:

🔴 Anchor cluster — trust model inverted exactly as suggested (idempotency hint, not a security boundary):

  1. Wrong trust modelcluster_id is now set only on a Bearer-authed create / adopt-backfill by the CLI/installer (the kubeconfig-holder), and the heartbeat never sets it (§6.3, §9). Shipped in backend#893 — incl. a test asserting the heartbeat ignores a spoofed cluster_id.
  2. Backfill carrier impossible via the heartbeat → backfill moved to the CLI/installer, which can read the kube-system UID; the heartbeat carrier is gone (§7.2, §10/R7).
  3. step-2a cross-account disclosure → the account-scoped check now gates adoption itself, not just the mint, so reading a live TB_CLIENT_ID off-cluster can't sidestep the 409 (§7.2).
  4. global unique vs account-scoped 409 → globally-unique cluster_id + an app-level account-scoped 409 (never a DB 500). The partial UniqueConstraint shipped in backend#898 (also closes feat(dataset): stage Pod + tar-over-exec stream (PR-b of #151) #9's authoritative-vs-cosmetic framing: cluster_id is authoritative, namespace-unique demoted to backend#863).
  5. step-2a guard on the unstable namespace → re-run reconcile keys on the cluster anchor + live-release probe, not the guessed namespace (§7.2).
  6. 1:1 vs BYO multi-namespace → declared one client per cluster explicitly; tenancy boundary = one account per cluster, multi-namespace handling stated (§3.1 / Q5 §12).

🟠 Second-order — also closed: logout revokes server-side (backend#845, §7.5) · §7.9 concurrent-mint loser now "fetches and adopts the winner," serialized by the constraint — mechanized at the DB in backend#898 this morning · /activate expiry / denied / wrong-account + RFC 8628 phishing mitigations specced (§ device-flow, §0, Appendix C) · device-token lifetime/scope pinned in §0 + Appendix C.

Net: the anchor was the part you flagged as not-final — it's both specced (Rev 6) and shipped now. If any of the above still reads as open, point me at it and I'll reopen.

🤖 Addressed by Claude Code

@saadqbal

Copy link
Copy Markdown
Collaborator Author

Correction to my resolution-map above — re: §7.5 logout server-side revoke.

FR'ing the connect flow on dev today (#877), this point was wrong in my earlier reply. tracebloc logout clears the local token but does not revoke server-side: I backed up the token → logout (auth status then shows "not signed in") → restored the copied token → client list still authenticated. A leaked/copied token survives logout (Lukas's original finding #7 stands).

So "logout revokes server-side (backend#845, §7.5)" is aspirational, not shipped: #845 added the revoke() primitive, but the POST /auth/revoke endpoint (#887) and the CLI logout→revoke call aren't wired. §7.5 overclaims and should be softened until #887 + the CLI half land. Evidence logged on #887.

🤖 Addressed by Claude Code

saadqbal and others added 14 commits July 3, 2026 17:27
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>
- §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>
…ential/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>
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>
…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>
…t 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>
…uarded 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>
…ion, 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>
…racts

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>
…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>
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>
…(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>
…laim (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>
…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
saadqbal force-pushed the docs/rfc-0001-cli-auth-provisioning branch from 75bc808 to 10608ba Compare July 3, 2026 12:27
@saadqbal saadqbal changed the title [DRAFT RFC] Browser-based auth & one-command client provisioning docs(rfc-0001): CLI auth & client provisioning — accepted design-of-record Jul 3, 2026
@saadqbal
saadqbal marked this pull request as ready for review July 3, 2026 12:27
@LukasWodka

Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 32 / 30

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

@saadqbal
saadqbal merged commit 6b7e03e into develop Jul 3, 2026
21 checks passed
@saadqbal
saadqbal requested review from aptracebloc and saqlainsyed007 and removed request for aptracebloc and saqlainsyed007 July 6, 2026 09:46
@saadqbal
saadqbal deleted the docs/rfc-0001-cli-auth-provisioning branch July 10, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants