aac-cli 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. aac_cli-0.1.2/PKG-INFO +549 -0
  2. aac_cli-0.1.2/README.md +533 -0
  3. aac_cli-0.1.2/aac_cli/__init__.py +13 -0
  4. aac_cli-0.1.2/aac_cli/admin_key_pem.py +236 -0
  5. aac_cli-0.1.2/aac_cli/api_key_rotation_state.py +192 -0
  6. aac_cli-0.1.2/aac_cli/cli.py +5411 -0
  7. aac_cli-0.1.2/aac_cli/config.py +911 -0
  8. aac_cli-0.1.2/aac_cli/idp_recovery.py +489 -0
  9. aac_cli-0.1.2/aac_cli/profiles.py +381 -0
  10. aac_cli-0.1.2/aac_cli/registration_state.py +799 -0
  11. aac_cli-0.1.2/aac_cli/sso_login.py +562 -0
  12. aac_cli-0.1.2/aac_cli.egg-info/PKG-INFO +549 -0
  13. aac_cli-0.1.2/aac_cli.egg-info/SOURCES.txt +29 -0
  14. {aac_cli-0.1.0 → aac_cli-0.1.2}/aac_cli.egg-info/requires.txt +1 -0
  15. {aac_cli-0.1.0 → aac_cli-0.1.2}/pyproject.toml +14 -9
  16. aac_cli-0.1.2/tests/test_aac_cli.py +3254 -0
  17. aac_cli-0.1.2/tests/test_admin_key_pem.py +331 -0
  18. aac_cli-0.1.2/tests/test_api_key_reissue_cli.py +313 -0
  19. aac_cli-0.1.2/tests/test_api_key_rotation_cli.py +895 -0
  20. aac_cli-0.1.2/tests/test_api_key_rotation_state.py +177 -0
  21. aac_cli-0.1.2/tests/test_idp_recovery_cli.py +1140 -0
  22. aac_cli-0.1.2/tests/test_profile_cli.py +841 -0
  23. aac_cli-0.1.2/tests/test_registration_recovery_cli.py +315 -0
  24. aac_cli-0.1.2/tests/test_registration_state.py +712 -0
  25. aac_cli-0.1.2/tests/test_sso_login_cli.py +1242 -0
  26. aac_cli-0.1.0/PKG-INFO +0 -157
  27. aac_cli-0.1.0/README.md +0 -142
  28. aac_cli-0.1.0/aac_cli/__init__.py +0 -8
  29. aac_cli-0.1.0/aac_cli/cli.py +0 -575
  30. aac_cli-0.1.0/aac_cli/config.py +0 -327
  31. aac_cli-0.1.0/aac_cli.egg-info/PKG-INFO +0 -157
  32. aac_cli-0.1.0/aac_cli.egg-info/SOURCES.txt +0 -14
  33. aac_cli-0.1.0/tests/test_aac_cli.py +0 -582
  34. {aac_cli-0.1.0 → aac_cli-0.1.2}/LICENSE +0 -0
  35. {aac_cli-0.1.0 → aac_cli-0.1.2}/aac_cli/__main__.py +0 -0
  36. {aac_cli-0.1.0 → aac_cli-0.1.2}/aac_cli.egg-info/dependency_links.txt +0 -0
  37. {aac_cli-0.1.0 → aac_cli-0.1.2}/aac_cli.egg-info/entry_points.txt +0 -0
  38. {aac_cli-0.1.0 → aac_cli-0.1.2}/aac_cli.egg-info/top_level.txt +0 -0
  39. {aac_cli-0.1.0 → aac_cli-0.1.2}/setup.cfg +0 -0
