Metadata-Version: 2.4
Name: aac-cli
Version: 0.1.6
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
Requires-Dist: cryptography>=42.0
Requires-Dist: PyYAML>=6.0
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` command

`aac` is the command-line tool for tenants of Agent Authority Cloud (AAC).
AAC lets an organisation's AI agents and services prove, on every request,
which tenant they belong to and what they were authorised to do. A
**tenant** is one organisation registered with AAC. Each of its workloads
runs beside an AAC sidecar that signs outgoing requests and verifies
incoming ones against public trust material the tenant publishes.

The `aac` command is how a tenant's operator does everything that is not
done by the sidecar itself: register the tenant, sign in, manage keys,
workloads and domains, and prepare a runnable local workload. Everything
works from a terminal or a script; there is no web console to click
through. Output is JSON by default, and every command that reports a
result also offers `--output table`.

Two kinds of tenant use the same tool:

* **Developer tenants** register themselves by signing in with GitHub or
  Google. `aac init` takes a developer from an installed CLI to a runnable
  local workload in one command.
* **Enterprise tenants** are registered through an onboarding ceremony with
  AAC operations and sign in through their own identity provider. Their
  keys, domains and workloads are managed with the `aac tenant` and
  `aac sso` commands described further down.

## Install

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

Python 3.10 or newer is required.

> **Mind the name.** `pip install aac` installs an unrelated project that
> happens to share the acronym. The AAC package is **`aac-cli`**; the
> command it installs is **`aac`**.

## Guided setup for developer tenants: `aac init`

`aac init` prepares the AAC side of one of your agents on this machine: it
registers the agent's identity with AAC and creates the keys, certificates
and configuration its sidecar needs. The agent itself is yours. The first
run also creates your profile and registers your tenant; run it again with
a new `--workspace` and `--workload-path`, and the same `--trust-url` and
`--idp`, for each further agent.

Along the way it signs you in, registers a developer tenant (or reuses the
one your profile is already bound to), takes the trust domain AAC assigns to the
tenant, registers a sample workload, generates development keys and
certificates, and writes a complete sidecar configuration together with the
files the trust-anchor publisher and Docker Compose need.

Point it at the endpoints your AAC environment gives you. The example below
uses the AAC stage environment:

```bash
aac init --profile stage \
  --admin-url https://api.stage.cascadeauth.dev \
  --data-plane-url https://api.stage.cascadeauth.dev \
  --trust-url https://trust.stage.cascadeauth.dev \
  --display-name 'Example Team' --contact 'dev@example.com' --idp github
```

The command shows what it will create and asks before registering the
tenant, because a tenant is permanent. In a script, where there is no
terminal to answer the question, pass `--create-tenant`:

```bash
aac init --profile stage \
  --admin-url https://api.stage.cascadeauth.dev \
  --data-plane-url https://api.stage.cascadeauth.dev \
  --trust-url https://trust.stage.cascadeauth.dev \
  --display-name 'Example Team' --contact 'dev@example.com' --idp github \
  --create-tenant
```

`--idp` names how you sign in: `github` or `google`, the two sign-ins AAC
offers developer tenants. If you need another sign-in provider, contact
support@cascadeauth.com.

`--trust-url` is required when a workspace is created: it is the public URL
where your AAC environment serves trust material, and for AAC stage it is
`https://trust.stage.cascadeauth.dev`. If you leave it out, the command
stops before creating anything and names the value your tenant already
uses, if it has one.

Setting up a new developer tenant takes two browser sign-ins: the first
registers the tenant, the second starts the tenant-admin session that
registers the workload. The command says so before the first one.

Progress lines `[1/5]` to `[5/5]` name the five steps: tenant, sign-in,
hosted trust domain, workload, and material; the configuration files are
rendered last. Rerunning the
command is safe: it resumes an interrupted setup, reports a complete
workspace, or names the exact conflict. It never overwrites private
material. The sign-in you chose with `--idp` is remembered, so a rerun
never asks you to pick between GitHub and Google again.

### What it creates, and where

