Skip to content
Merged
22 changes: 14 additions & 8 deletions AGENTS.md

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ atem repl # Interactive REPL with AI command inter
```bash
atem login # Log in with Agora Console (opens browser)
atem logout # Log out from SSO
atem pair # Pair with Astation, then verify this device
atem status # Account, pairing, verification and credentials (offline, read-only)
atem pair # Pair with Astation, then verify this device (shows the status when already paired)
atem pair --save # Same, and keep credentials for offline use
atem unpair [--yes] # Remove all Astation pairings (asks first)
atem pair verify # Verify this device with the paired Astation (no re-pairing)
atem unpair [--yes] # Remove all Astation pairings and saved sessions (asks first)
```

`atem login` unlocks a limited set: project list, tokens for the active
Expand All @@ -61,23 +63,33 @@ atem and your Mac each show a 12-character safety code. If they match, type
Either side can finish first. atem reads that `y` from the terminal itself,
never from piped input or keys typed earlier, so run `atem pair` in an
interactive terminal. If verification fails, the pairing stays saved, Astation
is told, and `atem pair` exits nonzero; run it again to verify. A connected or SSO-paired session alone does not
mean the device is verified; check `atem cred status`. With `--save`
is told, and `atem pair` exits nonzero; run `atem pair verify` to retry. A connected or SSO-paired session alone does not
mean the device is verified; check `atem status`. With `--save`
the credentials keep working when Astation disconnects; without it they stop
5 minutes after the disconnect (atem asks when `--save` isn't given).

Once paired, `atem pair` only prints the status (`Already paired. Verify this
device: atem pair verify · Replace the pairing: atem unpair, then atem pair`):
it never pairs again or changes keys, trust or sessions. `atem pair verify`
verifies the existing pairing over its saved session, does nothing when the
device is already verified, and refuses after a revocation (run `atem unpair`,
which sets the revoked keys aside, then `atem pair`). `atem status` reads only
local files and an already running key agent; it never connects, so
authorization and relay show as `unknown (stale)`.

### Device keys (`atem cred`)

```bash
atem cred status # Show whether this device is verified, its home Astation and its key agent
atem cred unlock # Unlock this device's keys (approve with Touch ID on your Mac)
atem cred lock # Wipe unlocked keys and stop the key agent
atem cred projects # Send known Git projects to Astation's credential picker (keys must be unlocked)
```

This device's keys are sealed on disk under a storage key that only the paired
Mac (the home Astation) holds. `atem cred unlock` asks the Mac for it (Touch
ID) and starts a background `atem key-agent` that holds the keys in memory.
ID) and starts a background `atem key-agent` that holds the keys in memory;
on a device no Astation verified it stops with `This device isn't verified.
Run atem pair verify first.` and never starts pairing or verification.
`atem cred lock` wipes them and stops the agent. After upgrading atem, run
`atem cred lock` then `atem cred unlock` to pick up the new agent.

Expand Down Expand Up @@ -260,7 +272,7 @@ such as timestamps and ciphertext sizes; it is not authoritative for device
trust or encryption state. Migration cannot prove that a server erased old
plaintext backups.

