carlos / platform Public

Plan 3: fixes from the executability review

An address sender now refuses to change its address in place, at both
doors, so a claim can never be stranded. The svcroutes pins, the
unexported sender lookup, the CLI role lists svcrole.Mail contradicts,
and the caller index that must stay below the new branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev e19fe019083aacd4f4bac00d32ee2403d3a8151e parent dfa3530
1 file changed, +166 −19
  • docs/superpowers/plans/2026-09-15-tenant-email-3-external-identities.md +166 −19
diff --git a/docs/superpowers/plans/2026-09-15-tenant-email-3-external-identities.md b/docs/superpowers/plans/2026-09-15-tenant-email-3-external-identities.md
index 625047e12..8fd7a1a41 100644
--- a/docs/superpowers/plans/2026-09-15-tenant-email-3-external-identities.md
+++ b/docs/superpowers/plans/2026-09-15-tenant-email-3-external-identities.md
@@ -85,6 +85,7 @@ type apiEmailSender struct{ ID, Scope, Source, Host, Address string; Active bool
- **No behaviour change for a domain sender.** `SendPolicyJSON` renders the same bytes it renders today, with no tenant condition added: nothing delivers `<P>_TENANT` to a domain sender, so a tenant condition on that document would be 554 on every send for the whole fleet. The address form is a second renderer, and a test pins the domain form byte-identical.
- **Wire-shape changes are additive.** `emailReply`'s existing fields keep their meanings; the address sender appears through plan 2's `senders` array plus the fields this plan adds to each entry. An older CLI keeps working.
- **Every door refuses the same way it refuses today.** An unknown account, an unknown app and someone else's account are one byte-identical 404 (`apiAccountApp`); the operator doors are `cliUser` -> `IsOperator` -> `mailOperator` -> a bare `http.NotFound`, exactly as `handleAPIEmailPause` (`apimail.go:1290-1304`) and `storeEntitlementOperator` (`apistore_entitlement.go:92`) do.
+- **Every line number here is PRE-PLAN-2.** They were read off the tree as it stands before plan 2 lands, and they are exact against that tree — but plan 2 inserts code above most of them in `walk.go`, `mail.go`, `provisioner.go` and `memprovisioner.go`, so by the time this plan is executed they will all have drifted down. Navigate by FUNCTION NAME and use the number as a hint about where to look. A citation that lands in the wrong place is a citation to re-read, not a reason to edit what is there.
- Copy: no em dashes. Full stop and a new sentence, lower case. Comments are full sentences saying why, never process history; log lines use `->` between values.
- Tasks 14 and 15 add member-facing CLI copy. Run the `copy-review` gate on the new strings before they land, as every user-facing string in this repo does.
- Commit after every task with the trailer `Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>`.
@@ -735,17 +736,17 @@ func (g *guardedProvisioner) DeleteIdentity(ctx context.Context, region, domain
`cmd/console/main.go`: the guard needs the deployment bucket, which is opened inline in the `Config` literal today (`Releases: openStore()`, `:269`). Lift it to a variable beside the other backends (plan 2's task 4 already lifts it for the legacy sweep; use that variable if it is there) and wrap:
```go
- relStore := openStore()
+ rel := openStore()
mailDep := mailDeployment()
mailProvisioner, dnsWriter := mailBackends(mailDep, mail.NewAWSProvisioner, dnsrec.NewRoute53Writer)
// The floor under every walk (senders design §7): an identity registered
// as external is read-only to CARLOS, whatever asks. Wrapped HERE, at the
// one place the real provisioner is constructed, so no console can be
// configured with an unguarded one.
- mailProvisioner = mail.GuardExternalIdentities(mailProvisioner, relStore)
+ mailProvisioner = mail.GuardExternalIdentities(mailProvisioner, rel)
```
-and `Releases: relStore,` in the literal.
+and `Releases: rel,` in the literal. The name is `rel` because that is what plan 2's task 4 hoists it as, for its own sweep; if plan 2 is in, this task adds only the wrap.
- [ ] **Step 4: Run it to verify it passes**
@@ -1531,9 +1532,18 @@ func identityNameFromARN(arn string) (string, bool) {
return "", false
}
resource := strings.SplitN(parts[5], "/", 2)
- if len(resource) != 2 || resource[0] != "identity" || resource[1] == "" {
+ if len(resource) != 2 || resource[0] != "identity" {
return "", false
}
+ // No `resource[1] == ""` clause, deliberately. The extraction has to
+ // answer the same question SendPolicyJSON asks today (policy.go:161-164),
+ // which accepts an empty identity name here and refuses it four lines
+ // later with "does not match any granted domain" — and this task
+ // promises the domain form's messages byte for byte. Nothing is let
+ // through either way: an empty name matches no granted domain in the
+ // domain form, and in the address form it is neither the address's
+ // domain nor a parent of it, since HasSuffix(domain, ".") is false for
+ // every valid domain.
return resource[1], true
}
@@ -1983,7 +1993,7 @@ git commit -m "mail: the external verification predicate and the foreign configu
### Task 8: `ClaimAddressSender`, where claiming and declaring an address are one call
**Files:**
-- Modify: `internal/mail/walk.go` (beside `Declare` at `:487`)
+- Modify: `internal/mail/walk.go` (beside `Declare` at `:487`, and inside it at `:521`)
- Create: `internal/mail/walk_address_test.go`
**Interfaces:**
@@ -2135,8 +2145,64 @@ func TestClaimAddressSenderKeepsDeclaresEnvPrefixLock(t *testing.T) {
t.Fatal("the env prefix moved after delivery")
}
}
+
+// A SENDER DOES NOT MOVE TO A DIFFERENT ADDRESS, and this is the test that
+// pins why. Overwriting Address would leave the old one claimed by this
+// account with nothing naming it: teardown releases only the address the
+// record currently carries, so the orphan would refuse every sibling app in
+// the account for ever and hold the identity's tenant association open by
+// itself. `carlos email enable --from other@tito.io` against a sender
+// already sending as jelly@tito.io is the exact command that would do it.
+func TestClaimAddressSenderRefusesASecondAddressOnTheSameSender(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 9, 15, 12, 0, 0, 0, time.UTC)
+ registerTitoIO(t, ctx, s, true)
+
+ if _, err := ClaimAddressSender(ctx, s, "jam", "jelly", SenderAppID,
+ "jelly@tito.io", "JELLY_SMTP", "owner@example.test", now); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := ClaimAddressSender(ctx, s, "jam", "jelly", SenderAppID,
+ "other@tito.io", "JELLY_SMTP", "owner@example.test", now); err == nil {
+ t.Fatal("a second address was adopted in place, stranding the first claim")
+ }
+ // Both halves of the leak: the first claim still stands and is still
+ // this sender's, and the second was never taken.
+ if c, _ := GetAddressClaim(ctx, s, "jelly@tito.io", "jam"); c == nil || c.App != "jelly" {
+ t.Fatalf("the refusal disturbed the live claim: %+v", c)
+ }
+ if c, _ := GetAddressClaim(ctx, s, "other@tito.io", "jam"); c != nil {
+ t.Fatalf("a refused claim was written anyway: %+v", c)
+ }
+ // The record is untouched, so the credential that is live keeps matching
+ // the address its policy names.
+ sn, _, err := GetSender(ctx, s, "jam", "jelly", SenderAppID)
+ if err != nil || sn.Address != "jelly@tito.io" || sn.Declared.From != "jelly@tito.io" {
+ t.Fatalf("the sender moved underneath its own policy: %+v %v", sn, err)
+ }
+}
+```
+
+**This closes a design gap the spec is being amended to match.** §7 describes the claim door as "Declare for an address sender" and Declare freely rewrites its From; for an address sender that is a claim leak rather than an edit, so both doors refuse a change of address and point at teardown. Task 9's `ensureAddressPrincipal` re-reads the claim on every enable, which is what makes "the record and the roster agree" an invariant rather than a hope.
+
+**`Declare` is the same hole through the other door, and it is closed in this task too.** `Declare` overwrites `Declared.From` unconditionally (`walk.go:521`) and knows nothing about claims. On an address sender that splits the record in two: `_FROM` is delivered from `Declared.From` while the policy is rendered from `sn.Address`, so the app sends as an address its credential may not use and gets a 554 on every message — and `EnvDeliveredSender` keeps reading healthy, because it compares `_FROM` against `Declared.From`, which is exactly the value that moved. Add the refusal beside the env-prefix lock:
+
+```go
+ // An address sender's From is its CLAIM, not a field. Changing it here
+ // would leave the delivered _FROM and the rendered policy describing
+ // different addresses, which is a 554 on every message with nothing in
+ // the record disagreeing about it. The address door is the only way in,
+ // and it refuses a move of its own (ClaimAddressSender).
+ if sn := rec.sender(); sn.Source == SourceAddress && sn.Address != "" && from != sn.Address {
+ return nil, fmt.Errorf(
+ "mail: %s/%s sends as %s under an identity this deployment does not own, so its From address is not declared here: claim a different address instead, or tear the sender down",
+ acct, app, sn.Address)
+ }
```
+with a test beside the one above: declaring `other@tito.io` over a claimed `jelly@tito.io` is refused, and `Declared.From` still reads `jelly@tito.io` afterwards. A declare that repeats the SAME address is still success, so a re-run of any flow that declares before it claims is unaffected.
+
- [ ] **Step 2: Run it to verify it fails**
```bash
@@ -2228,6 +2294,23 @@ func ClaimAddressSender(ctx context.Context, s release.Store, acct, app, sender,
"mail: %s/%s already sends at a verified domain, so it cannot be moved onto %s in place: tear it down first",
acct, app, address)
}
+ // AND NEITHER DOES AN ADDRESS SENDER MOVE TO A DIFFERENT ADDRESS. This
+ // is the same rule as the line above, and it is here because the
+ // alternative leaks a claim for ever: overwriting Address would leave the
+ // OLD address still claimed by this account, with nothing left in the
+ // system naming it. Teardown releases only the address the record
+ // currently carries, so nothing would ever release the old one, and it
+ // would refuse every sibling app in the account with ErrAddressClaimed
+ // while holding the identity's tenant association open on its own.
+ // Releasing it here instead was the other option and is worse: the
+ // release would have to happen before the new claim is taken, so a
+ // failure in between would leave a sender that has given up an address
+ // it is still sending as.
+ if sn.Source == SourceAddress && sn.Address != "" && sn.Address != address {
+ return nil, fmt.Errorf(
+ "mail: %s/%s already sends as %s, so it cannot be moved onto %s in place: tear it down first",
+ acct, app, sn.Address, address)
+ }
if err := ClaimAddress(ctx, s, address, acct, app, sender, actor, ext.Open, now); err != nil {
return nil, err
}
@@ -2653,6 +2736,17 @@ func ensureAddressPrincipal(ctx context.Context, s release.Store, p Provisioner,
// name gather, the "From is not at any verified domain" refusal, and
// EnsureUser. It assigns user and arn instead of declaring them.
}
+ // PLAN 2'S CALLER INDEX STAYS HERE, BELOW THE BRANCH, not inside the
+ // else. Plan 2 task 5 writes it "right after EnsureUser and before
+ // MintKey"; an address sender's principal is ensured in the other arm,
+ // and a version of this that carried the PutCallerIndex call into the
+ // domain branch along with the rest of :627-671 would leave every
+ // address sender's IAM user unindexed. SES events name the bare IAM user
+ // (§4.1), so an unindexed caller is a sender whose sends are attributed
+ // to nothing and counted against nothing.
+ if err := PutCallerIndex(ctx, s, user, acct, app, sn.ID, now); err != nil {
+ return nil, fmt.Errorf("mail: index caller %s: %w", user, err)
+ }
```
and the short-circuit at `:677-685` reads:
@@ -2972,9 +3066,11 @@ git commit -m "mail: teardown disassociates and releases an address sender, and
### Task 11: `svcrole.Mail`, and the service write budget behind the email doors
**Files:**
-- Modify: `internal/svcrole/svcrole.go` (the const block at `:30-56`, `catalogue` at `:59`), `internal/console/apimail.go` (`mailWriteGates` at `:136`, its ten call sites, `registerEmailRoutes` at `:501` and the role table above it at `:448-500`), `internal/console/svcroutes_test.go`
+- Modify: `internal/svcrole/svcrole.go` (the const block at `:30-56`, `catalogue` at `:59`), `internal/console/apimail.go` (`mailWriteGates` at `:136`, its ten call sites, `registerEmailRoutes` at `:501` and the role table above it at `:448-500`), `internal/console/svcroutes_test.go`, `services.go` (`servicesUsage` at `:33`, the `--role` flag's usage at `:157`, `usageMsg` at `:164`)
- Create: `internal/svcrole/mail_role_test.go`
+**The catalogue is spelled out by hand in three places in the root CLI, and they are the reason `services.go` is on this list.** `svcrole.Parse` (`services.go:174`) accepts anything in the catalogue, and the refusal beside it already lists `roleNames()`, which reads `svcrole.All()` — so the moment `Mail` joins, `carlos services create --role mail` works while all three strings a person reads say the role does not exist. Each becomes `<publish|instance|operate|service|mail|admin>` (`:33`, `:164`) and `publish, instance, operate, service, mail or admin` (`:157`). They are member-facing, so they go through the `copy-review` gate the same way tasks 14 and 15's strings do, before they land.
+
**Interfaces:**
- Consumes: `svcrole.Allow` (`svcrole.go:101`), `principalFrom` (`authcli.go:385`), `allowWrite` (`svcprincipal.go:394`), `(s *Server) doorsFor` (`apiservices.go:132`).
- Produces:
@@ -3212,7 +3308,7 @@ Expected: PASS, including `TestEveryCLIDoorNamesItsRoles` (`closedDoors` is unch
```bash
go build ./... && go vet ./... && go test ./... -count=1 && gofmt -l .
-git add internal/svcrole internal/console
+git add internal/svcrole internal/console services.go
git commit -m "console: svcrole.Mail, and the email write gate takes the service budget (plan 3, task 11)"
```
@@ -3453,7 +3549,20 @@ func (s *Server) mailExternalOperator(w http.ResponseWriter, r *http.Request) (s
The five handlers, each: gate, then `s.allowWrite(Principal{Address: addr}, time.Now())` for the three that write, then `s.mailAvailable(w)`, then the work.
-- **register** (`PUT`) reads `{region, identity_arn, open, note}`, defaults `region` to `s.cfg.MailDeployment.Region`, reads the current record for its etag so a re-registration EDITS rather than 409s (that is how `Open` is flipped), and hands the rest to `mail.PutExternalIdentity`, whose own validation is the one that refuses an ARN disagreeing with the domain. `auditSystem(addr, "email-external-register", domain+" -> "+arn)`. This is the deployment-wide act, so it audits against the ops account like every other one.
+- **register** (`PUT`) reads `{region, identity_arn, open, note}`, defaults `region` to `s.cfg.MailDeployment.Region` (`apimail.go:1002` reads it the same way), reads the current record for its etag so a re-registration EDITS rather than 409s (that is how `Open` is flipped), and hands the rest to `mail.PutExternalIdentity`, whose own validation is the one that refuses an ARN disagreeing with the domain. `auditSystem(addr, "email-external-register", domain+" -> "+arn)`. This is the deployment-wide act, so it audits against the ops account like every other one.
+
+ It also refuses a region that is not this deployment's, with a 400, rather than leaving that to enable:
+
+ ```go
+ if region != s.cfg.MailDeployment.Region {
+ jsonError(w, http.StatusBadRequest, fmt.Sprintf(
+ "this deployment sends from %s. an identity in %s cannot be associated with a tenant here, because tenants and identities are both per region",
+ s.cfg.MailDeployment.Region, region))
+ return
+ }
+ ```
+
+ §7 is explicit that "the region must match the deployment's", and without this the mismatch is caught by `ensureAddressPrincipal` (task 9) instead: an operator registers, a member claims an address, and the first enable is what finally says the registration was never usable. Refusing at the registration puts the refusal in front of the person who can fix it, while they are still typing.
- **deregister** (`DELETE`) calls `mail.ListIdentityClaims` FIRST and refuses with a 409 while any claim stands. Deregistering underneath live senders would remove the only record saying "CARLOS must not touch this", and the next `EnsureDomain` on the name would sail through the guard and adopt somebody else's identity. The 409 names the count, not the accounts: the roster door is where those belong. An already-absent registration is a 200, because a retried deregistration is not an error.
- **list** (`GET`) answers `{"identities": [...]}`, never null.
- **claims** (`GET .../claims`) answers `{"domain": d, "claims": [...]}` from `ListIdentityClaims`, never null.
@@ -3496,6 +3605,13 @@ and `policyDoors` gains one substring covering all five, because these are the I
policyDoors = []string{"/instances-manifest", "/{app}/routes", "/email/pause", "/build-probe", "/email/external-identities"}
```
+**`closedDoors` is walked by a second test, and three more things in that file have to move with it.** `TestEveryClosedDoorRefusesAServiceCredential` (`svcroutes_test.go:753`) dials every pattern on the list over HTTP, so adding an entry is never bookkeeping alone:
+
+- the wildcard filler at `:765-773` is a `strings.NewReplacer` that knows `{account} {app} {fleet} {label} {name} {host} {id}` and `t.Fatalf`s on anything left over (`:774-776`). Three of the five new entries carry `{domain}`, so add `"{domain}", "tito.io",` to the replacer. The value only has to be a well-formed domain: the role gate refuses before any handler looks at it, which is the whole point of the test.
+- the count at `:790` reads `seen != len(closedDoors) || seen != 31`. Move `31` to `36` in both the condition and the message.
+- the prose list in that same message enumerates the 31 by family. Extend it with `five external-identity doors`, in the place the reader expects it, beside the two email credentials and email pause.
+- while the file is open, correct the stale count at `:40`: the comment says closedDoors says "these twelve are registered with the zero Set" and it has said thirty-one for a while. Make it "these are registered with the zero Set", with no number, so the sentence stops going stale every time a door is closed.
+
with the paragraph above the list extended to say why: the registration decides which identities CARLOS may never write, and the grant decides who may send as an address under somebody else's domain. Both are policy about the deployment, not work inside one app.
- [ ] **Step 4: Run it to verify it passes**
@@ -3693,19 +3809,37 @@ registered beside the other member doors:
prefix := mailEffectivePrefix(rec)
host, port, user, pass, from := mail.EnvNames(prefix)
names := []string{host, port, user, pass, from}
+ // The app sender, read the way this function already reads senders:
+ // mail.ListSenders. Record.sender is unexported and this is package
+ // console, so hoist plan 2's own ListSenders call (its task 13 puts it
+ // just before the returned map) up to here and let the senders loop
+ // below reuse `all` rather than listing twice.
+ all, err := mail.ListSenders(ctx, s.cfg.Releases, acct.Sqid, app)
+ if err != nil {
+ return nil, err
+ }
+ var sn *mail.Sender
+ for _, cand := range all {
+ if cand.ID == mail.SenderAppID {
+ sn = cand
+ break
+ }
+ }
// An address sender is delivered two more, and they are part of "can
// this app send" rather than a detail only the walk knows: its policy
// carries a tenant condition, so a message that names no tenant is
// refused outright, and a message that names no configuration set is
// attributed to the borrowed identity's own default and refused too
- // (senders design §7).
- sn := rec.sender()
- if sn.Source == mail.SourceAddress {
+ // (senders design §7). A nil sn keeps today's five-name answer: an app
+ // with a record always has an app sender (ListSenders synthesises one
+ // for an unmigrated record, plan 2 task 3), so this is a guard rather
+ // than a branch anything reaches.
+ if sn != nil && sn.Source == mail.SourceAddress {
configSetName, tenantName := mail.EnvNamesAddress(prefix)
names = append(names, configSetName, tenantName)
}
missing := []string{}
- if rec.Provisioned != nil {
+ if rec.Provisioned != nil && sn != nil {
got, err := mail.EnvDeliveredSender(ctx, s.cfg.Releases, acct.Sqid, app, sn, rec.Provisioned.KeyID)
if err != nil {
return nil, err
@@ -3714,8 +3848,6 @@ registered beside the other member doors:
}
```
-(`rec.sender()` is plan 2's, unexported in `internal/mail`; expose whatever plan 2 left the console reading senders through — plan 2's `senders` array in this same function already reads them, so use that accessor rather than adding a second one.)
-
The `senders` entry each sender contributes gains `"identity": sn.Identity` beside the `source` and `address` plan 2 already puts there. Every other top-level field keeps its meaning, so an older CLI reads this reply exactly as it reads today.
- [ ] **Step 4: Run it to verify it passes**
@@ -3739,10 +3871,10 @@ git commit -m "console: the address door, where claiming and declaring are one c
### Task 14: `carlos email external-identity`, the operator's five verbs
**Files:**
-- Modify: `mail_cli.go` (`emailUsage` at `:60`, `runEmail`'s switch at `:137-159`, the `apiSession` wire calls from `:352`), `apimode.go` (the reply types, beside the store operator family at `:1775-1800`), `mail_cli_test.go`
+- Modify: `mail_cli.go` (`emailUsage` at `:60`, `runEmail`'s switch at `:137-159`, the `apiSession` wire calls from `:352`), `apimode.go` (the reply types, beside the store operator family at `:1786-1810`), `mail_cli_test.go`
**Interfaces:**
-- Consumes: `requireMailAPISession` (`mail_cli.go:166`), `consoleOption` (`flags.go:39`), `appFlag`, `accountFlags`, `flagUsage`, `s.getJSON`/`postJSON`/`putJSON`/`deleteJSON` (`apimode.go:760`, `:822`, `:2347`, `:2697`), `pathEscape` (`mail_cli.go:517`).
+- Consumes: `requireMailAPISession` (`mail_cli.go:166`), `consoleOption` (`flags.go:39`), `appFlag`, `accountFlags`, `flagUsage`, `s.getJSON`/`postJSON`/`putJSON`/`deleteJSON` (`apimode.go:760`, `:822`, `:2360`, `:2710`), `pathEscape` (`mail_cli.go:517`).
- Produces:
```go
func runEmailExternalIdentity(args []string) // register|deregister|list|claims|grant
@@ -3805,6 +3937,7 @@ The strings to review:
- the `external-identity` block of `emailUsage`, five verbs;
- `registered <domain> in <region>. this deployment will never write that identity`;
+- the cross-team contract, printed by `register` and by nothing else, because §7 says the registration step is where it is stated and this is that step: `while an app here is sending under <domain>, its owner cannot delete that identity at SES. the association is what blocks it, and `carlos email enable` is what creates one. tearing the last sender down releases it.` An operator who registers an identity is making a promise to somebody outside CARLOS, and this is the only place they are told what it is;
- `deregistered <domain>`;
- `<n> accounts still hold addresses under <domain>. run `carlos email external-identity claims <domain>` to see them`;
- `granted <address> to <account>/<app>`;
@@ -3894,7 +4027,7 @@ git commit -m "carlos: email external-identity register, deregister, list, claim
### Task 15: `carlos email enable --from jelly@tito.io` takes the address path
**Files:**
-- Modify: `internal/console/apimail.go` (`handleAPIEmailAddress`, task 13), `mail_cli.go` (`runEmailEnableFlow` at `:652`, `printEmailEnabled` at `:1253`, `emailUsage`'s `enable` block at `:62`), `mail_cli_test.go`
+- Modify: `internal/console/apimail.go` (`handleAPIEmailAddress`, task 13), `mail_cli.go` (`runEmailEnableFlow` at `:652`, `printEmailStatus` at `:1129`, `printEmailEnabled` at `:1253`, `emailUsage`'s `enable` block at `:62`), `mail_cli_test.go`
**Interfaces:**
- Consumes: `sess.emailStatus`, `sess.emailEnable`, `apiMailError` (`mail_cli.go:312`), `apiEmailReply.Domains`/`DefaultHost`.
@@ -3956,15 +4089,26 @@ func TestEnabledOutputNamesTheTwoHeadersAnAddressSenderMustSet(t *testing.T) {
// <P>_CONFIG_SET and <P>_TENANT and says which header each one goes in.
// Over a domain sender it prints exactly what it prints today.
}
+
+// The honour system, said out loud (design §7). An address sender's events
+// reach CARLOS only when the message names CARLOS's configuration set, and
+// nothing here can enforce that, so zero counts have two explanations and the
+// status must not quietly pick one. A domain sender's zero is not ambiguous
+// and gains no line.
+func TestStatusSaysWhenAnAddressSenderHasCountedNothing(t *testing.T) {
+ // a provisioned address sender with all-zero counts -> the line appears.
+ // the same sender with one send -> it does not.
+ // a provisioned DOMAIN sender with all-zero counts -> it does not.
+}
```
- [ ] **Step 2: Run it to verify it fails**
```bash
-go test -run 'TestEnableWithAnExternalAddress|TestEnableFallsBack|TestEnableDoesNotAskTheAddressDoor|TestEnableSurfacesAClosedIdentity|TestEnabledOutputNames' ./
+go test -run 'TestEnableWithAnExternalAddress|TestEnableFallsBack|TestEnableDoesNotAskTheAddressDoor|TestEnableSurfacesAClosedIdentity|TestEnabledOutputNames|TestStatusSaysWhenAnAddressSender' ./
```
-Expected: the trace shows `POST .../email/domains` for the external address, and `printEmailEnabled` says nothing about headers.
+Expected: the trace shows `POST .../email/domains` for the external address, `printEmailEnabled` says nothing about headers, and the status prints no line about what it has not counted.
- [ ] **Step 3: Take the new strings through the copy-review gate**
@@ -3972,6 +4116,7 @@ This is member-facing copy, so the gate is not optional. Extract, review, apply
- `<domain> is an identity this deployment does not own, so <address> was claimed rather than added as a sending domain.`
- `your app must set two headers on every message it sends. X-SES-CONFIGURATION-SET from <P>_CONFIG_SET, and X-SES-TENANT from <P>_TENANT. a message missing either one is refused by SES, so this fails loudly rather than sending uncounted.`
+- the status line for a provisioned address sender that has no counts at all: `nothing has been counted for this sender. either it has sent nothing, or it is sending without the X-SES-CONFIGURATION-SET header, and only the app can tell you which.` §7 asks for exactly this, and it is the honest half of the honour system: a domain sender with no counts has sent nothing, but an address sender with no counts might be sending perfectly well into somebody else's event destination, and a status that said "0 sends" flat would be stating something this deployment cannot know;
- the `enable` usage block's new sentence: `--from at a registered external identity claims the single address instead of bringing a sending domain into existence, and skips the verification wait entirely.`
- [ ] **Step 4: Write the code**
@@ -4048,6 +4193,8 @@ where `knownSendingDomain(cur apiEmailReply, domain string) bool` answers from `
`printEmailEnabled` (`:1253`) gains the header sentence when the reply's sender is an address sender, after the delivered names it already prints. It is the one thing a member cannot discover from anywhere else: the two variables arrive in the bundle, and nothing in the bundle says they are headers.
+`printEmailStatus` (`:1129`) gains the counted-nothing line, on an address sender that is provisioned and whose counts are all zero. It reads off the sender entry plan 2's task 14 already prints a line per sender from, so it is one `if` inside that loop rather than a second pass.
+
`emailUsage`'s `enable` block gains the reviewed sentence.
- [ ] **Step 5: Run it to verify it passes**