* `~/.aac/workspaces/<workspace>/` is the **workspace** for one workload
  (default name `starter`). `pki/` holds what the sidecar container mounts:
  the workload, terminal-attestation and localhost TLS key pairs, this
  workspace's root-signing key and the outbound CA bundle. `pair/` holds
  what both containers mount as secrets: the pairing secret and the public
  development CA certificate. `ca/` holds the development CA private key,
  kept out of both mounts so no container ever sees the key that mints
  identities. Beside them sit `sidecar-config.yaml`, `compose.env` and a
  manifest that lets a rerun resume.
* `~/.aac/tenants/<tenant-id>/` is **tenant-level** material shared by every
  workspace of that tenant: the tenant-admin signing key, the publisher's
  input directories (`root-keys/` and `spiffe-bundle/`, public halves only)
  and the rendered `publisher.env`.
* `~/.aac/credentials/<tenant-id>` is the tenant API key. The rendered
  configuration references it by path.

**Everything `aac init` generates is development material.** The
development CA lives seven days and the leaf certificates one day. Nothing
here qualifies for production, whatever the tenant's domain. Moving to
production means a tenant-admin key under production custody
(`aac tenant rotate-admin-key`), a new root key id published by your
publisher, and certificates from your approved issuer.

### What a workspace is for, and how to use it

A workspace holds everything one workload needs to run beside an AAC
sidecar, kept apart from every other workload: its identity in your
tenant's trust domain, its own root signing key and key id, its development
CA and certificates, the pairing secret its sidecar and agent share, and the
configuration files rendered for them. It also records how far setup got,
so rerunning `aac init` resumes where it stopped.

Use one workspace per workload. `aac init --workspace <name>` creates a
workspace (with `--trust-url`) or resumes it (the default name is
`starter`), and three commands look after it:

```bash
aac workspace status --workspace starter           # what exists, what is due, and the next step
aac workspace renew --workspace starter            # fresh certificates and keys
aac workspace render --workspace starter --force   # rewrite the configuration files
```

A second workload is a second workspace with its own workload path:

```bash
aac init --profile stage --workspace worker --workload-path demo/worker \
  --idp github --trust-url https://trust.stage.cascadeauth.dev
```

Pass the same `--trust-url` and `--idp` as for your first workspace. Each
workspace records its own, and the trust URL also goes into the tenant's
`publisher.env`, which every workspace of the tenant shares. A render never
changes that file's endpoints, so a workspace created with a different
trust URL stops at its last step and has to be set up again under a new
name.

`--workspace` names the local files; `--workload-path` chooses the
workload's identity, the path at the end of its SPIFFE ID (`demo/agent`
unless you choose another). Two workspaces with the same workload path are
the same workload holding different keys: that is what a replacement
computer wants, and what a second workload does not. The workspace name is
permanent, because the root key id is made from it. `aac init` needs the
profile the workspace was created with; the `aac workspace` commands read it
from the workspace itself.

### What "render" means

To render is to write the configuration files that follow from what the
workspace records, without touching any key: `sidecar-config.yaml` and
`compose.env` in the workspace, and `publisher.env` in the tenant
directory. They hold endpoints, identifiers, settings and the paths of the
key files, never key material.

`aac init` renders them as its last step. Render again with
`aac workspace render --workspace <name> --force` when a configuration file
is lost or to change the layout; without `--force`, `render` refuses to
overwrite files that exist. The layout decides the paths written into the
sidecar configuration: `host` (the default) uses paths on this machine, for
a sidecar that runs directly on it, and `container` uses the paths where the
files are mounted inside the containers. `compose.env` always holds the
paths on this machine that Docker mounts from, and `publisher.env` the
paths a publisher running on this machine reads.

`publisher.env` belongs to the tenant, and every workspace of the tenant
renders it. A render rewrites its paths and poll interval for this machine
(hand edits to those are replaced), but refuses, even with `--force`, to
change its tenant ID, trust domain or URLs, because that would repoint the
publisher for every workspace. The refusal names the values that differ.
If the file is the stale one, for example after you deliberately moved the
tenant to new endpoints and set up its workspaces again, move it aside
(`mv publisher.env publisher.env.old` in the tenant directory) and render
again from a workspace with the intended endpoints. The admin key and the
published public keys stay where they are.

### How profiles, workspaces and tenant data fit together

```text
profile (~/.aac/config) ── bound to ──> tenant
                                          ├── credentials:      ~/.aac/credentials/<tenant-id>
                                          ├── tenant directory: ~/.aac/tenants/<tenant-id>/
                                          └── workspaces:       ~/.aac/workspaces/<name>/, one per workload
```