The device must be verified (`atem pair`) and unlocked (`atem cred unlock`).
The device must be verified (`atem pair`, or `atem pair verify` to retry) and unlocked (`atem cred unlock`).
While it is locked, `atem sync` keeps changes queued. Switching encryption on
or off migrates existing data; each verified atem finishes its part at its next
`atem cred unlock` (`atem sync` alone doesn't contact Astation). **Turning on**
Expand Down
41 changes: 24 additions & 17 deletions designs/e2e-encryption.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ macOS login keychain is locked in SSH sessions, and most atems run over SSH.
unlocked one is never restarted silently: the command warns that `atem
cred lock`, then `atem cred unlock` switches to the new agent, keeps
using the old one, and turns its rejection of a request it doesn't know
into the same hint. `atem cred status` marks it "(started by another atem
into the same hint. `atem status` marks it "(started by another atem
build; …)". Builds are compared for equality only: a dev build and the
npm release of the same version differ, and neither counts as newer.
- **Home Astation.** One storage key per device, held by the device's first
Expand All @@ -484,12 +484,13 @@ macOS login keychain is locked in SSH sessions, and most atems run over SSH.
device keeps its plain keys, as in step 1.
- **`escrowed_storage_kid`** in `cred_state.json` is the last storage kid
Astation is known to hold (set at every confirmed escrow or rotation and
by an unlock that opened the current file). `atem cred status` uses it to
flag a sealed file Astation may not hold, and new storage kids never
by an unlock that opened the current file). It tells an escrowed sealed
file from one Astation may not hold, and new storage kids never
reuse it, the current kid, or any kid the device ever abandoned
(`abandoned_kids`). `escrow_unanswered` records that the home Astation
never answered the first escrow (cleared by the first confirmed one), so
`atem cred status` can say the key goes over once Astation supports it.
never answered the first escrow (cleared by the first confirmed one).
`atem status` flags an unlocked agent whose storage key Astation doesn't
hold yet ("storage key not yet held by Astation; run atem cred unlock").
- **Rotation at every unlock, crash-safe in three phases.** (1) The agent
writes `device_keys.sealed.next` under a new storage key and atem sends
`storageKeyRotate`. (2) Astation stores the new key as pending, keeps the
Expand Down Expand Up @@ -568,7 +569,7 @@ macOS login keychain is locked in SSH sessions, and most atems run over SSH.
`data_keys.enc` are serialized into one exactly sized, wiped buffer, so
no unwiped reallocation copy of `K` or the device keys is left behind. A client that
gets a hang-up from a running v1 agent (for example while writing a large
first request) reports the stop-and-retry hint, and `atem cred status`
first request) reports the stop-and-retry hint, and `atem status`
against a v1 agent prints the version-mismatch line and the same hint
instead of failing.
- **Mode and kid** come only from the latest signed account state in
Expand Down Expand Up @@ -647,7 +648,7 @@ macOS login keychain is locked in SSH sessions, and most atems run over SSH.
be re-grantable.
- A step-2a binary refuses a v2 `device_keys.sealed` (see above). To
downgrade after a `K` was installed, follow the reset steps that
`atem cred status` prints, or wait for a fresh grant.
`atem status` prints, or wait for a fresh grant.
- A key agent left running across the upgrade speaks protocol v1; the
new atem tells you to stop it. After the upgrade a verified device needs
`atem cred unlock` once (protocol v2 agent starts locked) before sync
Expand Down Expand Up @@ -1196,7 +1197,7 @@ atem cred get <name> [--project <key|short-name> | --global]
# value only → stdout; current project, then global
atem cred list [--all]
# name, scope, version, updated; never values
atem cred status # verified? safety code, locked?, unlock mode, granted projects
atem status # verified? safety code, locked? (the only status command)
atem cred unlock # ask Astation to unlock now (get also triggers it)
atem cred lock # wipe keys from agent memory now and stop the agent
atem cred add <name> [--global]
Expand Down Expand Up @@ -1519,9 +1520,10 @@ characters.
An `unlockDenied` with `revoked: true` (or, from an Astation that doesn't
send the flag yet, "revoked" or "does not belong to this verified device" in
`reason`, the answer of an Astation that has forgotten the device) is recorded in `cred_state.json`;
`atem cred status` shows it, and the next `atem pair` with the agent locked
renames the old key files to `<name>.revoked-<stamp>` and verifies with new
keys. The signal is unsigned too, but acting on it only renames old files
`atem status` shows it ("revoked by <id>" for the paired Astation) and
`atem pair verify` refuses; `atem unpair` with the agent locked renames the
old key files to `<name>.revoked-<stamp>`, and the next `atem pair` verifies
with new keys. Verification itself never sets keys aside. The signal is unsigned too, but acting on it only renames old files
aside and re-verifies with a fresh safety code, so a forged "revoked" can
cost a re-pair but never keys or trust.

Expand Down Expand Up @@ -1669,14 +1671,19 @@ Nothing new is stored.

