aac-cli 0.1.0__tar.gz → 0.1.1__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 (30) hide show
  1. aac_cli-0.1.1/PKG-INFO +327 -0
  2. aac_cli-0.1.1/README.md +312 -0
  3. aac_cli-0.1.1/aac_cli/__init__.py +13 -0
  4. aac_cli-0.1.1/aac_cli/cli.py +2780 -0
  5. aac_cli-0.1.1/aac_cli/config.py +921 -0
  6. aac_cli-0.1.1/aac_cli/profiles.py +379 -0
  7. aac_cli-0.1.1/aac_cli/registration_state.py +739 -0
  8. aac_cli-0.1.1/aac_cli/sso_login.py +541 -0
  9. aac_cli-0.1.1/aac_cli.egg-info/PKG-INFO +327 -0
  10. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/SOURCES.txt +8 -1
  11. {aac_cli-0.1.0 → aac_cli-0.1.1}/pyproject.toml +10 -8
  12. aac_cli-0.1.1/tests/test_aac_cli.py +1734 -0
  13. aac_cli-0.1.1/tests/test_profile_cli.py +882 -0
  14. aac_cli-0.1.1/tests/test_registration_recovery_cli.py +264 -0
  15. aac_cli-0.1.1/tests/test_registration_state.py +676 -0
  16. aac_cli-0.1.1/tests/test_sso_login_cli.py +1170 -0
  17. aac_cli-0.1.0/PKG-INFO +0 -157
  18. aac_cli-0.1.0/README.md +0 -142
  19. aac_cli-0.1.0/aac_cli/__init__.py +0 -8
  20. aac_cli-0.1.0/aac_cli/cli.py +0 -575
  21. aac_cli-0.1.0/aac_cli/config.py +0 -327
  22. aac_cli-0.1.0/aac_cli.egg-info/PKG-INFO +0 -157
  23. aac_cli-0.1.0/tests/test_aac_cli.py +0 -582
  24. {aac_cli-0.1.0 → aac_cli-0.1.1}/LICENSE +0 -0
  25. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli/__main__.py +0 -0
  26. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/dependency_links.txt +0 -0
  27. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/entry_points.txt +0 -0
  28. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/requires.txt +0 -0
  29. {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/top_level.txt +0 -0
  30. {aac_cli-0.1.0 → aac_cli-0.1.1}/setup.cfg +0 -0
aac_cli-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.4
2
+ Name: aac-cli
3
+ Version: 0.1.1
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
+ Provides-Extra: test
12
+ Requires-Dist: pytest>=8.0; extra == "test"
13
+ Requires-Dist: pytest-httpx>=0.30; extra == "test"
14
+ Dynamic: license-file
15
+
16
+ # aac-cli — the `aac` platform CLI
17
+
18
+ The headless operator interface to the AAC control plane (Eng Spec
19
+ §XII; Equifax/KPI-6 requirement: every operational task doable without
20
+ a web console).
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install aac-cli
26
+ ```
27
+
28
+ > **Careful with the name:** `pip install aac` installs an UNRELATED
29
+ > project (an MBSE modeling tool that happens to share the acronym).
30
+ > The AAC platform CLI's PyPI distribution is **`aac-cli`**; the
31
+ > command it installs is `aac` — the awscli precedent (dist name ≠
32
+ > command name).
33
+
34
+ Two personas, two invocation styles:
35
+
36
+ * **Installed operators** (`pip install aac-cli`) run the bare
37
+ command: `aac tenant register ...`
38
+ * **Repo developers** run it through the workspace without installing:
39
+ `uv run aac ...`
40
+
41
+ The examples below use the bare form; prefix `uv run` if you're in the
42
+ repo.
43
+
44
+ The Python argparse CLI ships the Stage-2 subset of §XII's full command
45
+ surface, grown since (B130 trust-anchor, B116 tenant verbs, B170
46
+ profiles — which replaced the earlier B116/B117 `aac configure`
47
+ family). This Python CLI IS the product CLI — the formerly-planned Go
48
+ rewrite was descoped (user ruling 2026-07-23; no separate Go
49
+ implementation is coming):
50
+
51
+ ```bash
52
+ # B170 — profiles are a first-class resource (CLI Config+Profile
53
+ # Spec). `main` is the reserved baseline; `aac configure` is GONE.
54
+ aac profile list # every profile incl. virtual [main]
55
+ aac profile show # the SELECTED profile, stored vs effective
56
+ aac profile show prod --output table
57
+ aac profile create dev --admin-url https://dev-admin.example.com \
58
+ --data-plane-url https://dev-data.example.com
59
+ aac profile update main # materialize/edit the baseline
60
+ aac profile delete dev # local-only; never touches the server
61
+
62
+ # B154 PR 3 — the tenant id is SERVER-ALLOCATED (canonical
63
+ # tnt-<lowercase UUIDv4>; there is no --tenant-id and you never choose
64
+ # one). Deployment note: this CLI requires a PR-3+ control plane —
65
+ # rollout order + compatibility matrix in RELEASING.md.
66
+ # The response prints the id EXACTLY ONCE with the api_key; the
67
+ # CLI stores the credential under it AND binds the SELECTED profile to
68
+ # it (a bound profile refuses registration — use an unbound one). A
69
+ # NAMED profile must exist before it can be selected (§6.1):
70
+ aac profile create acme
71
+ aac tenant register --profile acme --display-name "ACME Corporation" \
72
+ --contact ops@acme.example \
73
+ --workload-spiffe-id spiffe://acme.com/treasury-agent/v1 \
74
+ --tenant-admin-pubkey-file ./tenant-admin.public.pem \
75
+ --bootstrap-token dev-bootstrap-token-compose-only
76
+ # Workload authorities must already be lowercase. The path is
77
+ # case-sensitive, so spiffe://acme.com/Treasury-Agent is valid and is
78
+ # preserved exactly; spiffe://ACME.com/treasury-agent is rejected.
79
+ # If the response is lost, retry the exact frozen request with no stable
80
+ # arguments. The selected profile supplies its AAC-managed recovery state:
81
+ aac tenant register --profile acme
82
+ # Registration is a gated ceremony (Eng Spec §XVII.6): the control
83
+ # plane rejects it unless X-AAC-Bootstrap-Token matches its
84
+ # AAC_CP_BOOTSTRAP_TOKEN. --bootstrap-token falls back to
85
+ # $AAC_BOOTSTRAP_TOKEN (note: the CLI-side env var has no CP_ — the
86
+ # server-side name AAC_CP_BOOTSTRAP_TOKEN is NOT read by the CLI).
87
+
88
+ # B142 — developer-tier SELF-SERVE registration (no ceremony token):
89
+ # sign in with GitHub or Google; your verified identity becomes the
90
+ # tenant's first tenant-admin. Needs a deployment with self-serve
91
+ # enabled + a shared connection for the family.
92
+ aac profile create dev # (re)create it — deleted in the B170 example above
93
+ aac tenant register --profile dev --display-name "ACME Dev" \
94
+ --contact dev@acme.example --idp github
95
+ aac sso login --profile dev # thereafter: plain login
96
+
97
+ # B116 — admin-surface tenant inspection + attribute update.
98
+ # Tenant ids are the server-allocated canonical tnt-<uuid> form
99
+ # (B154 PR 5) — captured at registration / shown by `aac profile show`.
100
+ aac tenant list # which tenant ids exist (no 409 probe)
101
+ aac tenant describe --tenant-id tnt-<uuid> # workloads + key METADATA
102
+ aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"
103
+
104
+ # B172 — post-registration workload lifecycle. --tenant-id is optional
105
+ # here: the selected profile supplies it by default. JSON is default;
106
+ # every command also supports --output table.
107
+ aac tenant add-workload --spiffe-id spiffe://acme.com/payroll/v1 \
108
+ --display-name "Payroll"
109
+ aac tenant list-workloads # ACTIVE only
110
+ aac tenant list-workloads --all # + DEACTIVATED history
111
+ aac tenant describe-workload --workload-id <opaque-id>
112
+ aac tenant update-workload --workload-id <opaque-id> \
113
+ --display-name "Payroll v2"
114
+ aac tenant update-workload --workload-id <opaque-id> --clear-display-name
115
+ aac tenant deactivate-workload --workload-id <opaque-id> \
116
+ --reason "service retired"
117
+ # Deactivation is terminal; there is no delete/reactivate command, and
118
+ # the exact SPIFFE ID remains globally reserved.
119
+
120
+ aac chain show --token-id <64-hex token or chain root> --tenant-id tnt-<uuid>
121
+ aac chain show --token-id <id> --output table
122
+ aac chain show --token-id <id> --render --render-out ./aeg.html # headless
123
+ aac chain show --token-id <id> --render --open # + browser
124
+
125
+ # B130 — YOUR tenant's trust-anchor state (what .well-known never
126
+ # shows: REVOKED keys, expired-grace history, lifecycle timestamps,
127
+ # tenant-admin key status, the ingest ledger). Bearer-authenticated;
128
+ # tenant scoping is implicit in the credential.
129
+ aac trust-anchor list --tenant-id tnt-<uuid> # both roles, all states
130
+ aac trust-anchor list --role tenant-admin # ingest-signing keys only
131
+ aac trust-anchor describe --kid key-1 # lifecycle + public PEM
132
+ aac trust-anchor ingest-history --artifact-class root-keys --limit 20
133
+
134
+ # B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
135
+ # 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:
138
+ # (--tenant-id takes the canonical tnt-<uuid> id captured at your
139
+ # tenant's registration — B154 PR 5: a dotted value fails locally.)
140
+ aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
141
+ --file connection.json \
142
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
143
+
144
+ # B142 — register a SHARED developer-tier connection (platform
145
+ # operator ceremony ONLY; owned by no tenant). GitHub registers with
146
+ # jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
147
+ # provider); Google registers via discovery + public_client_secret:
148
+ aac sso register-idp --shared --file shared-github.json \
149
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
150
+
151
+ # B141 — sign in via your tenant's IdP (aws sso login semantics).
152
+ # Device flow by default (the browser can be on ANY machine — headless
153
+ # friendly); Google-family connections use loopback PKCE (Google's
154
+ # device grant is scope-restricted). Success caches a short-lived AAC
155
+ # session token AND writes the config profile in one motion; every
156
+ # admin verb (tenant list/describe/update, self-serve register-idp)
157
+ # then rides the session automatically.
158
+ aac sso login --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
159
+ aac sso login --issuer https://login.microsoftonline.com/<tid>/v2.0 # >1 IdP
160
+ aac sso login --flow pkce --no-browser # print the URL, don't launch
161
+
162
+ # Sessions are REFRESH-LESS by design (Eng Spec §XVII.2): on expiry
163
+ # any admin verb tells you to re-run `aac sso login` — there is no
164
+ # refresh token to steal, and no silent re-authentication.
165
+
166
+ # B155 — session niceties (cache-file operations; the CLI never
167
+ # parses the token itself). whoami exits 0 = live, 3 = expired/none (§14).
168
+ aac sso whoami --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
169
+ aac sso logout --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
170
+ ```
171
+
172
+ Naming: console script = `aac` (the §XVI-reserved name); import
173
+ package = `aac_cli` (the SDK owns the `aac` import package);
174
+ distribution = `aac-cli` (decided at first release, B119: bare `aac`
175
+ on PyPI belongs to an unrelated active project and is not claimable).
176
+
177
+ ## Configuration
178
+
179
+ Profiles live in `~/.aac/config` (UTF-8 INI with DIRECT sections —
180
+ `[main]`, `[dev]`, `[prod]`, never `[profile dev]`; override the
181
+ directory with `AAC_CLI_HOME`). `aac profile create/update` are the
182
+ writers; the file stays hand-editable INI, but note a rewrite does not
183
+ preserve comments (aws-cli behaves the same):
184
+
185
+ ```ini
186
+ [main]
187
+ admin_url = http://127.0.0.1:8000
188
+ data_plane_url = http://127.0.0.1:9000
189
+ tenant_id = tnt-550e8400-e29b-41d4-a716-446655440000
190
+
191
+ [dev]
192
+ admin_url = https://dev-admin.example.com
193
+ data_plane_url = https://dev-data.example.com
194
+ ```
195
+
196
+ **`main` is the reserved baseline profile.** It always exists —
197
+ virtually (built-in localhost defaults) until `aac profile update
198
+ main` materializes it — and cannot be created or deleted. Profile
199
+ SELECTION for operational commands is `--profile <name>` >
200
+ `AAC_PROFILE` > `main`; a selected profile that doesn't exist is an
201
+ error, never a silent fallback. Prototype-era `[default]` sections
202
+ migrate to `[main]` automatically on first touch (both-exist-and-
203
+ differ fails closed with manual remediation).
204
+
205
+ **`tenant_id` is system-managed.** Profile commands display it but
206
+ never prompt for or accept it — only authenticated workflows
207
+ (`aac tenant register`, `aac sso login`) write it. Canonical
208
+ `tnt-<uuid>` validation is MANDATORY (B154 PR 5): a profile holding a
209
+ prototype-era dotted value (`acme.com`) now fails every command, exit
210
+ 3, with remediation printed (delete the stale `tenant_id` line, then
211
+ re-register or `aac sso login` to bind a canonical id).
212
+
213
+ Setting precedence within the selected profile: flag > env
214
+ (`AAC_ADMIN_URL` / `AAC_DATA_PLANE_URL` / `AAC_TENANT_ID`; also
215
+ `AAC_BOOTSTRAP_TOKEN` for `tenant register --bootstrap-token`) >
216
+ profile > localhost defaults. `aac profile show` prints stored AND
217
+ effective values with each one's source (`flag` / `env:AAC_*` /
218
+ `profile:<name>` / `default` / `unset`).
219
+
220
+ Guardrails (deliberate bounded complexity): at most 50 profiles
221
+ including `main`, 50 keys per section, 30-character keys, and
222
+ 512-character values — exceeding any is a deterministic exit-3
223
+ invalid-config error. Unknown keys are preserved by writes and
224
+ reported (by name) by `aac profile show`.
225
+
226
+ `~/.aac/state/tenant-registrations/<profile>.json` is AAC-managed
227
+ registration recovery state (one record per profile; dir 0700, file
228
+ 0600). Do not edit, rename, copy between profiles, or delete these
229
+ files while a registration is unresolved — deleting one cannot cancel
230
+ a request that may already have committed. A profile with such a
231
+ record cannot be deleted. `aac tenant register` creates the record before
232
+ authentication or HTTP and holds a nonblocking per-profile process lock across
233
+ the entire attempt. A transport interruption retains the same request id and
234
+ frozen stable intent; a bare retry resumes it with fresh ceremony/evidence
235
+ credentials. Supplying identical stable arguments is also accepted, while a
236
+ difference fails locally before network access. The CLI deletes pending state
237
+ only after success is safely persisted or the server returns the exact paired
238
+ `Idempotency-Key` + `AAC-Registration-Status: not_created` guarantee.
239
+
240
+ B183 applies the same canonical workload-ID boundary before either a durable
241
+ registration-state write or an add-workload network request: a non-lowercase SPIFFE
242
+ authority is rejected, never normalized. An accepted path retains its exact
243
+ case in the frozen stable intent, fingerprint input, state file, and every
244
+ retry comparison.
245
+
246
+ A completed server recovery returns tenant identity but cannot replay the
247
+ one-time API key. The CLI binds the profile and transitions the state to
248
+ `completed_credential_reissue_required`, directing the operator to the
249
+ separate B173 reissue capability; B171 does not implement reissue.
250
+
251
+ ### `AAC_CLI_HOME` — isolated homes
252
+
253
+ `AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
254
+ credentials). Two everyday uses:
255
+
256
+ ```bash
257
+ # 1. Throwaway smoke/test home — nothing touches your real config:
258
+ AAC_CLI_HOME=/tmp/aac-smoke aac tenant register --display-name "Smoke" ...
259
+
260
+ # 2. Multi-account operators — hard-wall separation beyond profiles
261
+ # (separate credential files, not just separate config sections):
262
+ alias aac-prod='AAC_CLI_HOME=$HOME/.aac-prod aac'
263
+ alias aac-staging='AAC_CLI_HOME=$HOME/.aac-staging aac'
264
+ ```
265
+
266
+ Profiles share one credentials directory; `AAC_CLI_HOME` gives each
267
+ context its own.
268
+
269
+ Credentials — TWO kinds coexist in `~/.aac/credentials/` (documented
270
+ coexistence, Eng Spec §XVII.9):
271
+
272
+ * `{tenant_id}` — the bare `aac_ak_...` string at mode 0600, written
273
+ by `aac tenant register` (the key is printed EXACTLY ONCE — only its
274
+ peppered hash exists server-side) and presented as `Authorization:
275
+ Bearer` by the DATA-PLANE verbs (`chain show`, `trust-anchor *`).
276
+ Same bare-string format the joined compose smoke writes into sidecar
277
+ key dirs — this file never grows structure.
278
+ * `{tenant_id}.session` — JSON, mode 0600, written by `aac sso login`:
279
+ the short-lived AAC session token + its client-computed expiry. The
280
+ ADMIN-surface verbs ride it automatically. A session within 30s of
281
+ expiry fails fast locally with the re-login message instead of
282
+ dialing out; the server's own session 401s carry the same
283
+ remediation (the server stays authoritative). `aac sso logout`
284
+ removes the file; `aac sso whoami` reports its tenant + expiry
285
+ (B155 — both operate on the cache file only).
286
+
287
+ ## Your tenant_id
288
+
289
+ `tenant_id` is your org's FEDERATION IDENTIFIER — the stable handle
290
+ every other tenant verifies your signatures under: it appears in
291
+ `X-AAC-Originator-Tenant-Id` headers, `/.well-known/aac-root-keys/
292
+ {tenant_id}` paths, SIEM stream directories, and supplier whitelists.
293
+ You do NOT choose it (B154 PR 5, Tenant Identifier Policy): the
294
+ control plane allocates the canonical **`tnt-<lowercase UUIDv4>`**
295
+ (exactly 40 characters, e.g.
296
+ `tnt-550e8400-e29b-41d4-a716-446655440000`) at registration, the CLI
297
+ captures it into the selected profile, and it is immutable, public,
298
+ and never reused — like an AWS account ID. `--parent-tenant-id` takes
299
+ the parent org's canonical `tnt-` id — the server resolves handles to
300
+ 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.
304
+
305
+ ## Semantics worth knowing
306
+
307
+ * `aac chain show` accepts ANY hop's token id or the chain root id
308
+ (`--root-token-id` is an accepted alias); visibility is
309
+ participant-tenant over the symmetric composite closure (Week 12) —
310
+ non-participants get the same 404 as unknown tokens.
311
+ * Timelines are §V.2.1 METADATA-TIER: predicates and business
312
+ narratives stay in your tenant SIEM stream; the table output's
313
+ footer says so (§6.1 federated join is the full-fidelity story).
314
+ * Exit codes (CLI Config+Profile Spec §14, ratified 2026-08-04 —
315
+ supersedes the originator-cli-era convention): 0 success / 1 remote
316
+ outcome (server rejected/denied, or the server-side resource doesn't
317
+ exist) / 2 invalid command-line usage / 3 invalid local
318
+ configuration or state (missing profile/credential, malformed INI,
319
+ bad registration state) / 4 transport failure (unreachable or timed
320
+ out — the retryable class).
321
+
322
+ ## Tests
323
+
324
+ `uv run pytest cli/aac/tests/` — pytest-httpx mocks; no control plane
325
+ needed. Against a live stack: bring up the joined topology
326
+ (`./bin/run-wedge-a-control-plane-compose.sh --keep-up`) and point the
327
+ flags at localhost.
@@ -0,0 +1,312 @@
1
+ # aac-cli — the `aac` platform CLI
2
+
3
+ The headless operator interface to the AAC control plane (Eng Spec
4
+ §XII; Equifax/KPI-6 requirement: every operational task doable without
5
+ a web console).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install aac-cli
11
+ ```
12
+
13
+ > **Careful with the name:** `pip install aac` installs an UNRELATED
14
+ > project (an MBSE modeling tool that happens to share the acronym).
15
+ > The AAC platform CLI's PyPI distribution is **`aac-cli`**; the
16
+ > command it installs is `aac` — the awscli precedent (dist name ≠
17
+ > command name).
18
+
19
+ Two personas, two invocation styles:
20
+
21
+ * **Installed operators** (`pip install aac-cli`) run the bare
22
+ command: `aac tenant register ...`
23
+ * **Repo developers** run it through the workspace without installing:
24
+ `uv run aac ...`
25
+
26
+ The examples below use the bare form; prefix `uv run` if you're in the
27
+ repo.
28
+
29
+ The Python argparse CLI ships the Stage-2 subset of §XII's full command
30
+ surface, grown since (B130 trust-anchor, B116 tenant verbs, B170
31
+ profiles — which replaced the earlier B116/B117 `aac configure`
32
+ family). This Python CLI IS the product CLI — the formerly-planned Go
33
+ rewrite was descoped (user ruling 2026-07-23; no separate Go
34
+ implementation is coming):
35
+
36
+ ```bash
37
+ # B170 — profiles are a first-class resource (CLI Config+Profile
38
+ # Spec). `main` is the reserved baseline; `aac configure` is GONE.
39
+ aac profile list # every profile incl. virtual [main]
40
+ aac profile show # the SELECTED profile, stored vs effective
41
+ aac profile show prod --output table
42
+ aac profile create dev --admin-url https://dev-admin.example.com \
43
+ --data-plane-url https://dev-data.example.com
44
+ aac profile update main # materialize/edit the baseline
45
+ aac profile delete dev # local-only; never touches the server
46
+
47
+ # B154 PR 3 — the tenant id is SERVER-ALLOCATED (canonical
48
+ # tnt-<lowercase UUIDv4>; there is no --tenant-id and you never choose
49
+ # one). Deployment note: this CLI requires a PR-3+ control plane —
50
+ # rollout order + compatibility matrix in RELEASING.md.
51
+ # The response prints the id EXACTLY ONCE with the api_key; the
52
+ # CLI stores the credential under it AND binds the SELECTED profile to
53
+ # it (a bound profile refuses registration — use an unbound one). A
54
+ # NAMED profile must exist before it can be selected (§6.1):
55
+ aac profile create acme
56
+ aac tenant register --profile acme --display-name "ACME Corporation" \
57
+ --contact ops@acme.example \
58
+ --workload-spiffe-id spiffe://acme.com/treasury-agent/v1 \
59
+ --tenant-admin-pubkey-file ./tenant-admin.public.pem \
60
+ --bootstrap-token dev-bootstrap-token-compose-only
61
+ # Workload authorities must already be lowercase. The path is
62
+ # case-sensitive, so spiffe://acme.com/Treasury-Agent is valid and is
63
+ # preserved exactly; spiffe://ACME.com/treasury-agent is rejected.
64
+ # If the response is lost, retry the exact frozen request with no stable
65
+ # arguments. The selected profile supplies its AAC-managed recovery state:
66
+ aac tenant register --profile acme
67
+ # Registration is a gated ceremony (Eng Spec §XVII.6): the control
68
+ # plane rejects it unless X-AAC-Bootstrap-Token matches its
69
+ # AAC_CP_BOOTSTRAP_TOKEN. --bootstrap-token falls back to
70
+ # $AAC_BOOTSTRAP_TOKEN (note: the CLI-side env var has no CP_ — the
71
+ # server-side name AAC_CP_BOOTSTRAP_TOKEN is NOT read by the CLI).
72
+
73
+ # B142 — developer-tier SELF-SERVE registration (no ceremony token):
74
+ # sign in with GitHub or Google; your verified identity becomes the
75
+ # tenant's first tenant-admin. Needs a deployment with self-serve
76
+ # enabled + a shared connection for the family.
77
+ aac profile create dev # (re)create it — deleted in the B170 example above
78
+ aac tenant register --profile dev --display-name "ACME Dev" \
79
+ --contact dev@acme.example --idp github
80
+ aac sso login --profile dev # thereafter: plain login
81
+
82
+ # B116 — admin-surface tenant inspection + attribute update.
83
+ # Tenant ids are the server-allocated canonical tnt-<uuid> form
84
+ # (B154 PR 5) — captured at registration / shown by `aac profile show`.
85
+ aac tenant list # which tenant ids exist (no 409 probe)
86
+ aac tenant describe --tenant-id tnt-<uuid> # workloads + key METADATA
87
+ aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"
88
+
89
+ # B172 — post-registration workload lifecycle. --tenant-id is optional
90
+ # here: the selected profile supplies it by default. JSON is default;
91
+ # every command also supports --output table.
92
+ aac tenant add-workload --spiffe-id spiffe://acme.com/payroll/v1 \
93
+ --display-name "Payroll"
94
+ aac tenant list-workloads # ACTIVE only
95
+ aac tenant list-workloads --all # + DEACTIVATED history
96
+ aac tenant describe-workload --workload-id <opaque-id>
97
+ aac tenant update-workload --workload-id <opaque-id> \
98
+ --display-name "Payroll v2"
99
+ aac tenant update-workload --workload-id <opaque-id> --clear-display-name
100
+ aac tenant deactivate-workload --workload-id <opaque-id> \
101
+ --reason "service retired"
102
+ # Deactivation is terminal; there is no delete/reactivate command, and
103
+ # the exact SPIFFE ID remains globally reserved.
104
+
105
+ aac chain show --token-id <64-hex token or chain root> --tenant-id tnt-<uuid>
106
+ aac chain show --token-id <id> --output table
107
+ aac chain show --token-id <id> --render --render-out ./aeg.html # headless
108
+ aac chain show --token-id <id> --render --open # + browser
109
+
110
+ # B130 — YOUR tenant's trust-anchor state (what .well-known never
111
+ # shows: REVOKED keys, expired-grace history, lifecycle timestamps,
112
+ # tenant-admin key status, the ingest ledger). Bearer-authenticated;
113
+ # tenant scoping is implicit in the credential.
114
+ aac trust-anchor list --tenant-id tnt-<uuid> # both roles, all states
115
+ aac trust-anchor list --role tenant-admin # ingest-signing keys only
116
+ aac trust-anchor describe --kid key-1 # lifecycle + public PEM
117
+ aac trust-anchor ingest-history --artifact-class root-keys --limit 20
118
+
119
+ # B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
120
+ # 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:
123
+ # (--tenant-id takes the canonical tnt-<uuid> id captured at your
124
+ # tenant's registration — B154 PR 5: a dotted value fails locally.)
125
+ aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
126
+ --file connection.json \
127
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
128
+
129
+ # B142 — register a SHARED developer-tier connection (platform
130
+ # operator ceremony ONLY; owned by no tenant). GitHub registers with
131
+ # jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
132
+ # provider); Google registers via discovery + public_client_secret:
133
+ aac sso register-idp --shared --file shared-github.json \
134
+ --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
135
+
136
+ # B141 — sign in via your tenant's IdP (aws sso login semantics).
137
+ # Device flow by default (the browser can be on ANY machine — headless
138
+ # friendly); Google-family connections use loopback PKCE (Google's
139
+ # device grant is scope-restricted). Success caches a short-lived AAC
140
+ # session token AND writes the config profile in one motion; every
141
+ # admin verb (tenant list/describe/update, self-serve register-idp)
142
+ # then rides the session automatically.
143
+ aac sso login --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
144
+ aac sso login --issuer https://login.microsoftonline.com/<tid>/v2.0 # >1 IdP
145
+ aac sso login --flow pkce --no-browser # print the URL, don't launch
146
+
147
+ # Sessions are REFRESH-LESS by design (Eng Spec §XVII.2): on expiry
148
+ # any admin verb tells you to re-run `aac sso login` — there is no
149
+ # refresh token to steal, and no silent re-authentication.
150
+
151
+ # B155 — session niceties (cache-file operations; the CLI never
152
+ # parses the token itself). whoami exits 0 = live, 3 = expired/none (§14).
153
+ aac sso whoami --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
154
+ aac sso logout --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
155
+ ```
156
+
157
+ Naming: console script = `aac` (the §XVI-reserved name); import
158
+ package = `aac_cli` (the SDK owns the `aac` import package);
159
+ distribution = `aac-cli` (decided at first release, B119: bare `aac`
160
+ on PyPI belongs to an unrelated active project and is not claimable).
161
+
162
+ ## Configuration
163
+
164
+ Profiles live in `~/.aac/config` (UTF-8 INI with DIRECT sections —
165
+ `[main]`, `[dev]`, `[prod]`, never `[profile dev]`; override the
166
+ directory with `AAC_CLI_HOME`). `aac profile create/update` are the
167
+ writers; the file stays hand-editable INI, but note a rewrite does not
168
+ preserve comments (aws-cli behaves the same):
169
+
170
+ ```ini
171
+ [main]
172
+ admin_url = http://127.0.0.1:8000
173
+ data_plane_url = http://127.0.0.1:9000
174
+ tenant_id = tnt-550e8400-e29b-41d4-a716-446655440000
175
+
176
+ [dev]
177
+ admin_url = https://dev-admin.example.com
178
+ data_plane_url = https://dev-data.example.com
179
+ ```
180
+
181
+ **`main` is the reserved baseline profile.** It always exists —
182
+ virtually (built-in localhost defaults) until `aac profile update
183
+ main` materializes it — and cannot be created or deleted. Profile
184
+ SELECTION for operational commands is `--profile <name>` >
185
+ `AAC_PROFILE` > `main`; a selected profile that doesn't exist is an
186
+ error, never a silent fallback. Prototype-era `[default]` sections
187
+ migrate to `[main]` automatically on first touch (both-exist-and-
188
+ differ fails closed with manual remediation).
189
+
190
+ **`tenant_id` is system-managed.** Profile commands display it but
191
+ never prompt for or accept it — only authenticated workflows
192
+ (`aac tenant register`, `aac sso login`) write it. Canonical
193
+ `tnt-<uuid>` validation is MANDATORY (B154 PR 5): a profile holding a
194
+ prototype-era dotted value (`acme.com`) now fails every command, exit
195
+ 3, with remediation printed (delete the stale `tenant_id` line, then
196
+ re-register or `aac sso login` to bind a canonical id).
197
+
198
+ Setting precedence within the selected profile: flag > env
199
+ (`AAC_ADMIN_URL` / `AAC_DATA_PLANE_URL` / `AAC_TENANT_ID`; also
200
+ `AAC_BOOTSTRAP_TOKEN` for `tenant register --bootstrap-token`) >
201
+ profile > localhost defaults. `aac profile show` prints stored AND
202
+ effective values with each one's source (`flag` / `env:AAC_*` /
203
+ `profile:<name>` / `default` / `unset`).
204
+
205
+ Guardrails (deliberate bounded complexity): at most 50 profiles
206
+ including `main`, 50 keys per section, 30-character keys, and
207
+ 512-character values — exceeding any is a deterministic exit-3
208
+ invalid-config error. Unknown keys are preserved by writes and
209
+ reported (by name) by `aac profile show`.
210
+
211
+ `~/.aac/state/tenant-registrations/<profile>.json` is AAC-managed
212
+ registration recovery state (one record per profile; dir 0700, file
213
+ 0600). Do not edit, rename, copy between profiles, or delete these
214
+ files while a registration is unresolved — deleting one cannot cancel
215
+ a request that may already have committed. A profile with such a
216
+ record cannot be deleted. `aac tenant register` creates the record before
217
+ authentication or HTTP and holds a nonblocking per-profile process lock across
218
+ the entire attempt. A transport interruption retains the same request id and
219
+ frozen stable intent; a bare retry resumes it with fresh ceremony/evidence
220
+ credentials. Supplying identical stable arguments is also accepted, while a
221
+ difference fails locally before network access. The CLI deletes pending state
222
+ only after success is safely persisted or the server returns the exact paired
223
+ `Idempotency-Key` + `AAC-Registration-Status: not_created` guarantee.
224
+
225
+ B183 applies the same canonical workload-ID boundary before either a durable
226
+ registration-state write or an add-workload network request: a non-lowercase SPIFFE
227
+ authority is rejected, never normalized. An accepted path retains its exact
228
+ case in the frozen stable intent, fingerprint input, state file, and every
229
+ retry comparison.
230
+
231
+ A completed server recovery returns tenant identity but cannot replay the
232
+ one-time API key. The CLI binds the profile and transitions the state to
233
+ `completed_credential_reissue_required`, directing the operator to the
234
+ separate B173 reissue capability; B171 does not implement reissue.
235
+
236
+ ### `AAC_CLI_HOME` — isolated homes
237
+
238
+ `AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
239
+ credentials). Two everyday uses:
240
+
241
+ ```bash
242
+ # 1. Throwaway smoke/test home — nothing touches your real config:
243
+ AAC_CLI_HOME=/tmp/aac-smoke aac tenant register --display-name "Smoke" ...
244
+
245
+ # 2. Multi-account operators — hard-wall separation beyond profiles
246
+ # (separate credential files, not just separate config sections):
247
+ alias aac-prod='AAC_CLI_HOME=$HOME/.aac-prod aac'
248
+ alias aac-staging='AAC_CLI_HOME=$HOME/.aac-staging aac'
249
+ ```
250
+
251
+ Profiles share one credentials directory; `AAC_CLI_HOME` gives each
252
+ context its own.
253
+
254
+ Credentials — TWO kinds coexist in `~/.aac/credentials/` (documented
255
+ coexistence, Eng Spec §XVII.9):
256
+
257
+ * `{tenant_id}` — the bare `aac_ak_...` string at mode 0600, written
258
+ by `aac tenant register` (the key is printed EXACTLY ONCE — only its
259
+ peppered hash exists server-side) and presented as `Authorization:
260
+ Bearer` by the DATA-PLANE verbs (`chain show`, `trust-anchor *`).
261
+ Same bare-string format the joined compose smoke writes into sidecar
262
+ key dirs — this file never grows structure.
263
+ * `{tenant_id}.session` — JSON, mode 0600, written by `aac sso login`:
264
+ the short-lived AAC session token + its client-computed expiry. The
265
+ ADMIN-surface verbs ride it automatically. A session within 30s of
266
+ expiry fails fast locally with the re-login message instead of
267
+ dialing out; the server's own session 401s carry the same
268
+ remediation (the server stays authoritative). `aac sso logout`
269
+ removes the file; `aac sso whoami` reports its tenant + expiry
270
+ (B155 — both operate on the cache file only).
271
+
272
+ ## Your tenant_id
273
+
274
+ `tenant_id` is your org's FEDERATION IDENTIFIER — the stable handle
275
+ every other tenant verifies your signatures under: it appears in
276
+ `X-AAC-Originator-Tenant-Id` headers, `/.well-known/aac-root-keys/
277
+ {tenant_id}` paths, SIEM stream directories, and supplier whitelists.
278
+ You do NOT choose it (B154 PR 5, Tenant Identifier Policy): the
279
+ control plane allocates the canonical **`tnt-<lowercase UUIDv4>`**
280
+ (exactly 40 characters, e.g.
281
+ `tnt-550e8400-e29b-41d4-a716-446655440000`) at registration, the CLI
282
+ captures it into the selected profile, and it is immutable, public,
283
+ and never reused — like an AWS account ID. `--parent-tenant-id` takes
284
+ the parent org's canonical `tnt-` id — the server resolves handles to
285
+ 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.
289
+
290
+ ## Semantics worth knowing
291
+
292
+ * `aac chain show` accepts ANY hop's token id or the chain root id
293
+ (`--root-token-id` is an accepted alias); visibility is
294
+ participant-tenant over the symmetric composite closure (Week 12) —
295
+ non-participants get the same 404 as unknown tokens.
296
+ * Timelines are §V.2.1 METADATA-TIER: predicates and business
297
+ narratives stay in your tenant SIEM stream; the table output's
298
+ footer says so (§6.1 federated join is the full-fidelity story).
299
+ * Exit codes (CLI Config+Profile Spec §14, ratified 2026-08-04 —
300
+ supersedes the originator-cli-era convention): 0 success / 1 remote
301
+ outcome (server rejected/denied, or the server-side resource doesn't
302
+ exist) / 2 invalid command-line usage / 3 invalid local
303
+ configuration or state (missing profile/credential, malformed INI,
304
+ bad registration state) / 4 transport failure (unreachable or timed
305
+ out — the retryable class).
306
+
307
+ ## Tests
308
+
309
+ `uv run pytest cli/aac/tests/` — pytest-httpx mocks; no control plane
310
+ needed. Against a live stack: bring up the joined topology
311
+ (`./bin/run-wedge-a-control-plane-compose.sh --keep-up`) and point the
312
+ flags at localhost.
@@ -0,0 +1,13 @@
1
+ """aac-cli — the `aac` platform CLI (Eng Spec §XII, Stage 2 subset).
2
+
3
+ Import package is `aac_cli`, NOT `aac`: the SDK owns the `aac` import
4
+ package, and the console script named `aac` is this package's
5
+ entrypoint (`aac_cli.cli:entrypoint`).
6
+ """
7
+
8
+ # Kept as a synced literal (B180 D2 ruling, 2026-08-11): must equal
9
+ # [project] version in cli/aac/pyproject.toml — pinned by the parity
10
+ # test in bin/tests/test_cli_version_parity.py. A literal keeps the
11
+ # shipped code free of import-time metadata lookups (and their
12
+ # not-installed failure mode).
13
+ __version__ = "0.1.1"