aac_cli-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,549 @@
1
+ Metadata-Version: 2.4
2
+ Name: aac-cli
3
+ Version: 0.1.2
4
+ Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
5
+ Author: Agent Authority Cloud Project
6
+ License-Expression: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: httpx>=0.28
11
+ Requires-Dist: cryptography>=42.0
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=8.0; extra == "test"
14
+ Requires-Dist: pytest-httpx>=0.30; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # aac-cli — the `aac` platform CLI
18
+
19
+ The headless operator interface to the AAC control plane (Eng Spec
20
+ §XII; Equifax/KPI-6 requirement: every operational task doable without
21
+ a web console).
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install aac-cli
27
+ ```
28
+
29
+ > **Careful with the name:** `pip install aac` installs an UNRELATED
30
+ > project (an MBSE modeling tool that happens to share the acronym).
31
+ > The AAC platform CLI's PyPI distribution is **`aac-cli`**; the
32
+ > command it installs is `aac` — the awscli precedent (dist name ≠
33
+ > command name).
34
+
35
+ Two personas, two invocation styles:
36
+
37
+ * **Installed operators** (`pip install aac-cli`) run the bare
38
+ command: `aac tenant register ...`
39
+ * **Repo developers** run it through the workspace without installing:
40
+ `uv run aac ...`
41
+
42
+ The examples below use the bare form; prefix `uv run` if you're in the
43
+ repo.
44
+
45
+ The Python argparse CLI ships the Stage-2 subset of §XII's full command
46
+ surface, grown since (B130 trust-anchor, B116 tenant verbs, B170
47
+ profiles — which replaced the earlier B116/B117 `aac configure`
48
+ family). This Python CLI IS the product CLI — the formerly-planned Go
49
+ rewrite was descoped (user ruling 2026-07-23; no separate Go
50
+ implementation is coming):
51
+
52
+ ```bash
53
+ # B170 — profiles are a first-class resource (CLI Config+Profile
54
+ # Spec). `main` is the reserved baseline; `aac configure` is GONE.
55
+ aac profile list # every profile incl. virtual [main]
56
+ aac profile show # the SELECTED profile, stored vs effective
57
+ aac profile show prod --output table
58
+ aac profile create dev --admin-url https://dev-admin.example.com \
59
+ --data-plane-url https://dev-data.example.com
60
+ aac profile update main # materialize/edit the baseline
61
+ aac profile delete dev # local-only; never touches the server
62
+
63
+ # B154 PR 3 — the tenant id is SERVER-ALLOCATED (canonical
64
+ # tnt-<lowercase UUIDv4>; there is no --tenant-id and you never choose
65
+ # one). Deployment note: this CLI requires a PR-3+ control plane —
66
+ # rollout order + compatibility matrix in RELEASING.md.
67
+ # The response prints the id EXACTLY ONCE with the api_key; the
68
+ # CLI stores the credential under it AND binds the SELECTED profile to
69
+ # it (a bound profile refuses registration — use an unbound one). A
70
+ # NAMED profile must exist before it can be selected (§6.1):
71
+ aac profile create acme
72
+ aac tenant register --profile acme --display-name "ACME Corporation" \
73
+ --contact ops@acme.example \
74
+ --workload-spiffe-id spiffe://acme.com/treasury-agent/v1 \
75
+ --tenant-admin-pubkey-file ./tenant-admin.public.pem \
76
+ --bootstrap-token dev-bootstrap-token-compose-only
77
+ # Workload authorities must already be lowercase. The path is
78
+ # case-sensitive, so spiffe://acme.com/Treasury-Agent is valid and is
79
+ # preserved exactly; spiffe://ACME.com/treasury-agent is rejected.
80
+ # If the response is lost, retry the exact frozen request with no stable
81
+ # arguments. The selected profile supplies its AAC-managed recovery state:
82
+ aac tenant register --profile acme
83
+ # Registration is a gated ceremony (Eng Spec §XVII.6): the control
84
+ # plane rejects it unless X-AAC-Bootstrap-Token matches its
85
+ # AAC_CP_BOOTSTRAP_TOKEN. --bootstrap-token falls back to
86
+ # $AAC_BOOTSTRAP_TOKEN (note: the CLI-side env var has no CP_ — the
87
+ # server-side name AAC_CP_BOOTSTRAP_TOKEN is NOT read by the CLI).
88
+
89
+ # B142 — developer-tier SELF-SERVE registration (no ceremony token):
90
+ # sign in with GitHub or Google; your verified identity becomes the
91
+ # tenant's first tenant-admin. Needs a deployment with self-serve
92
+ # enabled + a shared connection for the family.
93
+ aac profile create dev # (re)create it — deleted in the B170 example above
94
+ aac tenant register --profile dev --display-name "ACME Dev" \
95
+ --contact dev@acme.example --idp github
96
+ aac sso login --profile dev # thereafter: plain login
97
+
98
+ # B116 — admin-surface tenant inspection + attribute update.
99
+ # Tenant ids are the server-allocated canonical tnt-<uuid> form
100
+ # (B154 PR 5) — captured at registration / shown by `aac profile show`.
101
+ aac tenant list # which tenant ids exist (no 409 probe)
102
+ aac tenant describe --tenant-id tnt-<uuid> # workloads + key METADATA
103
+ aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"
104
+
105
+ # B121/B228 — business-domain control proof and bounded claim lifecycle.
106
+ # Registration can capture up to 16 pending rows with repeatable --tenant-domain.
107
+ # The issue/verify/release/revoke commands
108
+ # require the owning tenant's tenant-admin session and default --tenant-id from
109
+ # the profile; the established tenant describe command keeps its explicit id.
110
+ aac tenant register --display-name "ACME" --contact ops@acme.example \
111
+ --tenant-domain acme.example
112
+ aac tenant issue-domain-challenge --domain acme.example
113
+ # Publish the exact record printed above, then:
114
+ aac tenant verify-domain --domain acme.example
115
+ # B182 keeps verification and binding explicit. The binding command consumes
116
+ # current exact-name evidence (and lazily revalidates when due); verify-domain
117
+ # never performs an implicit binding write.
118
+ aac tenant bind-trust-domain --trust-domain acme.example
119
+ aac tenant list-trust-domains --output table
120
+ aac tenant describe --tenant-id tnt-<uuid> --output table # status + TXT
121
+ aac tenant release-domain --domain acme.example --reason "planned transfer"
122
+ aac tenant revoke-domain --domain acme.example --reason "DNS control incident"
123
+
124
+ # If bind-trust-domain reports missing proof, its printed issue/verify/rerun
125
+ # commands pin the selected profile, effective admin URL, tenant id, and exact
126
+ # domain. They never print a session, bootstrap token, or challenge value.
127
+
128
+ # B153 — replace the tenant-admin key. Accepts and transmits the PUBLIC half
129
+ # ONLY. It never generates or writes private-key material, and it refuses a
130
+ # private key without transmitting or logging it — it does read the file you
131
+ # name, because refusing requires inspecting it.
132
+ # Replacement is IMMEDIATE and forward-only: the previous key stops
133
+ # verifying ingest JWSs at once (admin keys get NO grace window) and can
134
+ # never be reinstated.
135
+ openssl genpkey -algorithm ed25519 -out tenant-admin.pem
136
+ openssl pkey -in tenant-admin.pem -pubout -out tenant-admin.public.pem
137
+ aac tenant rotate-admin-key --tenant-admin-pubkey-file tenant-admin.public.pem
138
+ # BEFORE running it in production: stage the new PRIVATE key on the
139
+ # trust-anchor publisher host and be ready to restart it. The restart
140
+ # procedure DIFFERS by deployment path — a Docker publisher's env and key
141
+ # bind-mount are fixed at container creation, so a plain restart comes back
142
+ # on the OLD key. Follow trust_anchor_publisher/INSTALL.md, "Rotating the
143
+ # tenant-admin key". Publication stays paused until it restarts.
144
+ # --bootstrap-token (or $AAC_BOOTSTRAP_TOKEN) lets a ceremony operator
145
+ # rotate for a tenant that has no session.
146
+
147
+ # B172 — post-registration workload lifecycle. --tenant-id is optional
148
+ # here: the selected profile supplies it by default. JSON is default;
149
+ # every command also supports --output table.
150
+ aac tenant add-workload --spiffe-id spiffe://acme.com/payroll/v1 \
151
+ --display-name "Payroll"
152
+ aac tenant list-workloads # ACTIVE only
153
+ aac tenant list-workloads --all # + DEACTIVATED history
154
+ aac tenant describe-workload --workload-id <opaque-id>
155
+ aac tenant update-workload --workload-id <opaque-id> \
156
+ --display-name "Payroll v2"
157
+ aac tenant update-workload --workload-id <opaque-id> --clear-display-name
158
+ aac tenant deactivate-workload --workload-id <opaque-id> \
159
+ --reason "service retired"
160
+ # Deactivation is terminal; there is no delete/reactivate command, and
161
+ # the exact SPIFFE ID remains globally reserved.
162
+
163
+ aac chain show --token-id <64-hex token or chain root> --tenant-id tnt-<uuid>
164
+ aac chain show --token-id <id> --output table
165
+ aac chain show --token-id <id> --render --render-out ./aeg.html # headless
166
+ aac chain show --token-id <id> --render --open # + browser
167
+
168
+ # B130 — YOUR tenant's trust-anchor state (what .well-known never
169
+ # shows: REVOKED keys, expired-grace history, lifecycle timestamps,
170
+ # tenant-admin key status, the ingest ledger). Bearer-authenticated;
171
+ # tenant scoping is implicit in the credential.
172
+ aac trust-anchor list --tenant-id tnt-<uuid> # both roles, all states
173
+ aac trust-anchor list --role tenant-admin # ingest-signing keys only
174
+ aac trust-anchor describe --kid key-1 # lifecycle + public PEM
175
+ aac trust-anchor ingest-history --artifact-class root-keys --limit 20
176
+ # Session-only emergency/environment-retirement operation. Terminal, may
177
+ # revoke the last root, and cannot be authorized by bootstrap token/API key:
178
+ aac trust-anchor revoke --tenant-id tnt-<uuid> --kid key-1 \
179
+ --reason "root signing key suspected compromised"
180
+
181
+ # B144 — before the first tenant IdP, Tenant Ops generates an Ed25519
182
+ # offline recovery keypair. The private PEM stays offline and mode 0600;
183
+ # only the non-secret enrollment JSON crosses to AAC Ops/control plane.
184
+ aac sso generate-idp-recovery-key \
185
+ --private-key-file offline-idp-recovery-private.pem \
186
+ --enrollment-file idp-recovery-enrollment.json
187
+ aac sso enroll-idp-recovery-key \
188
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
189
+ --file idp-recovery-enrollment.json \
190
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
191
+
192
+ # B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
193
+ # noun). The connection config rides as a JSON file (nested claims
194
+ # mapping — see --help for an Entra example). Initial recovery-key
195
+ # enrollment above happens before first-IdP registration. First-IdP
196
+ # registration is the ops ceremony; later IdPs ride a cached session:
197
+ # (--tenant-id takes the canonical tnt-<uuid> id captured at your
198
+ # tenant's registration — B154 PR 5: a dotted value fails locally.)
199
+ aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
200
+ --file connection.json \
201
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
202
+
203
+ # B151 — discover the opaque connection id and current revision without a
204
+ # session. The public response contains only safe issuer metadata, a stable
205
+ # idp_connections.id, and the numeric ETag needed for correction:
206
+ aac sso list-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
207
+
208
+ # B151 — correct the COMPLETE connection document. Pin the revision you
209
+ # reviewed; the CLI sends it with the cached tenant-admin session and never
210
+ # substitutes a newer revision. Repeating after a lost response is safe:
211
+ aac sso replace-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
212
+ --connection-id <id-from-list-idp> \
213
+ --revision <revision-from-list-idp> \
214
+ --file corrected-connection.json
215
+
216
+ # B144 — if the IdP is unavailable, the same B151 replacement is authorized
217
+ # by a three-command two-party ceremony. Tenant Ops signs the exact corrected
218
+ # file and receives a request id plus REPAIR-INTENT fingerprint. AAC Ops
219
+ # compares both through the incident channel, approves that exact intent, and
220
+ # manually notifies Tenant Ops. Tenant Ops then runs replace-idp with the same
221
+ # request/file/revision/key. The CLI internally redeems a ten-minute,
222
+ # single-use capability, holds it only in memory, and never prints or stores it.
223
+ # It grants no login, data-plane access, or general administration.
224
+ aac sso request-idp-repair \
225
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
226
+ --connection-id <id-from-list-idp> \
227
+ --revision <revision-from-list-idp> \
228
+ --file corrected-connection.json \
229
+ --recovery-key-file offline-idp-recovery-private.pem
230
+ aac sso approve-idp-repair \
231
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
232
+ --connection-id <id-from-list-idp> \
233
+ --repair-request-id <tenant-provided-request-id> \
234
+ --repair-intent-fingerprint sha256:<tenant-provided-intent-hash> \
235
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
236
+ aac sso replace-idp \
237
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
238
+ --connection-id <id-from-list-idp> \
239
+ --revision <same-request-revision> \
240
+ --file corrected-connection.json \
241
+ --repair-request-id <approved-request-id> \
242
+ --recovery-key-file offline-idp-recovery-private.pem
243
+
244
+ # These fingerprints are different: recovery_key_fingerprint identifies the
245
+ # enrolled Ed25519 PUBLIC key; repair_intent_fingerprint identifies one exact
246
+ # tenant/connection/revision/document/key intent. Neither replaces the other.
247
+
248
+ # Recovery-key lifecycle is normal-session administration. Generate a fresh
249
+ # pair before rotation; old material cannot return. Revocation is terminal.
250
+ aac sso list-idp-recovery-keys --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
251
+ aac sso rotate-idp-recovery-key \
252
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
253
+ --file new-idp-recovery-enrollment.json \
254
+ --replaces-recovery-key-id <current-active-key-id>
255
+ aac sso revoke-idp-recovery-key \
256
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
257
+ --recovery-key-id <current-active-key-id> \
258
+ --reason "offline media lost"
259
+
260
+ # B142 — register a SHARED developer-tier connection (platform
261
+ # operator ceremony ONLY; owned by no tenant). GitHub registers with
262
+ # jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
263
+ # provider); Google registers via discovery + public_client_secret:
264
+ aac sso register-idp --shared --file shared-github.json \
265
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
266
+
267
+ # B141 — sign in via your tenant's IdP (aws sso login semantics).
268
+ # Device flow by default (the browser can be on ANY machine — headless
269
+ # friendly); Google-family connections use loopback PKCE (Google's
270
+ # device grant is scope-restricted). Success caches a short-lived AAC
271
+ # session token AND writes the config profile in one motion; every
272
+ # admin verb (tenant list/describe/update, self-serve register-idp)
273
+ # then rides the session automatically.
274
+ aac sso login --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
275
+ aac sso login --issuer https://login.microsoftonline.com/<tid>/v2.0 # >1 IdP
276
+ aac sso login --flow pkce --no-browser # print the URL, don't launch
277
+
278
+ # Sessions are REFRESH-LESS by design (Eng Spec §XVII.2): on expiry
279
+ # any admin verb tells you to re-run `aac sso login` — there is no
280
+ # refresh token to steal, and no silent re-authentication.
281
+
282
+ # B155 — session niceties (cache-file operations; the CLI never
283
+ # parses the token itself). whoami exits 0 = live, 3 = expired/none (§14).
284
+ aac sso whoami --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
285
+ aac sso logout --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
286
+ ```
287
+
288
+ Naming: console script = `aac` (the §XVI-reserved name); import
289
+ package = `aac_cli` (the SDK owns the `aac` import package);
290
+ distribution = `aac-cli` (decided at first release, B119: bare `aac`
291
+ on PyPI belongs to an unrelated active project and is not claimable).
292
+
293
+ ## Configuration
294
+
295
+ Profiles live in `~/.aac/config` (UTF-8 INI with DIRECT sections —
296
+ `[main]`, `[dev]`, `[prod]`, never `[profile dev]`; override the
297
+ directory with `AAC_CLI_HOME`). `aac profile create/update` are the
298
+ writers; the file stays hand-editable INI, but note a rewrite does not
299
+ preserve comments (aws-cli behaves the same):
300
+
301
+ ```ini
302
+ [main]
303
+ admin_url = http://127.0.0.1:8000
304
+ data_plane_url = http://127.0.0.1:9000
305
+ tenant_id = tnt-550e8400-e29b-41d4-a716-446655440000
306
+
307
+ [dev]
308
+ admin_url = https://dev-admin.example.com
309
+ data_plane_url = https://dev-data.example.com
310
+ ```
311
+
312
+ **`main` is the reserved baseline profile.** It always exists —
313
+ virtually (built-in localhost defaults) until `aac profile update
314
+ main` materializes it — and cannot be created or deleted. Profile
315
+ SELECTION for operational commands is `--profile <name>` >
316
+ `AAC_PROFILE` > `main`; a selected profile that doesn't exist is an
317
+ error, never a silent fallback. Prototype-era `[default]` sections
318
+ migrate to `[main]` automatically on first touch (both-exist-and-
319
+ differ fails closed with manual remediation).
320
+
321
+ **`tenant_id` is system-managed.** Profile commands display it but
322
+ never prompt for or accept it — only authenticated workflows
323
+ (`aac tenant register`, `aac sso login`) write it. Canonical
324
+ `tnt-<uuid>` validation is MANDATORY (B154 PR 5): a profile holding a
325
+ prototype-era dotted value (`acme.com`) now fails every command, exit
326
+ 3, with remediation printed (delete the stale `tenant_id` line, then
327
+ re-register or `aac sso login` to bind a canonical id).
328
+
329
+ Setting precedence within the selected profile: flag > env
330
+ (`AAC_ADMIN_URL` / `AAC_DATA_PLANE_URL` / `AAC_TENANT_ID`; also
331
+ `AAC_BOOTSTRAP_TOKEN` for `tenant register --bootstrap-token`) >
332
+ profile > localhost defaults. `aac profile show` prints stored AND
333
+ effective values with each one's source (`flag` / `env:AAC_*` /
334
+ `profile:<name>` / `default` / `unset`).
335
+
336
+ Guardrails (deliberate bounded complexity): at most 50 profiles
337
+ including `main`, 50 keys per section, 30-character keys, and
338
+ 512-character values — exceeding any is a deterministic exit-3
339
+ invalid-config error. Unknown keys are preserved by writes and
340
+ reported (by name) by `aac profile show`.
341
+
342
+ `~/.aac/state/tenant-registrations/<profile>.json` is AAC-managed
343
+ registration recovery state (one record per profile; dir 0700, file
344
+ 0600). Do not edit, rename, copy between profiles, or delete these
345
+ files while a registration is unresolved — deleting one cannot cancel
346
+ a request that may already have committed. A profile with such a
347
+ record cannot be deleted. `aac tenant register` creates the record before
348
+ authentication or HTTP and holds a nonblocking per-profile process lock across
349
+ the entire attempt. A transport interruption retains the same request id and
350
+ frozen stable intent; a bare retry resumes it with fresh ceremony/evidence
351
+ credentials. Supplying identical stable arguments is also accepted, while a
352
+ difference fails locally before network access. The CLI deletes pending state
353
+ only after success is safely persisted or the server returns the exact paired
354
+ `Idempotency-Key` + `AAC-Registration-Status: not_created` guarantee.
355
+
356
+ B183 applies the same canonical workload-ID boundary before either a durable
357
+ registration-state write or an add-workload network request: a non-lowercase SPIFFE
358
+ authority is rejected, never normalized. An accepted path retains its exact
359
+ case in the frozen stable intent, fingerprint input, state file, and every
360
+ retry comparison.
361
+
362
+ A completed server recovery returns tenant identity but cannot replay the
363
+ one-time API key. The CLI binds the profile and transitions the state to
364
+ `completed_credential_reissue_required`, directing the operator to the
365
+ separate B173 reissue capability; B171 does not implement reissue.
366
+
367
+ ### Replace or recover a tenant API key (B173)
368
+
369
+ After establishing an own-tenant `tenant-admin` session, run:
370
+
371
+ ```console
372
+ aac tenant reissue-api-key \
373
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
374
+ --profile production
375
+ ```
376
+
377
+ This is immediate replacement, not B98/B189 graceful rotation: the server
378
+ revokes every ACTIVE tenant API key and returns one fresh `aac_ak_*` value once.
379
+ The CLI makes one HTTP request per invocation and atomically replaces the
380
+ mode-`0600` credential file only after validating the complete success
381
+ response. A transport interruption leaves the local file untouched and tells
382
+ the administrator to run the command again; the next explicit invocation is a
383
+ fresh attempt that revokes any unknown prior result. There is no automatic
384
+ retry or secret-response journal. A successful B173 replacement also removes
385
+ any superseded non-secret B189 local rotation record. It does not silently
386
+ delete the separate staged key file; that value is now revoked and must be
387
+ removed through the approved secure-file procedure.
388
+
389
+ After success the CLI prints copy/paste-ready `aac trust-anchor list` and curl
390
+ commands for API-key-only verification against `GET /v1/trust-anchors`. The
391
+ curl recipe streams the stored credential through stdin configuration rather
392
+ than writing plaintext into shell history or an OS process argument.
393
+
394
+ Operator-assisted onboarding recovery is exceptional and requires both the
395
+ explicitly opened bootstrap ceremony and the matching completed registration
396
+ locator. Load `AAC_BOOTSTRAP_TOKEN` through the approved secret-handling
397
+ process, then run:
398
+
399
+ ```console
400
+ aac tenant reissue-api-key \
401
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
402
+ --registration-request-id treq-550e8400-e29b-41d4-a716-446655440000 \
403
+ --profile onboarding
404
+ ```
405
+
406
+ The server admits this only before a tenant login path exists. The `treq-*`
407
+ value is a locator, never a credential. Tenant API keys are separate from
408
+ tenant-admin Ed25519 keys and B144 offline IdP-recovery material. See
409
+ **SOP-010 — Recover or replace a tenant API key** in the AAC design vault's
410
+ `sops/` directory for the tenant-administrator and AAC Ops procedures.
411
+
412
+ ### Rotate a tenant API key without downtime (B189)
413
+
414
+ Planned rotation uses a nested command family and an own-tenant
415
+ `tenant-admin` AAC session:
416
+
417
+ ```console
418
+ aac tenant api-key list --profile production
419
+ aac tenant api-key issue --profile production
420
+ ```
421
+
422
+ `issue` saves a non-secret `akr-*` issuance identity before sending one HTTP
423
+ request. It writes the returned plaintext only to a new mode-`0600` staged
424
+ file (default `~/.aac/credentials/<tenant-id>.next`), refuses existing files
425
+ and symlinks, requires any custom parent directory to exist, preserves that
426
+ directory's permissions, and never replaces the current profile credential. Its success
427
+ output contains metadata and a curl recipe that reads the staged file without
428
+ putting plaintext in shell history or process arguments.
429
+
430
+ Move every client to the staged key through the approved secret store, verify
431
+ client traffic, then list again and select the old key by its exact non-secret
432
+ `key_id`:
433
+
434
+ ```console
435
+ aac tenant api-key retire \
436
+ --key-id ak_0123456789abcdef \
437
+ --yes \
438
+ --profile production
439
+ ```
440
+
441
+ Retirement requires two ACTIVE keys and is terminal. The CLI fetches the
442
+ authoritative set immediately before the request, names the exact key that will
443
+ remain, and never guesses by age or position. It does not silently promote the
444
+ staged file into the current credential location. Do that only through the
445
+ approved local secret-handling procedure after the selected old key retires.
446
+
447
+ If an issue response is ambiguous, rerun the same command. The saved identity
448
+ is reused: a non-committed attempt may return the new plaintext, while a
449
+ committed attempt reports that plaintext is unavailable and identifies the
450
+ unknown new row through `api-key list`. Retire that unknown row, leaving the
451
+ known old credential ACTIVE, then issue again. No automatic retry or plaintext
452
+ journal exists. A profile with unresolved rotation state cannot be deleted.
453
+ See **SOP-011 — Rotate a tenant API key without downtime** in the design vault.
454
+
455
+ `~/.aac/idp-repair-requests/<tenant_id>/<connection_id>.json` is B144's
456
+ separate AAC-managed IdP repair record (directory 0700, file 0600). It is
457
+ written before `request-idp-repair` transmits anything and contains the exact
458
+ connection document, document digest, request id, recovery-key fingerprint,
459
+ and public signature. It contains neither the offline private key nor the
460
+ short-lived AAC repair capability. An identical retry reuses the request id;
461
+ a different live intent is refused locally. Preserve this file with the
462
+ incident until replacement is complete, and do not edit, rename, copy between
463
+ tenants, or delete it to “cancel” a request that may already exist server-side.
464
+
465
+ ### `AAC_CLI_HOME` — isolated homes
466
+
467
+ `AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
468
+ credentials). Two everyday uses:
469
+
470
+ ```bash
471
+ # 1. Throwaway smoke/test home — nothing touches your real config:
472
+ AAC_CLI_HOME=/tmp/aac-smoke aac tenant register --display-name "Smoke" ...
473
+
474
+ # 2. Multi-account operators — hard-wall separation beyond profiles
475
+ # (separate credential files, not just separate config sections):
476
+ alias aac-prod='AAC_CLI_HOME=$HOME/.aac-prod aac'
477
+ alias aac-staging='AAC_CLI_HOME=$HOME/.aac-staging aac'
478
+ ```
479
+
480
+ Profiles share one credentials directory; `AAC_CLI_HOME` gives each
481
+ context its own.
482
+
483
+ Credentials — TWO kinds coexist in `~/.aac/credentials/` (documented
484
+ coexistence, Eng Spec §XVII.9):
485
+
486
+ * `{tenant_id}` — the bare `aac_ak_...` string at mode 0600, written
487
+ by `aac tenant register` (the key is printed EXACTLY ONCE — only its
488
+ peppered hash exists server-side) and presented as `Authorization:
489
+ Bearer` by the DATA-PLANE verbs (`chain show`, `trust-anchor *`).
490
+ Same bare-string format the joined compose smoke writes into sidecar
491
+ key dirs — this file never grows structure.
492
+ * `{tenant_id}.session` — JSON, mode 0600, written by `aac sso login`:
493
+ the short-lived AAC session token + its client-computed expiry. The
494
+ ADMIN-surface verbs ride it automatically. A session within 30s of
495
+ expiry fails fast locally with the re-login message instead of
496
+ dialing out; the server's own session 401s carry the same
497
+ remediation (the server stays authoritative). `aac sso logout`
498
+ removes the file; `aac sso whoami` reports its tenant + expiry
499
+ (B155 — both operate on the cache file only).
500
+
501
+ ## Your tenant_id
502
+
503
+ `tenant_id` is your org's FEDERATION IDENTIFIER — the stable handle
504
+ every other tenant verifies your signatures under: it appears in
505
+ `X-AAC-Originator-Tenant-Id` headers, `/.well-known/aac-root-keys/
506
+ {tenant_id}` paths, SIEM stream directories, and supplier whitelists.
507
+ You do NOT choose it (B154 PR 5, Tenant Identifier Policy): the
508
+ control plane allocates the canonical **`tnt-<lowercase UUIDv4>`**
509
+ (exactly 40 characters, e.g.
510
+ `tnt-550e8400-e29b-41d4-a716-446655440000`) at registration, the CLI
511
+ captures it into the selected profile, and it is immutable, public,
512
+ and never reused — like an AWS account ID. `--parent-tenant-id` takes
513
+ the parent org's canonical `tnt-` id — the server resolves handles to
514
+ internal row ids; no database identifier ever crosses the API (B118
515
+ D3/D4). B121 domain ownership verification is a separate, additive
516
+ control tracked in `tenant_domains`: `_aac-challenge.<domain>.` TXT
517
+ `aac-verify=<token>`, issued/verified through the tenant-admin commands
518
+ above. A verified business domain is evidence only. B182's separate
519
+ `bind-trust-domain` request admits a domain's first SPIFFE binding only from
520
+ B228-current evidence for the exact same name; parent-domain proof does not
521
+ cover subdomains. SPIFFE-valid values outside the DNS proof grammar remain
522
+ ceremony-only, and existing binding history remains B181.
523
+
524
+ ## Semantics worth knowing
525
+
526
+ * `aac chain show` accepts ANY hop's token id or the chain root id
527
+ (`--root-token-id` is an accepted alias); visibility is
528
+ participant-tenant over the symmetric composite closure (Week 12) —
529
+ non-participants get the same 404 as unknown tokens.
530
+ * Timelines are §V.2.1 METADATA-TIER: predicates and business
531
+ narratives stay in your tenant SIEM stream; the table output's
532
+ footer says so (§6.1 federated join is the full-fidelity story).
533
+ * Exit codes (CLI Config+Profile Spec §14, ratified 2026-08-04 —
534
+ supersedes the originator-cli-era convention): 0 success / 1 remote
535
+ outcome (server rejected/denied, or the server-side resource doesn't
536
+ exist) / 2 invalid command-line usage / 3 invalid local
537
+ configuration or state (missing profile/credential, malformed INI,
538
+ bad registration state) / 4 transport failure (unreachable or timed
539
+ out — the retryable class).
540
+ * B159 rate limiting: a numeric server `Retry-After` is rendered beside
541
+ the stable 429/503 error. The CLI never automatically replays a
542
+ mutation; wait for the displayed interval and rerun deliberately.
543
+
544
+ ## Tests
545
+
546
+ `uv run pytest cli/aac/tests/` — pytest-httpx mocks; no control plane
547
+ needed. Against a live stack: bring up the joined topology
548
+ (`./bin/run-wedge-a-control-plane-compose.sh --keep-up`) and point the
549
+ flags at localhost.