carlos / platform Public

docs: /api/cli/apps already serves memberships — no console change (#157)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3b62160d5466e9106652d3c2c74dcb7f76bbf993 parent 46272e2
2 files changed, +26 −52
  • docs/superpowers/plans/2026-08-12-account-default-resolution.md +11 −43
  • docs/superpowers/specs/2026-08-12-account-default-resolution-design.md +15 −9
diff --git a/docs/superpowers/plans/2026-08-12-account-default-resolution.md b/docs/superpowers/plans/2026-08-12-account-default-resolution.md
index dfa3b4c78..bab46d065 100644
--- a/docs/superpowers/plans/2026-08-12-account-default-resolution.md
+++ b/docs/superpowers/plans/2026-08-12-account-default-resolution.md
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-**Goal:** When `-account` is omitted in API mode, resolve it from config or the caller's memberships (one → use it; several → interactive chooser with a save-to-project prompt), and expose memberships via `GET /api/cli/accounts`, `carlos accounts list`, and `carlos auth whoami`. Closes #157.
+**Goal:** When `-account` is omitted in API mode, resolve it from config or the caller's memberships (one → use it; several → interactive chooser with a save-to-project prompt), and expose memberships via `GET /api/cli/apps` (already live), `carlos accounts list`, and `carlos auth whoami`. Closes #157.
**Architecture:** One new token-gated console endpoint returning `Store.AccountsFor(addr)`. CLI-side, a single resolution seam on `apiSession` (`defaultAccount`) called through a `resolvedAccount(sess, raw, rawID)` wrapper that replaces `apiAccount(...)` at every API-mode callsite; flag defaults flip `"ops"` → `""` (direct mode already reads `""` as ops, so boxes are untouched).
@@ -18,46 +18,14 @@
---
-### Task 1: Console endpoint `GET /api/cli/accounts`
+### Task 1: ~~Console endpoint~~ SUPERSEDED — endpoint already exists
-**Files:**
-- Modify: `internal/console/authcli.go` (route next to `GET /auth/cli/whoami` at :213; handler next to `handleCLIWhoami` at :787)
-- Test: `internal/console/authcli_test.go`
-
-**Interfaces:**
-- Consumes: `s.cliUser(w, r)` gate, `s.store.AccountsFor(addr)` (`[]Account` with `.Name`/`.Sqid`, deleted excluded, creation order), `writeJSON`, `jsonError`.
-- Produces: `200 {"accounts":[{"name":…,"sqid":…}]}` — always an array, never null; `401` bare/wrong token (from `cliUser`).
-
-- [ ] **Step 1: failing test** — in `authcli_test.go`, mimic the whoami test at :152 (fixture + `getJSON`): signed-up user with a personal account gets `200` and their account's name+sqid; a second minted account (use the fixture's account-create path) appears in creation order; no/wrong token → 401. Assert the JSON shape exactly (`accounts` key, array of `{name,sqid}`).
-- [ ] **Step 2: run** — `go test ./internal/console/ -run CLIAccounts -count=1` → FAIL (404).
-- [ ] **Step 3: implement**
-
-```go
-s.mux.HandleFunc("GET /api/cli/accounts", s.handleCLIAccounts)
-
-// handleCLIAccounts answers `carlos accounts list` (signed-in mode) and the
-// omitted--account default: the caller's OWN live memberships, nothing else
-// — resolved from the token's address, so there is no enumeration surface.
-func (s *Server) handleCLIAccounts(w http.ResponseWriter, r *http.Request) {
- addr, ok := s.cliUser(w, r)
- if !ok {
- return
- }
- accts, err := s.store.AccountsFor(addr)
- if err != nil {
- log.Printf("console: cli accounts: %v", err)
- jsonError(w, http.StatusInternalServerError, "could not list accounts")
- return
- }
- out := make([]map[string]string, 0, len(accts))
- for _, a := range accts {
- out = append(out, map[string]string{"name": a.Name, "sqid": a.Sqid})
- }
- writeJSON(w, http.StatusOK, map[string]any{"accounts": out})
-}
-```
-
-- [ ] **Step 4: test passes**; **Step 5: commit** `console: GET /api/cli/accounts — the caller's own memberships (#157)`.
+`GET /api/cli/apps` (handleAPIApps, internal/console/authcli.go:219, live
+since main 5856dc5 2026-08-03, deployed both consoles) already returns
+`{"accounts":[{"sqid","name","role","apps"}]}` from `Store.AccountsFor`,
+leak-tested by TestAPIAppsListsMemberships. No console change, no console
+deploy. Task 4's `listAccounts` decodes this shape; the 404 fallback covers
+only pre-2026-08-03 consoles.
### Task 2: Config `account` key + lossless rewrites
@@ -115,7 +83,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct
// it (verbs split modes before touching it).
```
-- [ ] **Step 1: failing tests** — httptest console serving `/api/cli/accounts`: one account → Seg=sqid+Exact; several (non-TTY test env) → `severalAccountsErr` text; 404 → `{Seg:"ops"}`; explicit `-account`/`-account-id` never call the endpoint (assert via handler hit-counter); config account + matching console → its name, endpoint untouched; config console mismatch → falls through to memberships; expired-token 401 surfaces as an error, not a fallback.
+- [ ] **Step 1: failing tests** — httptest console serving `/api/cli/apps`: one account → Seg=sqid+Exact; several (non-TTY test env) → `severalAccountsErr` text; 404 → `{Seg:"ops"}`; explicit `-account`/`-account-id` never call the endpoint (assert via handler hit-counter); config account + matching console → its name, endpoint untouched; config console mismatch → falls through to memberships; expired-token 401 surfaces as an error, not a fallback.
- [ ] **Step 2: run** → FAIL. **Step 3: implement.** **Step 4: pass.** **Step 5: commit** `cli: resolve omitted -account from config or memberships`.
### Task 5: Flag-default flip + callsite sweep
@@ -136,7 +104,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct
**Files:** modify `auth.go` (`authWhoami` :702), test in `auth_e2e_test.go` or the httptest pattern used by whoami today.
- [ ] **Step 1: failing test** — console serving both endpoints: output contains `accounts: tom@keymail.dev (bdk), acme (bce)`; console without the endpoint → whoami output unchanged, exit 0.
-- [ ] **Step 2:** after the identity print, `newConsoleClient(c.Console, c.Token).do(ctx, GET, "/api/cli/accounts", nil, &struct{ Accounts []apiAccountEntry }{…})`; on ANY error print nothing; else one muted line `accounts: n1 (s1), n2 (s2)`.
+- [ ] **Step 2:** after the identity print, `newConsoleClient(c.Console, c.Token).do(ctx, GET, "/api/cli/apps", nil, &struct{ Accounts []apiAccountEntry }{…})`; on ANY error print nothing; else one muted line `accounts: n1 (s1), n2 (s2)`.
- [ ] **Step 3: pass; commit** `cli: whoami prints account memberships`.
### Task 7: accounts list signed-in mode
@@ -144,7 +112,7 @@ func resolvedAccount(sess *apiSession, raw, rawID *string) apiAcct
**Files:** modify `accounts.go` (`runAccountsList` :204), test in `accounts` CLI test file (follow existing patterns; shell-outs use `carlosTestEnv` + scratch `cmd.Dir`).
- [ ] **Step 1: failing test** — with no registry file at `-registry` and a credential for an httptest console: NAME/SQID table of the caller's accounts; with a registry file present: today's registry table (existing behavior test stays green); old console (404) → error naming the console as predating account listing.
-- [ ] **Step 2:** add `-console` flag; if `*consoleFlag == ""` and `os.Stat(*regPath)` succeeds → existing registry path verbatim; else `loggedInCredential(*consoleFlag)` (fatal on err), GET `/api/cli/accounts`, on 404 fatal `"%s does not support account listing — it predates this CLI"`, else `newTable` NAME/SQID.
+- [ ] **Step 2:** add `-console` flag; if `*consoleFlag == ""` and `os.Stat(*regPath)` succeeds → existing registry path verbatim; else `loggedInCredential(*consoleFlag)` (fatal on err), GET `/api/cli/apps`, on 404 fatal `"%s does not support account listing — it predates this CLI"`, else `newTable` NAME/SQID.
- [ ] **Step 3: pass; commit** `cli: accounts list works signed-in (#157)`.
### Task 8: Gate + PR
diff --git a/docs/superpowers/specs/2026-08-12-account-default-resolution-design.md b/docs/superpowers/specs/2026-08-12-account-default-resolution-design.md
index 8e64c7904..8db27311c 100644
--- a/docs/superpowers/specs/2026-08-12-account-default-resolution-design.md
+++ b/docs/superpowers/specs/2026-08-12-account-default-resolution-design.md
@@ -28,17 +28,23 @@ caller's accounts, and when the answer is unambiguous, just use it.
signed-in mode.
- Direct/bucket mode is untouched: `ops` remains its genuine default.
-## Wire (additive)
+## Wire (no change needed)
-New endpoint, same token gate as whoami (`cliUser`):
+The endpoint already exists — discovered during implementation
+(2026-08-12): `GET /api/cli/apps` (handleAPIApps, live since main 5856dc5,
+2026-08-03, deployed on both production consoles) answers with the
+caller's own live memberships:
- GET /api/cli/accounts
- → 200 {"accounts": [{"name": "tom@keymail.dev", "sqid": "bdk"}, …]}
+ → 200 {"accounts": [{"sqid": "bdk", "name": "tom@keymail.dev",
+ "role": "owner", "apps": […]}, …]}
-- Only the **caller's own live memberships** (deleted accounts excluded),
- sorted by name for deterministic output.
-- Nothing else modelled. Old CLIs never call it; a new CLI against an old
- console gets the mux's 404 and falls back (below).
+- `Store.AccountsFor`-backed: deleted accounts excluded, creation order
+ (the console switcher's order) — the chooser's first entry is the
+ personal account minted at sign-up. Leak-tested
+ (TestAPIAppsListsMemberships).
+- The CLI decodes only `name`/`sqid` (plus `role` for `accounts list`).
+ **No new endpoint and no console deploy required**; the 404 fallback
+ below covers only consoles older than 2026-08-03.
## CLI resolution (API mode)
@@ -59,7 +65,7 @@ session, in order:
when that file's `console` matches the session's console, or the file
names no console** — a config pair points at one stack; `-console` to a
different stack must not drag the other stack's account along.
-3. **Memberships** via `GET /api/cli/accounts`:
+3. **Memberships** via `GET /api/cli/apps`:
- exactly one → use its **sqid, exact-matched** (`?exact=sqid`), and
print a muted `account: <name> (<sqid>)` line under the "using …"
banner so what was guessed is always visible;