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.
- aac_cli-0.1.1/PKG-INFO +327 -0
- aac_cli-0.1.1/README.md +312 -0
- aac_cli-0.1.1/aac_cli/__init__.py +13 -0
- aac_cli-0.1.1/aac_cli/cli.py +2780 -0
- aac_cli-0.1.1/aac_cli/config.py +921 -0
- aac_cli-0.1.1/aac_cli/profiles.py +379 -0
- aac_cli-0.1.1/aac_cli/registration_state.py +739 -0
- aac_cli-0.1.1/aac_cli/sso_login.py +541 -0
- aac_cli-0.1.1/aac_cli.egg-info/PKG-INFO +327 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/SOURCES.txt +8 -1
- {aac_cli-0.1.0 → aac_cli-0.1.1}/pyproject.toml +10 -8
- aac_cli-0.1.1/tests/test_aac_cli.py +1734 -0
- aac_cli-0.1.1/tests/test_profile_cli.py +882 -0
- aac_cli-0.1.1/tests/test_registration_recovery_cli.py +264 -0
- aac_cli-0.1.1/tests/test_registration_state.py +676 -0
- aac_cli-0.1.1/tests/test_sso_login_cli.py +1170 -0
- aac_cli-0.1.0/PKG-INFO +0 -157
- aac_cli-0.1.0/README.md +0 -142
- aac_cli-0.1.0/aac_cli/__init__.py +0 -8
- aac_cli-0.1.0/aac_cli/cli.py +0 -575
- aac_cli-0.1.0/aac_cli/config.py +0 -327
- aac_cli-0.1.0/aac_cli.egg-info/PKG-INFO +0 -157
- aac_cli-0.1.0/tests/test_aac_cli.py +0 -582
- {aac_cli-0.1.0 → aac_cli-0.1.1}/LICENSE +0 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli/__main__.py +0 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/dependency_links.txt +0 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/entry_points.txt +0 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/requires.txt +0 -0
- {aac_cli-0.1.0 → aac_cli-0.1.1}/aac_cli.egg-info/top_level.txt +0 -0
- {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.
|
aac_cli-0.1.1/README.md
ADDED
|
@@ -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"
|