service principals: default-deny's second half is the package, not a prefix
Found while reviewing Task 4. The spec said the second half of default-deny was "the /auth/cli/ family", which is not the whole set: /api/economics/bill is a bearer-authenticated door under /api/ but not under /api/cli/, and a rule phrased around that family leaves it unaccounted for. The code and its test were already right — the guard walks every plain mux registration in the package and asserts none resolves a bearer through anything but cliUser, so the economics door is covered. Only the prose was wrong. Restated over the package, with the door named as the reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UimU7fAfeAoBMm6HKCPwQ
1 file changed,
+17
−9
docs/superpowers/specs/2026-08-18-service-principals-design.md+17 −9
diff --git a/docs/superpowers/specs/2026-08-18-service-principals-design.md b/docs/superpowers/specs/2026-08-18-service-principals-design.md| index 2ebfa250a..d4c7fa2d6 100644 |
| --- a/docs/superpowers/specs/2026-08-18-service-principals-design.md |
| +++ b/docs/superpowers/specs/2026-08-18-service-principals-design.md |
| @@ -193,15 +193,23 @@ readable by every role, because a credential being able to see the thing it is |
| already bound to discloses nothing it does not already hold. This is the one |
| door whose *answer* is scoped rather than whose access is gated. |
| -**The `/auth/cli/` family** — `whoami`, `revoke`, `tokens/revoke`, |
| -`accounts` (which creates an ACCOUNT), and the `/settings` page — is outside |
| -the `/api/cli/` prefix and therefore outside `cliRoute`. It needs no role |
| -bindings and gets none: every one of those handlers gates on `cliUser`, which |
| -answers only for `kind=''`, so a service credential meets the same 401 a |
| -fabricated token meets. That is not an accident to preserve by luck. A test |
| -asserts that no handler outside `/api/cli/` resolves a bearer through anything |
| -but `cliUser`, so the day someone reaches for `CLIPrincipal` over there, the |
| -build says so. |
| +**Every door outside `/api/cli/`** is outside `cliRoute`. It needs no role |
| +bindings and gets none: those handlers gate on `cliUser`, which answers only |
| +for `kind=''`, so a service credential meets the same 401 a fabricated token |
| +meets. That is not an accident to preserve by luck. A test walks every plain |
| +mux registration in the package and asserts that none of them resolves a |
| +bearer through anything but `cliUser`, so the day someone reaches for |
| +`CLIPrincipal` over there, the build says so. |
| + |
| +The obvious members are the `/auth/cli/` family — `whoami`, `revoke`, |
| +`tokens/revoke`, `accounts` (which creates an ACCOUNT) — and the `/settings` |
| +page. But the rule is deliberately stated over the whole package rather than |
| +over that family, because the family is not the whole set: `/api/economics/bill` |
| +(`economicspass.go`) is a bearer-authenticated API door under `/api/` that is |
| +NOT under `/api/cli/`, and a rule phrased as "the `/auth/cli/` family" would |
| +have left it unaccounted for. It gates on `cliUser` and then on `IsOperator`, |
| +so a service credential meets the same 401 there too — but that is a fact the |
| +test proves, not one the prefix guarantees. |
| Default-deny therefore has two halves: inside `/api/cli/`, a door is |
| unreachable until `cliRoute` names a role; outside it, a door is unreachable |