If this device's keys can't be unlocked (the sealed file or
`unlock_auth_key` is damaged or missing, or Astation lost the storage key),
`atem cred status` and
`atem status` and
`atem cred unlock` print:

> To start over: delete ~/.config/atem/device_keys, device_keys.sealed,
> device_keys.sealed.next, device_keys.sealed.prev, unlock_auth_key and
> cred_state.json, then run `atem pair`. device_keys.sealed also holds the
> memory encryption key and its older keys: Astation grants the current key
> again, older keys may not come back.
> To set this device up again: move ~/.config/atem/device_keys,
> device_keys.sealed, device_keys.sealed.next, device_keys.sealed.prev,
> unlock_auth_key and cred_state.json into a backup folder (don't delete them:
> device_keys.sealed holds the memory encryption key and its older keys, which
> older data may need), then run `atem unpair` and `atem pair`. Astation grants
> the current memory key again; older keys come back only from that backup.

Key files are never deleted as routine recovery: setting a device up again
moves them aside, and `atem unpair` / `atem pair` establish the new trust
explicitly (verification never replaces trusted keys on its own).

An Astation rolled back or restored from a backup to an older storage key
counts as having lost it: the storage key rotates at every unlock, so the
Expand Down
57 changes: 57 additions & 0 deletions designs/plans/2026-10-15-status-pair-verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# atem status, pair, pair verify, cred (Atem CLI workflow) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development.

**Goal:** Implement the approved CLI workflow from `designs/session-multiplexing.md` §1 that needs no new wire contract: `atem status` (+ `atem cred status` alias), `atem pair` as a no-op when paired, `atem pair verify`, and `atem cred unlock` that never starts pairing/verification.