* A **profile**, in `~/.aac/config`, names the AAC admin and data-plane
  endpoints you use and, once registration or sign-in has bound it, the one
  tenant it acts for. The trust URL and the sign-in choice are recorded in
  each workspace instead. "Profiles and configuration", further down,
  covers profiles in full.
* The **tenant data** is shared by all of a tenant's workspaces on this
  machine: the tenant API key and the tenant-admin session in
  `~/.aac/credentials/`, and the tenant directory
  `~/.aac/tenants/<tenant-id>/` with the tenant-admin signing key, the public
  keys and certificates the publisher uploads, and `publisher.env`.
* A **workspace**, in `~/.aac/workspaces/<name>/`, holds one workload's
  material. It records the profile and the tenant it was created for and
  keeps them: one tenant can have many workspaces, and a workspace never
  moves to another tenant.

### What each file is for, and what to back up

The table lists every file the CLI creates, what it is for, how long it
lives and whether to back it up. `<workspace>` is the workspace name
(default `starter`) and `<tenant-id>` the tenant id AAC allocated.

<!-- inventory-table:start -->
| No. | Material | Where | Used for | Lifetime | Back up? |
|---|---|---|---|---|---|
| 1 | tenant_api_key | `~/.aac/credentials/<tenant-id>` | CLI data-plane calls; the sidecar's workload projection | until retired or rotated; `aac tenant reissue-api-key` | **Yes.** save a protected copy in your secret manager; loss needs `aac tenant reissue-api-key` |
| 2 | tenant_admin_session | `~/.aac/credentials/<tenant-id>.session` | CLI admin calls | hours; `aac sso login` | No. sign in again |
| 3 | tenant_admin_signing_key | `~/.aac/tenants/<tenant-id>/tenant-admin.pem`, `~/.aac/tenants/<tenant-id>/tenant-admin.pub.pem` | the publisher signs trust-material uploads; the public half is registered with AAC | until rotated; `aac tenant rotate-admin-key` | **Recommended.** recommended for developer tenants (loss needs `aac tenant rotate-admin-key`; a lost public half is derived from the private key); production custody belongs in an HSM/KMS, not a password manager |
| 4 | root_signing_key | `~/.aac/workspaces/<workspace>/pki/root.pem`, `~/.aac/workspaces/<workspace>/pki/root.pub.pem`, `~/.aac/tenants/<tenant-id>/root-keys/<workspace>-root-v1.pub.pem` | the sidecar mints authority chains; the public half is published under the key id | until you retire its key id (root key ids are fixed per workspace name) | No. never restore the private key: a replacement workspace gets a new name and therefore a new key id, published alongside the old one until the old one is retired; its public copies are recreated from it |
| 5 | development_ca | `~/.aac/workspaces/<workspace>/ca/dev-ca.key`, `~/.aac/workspaces/<workspace>/pki/dev-ca.crt`, `~/.aac/workspaces/<workspace>/pair/dev-ca.crt`, `~/.aac/tenants/<tenant-id>/spiffe-bundle/<workspace>-dev-ca.ca.pem` | signs the workload, terminal and TLS certificates; peers trust it via the published bundle | 7 days; `aac workspace renew --ca` | No. development only; reissue; a lost copy of the certificate is copied back from `pki/dev-ca.crt` |
| 6 | workload_svid | `~/.aac/workspaces/<workspace>/pki/workload.key`, `~/.aac/workspaces/<workspace>/pki/workload.crt` | the sidecar's SPIFFE identity: proves on every live request that this workload holds the key behind its certificate | 1 day; `aac workspace renew` | No. development only; reissue |
| 7 | terminal_attestation | `~/.aac/workspaces/<workspace>/pki/terminal.key`, `~/.aac/workspaces/<workspace>/pki/terminal.crt` | signs the receipt (terminal attestation) the sidecar issues when this workload completes a delegated request; auditors verify it offline against your published CA; kept separate from the workload key so neither can forge the other's role | 1 day; `aac workspace renew` | No. development only; reissue |
| 8 | localhost_tls | `~/.aac/workspaces/<workspace>/pki/server.key`, `~/.aac/workspaces/<workspace>/pki/server.crt` | the sidecar's HTTPS listener | 1 day; `aac workspace renew` | No. development only; reissue |
| 9 | outbound_ca_bundle | `~/.aac/workspaces/<workspace>/pki/outbound-ca.pem` | the sidecar verifies TLS to AAC and to itself | rebuilt by renew; `aac workspace renew` | No. derived |
| 10 | pairing_secret | `~/.aac/workspaces/<workspace>/pair/pairing.secret` | the sidecar and its agent authenticate each other (same bytes in both) | until regenerated | No. regenerable; both processes must read the same file |
| 11 | workspace_manifest | `~/.aac/workspaces/<workspace>/manifest.json` | the workspace's identities and progress; every `workspace` verb reads it first | for the life of the workspace | **Recommended.** cannot be recreated by any command; without it start a new workspace under a new name (see the new-computer setup procedure) |
| 12 | published_public_inputs | `~/.aac/tenants/<tenant-id>/root-keys`, `~/.aac/tenants/<tenant-id>/spiffe-bundle` | the publisher's input directories: every root public key and CA certificate this tenant has published, across all workspaces | for the life of the tenant | **Recommended.** back up with the tenant directory: a replacement workspace must publish its new root key alongside a still-ACTIVE previous one (the control plane refuses a root-key set that drops every ACTIVE key at once) |
| 13 | rendered_files | `~/.aac/workspaces/<workspace>/sidecar-config.yaml`, `~/.aac/workspaces/<workspace>/compose.env`, `~/.aac/tenants/<tenant-id>/publisher.env` | non-secret configuration derived from the manifest | re-rendered on demand; `aac workspace render --force` | No. derived; `aac workspace render --force` recreates them |
<!-- inventory-table:end -->

