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>
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 |