Metadata-Version: 2.4
Name: aac-cli
Version: 0.1.1
Summary: The aac platform CLI — headless operator interface to the AAC control plane (tenant registration, chain audit).
Author: Agent Authority Cloud Project
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.28
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pytest-httpx>=0.30; extra == "test"
Dynamic: license-file

# aac-cli — the `aac` platform CLI

The headless operator interface to the AAC control plane (Eng Spec
§XII; Equifax/KPI-6 requirement: every operational task doable without
a web console).

## Install

```bash
pip install aac-cli
```

> **Careful with the name:** `pip install aac` installs an UNRELATED
> project (an MBSE modeling tool that happens to share the acronym).
> The AAC platform CLI's PyPI distribution is **`aac-cli`**; the
> command it installs is `aac` — the awscli precedent (dist name ≠
> command name).

Two personas, two invocation styles:

* **Installed operators** (`pip install aac-cli`) run the bare
  command: `aac tenant register ...`
* **Repo developers** run it through the workspace without installing:
  `uv run aac ...`

The examples below use the bare form; prefix `uv run` if you're in the
repo.

The Python argparse CLI ships the Stage-2 subset of §XII's full command
surface, grown since (B130 trust-anchor, B116 tenant verbs, B170
profiles — which replaced the earlier B116/B117 `aac configure`
family). This Python CLI IS the product CLI — the formerly-planned Go
rewrite was descoped (user ruling 2026-07-23; no separate Go
implementation is coming):

