# Agent-readable `account list` — design **Date:** 2026-06-23 **Status:** Approved (brainstorm), ready for implementation plan **Author:** Steve + Claude ## Problem An agent process is launched with only `EMCLI_KEY` (the two-key model — see `2026-06-22-two-key-privilege-design.md`). Every agent command takes `--account NAME`, but the agent has no way to *discover* which accounts exist: `account list` is classified admin-only and is refused under the agent key. So account names must be supplied to the agent out of band, which is brittle and defeats the point of a self-directed agent. The two-key spec deliberately gated the whole `account` command to admin because `account add/edit/remove` mutate configuration. But `account list` is read-only and exposes no secrets — `store.ListAccounts` never decrypts the password (`enc_password` is scanned and discarded). Gating *discovery* behind admin is stricter than the threat model requires. ## Goal Let an agent holding only `EMCLI_KEY` run `account list` to discover the accounts it may operate on, while: - keeping `account add/edit/remove` admin-only (mutation stays gated); - exposing to the agent only what it needs — **account name, the send-as From address, and whether the account can send** — and *not* the IMAP host/port or login username; - preserving the admin's existing full-detail view unchanged. ## Constraints / decisions Settled during brainstorming: 1. **Scope is exactly `account list`.** `whitelist list`, `config get`, and `audit list` stay admin-only. `audit` in particular is oversight data and must remain invisible to the agent. 2. **Privilege-dependent rendering.** The admin keeps the current full table (`NAME MODE IMAP USER`, human-readable). The agent gets a *reduced* view containing only name, From, and send-capability. 3. **Agent output is JSON.** The agent is a machine consumer, so its `account list` emits the standard agent envelope (like `list`/`get`/`search`), not a text table. The admin path stays human-readable text. 4. **No secret exposure, no schema change.** `ListAccounts` already avoids decrypting passwords; nothing about the data model changes. ## Approach Reclassify `account list` to the agent role, and branch rendering on the caller's actual privilege (presence of the admin key). ### Routing (`internal/cli/run.go`) `commandRole` becomes subcommand-aware for `account`: - `account list` → `RoleAgent` - `account add | edit | remove` (and bare `account`) → `RoleAdmin` - all other commands unchanged. `commandRole` currently takes `cmd string`; it changes to take the full `args []string` so it can peek at the `account` subcommand. `Run` passes `args` through. This keeps `commandRole` the single source of truth for the classification table. Authorization mechanics are unchanged: `openStore(RoleAgent)` requires `EMCLI_KEY` (falling back to the admin key for a human who holds only that and runs `account list`). `account add/edit/remove` still hard-require `EMCLI_ADMIN_KEY` with no fallback. ### Rendering (`internal/cli/admin.go`, the `list` branch) Determine privilege from the environment: `_, err := crypto.AdminKeyFromEnv()`; `isAdmin := err == nil`. (Holding the admin key *is* being the admin in this trust model. A human with only the admin key still gets the admin view; an agent with only `EMCLI_KEY` gets the reduced view.) - **Admin** → existing full table, unchanged: ``` NAME MODE IMAP USER work RW imap.example.com:993 me@example.com ``` - **Agent** → JSON envelope to stdout: ```json {"error":false,"error_detail":{},"data":{"accounts":[ {"name":"work","from":"me@example.com","can_send":true}, {"name":"alerts","from":"alerts@example.com","can_send":false} ]}} ``` where `from = Account.SendFrom()` (the configured From address, falling back to the username) and `can_send = (Mode == "RW")` (RW accounts have SMTP configured; RO cannot send). The IMAP host/port and the raw login username are **not** emitted (when From falls back to the username it may coincide with it, which is acceptable — the user asked for From specifically). The reduced view reuses the existing `Success(...)` envelope and `Envelope.Write` helper; no new output machinery. ## Error handling - Agent key on `account add/edit/remove` → unchanged: `emcli: this command requires EMCLI_ADMIN_KEY (admin privilege)`, non-zero. - Agent `account list` with neither key set → the agent-command config error (`EMCLI_KEY is not set`), surfaced the same way other agent commands surface a missing key. Because the agent path now emits JSON, a missing-key failure on `account list` is a JSON `Failure(CodeConfig, …)` envelope, consistent with the other agent commands. - DB/list errors on the agent path → `Failure(CodeDB, …)` envelope; on the admin path → existing `list: ` text to stderr. ## Testing - **Routing:** extend the `commandRole` table test — `account list` → agent; `account add` / `account edit` / `account remove` → admin. - **Agent view:** with only `EMCLI_KEY`, `account list` exits 0, emits a valid envelope, and the `data.accounts` entries carry `name`/`from`/`can_send` — and the output does **not** contain the IMAP host or the login username. - **Admin view:** with `EMCLI_ADMIN_KEY`, `account list` still prints the full `NAME MODE IMAP USER` table (regression guard). - **`can_send`:** an RW account yields `can_send:true`, an RO account `can_send:false`; `from` reflects `SendFrom()` (explicit From, else username). - **Security invariant (`security_invariant_test.go`):** remove `{"account","list"}` from the refused-commands set (it is now allowed) and replace it with a *mutating* `account add …` attempt, so the "forced agent cannot run admin commands and the DB is byte-for-byte unchanged" invariant still covers the `account` family. ## Documentation updates - **USER-MANUAL:** role/command table — `account list` is agent-readable (reduced JSON view); `account add/edit/remove` remain admin. - **`skills/emcli` (SKILL.md / AGENTIC-MANUAL.md):** document that the agent discovers accounts via `account list`, including the JSON shape (`name`, `from`, `can_send`). ## Out of scope - Agent access to `whitelist list`, `config get`, or `audit list`. - Any change to the admin `account list` columns or to the data model. - JSON output for the admin `account list` path (stays human-readable text).