aac-cli 0.1.1__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 (33) hide show
  1. {aac_cli-0.1.1 → aac_cli-0.1.2}/PKG-INFO +228 -6
  2. {aac_cli-0.1.1 → aac_cli-0.1.2}/README.md +226 -5
  3. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli/__init__.py +1 -1
  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.1 → aac_cli-0.1.2}/aac_cli/config.py +27 -37
  8. aac_cli-0.1.2/aac_cli/idp_recovery.py +489 -0
  9. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli/profiles.py +16 -14
  10. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli/registration_state.py +135 -75
  11. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli/sso_login.py +55 -34
  12. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/PKG-INFO +228 -6
  13. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/SOURCES.txt +8 -0
  14. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/requires.txt +1 -0
  15. {aac_cli-0.1.1 → aac_cli-0.1.2}/pyproject.toml +5 -2
  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.1 → aac_cli-0.1.2}/tests/test_profile_cli.py +32 -73
  23. {aac_cli-0.1.1 → aac_cli-0.1.2}/tests/test_registration_recovery_cli.py +67 -16
  24. {aac_cli-0.1.1 → aac_cli-0.1.2}/tests/test_registration_state.py +71 -35
  25. {aac_cli-0.1.1 → aac_cli-0.1.2}/tests/test_sso_login_cli.py +204 -132
  26. aac_cli-0.1.1/aac_cli/cli.py +0 -2780
  27. aac_cli-0.1.1/tests/test_aac_cli.py +0 -1734
  28. {aac_cli-0.1.1 → aac_cli-0.1.2}/LICENSE +0 -0
  29. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli/__main__.py +0 -0
  30. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/dependency_links.txt +0 -0
  31. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/entry_points.txt +0 -0
  32. {aac_cli-0.1.1 → aac_cli-0.1.2}/aac_cli.egg-info/top_level.txt +0 -0
  33. {aac_cli-0.1.1 → aac_cli-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aac-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
5
5
  Author: Agent Authority Cloud Project
6
6
  License-Expression: Apache-2.0
@@ -8,6 +8,7 @@ Requires-Python: >=3.10
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
10
  Requires-Dist: httpx>=0.28
11
+ Requires-Dist: cryptography>=42.0
11
12
  Provides-Extra: test
12
13
  Requires-Dist: pytest>=8.0; extra == "test"
13
14
  Requires-Dist: pytest-httpx>=0.30; extra == "test"
@@ -101,6 +102,48 @@ aac tenant list # which tenant ids exist (no 409
101
102
  aac tenant describe --tenant-id tnt-<uuid> # workloads + key METADATA
102
103
  aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"
103
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
+
104
147
  # B172 — post-registration workload lifecycle. --tenant-id is optional
105
148
  # here: the selected profile supplies it by default. JSON is default;
106
149
  # every command also supports --output table.
@@ -130,17 +173,90 @@ aac trust-anchor list --tenant-id tnt-<uuid> # both roles, all states
130
173
  aac trust-anchor list --role tenant-admin # ingest-signing keys only
131
174
  aac trust-anchor describe --kid key-1 # lifecycle + public PEM
132
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"
133
191
 
134
192
  # B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
135
193
  # noun). The connection config rides as a JSON file (nested claims
136
- # mapping — see --help for an Entra example). First-IdP onboarding is
137
- # the ops ceremony; later IdPs ride your cached session instead:
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:
138
197
  # (--tenant-id takes the canonical tnt-<uuid> id captured at your
139
198
  # tenant's registration — B154 PR 5: a dotted value fails locally.)
140
199
  aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
141
200
  --file connection.json \
142
201
  --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
143
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
+
144
260
  # B142 — register a SHARED developer-tier connection (platform
145
261
  # operator ceremony ONLY; owned by no tenant). GitHub registers with
146
262
  # jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
@@ -248,6 +364,104 @@ one-time API key. The CLI binds the profile and transitions the state to
248
364
  `completed_credential_reissue_required`, directing the operator to the
249
365
  separate B173 reissue capability; B171 does not implement reissue.
250
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
+
251
465
  ### `AAC_CLI_HOME` — isolated homes
252
466
 
253
467
  `AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
@@ -298,9 +512,14 @@ captures it into the selected profile, and it is immutable, public,
298
512
  and never reused — like an AWS account ID. `--parent-tenant-id` takes
299
513
  the parent org's canonical `tnt-` id — the server resolves handles to
300
514
  internal row ids; no database identifier ever crosses the API (B118
301
- D3/D4). Domain ownership verification (DNS TXT) is a separate,
302
- additive control tracked in the `tenant_domains` table — the challenge
303
- flow ships in a follow-up increment.
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.
304
523
 
305
524
  ## Semantics worth knowing
306
525
 
@@ -318,6 +537,9 @@ flow ships in a follow-up increment.
318
537
  configuration or state (missing profile/credential, malformed INI,
319
538
  bad registration state) / 4 transport failure (unreachable or timed
320
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.
321
543
 
322
544
  ## Tests
323
545
 
@@ -86,6 +86,48 @@ aac tenant list # which tenant ids exist (no 409
86
86
  aac tenant describe --tenant-id tnt-<uuid> # workloads + key METADATA
87
87
  aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"
88
88
 
89
+ # B121/B228 — business-domain control proof and bounded claim lifecycle.
90
+ # Registration can capture up to 16 pending rows with repeatable --tenant-domain.
91
+ # The issue/verify/release/revoke commands
92
+ # require the owning tenant's tenant-admin session and default --tenant-id from
93
+ # the profile; the established tenant describe command keeps its explicit id.
94
+ aac tenant register --display-name "ACME" --contact ops@acme.example \
95
+ --tenant-domain acme.example
96
+ aac tenant issue-domain-challenge --domain acme.example
97
+ # Publish the exact record printed above, then:
98
+ aac tenant verify-domain --domain acme.example
99
+ # B182 keeps verification and binding explicit. The binding command consumes
100
+ # current exact-name evidence (and lazily revalidates when due); verify-domain
101
+ # never performs an implicit binding write.
102
+ aac tenant bind-trust-domain --trust-domain acme.example
103
+ aac tenant list-trust-domains --output table
104
+ aac tenant describe --tenant-id tnt-<uuid> --output table # status + TXT
105
+ aac tenant release-domain --domain acme.example --reason "planned transfer"
106
+ aac tenant revoke-domain --domain acme.example --reason "DNS control incident"
107
+
108
+ # If bind-trust-domain reports missing proof, its printed issue/verify/rerun
109
+ # commands pin the selected profile, effective admin URL, tenant id, and exact
110
+ # domain. They never print a session, bootstrap token, or challenge value.
111
+
112
+ # B153 — replace the tenant-admin key. Accepts and transmits the PUBLIC half
113
+ # ONLY. It never generates or writes private-key material, and it refuses a
114
+ # private key without transmitting or logging it — it does read the file you
115
+ # name, because refusing requires inspecting it.
116
+ # Replacement is IMMEDIATE and forward-only: the previous key stops
117
+ # verifying ingest JWSs at once (admin keys get NO grace window) and can
118
+ # never be reinstated.
119
+ openssl genpkey -algorithm ed25519 -out tenant-admin.pem
120
+ openssl pkey -in tenant-admin.pem -pubout -out tenant-admin.public.pem
121
+ aac tenant rotate-admin-key --tenant-admin-pubkey-file tenant-admin.public.pem
122
+ # BEFORE running it in production: stage the new PRIVATE key on the
123
+ # trust-anchor publisher host and be ready to restart it. The restart
124
+ # procedure DIFFERS by deployment path — a Docker publisher's env and key
125
+ # bind-mount are fixed at container creation, so a plain restart comes back
126
+ # on the OLD key. Follow trust_anchor_publisher/INSTALL.md, "Rotating the
127
+ # tenant-admin key". Publication stays paused until it restarts.
128
+ # --bootstrap-token (or $AAC_BOOTSTRAP_TOKEN) lets a ceremony operator
129
+ # rotate for a tenant that has no session.
130
+
89
131
  # B172 — post-registration workload lifecycle. --tenant-id is optional
90
132
  # here: the selected profile supplies it by default. JSON is default;
91
133
  # every command also supports --output table.
@@ -115,17 +157,90 @@ aac trust-anchor list --tenant-id tnt-<uuid> # both roles, all states
115
157
  aac trust-anchor list --role tenant-admin # ingest-signing keys only
116
158
  aac trust-anchor describe --kid key-1 # lifecycle + public PEM
117
159
  aac trust-anchor ingest-history --artifact-class root-keys --limit 20
160
+ # Session-only emergency/environment-retirement operation. Terminal, may
161
+ # revoke the last root, and cannot be authorized by bootstrap token/API key:
162
+ aac trust-anchor revoke --tenant-id tnt-<uuid> --kid key-1 \
163
+ --reason "root signing key suspected compromised"
164
+
165
+ # B144 — before the first tenant IdP, Tenant Ops generates an Ed25519
166
+ # offline recovery keypair. The private PEM stays offline and mode 0600;
167
+ # only the non-secret enrollment JSON crosses to AAC Ops/control plane.
168
+ aac sso generate-idp-recovery-key \
169
+ --private-key-file offline-idp-recovery-private.pem \
170
+ --enrollment-file idp-recovery-enrollment.json
171
+ aac sso enroll-idp-recovery-key \
172
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
173
+ --file idp-recovery-enrollment.json \
174
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
118
175
 
119
176
  # B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
120
177
  # noun). The connection config rides as a JSON file (nested claims
121
- # mapping — see --help for an Entra example). First-IdP onboarding is
122
- # the ops ceremony; later IdPs ride your cached session instead:
178
+ # mapping — see --help for an Entra example). Initial recovery-key
179
+ # enrollment above happens before first-IdP registration. First-IdP
180
+ # registration is the ops ceremony; later IdPs ride a cached session:
123
181
  # (--tenant-id takes the canonical tnt-<uuid> id captured at your
124
182
  # tenant's registration — B154 PR 5: a dotted value fails locally.)
125
183
  aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
126
184
  --file connection.json \
127
185
  --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
128
186
 
187
+ # B151 — discover the opaque connection id and current revision without a
188
+ # session. The public response contains only safe issuer metadata, a stable
189
+ # idp_connections.id, and the numeric ETag needed for correction:
190
+ aac sso list-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
191
+
192
+ # B151 — correct the COMPLETE connection document. Pin the revision you
193
+ # reviewed; the CLI sends it with the cached tenant-admin session and never
194
+ # substitutes a newer revision. Repeating after a lost response is safe:
195
+ aac sso replace-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
196
+ --connection-id <id-from-list-idp> \
197
+ --revision <revision-from-list-idp> \
198
+ --file corrected-connection.json
199
+
200
+ # B144 — if the IdP is unavailable, the same B151 replacement is authorized
201
+ # by a three-command two-party ceremony. Tenant Ops signs the exact corrected
202
+ # file and receives a request id plus REPAIR-INTENT fingerprint. AAC Ops
203
+ # compares both through the incident channel, approves that exact intent, and
204
+ # manually notifies Tenant Ops. Tenant Ops then runs replace-idp with the same
205
+ # request/file/revision/key. The CLI internally redeems a ten-minute,
206
+ # single-use capability, holds it only in memory, and never prints or stores it.
207
+ # It grants no login, data-plane access, or general administration.
208
+ aac sso request-idp-repair \
209
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
210
+ --connection-id <id-from-list-idp> \
211
+ --revision <revision-from-list-idp> \
212
+ --file corrected-connection.json \
213
+ --recovery-key-file offline-idp-recovery-private.pem
214
+ aac sso approve-idp-repair \
215
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
216
+ --connection-id <id-from-list-idp> \
217
+ --repair-request-id <tenant-provided-request-id> \
218
+ --repair-intent-fingerprint sha256:<tenant-provided-intent-hash> \
219
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
220
+ aac sso replace-idp \
221
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
222
+ --connection-id <id-from-list-idp> \
223
+ --revision <same-request-revision> \
224
+ --file corrected-connection.json \
225
+ --repair-request-id <approved-request-id> \
226
+ --recovery-key-file offline-idp-recovery-private.pem
227
+
228
+ # These fingerprints are different: recovery_key_fingerprint identifies the
229
+ # enrolled Ed25519 PUBLIC key; repair_intent_fingerprint identifies one exact
230
+ # tenant/connection/revision/document/key intent. Neither replaces the other.
231
+
232
+ # Recovery-key lifecycle is normal-session administration. Generate a fresh
233
+ # pair before rotation; old material cannot return. Revocation is terminal.
234
+ aac sso list-idp-recovery-keys --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
235
+ aac sso rotate-idp-recovery-key \
236
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
237
+ --file new-idp-recovery-enrollment.json \
238
+ --replaces-recovery-key-id <current-active-key-id>
239
+ aac sso revoke-idp-recovery-key \
240
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
241
+ --recovery-key-id <current-active-key-id> \
242
+ --reason "offline media lost"
243
+
129
244
  # B142 — register a SHARED developer-tier connection (platform
130
245
  # operator ceremony ONLY; owned by no tenant). GitHub registers with
131
246
  # jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
@@ -233,6 +348,104 @@ one-time API key. The CLI binds the profile and transitions the state to
233
348
  `completed_credential_reissue_required`, directing the operator to the
234
349
  separate B173 reissue capability; B171 does not implement reissue.
235
350
 
351
+ ### Replace or recover a tenant API key (B173)
352
+
353
+ After establishing an own-tenant `tenant-admin` session, run:
354
+
355
+ ```console
356
+ aac tenant reissue-api-key \
357
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
358
+ --profile production
359
+ ```
360
+
361
+ This is immediate replacement, not B98/B189 graceful rotation: the server
362
+ revokes every ACTIVE tenant API key and returns one fresh `aac_ak_*` value once.
363
+ The CLI makes one HTTP request per invocation and atomically replaces the
364
+ mode-`0600` credential file only after validating the complete success
365
+ response. A transport interruption leaves the local file untouched and tells
366
+ the administrator to run the command again; the next explicit invocation is a
367
+ fresh attempt that revokes any unknown prior result. There is no automatic
368
+ retry or secret-response journal. A successful B173 replacement also removes
369
+ any superseded non-secret B189 local rotation record. It does not silently
370
+ delete the separate staged key file; that value is now revoked and must be
371
+ removed through the approved secure-file procedure.
372
+
373
+ After success the CLI prints copy/paste-ready `aac trust-anchor list` and curl
374
+ commands for API-key-only verification against `GET /v1/trust-anchors`. The
375
+ curl recipe streams the stored credential through stdin configuration rather
376
+ than writing plaintext into shell history or an OS process argument.
377
+
378
+ Operator-assisted onboarding recovery is exceptional and requires both the
379
+ explicitly opened bootstrap ceremony and the matching completed registration
380
+ locator. Load `AAC_BOOTSTRAP_TOKEN` through the approved secret-handling
381
+ process, then run:
382
+
383
+ ```console
384
+ aac tenant reissue-api-key \
385
+ --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
386
+ --registration-request-id treq-550e8400-e29b-41d4-a716-446655440000 \
387
+ --profile onboarding
388
+ ```
389
+
390
+ The server admits this only before a tenant login path exists. The `treq-*`
391
+ value is a locator, never a credential. Tenant API keys are separate from
392
+ tenant-admin Ed25519 keys and B144 offline IdP-recovery material. See
393
+ **SOP-010 — Recover or replace a tenant API key** in the AAC design vault's
394
+ `sops/` directory for the tenant-administrator and AAC Ops procedures.
395
+
396
+ ### Rotate a tenant API key without downtime (B189)
397
+
398
+ Planned rotation uses a nested command family and an own-tenant
399
+ `tenant-admin` AAC session:
400
+
401
+ ```console
402
+ aac tenant api-key list --profile production
403
+ aac tenant api-key issue --profile production
404
+ ```
405
+
406
+ `issue` saves a non-secret `akr-*` issuance identity before sending one HTTP
407
+ request. It writes the returned plaintext only to a new mode-`0600` staged
408
+ file (default `~/.aac/credentials/<tenant-id>.next`), refuses existing files
409
+ and symlinks, requires any custom parent directory to exist, preserves that
410
+ directory's permissions, and never replaces the current profile credential. Its success
411
+ output contains metadata and a curl recipe that reads the staged file without
412
+ putting plaintext in shell history or process arguments.
413
+
414
+ Move every client to the staged key through the approved secret store, verify
415
+ client traffic, then list again and select the old key by its exact non-secret
416
+ `key_id`:
417
+
418
+ ```console
419
+ aac tenant api-key retire \
420
+ --key-id ak_0123456789abcdef \
421
+ --yes \
422
+ --profile production
423
+ ```
424
+
425
+ Retirement requires two ACTIVE keys and is terminal. The CLI fetches the
426
+ authoritative set immediately before the request, names the exact key that will
427
+ remain, and never guesses by age or position. It does not silently promote the
428
+ staged file into the current credential location. Do that only through the
429
+ approved local secret-handling procedure after the selected old key retires.
430
+
431
+ If an issue response is ambiguous, rerun the same command. The saved identity
432
+ is reused: a non-committed attempt may return the new plaintext, while a
433
+ committed attempt reports that plaintext is unavailable and identifies the
434
+ unknown new row through `api-key list`. Retire that unknown row, leaving the
435
+ known old credential ACTIVE, then issue again. No automatic retry or plaintext
436
+ journal exists. A profile with unresolved rotation state cannot be deleted.
437
+ See **SOP-011 — Rotate a tenant API key without downtime** in the design vault.
438
+
439
+ `~/.aac/idp-repair-requests/<tenant_id>/<connection_id>.json` is B144's
440
+ separate AAC-managed IdP repair record (directory 0700, file 0600). It is
441
+ written before `request-idp-repair` transmits anything and contains the exact
442
+ connection document, document digest, request id, recovery-key fingerprint,
443
+ and public signature. It contains neither the offline private key nor the
444
+ short-lived AAC repair capability. An identical retry reuses the request id;
445
+ a different live intent is refused locally. Preserve this file with the
446
+ incident until replacement is complete, and do not edit, rename, copy between
447
+ tenants, or delete it to “cancel” a request that may already exist server-side.
448
+
236
449
  ### `AAC_CLI_HOME` — isolated homes
237
450
 
238
451
  `AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
@@ -283,9 +496,14 @@ captures it into the selected profile, and it is immutable, public,
283
496
  and never reused — like an AWS account ID. `--parent-tenant-id` takes
284
497
  the parent org's canonical `tnt-` id — the server resolves handles to
285
498
  internal row ids; no database identifier ever crosses the API (B118
286
- D3/D4). Domain ownership verification (DNS TXT) is a separate,
287
- additive control tracked in the `tenant_domains` table — the challenge
288
- flow ships in a follow-up increment.
499
+ D3/D4). B121 domain ownership verification is a separate, additive
500
+ control tracked in `tenant_domains`: `_aac-challenge.<domain>.` TXT
501
+ `aac-verify=<token>`, issued/verified through the tenant-admin commands
502
+ above. A verified business domain is evidence only. B182's separate
503
+ `bind-trust-domain` request admits a domain's first SPIFFE binding only from
504
+ B228-current evidence for the exact same name; parent-domain proof does not
505
+ cover subdomains. SPIFFE-valid values outside the DNS proof grammar remain
506
+ ceremony-only, and existing binding history remains B181.
289
507
 
290
508
  ## Semantics worth knowing
291
509
 
@@ -303,6 +521,9 @@ flow ships in a follow-up increment.
303
521
  configuration or state (missing profile/credential, malformed INI,
304
522
  bad registration state) / 4 transport failure (unreachable or timed
305
523
  out — the retryable class).
524
+ * B159 rate limiting: a numeric server `Retry-After` is rendered beside
525
+ the stable 429/503 error. The CLI never automatically replays a
526
+ mutation; wait for the displayed interval and rerun deliberately.
306
527
 
307
528
  ## Tests
308
529
 
@@ -10,4 +10,4 @@ entrypoint (`aac_cli.cli:entrypoint`).
10
10
  # test in bin/tests/test_cli_version_parity.py. A literal keeps the
11
11
  # shipped code free of import-time metadata lookups (and their
12
12
  # not-installed failure mode).
13
- __version__ = "0.1.1"
13
+ __version__ = "0.1.2"