**Spec:** `designs/session-multiplexing.md` §1 and §11 rows 2–5 (copy on branch `feat/agent-runtime`; hosted at https://truck.netbird.cloud:58310/session-multiplexing.md). Authoritative wording is quoted in Task 1.

## Global Constraints

- Branch `feat/status-pair-verify` from `main`. Commits end with a blank line and `🤖 Built with SMT <smt@agora.build>`. Never push.
- `atem status` is read-only and works offline: no network, no Astation connection, never starts the key agent (query only a running one), never writes files. Cached remote information is marked stale. User-facing label `Credentials`, never `Keys`.
- `atem pair` on an already paired profile (any paired credential entry or saved Astation session) prints the status plus `Already paired. Verify this device: atem pair verify · Replace the pairing: atem unpair, then atem pair` and exits 0 without connecting, changing identity, trust, keys, sessions or files. Applies also when verification is incomplete.
- `atem pair verify` requires an existing pairing (else: `Not paired. Run atem pair first.`, exit 1). Already validly verified with the paired Astation → `Already verified (safety code …).`, no connection, exit 0. Revoked by that Astation → no verification; message: `This device was revoked by Astation <id>. Run atem unpair, then atem pair, to set it up again.` exit 1 — verification never moves keys aside or replaces trusted keys. Otherwise run the existing verification ceremony (`src/pair_verify.rs` + the verification half of `run_pair`) on a connection authenticated with the saved session (no pairing code, `allow_pairing = false`); cancellation/interruption keeps the pairing.
- `atem pair` (not yet paired) keeps today's pairing flow, then runs verification as today (first-time setup stays one command); the revoked-keys-moved-aside branch is removed from `atem pair` (now only reachable after `atem unpair`, where the device has no pin for that Astation).
- `atem cred unlock` on an unverified profile: `This device isn't verified. Run atem pair verify first.` exit 1, no connection. `atem cred lock` unchanged (offline, always allowed). Neither starts pairing or verification.
- `atem cred status` prints exactly what `atem status` prints (one implementation).
- Never rustfmt `src/cli.rs`, `src/main.rs`, `src/websocket_client.rs`, `src/memory/mod.rs`, `src/memory/crypto.rs`, `src/memory/secrets.rs`; check others with `--check` first. Never project-wide `cargo fmt`.
- Never run the installed `atem`, never touch the real `~/.config/atem`, the user's key agent or running atem processes; tests use temp config dirs (existing patterns in src/memory tests and `cli.rs` tests).
- Full `cargo test` reading every `test result` line; clippy not above `main`'s count (measure first).

### Task 1: status, pair no-op, pair verify, cred gating

Files: `src/cli.rs` (commands, `run_pair`, new `run_status`, `PairCommands::Verify`), new `src/status.rs` (state gathering + rendering, pure and testable), `src/memory/unlock.rs` (cred status → status; unlock gate), `src/pair_verify.rs` (verification entry usable without pairing), tests; AGENTS.md / README.md / npm/README.md command lists.

`src/status.rs` interface:

```rust
pub struct StatusReport {
pub account: Line, // "signed in" (SSO or paired credentials present and not expired) | "signed in (expired)" | "signed out"
pub pairing: Line, // "paired (<astation id>)" | "not paired"
pub verification: Line, // "verified (safety code …)" | "not verified" | "revoked by <id>" | "—" when not paired
pub credentials: Line, // "unlocked" | "locked" | "no key agent running" (=locked) | "not set up"
pub authorization: Line, // "unknown (stale)" until the freshness contract lands — never "fresh" without proof
pub relay: Line, // "unknown (offline check)" — status never connects
pub local_agents: Line, // "unknown" until the runtime lands (feat/agent-runtime) — or a count if a runtime answers without starting it
}
pub struct Line { pub value: String, pub stale: bool }
pub fn gather(config_dir: &Path) -> StatusReport; // reads files + a running key agent only
pub fn render(r: &StatusReport) -> String; // the aligned block from the spec, "(stale)" suffix when stale
```

Output format (labels exactly):

```text
Account: signed in
Pairing: paired (astation-…)
Verification: verified (safety code MN3H-A74N-FJE4)
Credentials: locked
Authorization: unknown (stale)
Relay: unknown (stale)
Local agents: unknown
```

Tests (first): render labels/alignment; gather on temp config dirs: nothing → signed out / not paired / — / not set up; paired+unverified; paired+verified; revoked; `atem pair` when paired makes no connection (point ASTATION_WS at a closed port and assert it never dials, e.g. via a listener that must not get a connection) and leaves files unchanged (hash the config dir before/after); `pair verify` already verified → no connection; not paired → error text; revoked → error text, no files changed; `cred unlock` unverified → error text, no connection; `cred status` output == `status` output; CLI parsing for `status`, `pair verify`.

- [ ] tests → implement → verify → commit `feat(cli): atem status, pair verify, and pair/unlock that never re-pair`.
8 changes: 4 additions & 4 deletions docs/credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ running after the CLI has been updated; see the upgrade steps below.
the pairing command open until it reports device verification.
4. If Astation asks you to save a recovery kit, save the complete kit and
confirm. Finish any key-escrow/unlock approval requested by the pairing
command, then run `atem cred status`.
command, then run `atem status`.

The status must say the device is verified. **Connected** or **Paired with
Astation** alone confirms neither device verification nor key-agent unlock.
Expand All @@ -51,7 +51,7 @@ the relay or remove the Mac approval required to unlock sealed device keys.
## Unlock and upgrade the key agent

```sh
atem cred status
atem status
atem cred unlock
```

Expand All @@ -67,7 +67,7 @@ development build next to the npm one. With the atem you want to use, run:
```sh
atem cred lock
atem cred unlock
atem cred status
atem status
```

Lock wipes keys from memory and stops the agent. Unlock starts the current
Expand Down Expand Up @@ -138,7 +138,7 @@ by account-data encryption. See the [encryption design](../designs/e2e-encryptio

## Current availability

`atem cred status`, `unlock`, `lock` and `projects` are implemented. The
`atem status` and `atem cred unlock`, `lock` and `projects` are implemented. The
planned `atem cred get/list` service for retrieving values from Astation's
Credentials editor is not implemented. Use that editor on the Mac to add,
update, copy or delete its values with Touch ID. Memory and skills must contain
Expand Down
Loading