carlos / platform Public

Design: a narrow mail role, and the token hole filed as #155

Email doors become Allow(svcrole.Mail) so the hub can onboard and
nothing else; per-instance tokens return as step 6, after #148.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 78c41b55ef7d9a930457db7b6bc5ca0e0ae920bd parent 6e922aa
1 file changed, +21 −9
  • docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md +21 −9
diff --git a/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md b/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md
index 547912255..0760ffd74 100644
--- a/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md
+++ b/docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md
@@ -45,9 +45,10 @@ every other customer's domain.
API**, with the CLI as a wrapper over the same doors.
4. **For #147, an operator registers the external identity once and may
mark it open; accounts then claim addresses under it themselves.**
-5. **A central hub does the onboarding, holding one credential** (§6.1).
- Customer instances never call these doors, so the doors keep today's
- `Allow(svcrole.Admin)` and no per-instance machine token is needed.
+5. **A central hub does the onboarding, holding one credential** (§6.1),
+ so no per-instance machine token is needed for this work. The hub's
+ credential carries a new narrow role, `svcrole.Mail`, rather than
+ Admin.
6. **Instance backing is mixed today, and moving to the isolating one is
the intent.** So `enable` checks the route's backing rather than
assuming it (§6.4).
@@ -245,10 +246,15 @@ That inverts why those doors are Admin in the first place
(`apimail.go:460`): Admin was chosen to be *narrower* than Publish or
Instance, so that a deploy credential could not acquire a sending
identity. For a hub whose only job is onboarding, it means the opposite
-— it must be able to deploy in order to onboard. The alternative is one
-new entry in the closed role catalogue, `svcrole.Mail`, with the email
-doors declared `Allow(svcrole.Mail)`; that is a small change and it is
-the one this design recommends if the reach above is unacceptable.
+— it must be able to deploy in order to onboard.
+
+**So this design adds one entry to the closed role catalogue,
+`svcrole.Mail`**, and every email door — the app-level ones that exist
+and the instance-level ones in §6.4 — becomes `Allow(svcrole.Mail)`.
+Because `Allow` always adds Admin (`svcrole.go:120`), today's Admin
+credentials keep reaching them, so nothing breaks; what changes is that
+titogo's hub can hold a credential that onboards customer domains and
+does nothing else. `svcroutes_test.go` pins the new role's door list.
**Rate budget.** `mailWriteGates` uses the person-sized write limiter
keyed on the audit string (`apimail.go:136`), not `allowWrite` for
@@ -354,8 +360,8 @@ GET .../instances/{host}/email status
DELETE .../instances/{host}/email teardown
```
-All are `Allow(svcrole.Admin)`, like the app-level email doors, so the
-`Instance` role pin is untouched and no widening is recorded. Pause,
+All are `Allow(svcrole.Mail)`, like the app-level email doors after
+§6.1, so the `Instance` role pin is untouched. Pause,
resume and cap changes stay operator-only through the existing
`POST …/email/pause`, which gains an `instance` field.
@@ -632,6 +638,12 @@ sender, and is where a non-SES SMTP provider attaches.
4. **Per-instance config layer and acks** (§6.2, §6.5).
5. **#148** (§6.3, §6.4): instance senders, per-instance mode, the
doors, the uid check.
+6. **Per-instance machine tokens**, after #148 rather than before it.
+ Not needed while a hub onboards, but the app-wide
+ `CARLOS_INSTANCE_TOKEN` every customer instance holds already reaches
+ the instance create, patch and delete doors (`authcli.go:612`,
+ `:628`), so one customer's code can delete another's instance today.
+ That is filed separately; this is where the fix lands.
## 12. Quotas and costs to watch