Files
emcli/docs/superpowers/specs/2026-06-23-agent-account-list-design.md
steve 7039371f70 docs(spec): agent-readable account list (reduced JSON view)
Let an agent holding only EMCLI_KEY discover accounts via `account list`,
exposing name/from/can_send (not host/username); admin keeps the full
text table. account add/edit/remove stay admin-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:29:07 +01:00

6.4 KiB

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 listRoleAgent
  • 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:
    {"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: <err> 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).