```bash
# B170 — profiles are a first-class resource (CLI Config+Profile
# Spec). `main` is the reserved baseline; `aac configure` is GONE.
aac profile list                   # every profile incl. virtual [main]
aac profile show                   # the SELECTED profile, stored vs effective
aac profile show prod --output table
aac profile create dev --admin-url https://dev-admin.example.com \
    --data-plane-url https://dev-data.example.com
aac profile update main            # materialize/edit the baseline
aac profile delete dev             # local-only; never touches the server

# B154 PR 3 — the tenant id is SERVER-ALLOCATED (canonical
# tnt-<lowercase UUIDv4>; there is no --tenant-id and you never choose
# one). Deployment note: this CLI requires a PR-3+ control plane —
# rollout order + compatibility matrix in RELEASING.md.
# The response prints the id EXACTLY ONCE with the api_key; the
# CLI stores the credential under it AND binds the SELECTED profile to
# it (a bound profile refuses registration — use an unbound one). A
# NAMED profile must exist before it can be selected (§6.1):
aac profile create acme
aac tenant register --profile acme --display-name "ACME Corporation" \
    --contact ops@acme.example \
    --workload-spiffe-id spiffe://acme.com/treasury-agent/v1 \
    --tenant-admin-pubkey-file ./tenant-admin.public.pem \
    --bootstrap-token dev-bootstrap-token-compose-only
# Workload authorities must already be lowercase. The path is
# case-sensitive, so spiffe://acme.com/Treasury-Agent is valid and is
# preserved exactly; spiffe://ACME.com/treasury-agent is rejected.
# If the response is lost, retry the exact frozen request with no stable
# arguments. The selected profile supplies its AAC-managed recovery state:
aac tenant register --profile acme
# Registration is a gated ceremony (Eng Spec §XVII.6): the control
# plane rejects it unless X-AAC-Bootstrap-Token matches its
# AAC_CP_BOOTSTRAP_TOKEN. --bootstrap-token falls back to
# $AAC_BOOTSTRAP_TOKEN (note: the CLI-side env var has no CP_ — the
# server-side name AAC_CP_BOOTSTRAP_TOKEN is NOT read by the CLI).

# B142 — developer-tier SELF-SERVE registration (no ceremony token):
# sign in with GitHub or Google; your verified identity becomes the
# tenant's first tenant-admin. Needs a deployment with self-serve
# enabled + a shared connection for the family.
aac profile create dev        # (re)create it — deleted in the B170 example above
aac tenant register --profile dev --display-name "ACME Dev" \
    --contact dev@acme.example --idp github
aac sso login --profile dev            # thereafter: plain login

# B116 — admin-surface tenant inspection + attribute update.
# Tenant ids are the server-allocated canonical tnt-<uuid> form
# (B154 PR 5) — captured at registration / shown by `aac profile show`.
aac tenant list                                # which tenant ids exist (no 409 probe)
aac tenant describe --tenant-id tnt-<uuid>     # workloads + key METADATA
aac tenant update --tenant-id tnt-<uuid> --display-name "ACME Manufacturing Inc"

# B172 — post-registration workload lifecycle. --tenant-id is optional
# here: the selected profile supplies it by default. JSON is default;
# every command also supports --output table.
aac tenant add-workload --spiffe-id spiffe://acme.com/payroll/v1 \
    --display-name "Payroll"
aac tenant list-workloads                       # ACTIVE only
aac tenant list-workloads --all                 # + DEACTIVATED history
aac tenant describe-workload --workload-id <opaque-id>
aac tenant update-workload --workload-id <opaque-id> \
    --display-name "Payroll v2"
aac tenant update-workload --workload-id <opaque-id> --clear-display-name
aac tenant deactivate-workload --workload-id <opaque-id> \
    --reason "service retired"
# Deactivation is terminal; there is no delete/reactivate command, and
# the exact SPIFFE ID remains globally reserved.

aac chain show --token-id <64-hex token or chain root> --tenant-id tnt-<uuid>
aac chain show --token-id <id> --output table
aac chain show --token-id <id> --render --render-out ./aeg.html   # headless
aac chain show --token-id <id> --render --open                    # + browser

# B130 — YOUR tenant's trust-anchor state (what .well-known never
# shows: REVOKED keys, expired-grace history, lifecycle timestamps,
# tenant-admin key status, the ingest ledger). Bearer-authenticated;
# tenant scoping is implicit in the credential.
aac trust-anchor list --tenant-id tnt-<uuid>        # both roles, all states
aac trust-anchor list --role tenant-admin           # ingest-signing keys only
aac trust-anchor describe --kid key-1               # lifecycle + public PEM
aac trust-anchor ingest-history --artifact-class root-keys --limit 20

# B140 — register a tenant↔IdP connection (Eng Spec §XVII.4; the sso
# noun). The connection config rides as a JSON file (nested claims
# mapping — see --help for an Entra example). First-IdP onboarding is
# the ops ceremony; later IdPs ride your cached session instead:
# (--tenant-id takes the canonical tnt-<uuid> id captured at your
# tenant's registration — B154 PR 5: a dotted value fails locally.)
aac sso register-idp --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000 \
    --file connection.json \
    --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"

# B142 — register a SHARED developer-tier connection (platform
# operator ceremony ONLY; owned by no tenant). GitHub registers with
# jwks_static {"keys": []} + explicit endpoints (GitHub is not an OIDC
# provider); Google registers via discovery + public_client_secret:
aac sso register-idp --shared --file shared-github.json \
    --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"

# B141 — sign in via your tenant's IdP (aws sso login semantics).
# Device flow by default (the browser can be on ANY machine — headless
# friendly); Google-family connections use loopback PKCE (Google's
# device grant is scope-restricted). Success caches a short-lived AAC
# session token AND writes the config profile in one motion; every
# admin verb (tenant list/describe/update, self-serve register-idp)
# then rides the session automatically.
aac sso login --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
aac sso login --issuer https://login.microsoftonline.com/<tid>/v2.0  # >1 IdP
aac sso login --flow pkce --no-browser        # print the URL, don't launch

# Sessions are REFRESH-LESS by design (Eng Spec §XVII.2): on expiry
# any admin verb tells you to re-run `aac sso login` — there is no
# refresh token to steal, and no silent re-authentication.

# B155 — session niceties (cache-file operations; the CLI never
# parses the token itself). whoami exits 0 = live, 3 = expired/none (§14).
aac sso whoami --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
aac sso logout --tenant-id tnt-550e8400-e29b-41d4-a716-446655440000
```

