# ADR 0004: Human-Controlled Account Locks

Audience: AI coding agents first.

## Status

Accepted.

## Context

Routing normally chooses among a default account, project rules, and fallback chains. When other accounts are rate-limited, an autonomous caller can explicitly request an owner account with `--account`, even when the owner does not want any new work charged to it. A display-only preference is insufficient: every supported launch path needs the same persistent admission decision, and invalid policy state must not silently reopen an account.

## Decision

- Store account locks in the canonical runtime shared tier (`$SYSTRAY_RUNTIME_DIR/account_locks.json`, otherwise `~/.local/state/overdeck/systray/runtime/account_locks.json`), schema `account-lock/v1`, keyed by `tool:slug` for `codex`, `claude`, and `grok`. The canonical policy applies to matching account copies found through legacy compatibility roots as well.
- Systray AI renders `🔒` or `🔓` beside every account. A locked account cannot be selected as default. The tray can lock immediately and requires an explicit confirmation dialog before unlocking.
- The `account-lock` terminal command can inspect, lock, and unlock accounts. Mutations require a non-agent human terminal and the exact confirmation phrase read from `/dev/tty`.
- `cdx`, `cld`, and direct account selection inside `claudex` enforce the same store. Locked accounts are removed from automatic routing and explicit fallback chains. If every candidate is locked, launch fails with exit code 77 and a machine-readable refusal.
- Locking the current default removes its live credential link. If a provider replaced that link with a regular credential file, the file is moved back to the real per-account tier with mode `0600` before the live path is removed.
- A lock blocks new account-backed sessions, live health probes, and re-authentication. It does not terminate a process that was already running when the lock changed.
- A human may use an explicitly selected locked account for one top-level launch with `--human-override-lock`. Each affected provider must resolve from exactly one explicit selector. The launcher rejects agent or container ancestry, requires a controlling human terminal, reads the exact `USE tool:slug ONCE` phrase from `/dev/tty`, and leaves the persistent lock set.
- Launchers revalidate immediately before process creation so a lock applied after routing still blocks the launch. Malformed, unsafe, unreadable, or race-changed lock state fails closed.
- Renaming an account moves its lock to the new slug. Removing an account removes its lock. Both operations restore prior lock state if their registry transaction rolls back.

## Security Boundary

This decision is a hard policy boundary inside the supported Systray AI launchers, not a Unix privilege boundary. A process with the same UID and unrestricted filesystem or executable access can delete policy state, set provider home variables, or invoke provider binaries directly. Existing agent cgroup and command confinement must prevent those bypasses. A cryptographic or privilege-separated guarantee would require a separately privileged broker or a different UID that exclusively owns credentials and lock state.

## Consequences

- Owner accounts stay unavailable to automatic fallback and agent-supplied `--account` requests while locked.
- Existing sessions remain stable; the feature controls admission rather than process revocation.
- Health data for a locked account can become stale by design because background probes do not touch it.
- Invalid lock state can temporarily block all supported launches until a human repairs the state.
- Every new account-backed launcher must call the shared authorization and pre-launch revalidation APIs.

## Checks

- A locked default has no active credential link and cannot start through `cdx`, `cld`, or `claudex`.
- Routing skips locked candidates and returns exit 77 when all candidates are locked.
- Re-authentication and live health refresh are disabled while locked.
- A one-shot human override prompts once, starts only the explicitly named account, and leaves it locked.
- Agent ancestry, container ancestry, missing TTY, wrong confirmation text, and invalid lock state all deny access.