In short: back up the tenant API key and the whole tenant directory while
the machine is healthy. Everything else is regenerated.

### Inspect and maintain a workspace

```bash
aac workspace status --workspace starter --output table   # what exists, expiry, permissions, next command
aac workspace status --workspace starter --remote         # also: is the public trust material visible?
aac workspace renew --workspace starter                   # fresh leaf keys and certificates; old ones archived
aac workspace render --workspace starter --layout container --force   # container path layout
```

`status` works offline by default and exits with code 3 when material is
missing, expired or unsafely permissioned. `renew` reissues the one-day
certificates with fresh keys, and reissues the development CA only when it
is expired or within a day of expiry (or when `--ca` is given); a new CA is
published under the next anchor id and needs the publisher restarted.
`render` never touches keys and refuses to overwrite rendered files unless
`--force` is given.

`status` also names one next step, `next_command`. When material is
missing, it is the fix for the first missing item: restore it from your
backup, recreate a lost public copy from the file it copies, renew, or
render. Otherwise it is renewal when a certificate is due. After the step,
run `status` again. Commands it names that change your tenant carry the
workspace's own `--tenant-id` and `--admin-url`, so a value left in
`AAC_TENANT_ID` or `AAC_ADMIN_URL` cannot aim them at another tenant. The
file recipes use `openssl`, which must be OpenSSL 1.1.1 or newer: the
`openssl` that ships with macOS cannot read Ed25519 keys, so install a
current OpenSSL there, for example with Homebrew.

Every private file is created once, readable only by you, and never
overwritten. Before generating anything, `init` checks every destination
and refuses with the full list if one already exists. `aac init` never
installs or replaces a tenant-admin key for an existing tenant: the key on
this machine must already be the tenant's active admin key, and replacing
it is always the deliberate `aac tenant rotate-admin-key` command, never a
side effect of setup. A second workspace for the same tenant
(`--workspace other`) reuses the tenant-admin key and gets its own root key
id.

### What to do when keys and certificates expire

It is the certificates that expire, and renewing replaces their keys with
them. `aac workspace status` shows when each certificate runs out and flags
it before it does: six hours ahead for the one-day certificates, a day
ahead for the development CA.