Naming: console script = `aac` (the §XVI-reserved name); import
package = `aac_cli` (the SDK owns the `aac` import package);
distribution = `aac-cli` (decided at first release, B119: bare `aac`
on PyPI belongs to an unrelated active project and is not claimable).

## Configuration

Profiles live in `~/.aac/config` (UTF-8 INI with DIRECT sections —
`[main]`, `[dev]`, `[prod]`, never `[profile dev]`; override the
directory with `AAC_CLI_HOME`). `aac profile create/update` are the
writers; the file stays hand-editable INI, but note a rewrite does not
preserve comments (aws-cli behaves the same):

```ini
[main]
admin_url = http://127.0.0.1:8000
data_plane_url = http://127.0.0.1:9000
tenant_id = tnt-550e8400-e29b-41d4-a716-446655440000

[dev]
admin_url = https://dev-admin.example.com
data_plane_url = https://dev-data.example.com
```

**`main` is the reserved baseline profile.** It always exists —
virtually (built-in localhost defaults) until `aac profile update
main` materializes it — and cannot be created or deleted. Profile
SELECTION for operational commands is `--profile <name>` >
`AAC_PROFILE` > `main`; a selected profile that doesn't exist is an
error, never a silent fallback. Prototype-era `[default]` sections
migrate to `[main]` automatically on first touch (both-exist-and-
differ fails closed with manual remediation).

**`tenant_id` is system-managed.** Profile commands display it but
never prompt for or accept it — only authenticated workflows
(`aac tenant register`, `aac sso login`) write it. Canonical
`tnt-<uuid>` validation is MANDATORY (B154 PR 5): a profile holding a
prototype-era dotted value (`acme.com`) now fails every command, exit
3, with remediation printed (delete the stale `tenant_id` line, then
re-register or `aac sso login` to bind a canonical id).

Setting precedence within the selected profile: flag > env
(`AAC_ADMIN_URL` / `AAC_DATA_PLANE_URL` / `AAC_TENANT_ID`; also
`AAC_BOOTSTRAP_TOKEN` for `tenant register --bootstrap-token`) >
profile > localhost defaults. `aac profile show` prints stored AND
effective values with each one's source (`flag` / `env:AAC_*` /
`profile:<name>` / `default` / `unset`).

Guardrails (deliberate bounded complexity): at most 50 profiles
including `main`, 50 keys per section, 30-character keys, and
512-character values — exceeding any is a deterministic exit-3
invalid-config error. Unknown keys are preserved by writes and
reported (by name) by `aac profile show`.

`~/.aac/state/tenant-registrations/<profile>.json` is AAC-managed
registration recovery state (one record per profile; dir 0700, file
0600). Do not edit, rename, copy between profiles, or delete these
files while a registration is unresolved — deleting one cannot cancel
a request that may already have committed. A profile with such a
record cannot be deleted. `aac tenant register` creates the record before
authentication or HTTP and holds a nonblocking per-profile process lock across
the entire attempt. A transport interruption retains the same request id and
frozen stable intent; a bare retry resumes it with fresh ceremony/evidence
credentials. Supplying identical stable arguments is also accepted, while a
difference fails locally before network access. The CLI deletes pending state
only after success is safely persisted or the server returns the exact paired
`Idempotency-Key` + `AAC-Registration-Status: not_created` guarantee.

B183 applies the same canonical workload-ID boundary before either a durable
registration-state write or an add-workload network request: a non-lowercase SPIFFE
authority is rejected, never normalized. An accepted path retains its exact
case in the frozen stable intent, fingerprint input, state file, and every
retry comparison.

A completed server recovery returns tenant identity but cannot replay the
one-time API key. The CLI binds the profile and transitions the state to
`completed_credential_reissue_required`, directing the operator to the
separate B173 reissue capability; B171 does not implement reissue.

