Skip to content

feat(cli): validate --location against vendored zone list (#94) - #95

Closed
LukasWodka wants to merge 4 commits into
feat/84-location-autodetectfrom
feat/84-location-validation
Closed

feat(cli): validate --location against vendored zone list (#94)#95
LukasWodka wants to merge 4 commits into
feat/84-location-autodetectfrom
feat/84-location-validation

Conversation

@LukasWodka

Copy link
Copy Markdown
Contributor

Summary

Closes#94. EdgeDevice.location is CharField(choices=ZONE_CHOICES) (electricityMaps zones), so the backend's ChoiceField rejects any zone not in the list. The CLI didn't validate --location / the prompt, so a bad value (Germany, eu-central-1, a typo) only failed as an opaque 400 at create time. This validates against a vendored copy of the list and fails fast with suggestions.

Stacked on #93 (feat/84-location-autodetect, itself on #92) — retarget down the chain as each merges.

What's here

  • internal/zones — the backend's ZONE_CHOICES vendored as zones.json (352 zones: countries + sub-zones like US-CAL-CISO), embedded via go:embed. Valid / Name / Suggest.
  • scripts/sync-zones.sh — regenerates zones.json from the backend's metaApi/models/zone_choices.py (the source of truth — it's what the API validates against), with a --check drift mode for CI.
  • client create--location and the location prompt validate against the list, failing fast with close-match suggestions ("germany"did you mean: DE?; "de"DE). The feat(cli): location auto-detect for client create (#84) #93 auto-detect only pre-fills a detected zone if it's actually valid.

Why a local-file sync (not a URL like sync-schema.sh)

sync-schema.sh curls data-ingestors (a public repo). backend is private, so an anonymous curl of zone_choices.py won't work — and the backend file is the authoritative validation source, so the script reads it from a sibling checkout (ZONES_SOURCE to override). The committed zones.json is what the binary uses; the script is the maintenance / CI-drift tool.

Tests

internal/zones: Valid/Name (case-sensitivity + sub-zones), Suggest (name match, wrong-case, prefix, garbage → none). internal/cli: an invalid --location errors with a DE suggestion before any API call; the existing flow/interactive tests still pass (DE/US/detected FR are valid). Full go build / vet / test ./... green.

🤖 Generated with Claude Code

LukasWodkaand others added 4 commits June 20, 2026 11:24
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>
…eractive 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>
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>
EdgeDevice.location is CharField(choices=ZONE_CHOICES) (electricityMaps zones),
so the backend rejects any zone not in the list. The CLI didn't validate, so a
bad --location (or a prompt typo) only failed as an opaque 400 at create time.
- internal/zones: the backend's ZONE_CHOICES vendored as zones.json (352 zones)
and go:embed-ed; Valid / Name / Suggest. scripts/sync-zones.sh regenerates it
from the backend's metaApi/models/zone_choices.py (the source of truth — a
private repo, so a local sibling read rather than a curl), with a --check
drift mode for CI, mirroring sync-schema.sh.
- client create: --location and the location prompt validate against the list
and fail fast with close-match suggestions ("germany" → did you mean DE?). The
cli#93 auto-detect only pre-fills a detected zone if it's valid.
Tests: zones Valid/Name/Suggest; cli rejects an invalid --location before any
API call. go build/vet/test green.
Closes#94.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@LukasWodka
LukasWodka requested a review from saadqbalJune 22, 2026 06:52
@saadqbal
saadqbal marked this pull request as draft June 22, 2026 07:34
@saadqbal
saadqbalforce-pushed the feat/84-location-autodetect branch from 7efdd9c to edb8dedCompareJuly 6, 2026 14:27
saadqbal added a commit that referenced this pull request Jul 7, 2026
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>
saadqbal added a commit that referenced this pull request Jul 7, 2026
…arked (#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>
@saadqbal

Copy link
Copy Markdown
Collaborator

Closing — superseded by the cli#137 direction (PR #144).

Per the review on #144 (finding #7), internal/geo (the auto-detect this chain stacked on) is removed, and --location is now optional with the backend as the source of truth for valid zones: a bad --location (Germany, a typo) surfaces as its real create error inline — the installer already does this via _report_create_failure (tracebloc/client#307) — so CLI-side validation against a vendored zone list is no longer the plan.

Reopen if we ever revisit client-side zone validation. Closing the linked #94 for the same reason.

@saadqbalsaadqbal closed this Jul 7, 2026
saadqbal added a commit that referenced this pull request Jul 7, 2026
…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>
@LukasWodka
LukasWodka deleted the feat/84-location-validation branch July 9, 2026 11:41
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

@LukasWodka@saadqbal