| Material | Lasts | When it runs out | What to do |
|---|---|---|---|
| Workload, terminal-attestation and localhost TLS certificates | 1 day | the sidecar can no longer prove its identity, sign receipts or serve HTTPS | `aac workspace renew --workspace <name>`, then restart the sidecar so it loads them |
| Development CA | 7 days | certificates it signed stop being trusted, and no new ones can be issued | `renew` also replaces the CA when it is expired or within a day of expiry, or when `--ca` is given, and then reissues every certificate under it: restart the publisher so the new CA is published, restart the sidecar and anything else that trusted the old CA, and confirm with `aac workspace status --workspace <name> --remote` |
| Tenant-admin session | hours | administration commands ask you to sign in again | `aac sso login` |
| Tenant API key, tenant-admin signing key, root signing key | do not expire | — | replaced only on purpose: see "Keys" and "Setting up aac-cli on a new computer" |

A simple routine: when you start work, run
`aac workspace status --workspace <name>`, and when it names renewal, run
`aac workspace renew --workspace <name>` and restart the sidecar. About once
a week that renewal also replaces the development CA (`renew` then reports
`republication_required: true`): restart the publisher, the sidecar and the
agent, and anything else that trusted the old CA. Renewing is safe to
repeat, but it is not a no-op: every run issues fresh keys and moves the old
files to the workspace's `archive/` directory, so whatever reads them must
reload. With the
[AAC Compose starter](https://github.com/CascadeAuth/aac-compose-starter),
`./starter up` recreates the containers so they load the new files; it
does not renew anything itself.

All of this is development material. In production, certificates come from
your own issuer and follow its renewal process.

## Setting up aac-cli on a new computer

This procedure covers a lost machine and a second machine alike. It needs
the two backups named above: the tenant API key from
`~/.aac/credentials/<tenant-id>` and the tenant directory
`~/.aac/tenants/<tenant-id>/`. The tenant id is the `tnt-…` value in your
profile; `aac profile show` prints it, and the example below uses
`tnt-550e8400-e29b-41d4-9716-446655440000`.

1. Install the CLI, create the profile, and sign in. Signing in needs the
   tenant id, because a fresh profile is not yet bound to a tenant:

   ```bash
   pip install aac-cli
   aac profile create stage \
     --admin-url https://api.stage.cascadeauth.dev \
     --data-plane-url https://api.stage.cascadeauth.dev
   aac sso login --profile stage --idp github \
     --tenant-id tnt-550e8400-e29b-41d4-9716-446655440000
   ```

   An enterprise tenant signs in through its own identity provider instead,
   naming the connection by its URL:

   ```bash
   aac sso login --profile stage \
     --idp-url https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0 \
     --tenant-id tnt-550e8400-e29b-41d4-9716-446655440000
   ```

2. Restore the tenant API key to `~/.aac/credentials/<tenant-id>`, readable
   only by you, or issue a new one if it was not backed up:

   ```bash
   aac tenant reissue-api-key --profile stage
   ```

   Restore `~/.aac/tenants/<tenant-id>/` from the backup, with directories
   readable only by you and files likewise. Its `publisher.env` still names
   the old machine's paths; step 3 rewrites them for this one.

   If the tenant directory was not backed up, first check whether another
   machine still holds `~/.aac/tenants/<tenant-id>/`: every machine that has
   run `aac init` for this tenant has one. If so, copy it from there. That
   is the backup, and the rest of this step does not apply.

   If no copy exists anywhere, the tenant-admin key and every root key that
   lived on the lost machine are gone for good. Install a new admin key,
   then account for every root key the tenant has published (the `openssl`
   commands need OpenSSL 1.1.1 or newer, which macOS does not ship):

   ```bash
   openssl genpkey -algorithm ed25519 -out tenant-admin.pem
   openssl pkey -in tenant-admin.pem -pubout -out tenant-admin.public.pem
   aac tenant rotate-admin-key --profile stage \
     --tenant-admin-pubkey-file tenant-admin.public.pem
   aac trust-anchor list --profile stage --role root-signing --output table
   aac trust-anchor revoke --profile stage --kid starter-root-v1 \
     --reason 'machine lost'
   ```

   The list shows each root key with its state. A publisher may drop a key
   from its published set only while another active key stays in the set,
   so a set holding nothing but the new key is accepted only once no other
   key is active. Revoke every active key whose private half was lost, one
   `revoke` per key id, and no other key: a root key that is still in use
   elsewhere must stay active, and its public file must be in the tenant
   directory the publisher runs from. Then run step 3 with
   `--tenant-admin-key-file tenant-admin.pem` added.

3. Run the guided setup with a **new workspace name**. A developer tenant
   passes its sign-in choice, so an expired session is renewed without a
   prompt:

   ```bash
   aac init --profile stage --workspace laptop-2 --idp github \
     --trust-url https://trust.stage.cascadeauth.dev
   ```

   An enterprise tenant runs the same command without `--idp`; `--idp`
   selects only the GitHub and Google developer sign-ins. The guided setup
   then uses the session from step 1. If that session has expired, repeat
   the `aac sso login --idp-url …` command from step 1 first:

   ```bash
   aac init --profile stage --workspace laptop-2 \
     --trust-url https://trust.stage.cascadeauth.dev
   ```

4. Start the publisher from the tenant directory, restored or written by
   step 3. A restored directory's root-key set holds the old and the new
   public keys, which is exactly what AAC requires: a set that drops every
   currently active key at once is refused. Retire the old key later, by
   removing its public file, once nothing signs with it. A directory written
   fresh after the revocations in step 2 holds only the new key, which is
   accepted because no other key is active any more.

### Why the replacement workspace needs a new name

The workspace name is baked into two identifiers that are published for
the whole tenant: the root key id `<workspace>-root-v1` and the development
CA anchor id `<workspace>-dev-ca`. Other parties look up your keys and
certificates by those ids. AAC therefore never accepts new key material
under an id that has already been published: a key id names one key, for
good. A new machine has to generate new keys, so it has to publish them
under new ids, which means a new workspace name. The new ids are published
alongside the old ones until the old ones are retired.

Reusing the old workspace name on the new machine is refused on purpose:
the restored tenant directory still holds that name's published root key,
and `init` never generates different key material under an existing key
id. The development certificates and the pairing secret are regenerated by
step 3; they are never restored.

## Profiles and configuration

A **profile** is a named set of endpoints plus the tenant the profile is
bound to. Profiles live in `~/.aac/config`, a plain INI file with one
section per profile:

```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-9716-446655440000

[stage]
admin_url = https://api.stage.cascadeauth.dev
data_plane_url = https://api.stage.cascadeauth.dev
```

```bash
aac profile list                                  # every profile, including the built-in main
aac profile show stage --output table             # stored and effective values, with sources
aac profile create prod --admin-url https://aac-admin.example.com \
  --data-plane-url https://aac-data.example.com
aac profile update main                           # edit the built-in baseline
aac profile delete prod                           # local only; never touches the server
```

`main` is the reserved baseline profile. It always exists, with localhost
defaults until `aac profile update main` stores real values, and cannot be
created or deleted. Every operational command selects its profile in this
order: `--profile <name>`, then the `AAC_PROFILE` environment variable, then
`main`. A selected profile that does not exist is an error, never a silent
fallback. Within the selected profile, a flag beats an environment variable
(`AAC_ADMIN_URL`, `AAC_DATA_PLANE_URL`, `AAC_TENANT_ID`), which beats the
stored value.

`tenant_id` is managed for you. Profile commands display it but never ask
for it; only `aac tenant register`, `aac init` and `aac sso login` write it.
It is your organisation's identifier in the federation, allocated by AAC at
registration in the form `tnt-<uuid>`, and it never changes.

`AAC_CLI_HOME` relocates the whole `~/.aac` tree. Use it for a throwaway
test home, or to keep separate accounts hard-walled from each other:

```bash
AAC_CLI_HOME=$HOME/.aac-prod aac profile list
```

### Credentials

Two credential files can coexist under `~/.aac/credentials/`:

* `<tenant-id>` holds the tenant API key, written once by registration and
  printed exactly once. The sidecar and the data-plane commands
  (`aac chain`, `aac trust-anchor`) present it. Only a hash of it exists
  on the server, so a lost key is replaced, never recovered.
* `<tenant-id>.session` holds the short-lived session that `aac sso login`
  creates. The administration commands (`aac tenant`, `aac sso`) use it
  automatically. Sessions cannot be refreshed: when one expires, any
  administration command tells you to sign in again, and there is no
  refresh token that could be stolen.

## Signing in

```bash
aac sso login --profile stage --idp github                # developer tenant: GitHub or Google
aac sso login --profile prod \
  --idp-url https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0
aac sso login --profile prod \
  --idp-url https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0 \
  --flow pkce --no-browser                                # print the URL instead of opening a browser
aac sso whoami --profile stage --output table            # cached session and the tenant's assigned domain
aac sso logout --profile stage
```

A developer tenant signs in with `--idp github` or `--idp google`; if you
need another sign-in provider, contact support@cascadeauth.com. An
enterprise tenant signs in through its Microsoft Entra ID connection, named
by the connection's exact URL with `--idp-url`; when several connections
are registered, the error message lists the exact commands to choose one.

Sign-in uses the device flow by default, so the browser can be on another
machine and headless hosts work. Google connections use a loopback flow
instead, because Google's device flow is scope-restricted; `--flow`
overrides the choice. `whoami` and `logout` work offline and never contact
AAC.

## Output and exit codes

Every command that reports a result prints one JSON document to standard
output by default, with progress notes and diagnostics on standard error,
so the JSON stays parseable in scripts; `--output table` prints a readable
table instead. The three profile writers are the exception: `aac profile
create`, `update` and `delete` confirm on standard error, leave standard
output empty and take no `--output`.

| Exit code | Meaning |
|---|---|
| `0` | Success. |
| `1` | AAC or the identity provider rejected the request. |
| `2` | Usage error: an invalid flag, value or flag combination. |
| `3` | A local configuration or state problem: profile, credential file, cached session or workspace. |
| `4` | Transport failure: an endpoint could not be reached. |

If AAC asks you to slow down, the command shows the interval to wait. The
CLI never replays a change on its own; wait and rerun deliberately.

## Enterprise tenants

Enterprise tenants are registered through an onboarding ceremony: AAC
operations opens a registration window and hands the tenant operator a
bootstrap token, which the commands below read from `--bootstrap-token` or
the `AAC_BOOTSTRAP_TOKEN` environment variable. The token authorises only
the onboarding steps shown with it; every other administration command runs
under a signed-in session.

For enterprise sign-in, AAC supports Microsoft Entra ID (also known as
Azure Active Directory or Azure AD). If your organisation needs another
identity provider, contact support@cascadeauth.com.

### Register the tenant

Generate the tenant-admin key pair first. The CLI transmits the public half
only and refuses a private key:

```bash
openssl genpkey -algorithm ed25519 -out tenant-admin.pem
openssl pkey -in tenant-admin.pem -pubout -out tenant-admin.public.pem
aac profile create prod --admin-url https://aac-admin.example.com \
  --data-plane-url https://aac-data.example.com
aac tenant register --profile prod --display-name 'Example Corporation' \
  --contact ops@example.com \
  --workload-spiffe-id spiffe://example.com/treasury-agent/v1 \
  --tenant-admin-pubkey-file ./tenant-admin.public.pem \
  --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
```

AAC allocates the tenant id; you never choose one. The response prints the
id and the tenant API key exactly once, and the CLI saves both. A profile
that is already bound to a tenant refuses to register another; use an
unbound profile. If the response is lost, rerun the same command with no
changes to the identifying flags: the CLI resumes the frozen request rather
than registering twice.

```bash
aac tenant describe --profile prod --output table
aac tenant update --profile prod --tenant-id tnt-550e8400-e29b-41d4-9716-446655440000 \
  --display-name 'Example Corporation Ltd'
```

### Connect your identity provider

An identity-provider connection is described by a JSON file
(`aac sso register-idp --help` shows the fields, with an example for a
Microsoft Entra ID tenant). Before the first
connection, generate an offline recovery key so the connection can be
repaired if the provider is ever unavailable; only the enrollment file
leaves the machine:

```bash
aac sso generate-idp-recovery-key \
  --private-key-file offline-idp-recovery-private.pem \
  --enrollment-file idp-recovery-enrollment.json
aac sso enroll-idp-recovery-key --profile prod \
  --file idp-recovery-enrollment.json --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
aac sso register-idp --profile prod --file connection.json \
  --bootstrap-token "$AAC_BOOTSTRAP_TOKEN"
aac sso list-idp --profile prod
```

Later connections, corrections (`aac sso replace-idp`) and the two-party
repair ceremony (`aac sso request-idp-repair`, `aac sso approve-idp-repair`)
are described by their `--help`.

### Keys

Replacing the tenant-admin key is immediate and forward-only: the previous
key stops verifying at once and can never be reinstated. The trust-anchor
publisher signs its uploads with this key and loads it once, at start, so
sequence the change: stop the publisher, register the new public half, put
the new private key where the publisher's configuration points, start the
publisher, and confirm that its first upload is accepted under the new key.
A publisher installed as a system service reads the key path from its
environment file, which you can point at the new file. A Docker publisher's
environment and key mount are fixed when the container is created, so
overwrite the key at the mounted path and recreate the container; a plain
restart would come back on the old key.

```bash
aac tenant rotate-admin-key --profile prod \
  --tenant-admin-pubkey-file tenant-admin.public.pem
```

A developer tenant set up with `aac init` keeps its tenant-admin key in its
tenant directory, where the publisher's configuration points. To replace a
lost key there, write the new pair in place, readable only by you, sign in,
register the new public half, and then recreate every publisher container.
Pass the tenant id and admin URL explicitly, as `aac workspace status`
does, so the change cannot land on another tenant. The `openssl` commands
need OpenSSL 1.1.1 or newer, which macOS does not ship:

```bash
cd ~/.aac/tenants/tnt-550e8400-e29b-41d4-9716-446655440000
(umask 077 && openssl genpkey -algorithm ed25519 -out tenant-admin.pem.tmp && mv tenant-admin.pem.tmp tenant-admin.pem)
(umask 077 && openssl pkey -in tenant-admin.pem -pubout -out tenant-admin.pub.pem.tmp && mv tenant-admin.pub.pem.tmp tenant-admin.pub.pem)
aac sso login --profile stage --idp github \
  --tenant-id tnt-550e8400-e29b-41d4-9716-446655440000 \
  --admin-url https://api.stage.cascadeauth.dev
aac tenant rotate-admin-key --profile stage \
  --tenant-id tnt-550e8400-e29b-41d4-9716-446655440000 \
  --admin-url https://api.stage.cascadeauth.dev \
  --tenant-admin-pubkey-file tenant-admin.pub.pem
```

The tenant API key can be rotated without downtime. `issue` stages a second
active key in a new file and never replaces the current one; move every
client to it, then retire the old key by its exact id:

```bash
aac tenant api-key list --profile prod
aac tenant api-key issue --profile prod
aac tenant api-key retire --profile prod --key-id ak_0123456789abcdef --yes
```

A lost or suspected-compromised key is replaced instead: every active key is
revoked and one fresh key is printed once.

```bash
aac tenant reissue-api-key --profile prod
```

### Domains and workloads

Every tenant gets a hosted trust domain from AAC; registration saves it in
the profile, and `aac tenant assign-hosted-domain` assigns one to a tenant
that has none yet. To use your own DNS domain as a trust domain instead,
prove control of it with a DNS TXT record, then bind it:

```bash
aac tenant issue-domain-challenge --profile prod --domain example.com
# Publish the TXT record the command prints, then:
aac tenant verify-domain --profile prod --domain example.com
aac tenant bind-trust-domain --profile prod --trust-domain example.com
aac tenant list-trust-domains --profile prod --output table
```

Workloads are the identities your sidecars run under:

```bash
aac tenant add-workload --profile prod --spiffe-id spiffe://example.com/payroll/v1 \
  --display-name Payroll
aac tenant list-workloads --profile prod
aac tenant deactivate-workload --profile prod --workload-id wl-2f9c1e --reason 'service retired'
```

Deactivation is terminal, and a deactivated SPIFFE id stays reserved.

### Inspect trust material and audit a chain

```bash
aac trust-anchor list --profile prod --output table          # every published key, every lifecycle state
aac trust-anchor describe --profile prod --kid starter-root-v1
aac chain show --profile prod \
  --token-id c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9 --output table
aac chain show --profile prod \
  --token-id c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9c3d5e7f9 \
  --render --render-out ./chain.html
```

`chain show` accepts any hop's token id or the chain root id and shows the
chain to any tenant that took part in it. The timeline carries metadata
only; the business content of each step stays in your own audit stream.

## Getting help

`aac --help` lists the command groups and `aac <command> --help` shows every
flag with its default. Errors name the exact command to run next.

## License

Apache-2.0.