### `AAC_CLI_HOME` — isolated homes

`AAC_CLI_HOME` relocates the whole `~/.aac` tree (config +
credentials). Two everyday uses:

```bash
# 1. Throwaway smoke/test home — nothing touches your real config:
AAC_CLI_HOME=/tmp/aac-smoke aac tenant register --display-name "Smoke" ...

# 2. Multi-account operators — hard-wall separation beyond profiles
#    (separate credential files, not just separate config sections):
alias aac-prod='AAC_CLI_HOME=$HOME/.aac-prod aac'
alias aac-staging='AAC_CLI_HOME=$HOME/.aac-staging aac'
```

Profiles share one credentials directory; `AAC_CLI_HOME` gives each
context its own.

Credentials — TWO kinds coexist in `~/.aac/credentials/` (documented
coexistence, Eng Spec §XVII.9):

* `{tenant_id}` — the bare `aac_ak_...` string at mode 0600, written
  by `aac tenant register` (the key is printed EXACTLY ONCE — only its
  peppered hash exists server-side) and presented as `Authorization:
  Bearer` by the DATA-PLANE verbs (`chain show`, `trust-anchor *`).
  Same bare-string format the joined compose smoke writes into sidecar
  key dirs — this file never grows structure.
* `{tenant_id}.session` — JSON, mode 0600, written by `aac sso login`:
  the short-lived AAC session token + its client-computed expiry. The
  ADMIN-surface verbs ride it automatically. A session within 30s of
  expiry fails fast locally with the re-login message instead of
  dialing out; the server's own session 401s carry the same
  remediation (the server stays authoritative). `aac sso logout`
  removes the file; `aac sso whoami` reports its tenant + expiry
  (B155 — both operate on the cache file only).

## Your tenant_id

`tenant_id` is your org's FEDERATION IDENTIFIER — the stable handle
every other tenant verifies your signatures under: it appears in
`X-AAC-Originator-Tenant-Id` headers, `/.well-known/aac-root-keys/
{tenant_id}` paths, SIEM stream directories, and supplier whitelists.
You do NOT choose it (B154 PR 5, Tenant Identifier Policy): the
control plane allocates the canonical **`tnt-<lowercase UUIDv4>`**
(exactly 40 characters, e.g.
`tnt-550e8400-e29b-41d4-a716-446655440000`) at registration, the CLI
captures it into the selected profile, and it is immutable, public,
and never reused — like an AWS account ID. `--parent-tenant-id` takes
the parent org's canonical `tnt-` id — the server resolves handles to
internal row ids; no database identifier ever crosses the API (B118
D3/D4). Domain ownership verification (DNS TXT) is a separate,
additive control tracked in the `tenant_domains` table — the challenge
flow ships in a follow-up increment.

## Semantics worth knowing

* `aac chain show` accepts ANY hop's token id or the chain root id
  (`--root-token-id` is an accepted alias); visibility is
  participant-tenant over the symmetric composite closure (Week 12) —
  non-participants get the same 404 as unknown tokens.
* Timelines are §V.2.1 METADATA-TIER: predicates and business
  narratives stay in your tenant SIEM stream; the table output's
  footer says so (§6.1 federated join is the full-fidelity story).
* Exit codes (CLI Config+Profile Spec §14, ratified 2026-08-04 —
  supersedes the originator-cli-era convention): 0 success / 1 remote
  outcome (server rejected/denied, or the server-side resource doesn't
  exist) / 2 invalid command-line usage / 3 invalid local
  configuration or state (missing profile/credential, malformed INI,
  bad registration state) / 4 transport failure (unreachable or timed
  out — the retryable class).

## Tests

`uv run pytest cli/aac/tests/` — pytest-httpx mocks; no control plane
needed. Against a live stack: bring up the joined topology
(`./bin/run-wedge-a-control-plane-compose.sh --keep-up`) and point the
flags at localhost.
