census: apply census-ruling-console-invite-delivery (platform-1e R1-R5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+215
−37
docs/superpowers/notes/census-proposal-console-invite-delivery.md+125 −16internal/provision/plan14_root_reads_test.go+13 −0internal/provision/root_binary_deps_test.go+56 −0internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv+21 −21
diff --git a/docs/superpowers/notes/census-proposal-console-invite-delivery.md b/docs/superpowers/notes/census-proposal-console-invite-delivery.md| index 0fd573a37..047aa1b50 100644 |
| --- a/docs/superpowers/notes/census-proposal-console-invite-delivery.md |
| +++ b/docs/superpowers/notes/census-proposal-console-invite-delivery.md |
| @@ -1,8 +1,118 @@ |
| # Census: console invite delivery |
| -Status: PROPOSED, awaiting platform-1e. Nothing here is ruled. The overlay |
| -is drafted, the test does not load it yet, and the seal in the test is not |
| -moved. platform-1e rules as the Plan 14 census owner. |
| +Status: RULED as census-ruling-console-invite-delivery (platform-1e, R1–R5), |
| +on 5aef01db. The ruling is |
| +`.superpowers/sdd/2026-10-02-console-invite-delivery/census-ruling.md`. |
| +The overlay is loaded last in `TestPlan14RootReadCensus`, its rows carry |
| +`rev3=census-ruling-console-invite-delivery`, and the seal is moved to |
| +`fcdf9de0…d7a3`. platform-1e rules as the Plan 14 census owner. The |
| +proposal below is kept as it was put, with the PROPOSED wording brought up |
| +to date where the ruling settled it. |
| + |
| +## The ruling |
| + |
| +- **R1. Accepted.** The seven units (1 to 7 below) are accepted legacy under |
| + the identities named: `serve`, `NewServer`, `Keymail.do`, |
| + `Keymail.sealEnvelope` and `Store.CountPendingInvites` run in the console |
| + (cmd/console serve under console.service, user carlos-console); the two |
| + region-host files are run by tofu on an operator's machine. The basis is |
| + that the root `carlos` binary links neither `internal/console` nor |
| + `internal/mail`. No check pinned that before this ruling, so one is added: |
| + `TestRootBinaryLinksNoConsoleOrMail` |
| + (`internal/provision/root_binary_deps_test.go:18`) runs |
| + `go list -deps .` for linux, darwin and windows and fails if either |
| + package, or anything under it, appears. It skips when `go` is not on |
| + PATH. This ruling is the separate scope ruling the legacy freeze needs, |
| + so the seal moves. |
| +- **R2. Accepted.** `Keymail.SendApp`'s read row is retired by an H3 |
| + census-ruling removal, in the form census ruling carlos-run used for |
| + `apiSession.getJSON` (`plan14-root-reads-carlos-run.tsv:126-127`). Its |
| + one read, `crypto/rand.Read` for the envelope id, moved unchanged into |
| + `sealEnvelope`, the same class. |
| +- **R3. Out of census scope.** The `ses:SendEmail` grant goes on the shared |
| + host role, which every process holding the box's instance credentials can |
| + use. Merging is fine, because the policy has count 0 by default. It needs |
| + Paul's own security ruling before anyone sets `console_mail_from`. The |
| + rollout gate, verbatim: "Paul approves the host-role SES grant (or a |
| + console-only role) before console_mail_from is set". Whether tenant code |
| + can reach the instance credentials is answered under "R3: who can reach |
| + the instance credentials" below. |
| +- **R4. Rewritten.** The `PROPOSED-console-invite-delivery` marker is now |
| + `census-ruling-console-invite-delivery` in every row, as box-publish-record |
| + and the step 6 overlays did, and the overlay header names the ruling. The |
| + `@legacy` fingerprints were recomputed for the new basis text. The seal, |
| + recomputed with the overlay loaded after box-publish-record, is |
| + `fcdf9de0ba39db40d6bc7d1d2137d4f40d8248df8d286a380b2eae4e8b31d7a3`, the |
| + value the drafter computed for this form. |
| +- **R5. Not a merge blocker** (HostClaim is off, and ruling 6 holds). It is a |
| + claim-activation blocker for any console that sends mail, filed on #250. |
| + With `CARLOS_HOST_CLAIM` on, the console mailer's `EnsureDomain` and |
| + `VerifyDomain` must go through the claim mail gate: either console-mail is |
| + registered as a platform identity the admitter knows, or the mailer refuses |
| + to start when HostClaim is on. Until then a console under claims must not |
| + have console mail enabled. |
| + |
| +## R3: who can reach the instance credentials |
| + |
| +The question is whether tenant code on a box can reach the EC2 instance |
| +metadata service (IMDS, 169.254.169.254) and so the host role's |
| +credentials. If it can, it can send as `console_mail_from` once the grant is |
| +on. |
| + |
| +What holds today, from this repo: |
| + |
| +- **Exec-backed instances in the flat layout can reach IMDS.** The flat |
| + layout is the default: `ExecBackend.Start` runs every row that is not |
| + nested, fenced or in conflict as an exec child of the agent |
| + (`internal/activator/backend_exec.go:1829-1837`; flat is "today's exec |
| + child", `internal/activator/layout.go:15-17`). Those children run as |
| + uid carlos inside `carlos-edge.service`'s delegated cgroup |
| + (`internal/host/units/carlos-edge.service:28` `User=carlos`, `:64` "The |
| + agent and its flat children", `:95-101` `Delegate=`). That unit has no |
| + `IPAddressDeny=` line, because the agent itself reads IMDS |
| + (`internal/host/agent.go:2964`, `internal/host/memwatch.go:186-189`). The |
| + only other fence is the nftables skuid gate, and it lets uid carlos |
| + through by construction (`memwatch.go:189`). |
| +- **Exec-backed instances in the nested layout (#126) cannot.** They are |
| + still uid carlos, not a separate uid |
| + (`internal/host/units/carlos-instance@.service:4` and `:28`), but each |
| + template carries a unit-level `IPAddressDeny=` that covers |
| + 169.254.169.254/32 and fd00:ec2::254/128 |
| + (`carlos-instance@.service:142`, `carlos-instance-sidecar@.service:142`, |
| + `carlos-instance-cmd@.service:133`, `carlos-instance-gen@.service:115`, |
| + `carlos-instance-seed@.service:143`). systemd applies it as a cgroup BPF |
| + firewall, so it holds whatever the nftables rules say. |
| +- **Unit-backed instances (DynamicUser) cannot, unless they hold |
| + `host_role`.** `carlos-app@.service:40` is `DynamicUser=yes` and `:69` is |
| + `IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128`. On a hardened box |
| + the skuid gate also refuses dynamic uids (`carlos-app@.service:9-15`). A |
| + tenant granted `host_role` gets a drop-in with |
| + `IPAddressAllow=169.254.169.254/32 fd00:ec2::254/128`, which wins over |
| + the deny (`internal/caps/render.go:252-259`), and then only the skuid gate |
| + stands between it and the role. |
| + |
| +What depends on settings this repo does not hold: |
| + |
| +- **The skuid gate.** It is written by the infra repo's |
| + `scripts/harden-host.sh`, not by anything here |
| + (`internal/host/adopt_advice.go:204-207`). A box without it has no |
| + nftables fence at all (`adopt_advice.go:156`; tito, issue #351, per |
| + `carlos-app@.service:10-11`). On such a box a `host_role` unit tenant |
| + reaches IMDS too. |
| +- **IMDS options.** `aws_instance.host` |
| + (`infra/modules/region-host/main.tf:482-511`) sets no `metadata_options`, |
| + so `http_tokens` and `http_put_response_hop_limit` take the AMI's and the |
| + account's defaults (the scanner host sets `http_tokens = "required"` and a |
| + hop limit of 1, `infra/modules/scanner-host/main.tf:126-129`; the region |
| + host does not). Neither setting would fence these tenants anyway. They |
| + share the host's network namespace, so a hop limit of 1 reaches them, and |
| + any local process can do the IMDSv2 token PUT. |
| +- **Which layout a box's rows are in.** A row is nested only after plan 4's |
| + migration moves it. Until then, on any box that runs flat exec-backed |
| + tenants, including the console's box if it hosts them, tenant code can |
| + fetch the host role's credentials and, with the grant on, send as |
| + `console_mail_from`. A console-only role, or a skuid gate that separates |
| + the console's uid from carlos, would close that. |
| Branch `design-console-invite-delivery`, head 05cfbec0, with origin/main |
| merged in at c80e2f3b. Design: |
| @@ -37,7 +147,7 @@ What the root-read census sees: |
| None of this runs as root. The root `carlos` binary links neither |
| `internal/console` nor `internal/mail`: `go list -deps .` names neither, and |
| the only importers of `internal/mail` are `cmd/console` and |
| -`internal/console`. The `.tf` files are run by tofu on an operator's machine. |
| +`internal/console`. `TestRootBinaryLinksNoConsoleOrMail` pins that (R1). The `.tf` files are run by tofu on an operator's machine. |
| No binary embeds them and no box reads them. |
| ## Root-read census (TestPlan14RootReadCensus) |
| @@ -47,11 +157,10 @@ to load last, after `plan14-root-reads-box-publish-record.tsv`. |
| After the merge the test reports 18 discrepancies. With the overlay loaded, |
| 17 clear and one is left, "changed legacy freeze; separate scope ruling |
| -required", which is the seal. With the proposed seal set as well, every |
| -`TestPlan14*` test passes. I checked that on a scratch edit of the test and |
| -then reverted it. |
| +required", which is the seal. With the ruled seal set as well, every |
| +`TestPlan14*` test passes. |
| -Every row is accepted-legacy and tagged `rev3=PROPOSED-console-invite-delivery`. |
| +Every row is accepted-legacy and tagged `rev3=census-ruling-console-invite-delivery` (R4). |
| Each basis names the executing identity, in the form census rulings carlos-run |
| and seeded-instances use for console rows ("the console (cmd/console serve |
| under console.service, user carlos-console): ..."). |
| @@ -139,8 +248,8 @@ Each of these was a "changed root read" and a "changed legacy declaration". |
| **Proposed:** `@remove` and `@removed … H3`, the form census ruling |
| carlos-run used for `apiSession.getJSON` and `ensureExe`. The loader |
| accepts an H3 removal only with a `census-ruling-` basis, so the row reads |
| - `census-ruling-console-invite-delivery (PROPOSED, awaiting platform-1e): |
| - …`. A removal is not part of the seal. |
| + `census-ruling-console-invite-delivery: …` (R2). A removal is not part of |
| + the seal. |
| ### Source envelopes |
| @@ -163,13 +272,13 @@ The overlay adds seven `@legacy` rows, which takes the frozen set from 2147 |
| to 2154. Computed with the overlay loaded after box-publish-record: |
| - As drafted (the marker kept on ruling, as census ruling seed did): |
| - **`5ebadd0439ec247784a13739c74ab7363e22a3a9fb8da3d7ef03a76f1ab75468`** |
| -- If platform-1e wants the tag rewritten to |
| - `rev3=census-ruling-console-invite-delivery` instead: |
| - `fcdf9de0ba39db40d6bc7d1d2137d4f40d8248df8d286a380b2eae4e8b31d7a3` |
| + `5ebadd0439ec247784a13739c74ab7363e22a3a9fb8da3d7ef03a76f1ab75468` |
| +- With the tag rewritten to `rev3=census-ruling-console-invite-delivery`, |
| + as R4 ruled, and the seal the test now uses: |
| + **`fcdf9de0ba39db40d6bc7d1d2137d4f40d8248df8d286a380b2eae4e8b31d7a3`** |
| -The test edit, for platform-1e to make or authorize. It goes after the |
| -box-publish-record load in `TestPlan14RootReadCensus`: |
| +The test edit as proposed is below. As applied, it sits after the |
| +box-publish-record load in `TestPlan14RootReadCensus` with R4's seal: |
| ```go |
| // Census overlay console-invite-delivery (console invite delivery), |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 397cfddf4..67ecf4703 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -835,6 +835,19 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-box-publish-record.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| + // Census overlay console-invite-delivery (console invite delivery), |
| + // loaded last, under census-ruling-console-invite-delivery (platform-1e, |
| + // R1-R5): seven units accepted legacy under the console (carlos-console) |
| + // or tofu on an operator's machine, since the root carlos binary links |
| + // neither internal/console nor internal/mail (R1, pinned by |
| + // TestRootBinaryLinksNoConsoleOrMail); Keymail.SendApp retired by an H3 |
| + // census-ruling removal (R2); the rev3 tag rewritten to the ruling's |
| + // name (R4). The ruling is the scope ruling the freeze needs, and the |
| + // seal moves. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + seal = "fcdf9de0ba39db40d6bc7d1d2137d4f40d8248df8d286a380b2eae4e8b31d7a3" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/root_binary_deps_test.go b/internal/provision/root_binary_deps_test.go| new file mode 100644 |
| index 000000000..2ba33e469 |
| --- /dev/null |
| +++ b/internal/provision/root_binary_deps_test.go |
| @@ -0,0 +1,56 @@ |
| +package provision |
| + |
| +import ( |
| + "bytes" |
| + "os" |
| + "os/exec" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// TestRootBinaryLinksNoConsoleOrMail pins the basis of |
| +// census-ruling-console-invite-delivery R1 (platform-1e; |
| +// docs/superpowers/notes/census-proposal-console-invite-delivery.md): the |
| +// root carlos binary (the package at the repo root) links neither |
| +// internal/console nor internal/mail, so the console's units in that overlay |
| +// never run as root. go list -deps names the package's non-test imports, |
| +// direct and indirect, for each GOOS the release builds. |
| +func TestRootBinaryLinksNoConsoleOrMail(t *testing.T) { |
| + goBin, err := exec.LookPath("go") |
| + if err != nil { |
| + t.Skip("go not on PATH") |
| + } |
| + root := censusRoot(t) |
| + banned := []string{ |
| + "github.com/carlosframework/platform/internal/console", |
| + "github.com/carlosframework/platform/internal/mail", |
| + } |
| + for _, goos := range []string{"linux", "darwin", "windows"} { |
| + cmd := exec.Command(goBin, "list", "-deps", "-f", "{{.ImportPath}}", ".") |
| + cmd.Dir = root |
| + cmd.Env = append(os.Environ(), "GOOS="+goos, "CGO_ENABLED=0") |
| + var stderr bytes.Buffer |
| + cmd.Stderr = &stderr |
| + out, err := cmd.Output() |
| + if err != nil { |
| + t.Fatalf("go list -deps . (GOOS=%s): %v: %s", goos, err, stderr.String()) |
| + } |
| + deps := map[string]bool{} |
| + for _, line := range strings.Split(string(out), "\n") { |
| + deps[strings.TrimSpace(line)] = true |
| + } |
| + if !deps["github.com/carlosframework/platform"] { |
| + t.Fatalf("go list -deps . (GOOS=%s) did not list the root package itself", goos) |
| + } |
| + for _, pkg := range banned { |
| + if deps[pkg] { |
| + t.Errorf("GOOS=%s: the root carlos binary links %s; census-ruling-console-invite-delivery R1 assumed it does not", goos, pkg) |
| + } |
| + for dep := range deps { |
| + if strings.HasPrefix(dep, pkg+"/") { |
| + t.Errorf("GOOS=%s: the root carlos binary links %s, under %s", goos, dep, pkg) |
| + } |
| + } |
| + } |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv b/internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv| index b48449b30..e57a6df04 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-console-invite-delivery.tsv |
| @@ -1,25 +1,25 @@ |
| -# Plan 14 census overlay console-invite-delivery (console invite delivery: Keymail federation first, then the console's own email, then the hand-off link; docs/superpowers/specs/2026-09-28-console-invite-delivery-design.md), loaded last, after box-publish-record. PROPOSED, NOT RULED: every row is a draft for platform-1e, the census owner; the proposal and its questions are docs/superpowers/notes/census-proposal-console-invite-delivery.md. Until platform-1e rules, the test does not load this file and its seal is not moved. |
| -# Every unit here runs in the console (cmd/console, user carlos-console) or in tofu on an operator's machine. The root carlos binary links neither internal/console nor internal/mail (go list -deps . names neither; only cmd/console and internal/console import internal/mail). No unit here makes or feeds a root decision, so each is proposed accepted legacy, its basis naming the executing identity, as main's console rows are (census rulings carlos-run and seeded-instances). |
| -# The rev3 tag is PROPOSED-console-invite-delivery. If ruled as written, the marker text is kept (census ruling seed's practice), because frozen rows fingerprint their basis; the proposed seal in the note is computed with it. |
| -# Five changed units are re-frozen with their read sites unchanged (serve, NewServer, Keymail.do, and the two region-host files). Two units are new: Keymail.sealEnvelope (crypto/rand.Read, the site SendApp held) and Store.CountPendingInvites (one SQLite QueryRow). Keymail.SendApp is still declared and reads nothing now, so it is retired as census ruling carlos-run retired apiSession.getJSON and ensureExe. |
| -@legacy f577afc656d082a1433a824c2c2f00715a36c1ecdc7c1899c7db248767483835 cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the console mailer built once (consoleMailer, over the release store, mail provisioner and DNS writer serve already holds), handed to NewServer as Config.ConsoleMail, and its identity ensure started in a goroutine bounded at two minutes beside the alert watcher; read sites unchanged: the same three /data literals (the registry and console database defaults), each moved by the one inserted line data-literal@3446,data-literal@4547,data-literal@4784 a9ecdd3b5d5155bf81f72ae95fd71c9b5a479873f0f62dd9cc1747f3411b9825 |
| -cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the console mailer built once (consoleMailer, over the release store, mail provisioner and DNS writer serve already holds), handed to NewServer as Config.ConsoleMail, and its identity ensure started in a goroutine bounded at two minutes beside the alert watcher; read sites unchanged: the same three /data literals (the registry and console database defaults), each moved by the one inserted line data-literal@3446,data-literal@4547,data-literal@4784 a9ecdd3b5d5155bf81f72ae95fd71c9b5a479873f0f62dd9cc1747f3411b9825 |
| -@legacy a85ea996fc7d687c114ca9ca5475ef5f9b94fe9b2c38e678f47ae7dcbf54b4c1 infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file file:external@0 6ae47ec38e0dbace114634c73575dc00970461521c3bce3e36baeb7942229c5b |
| -infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file file:external@0 6ae47ec38e0dbace114634c73575dc00970461521c3bce3e36baeb7942229c5b |
| -@legacy 210e2c3df25a8b865d8f60924e195192c56a5e6cefac87eca9580989afee69c8 infra/modules/region-host/variables.tf external legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's variables with console_mail_from (validated as one lowercase address) and console_mail_region, both empty by default; no read is added and nothing on a box reads the file file:external@0 ffdad585e18d905f0f4a97b9b666777200fd505868bcd5bde0cfeae216b6250e |
| -infra/modules/region-host/variables.tf external legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's variables with console_mail_from (validated as one lowercase address) and console_mail_region, both empty by default; no read is added and nothing on a box reads the file file:external@0 ffdad585e18d905f0f4a97b9b666777200fd505868bcd5bde0cfeae216b6250e |
| -@legacy 2c587335836974df572918d80b0080df3eac80aee8552bc810d8b75a2d428661 internal/console/keymail.go Keymail.do legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with a non-200 answer returned as the typed keymailError (the same Error text, plus Status and Msg for IsKeymailBlocked); its one read, io.ReadAll of Keymail's HTTP response body capped at 1 MiB, unchanged file:ReadAll@594 e2f193e479472dff43846bb8d212124a1439b5765684648d499e607e30ede7be |
| -internal/console/keymail.go Keymail.do legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with a non-200 answer returned as the typed keymailError (the same Error text, plus Status and Msg for IsKeymailBlocked); its one read, io.ReadAll of Keymail's HTTP response body capped at 1 MiB, unchanged file:ReadAll@594 e2f193e479472dff43846bb8d212124a1439b5765684648d499e607e30ede7be |
| -@legacy 59b99596ef6858d4a311958122a1b649da32059b2e558bd4956a23801eb53d18 internal/console/keymail.go Keymail.sealEnvelope legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: SendApp's envelope sealing moved unchanged into its own method, which SendApp and SendFederated share; its one read is crypto/rand.Read of the 16-byte envelope id from the kernel's random source, the site SendApp held as file:Read@949 (accepted-legacy, J4 root-input, on main); no file, no store, no box state file:Read@387 b140fde1ca8b9b45403e55e72eebfb922ad5847c43c2ca836324ad6b46d77658 |
| -internal/console/keymail.go Keymail.sealEnvelope legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: SendApp's envelope sealing moved unchanged into its own method, which SendApp and SendFederated share; its one read is crypto/rand.Read of the 16-byte envelope id from the kernel's random source, the site SendApp held as file:Read@949 (accepted-legacy, J4 root-input, on main); no file, no store, no box state file:Read@387 b140fde1ca8b9b45403e55e72eebfb922ad5847c43c2ca836324ad6b46d77658 |
| -@legacy 8a13ba0424ef597844cb340b2ef9bc058ea07a5b8322bdca084937824f5cfac9 internal/console/server.go NewServer legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the invite delivery fields set (consoleMail from Config.ConsoleMail, inviteTimeout, inviteKeymailShare) and sendFederated bound to km.SendFederated beside sendApp; no goroutine; read sites unchanged: the one data-literal is the route pattern GET /a/{sqid}/apps/{app}/logs/data, not a path data-literal@9108 4d82a68616a6754966ced46d8d12541fcda64de9e1856c3440a127a9c533b032 |
| -internal/console/server.go NewServer legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the invite delivery fields set (consoleMail from Config.ConsoleMail, inviteTimeout, inviteKeymailShare) and sendFederated bound to km.SendFederated beside sendApp; no goroutine; read sites unchanged: the one data-literal is the route pattern GET /a/{sqid}/apps/{app}/logs/data, not a path data-literal@9108 4d82a68616a6754966ced46d8d12541fcda64de9e1856c3440a127a9c533b032 |
| -@legacy e876524dcb39500eb7a44ebb7b340e84bdeaf25fa9a2b4192159d2b5a72af67a internal/console/store.go Store.CountPendingInvites legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: one SQLite QueryRow, a COUNT(*) of an account's unaccepted, unexpired rows in the console's own invites table (CONSOLE_DB), for handleInvite's pending cap; it only refuses an invite; the same class as Store.PendingInvites, CreateInvite and RevokeInvite (accepted-legacy, J4 root-input, on main) file:QueryRow@102 1733078d53728ce629a956cf10c77e43ce9fe4fab21b1a45c408743bddb818e5 |
| -internal/console/store.go Store.CountPendingInvites legacy accepted-legacy accepted-legacy;rev3=PROPOSED-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: one SQLite QueryRow, a COUNT(*) of an account's unaccepted, unexpired rows in the console's own invites table (CONSOLE_DB), for handleInvite's pending cap; it only refuses an invite; the same class as Store.PendingInvites, CreateInvite and RevokeInvite (accepted-legacy, J4 root-input, on main) file:QueryRow@102 1733078d53728ce629a956cf10c77e43ce9fe4fab21b1a45c408743bddb818e5 |
| -# PROPOSED: Keymail.SendApp retired (H3, the carlos-run form). The loader needs the census-ruling- prefix on an H3 removal; the parenthesis says it is not yet given. A removal is not in the seal. |
| +# Plan 14 census overlay console-invite-delivery (console invite delivery: Keymail federation first, then the console's own email, then the hand-off link; docs/superpowers/specs/2026-09-28-console-invite-delivery-design.md), loaded last, after box-publish-record. RULED by platform-1e as census-ruling-console-invite-delivery (R1-R5), on 5aef01db; the ruling and its reasons are docs/superpowers/notes/census-proposal-console-invite-delivery.md. |
| +# R1: every unit here runs in the console (cmd/console serve under console.service, user carlos-console) or in tofu on an operator's machine, and each is accepted legacy under that identity. The root carlos binary links neither internal/console nor internal/mail; TestRootBinaryLinksNoConsoleOrMail (root_binary_deps_test.go) pins it with go list -deps. This ruling is the separate scope ruling the legacy freeze needs, and the seal moves. |
| +# R2: Keymail.SendApp is retired by an H3 census-ruling removal (the carlos-run getJSON form): its one read moved unchanged into Keymail.sealEnvelope, the same class. R3: the host role's ses:SendEmail grant is outside this census; Paul approves the host-role SES grant (or a console-only role) before console_mail_from is set. R4: the rev3 tag is census-ruling-console-invite-delivery, as box-publish-record and the step 6 overlays did. R5: not a merge blocker; with CARLOS_HOST_CLAIM on, the console mailer must go through the claim mail gate before console mail is enabled (#250). |
| +# Five changed units are re-frozen with their read sites unchanged (serve, NewServer, Keymail.do, and the two region-host files). Two units are new: Keymail.sealEnvelope (crypto/rand.Read, the site SendApp held) and Store.CountPendingInvites (one SQLite QueryRow). |
| +@legacy 803edfd195ed277c5a93a4d6a57fa8b8d89347a2d910d1123469045ef927aae1 cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the console mailer built once (consoleMailer, over the release store, mail provisioner and DNS writer serve already holds), handed to NewServer as Config.ConsoleMail, and its identity ensure started in a goroutine bounded at two minutes beside the alert watcher; read sites unchanged: the same three /data literals (the registry and console database defaults), each moved by the one inserted line data-literal@3446,data-literal@4547,data-literal@4784 a9ecdd3b5d5155bf81f72ae95fd71c9b5a479873f0f62dd9cc1747f3411b9825 |
| +cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the console mailer built once (consoleMailer, over the release store, mail provisioner and DNS writer serve already holds), handed to NewServer as Config.ConsoleMail, and its identity ensure started in a goroutine bounded at two minutes beside the alert watcher; read sites unchanged: the same three /data literals (the registry and console database defaults), each moved by the one inserted line data-literal@3446,data-literal@4547,data-literal@4784 a9ecdd3b5d5155bf81f72ae95fd71c9b5a479873f0f62dd9cc1747f3411b9825 |
| +@legacy 2a3abb0b02d1f9b17ba5d964cdb9703fa962099560ad94d9fb725c169cfe9aee infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file file:external@0 6ae47ec38e0dbace114634c73575dc00970461521c3bce3e36baeb7942229c5b |
| +infra/modules/region-host/main.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's module with one optional aws_iam_role_policy, host_console_mail, count 0 unless console_mail_from is set; it grants the host role ses:SendEmail on the one sending identity, the carlos-bab configuration set and its tenants, conditioned on ses:FromAddress equal to console_mail_from; no read is added and nothing on a box reads the file file:external@0 6ae47ec38e0dbace114634c73575dc00970461521c3bce3e36baeb7942229c5b |
| +@legacy 1507a927643a2e468dd76599f7be431bc6c1ac4669d374c1774ccad1185bac46 infra/modules/region-host/variables.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's variables with console_mail_from (validated as one lowercase address) and console_mail_region, both empty by default; no read is added and nothing on a box reads the file file:external@0 ffdad585e18d905f0f4a97b9b666777200fd505868bcd5bde0cfeae216b6250e |
| +infra/modules/region-host/variables.tf external legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=tofu on an operator's machine with the operator's AWS credentials (no box unit; no binary embeds the file): main's variables with console_mail_from (validated as one lowercase address) and console_mail_region, both empty by default; no read is added and nothing on a box reads the file file:external@0 ffdad585e18d905f0f4a97b9b666777200fd505868bcd5bde0cfeae216b6250e |
| +@legacy 0a5b5cdd3c191bd712520c32910dc813906bb049b241eea61e40c13202fda136 internal/console/keymail.go Keymail.do legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with a non-200 answer returned as the typed keymailError (the same Error text, plus Status and Msg for IsKeymailBlocked); its one read, io.ReadAll of Keymail's HTTP response body capped at 1 MiB, unchanged file:ReadAll@594 e2f193e479472dff43846bb8d212124a1439b5765684648d499e607e30ede7be |
| +internal/console/keymail.go Keymail.do legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with a non-200 answer returned as the typed keymailError (the same Error text, plus Status and Msg for IsKeymailBlocked); its one read, io.ReadAll of Keymail's HTTP response body capped at 1 MiB, unchanged file:ReadAll@594 e2f193e479472dff43846bb8d212124a1439b5765684648d499e607e30ede7be |
| +@legacy 58d32b4b120d00d668186e0fb2f4e645fb363216cbc7bbc6581f0c58bc6c9af4 internal/console/keymail.go Keymail.sealEnvelope legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: SendApp's envelope sealing moved unchanged into its own method, which SendApp and SendFederated share; its one read is crypto/rand.Read of the 16-byte envelope id from the kernel's random source, the site SendApp held as file:Read@949 (accepted-legacy, J4 root-input, on main); no file, no store, no box state file:Read@387 b140fde1ca8b9b45403e55e72eebfb922ad5847c43c2ca836324ad6b46d77658 |
| +internal/console/keymail.go Keymail.sealEnvelope legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: SendApp's envelope sealing moved unchanged into its own method, which SendApp and SendFederated share; its one read is crypto/rand.Read of the 16-byte envelope id from the kernel's random source, the site SendApp held as file:Read@949 (accepted-legacy, J4 root-input, on main); no file, no store, no box state file:Read@387 b140fde1ca8b9b45403e55e72eebfb922ad5847c43c2ca836324ad6b46d77658 |
| +@legacy c2fa720d59cc3927e15b859e52a3241f9d6b007a5fb8a038fe7fad7be78e4d41 internal/console/server.go NewServer legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the invite delivery fields set (consoleMail from Config.ConsoleMail, inviteTimeout, inviteKeymailShare) and sendFederated bound to km.SendFederated beside sendApp; no goroutine; read sites unchanged: the one data-literal is the route pattern GET /a/{sqid}/apps/{app}/logs/data, not a path data-literal@9108 4d82a68616a6754966ced46d8d12541fcda64de9e1856c3440a127a9c533b032 |
| +internal/console/server.go NewServer legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the invite delivery fields set (consoleMail from Config.ConsoleMail, inviteTimeout, inviteKeymailShare) and sendFederated bound to km.SendFederated beside sendApp; no goroutine; read sites unchanged: the one data-literal is the route pattern GET /a/{sqid}/apps/{app}/logs/data, not a path data-literal@9108 4d82a68616a6754966ced46d8d12541fcda64de9e1856c3440a127a9c533b032 |
| +@legacy 2c2e5edb430e5d8a9f42c77f8cb9cdb875b896caaf022fef3871e8849166fedf internal/console/store.go Store.CountPendingInvites legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: one SQLite QueryRow, a COUNT(*) of an account's unaccepted, unexpired rows in the console's own invites table (CONSOLE_DB), for handleInvite's pending cap; it only refuses an invite; the same class as Store.PendingInvites, CreateInvite and RevokeInvite (accepted-legacy, J4 root-input, on main) file:QueryRow@102 1733078d53728ce629a956cf10c77e43ce9fe4fab21b1a45c408743bddb818e5 |
| +internal/console/store.go Store.CountPendingInvites legacy accepted-legacy accepted-legacy;rev3=census-ruling-console-invite-delivery;basis=the console (cmd/console serve under console.service, user carlos-console): new: one SQLite QueryRow, a COUNT(*) of an account's unaccepted, unexpired rows in the console's own invites table (CONSOLE_DB), for handleInvite's pending cap; it only refuses an invite; the same class as Store.PendingInvites, CreateInvite and RevokeInvite (accepted-legacy, J4 root-input, on main) file:QueryRow@102 1733078d53728ce629a956cf10c77e43ce9fe4fab21b1a45c408743bddb818e5 |
| +# R2: Keymail.SendApp retired (H3, the carlos-run form). A removal is not in the seal. |
| @remove internal/console/keymail.go Keymail.SendApp |
| -@removed internal/console/keymail.go Keymail.SendApp H3 census-ruling-console-invite-delivery (PROPOSED, awaiting platform-1e): still declared, and reads nothing now: its one read, crypto/rand.Read of the envelope id, moved unchanged into Keymail.sealEnvelope, an accepted-legacy row in this overlay. It runs in the console as carlos-console. |
| -# PROPOSED: envelopes re-pinned. cmd/console: main.go (serve, consoleMailer). infra/modules/region-host: main.tf and variables.tf (rows above). internal/console: consolemail.go, keymailfed.go and keymailpow.go are new and the scan finds no read site in them; appsettings.go, keymail.go, server.go and store.go changed, and their only read-site changes are the rows above. internal/mail: consolemail.go and sessend.go are new, walk.go gains the console-mail guards; the scan finds no read site in any of them (NewSESEmailSender's SDK config load is the console's default credential chain, the host role, and is in TestClaimAuthoritySourceCensus as a send-only client). |
| +@removed internal/console/keymail.go Keymail.SendApp H3 census-ruling-console-invite-delivery: still declared, and reads nothing now: its one read, crypto/rand.Read of the envelope id, moved unchanged into Keymail.sealEnvelope, an accepted-legacy row in this overlay. It runs in the console as carlos-console. |
| +# Envelopes re-pinned. cmd/console: main.go (serve, consoleMailer). infra/modules/region-host: main.tf and variables.tf (rows above). internal/console: consolemail.go, keymailfed.go and keymailpow.go are new and the scan finds no read site in them; appsettings.go, keymail.go, server.go and store.go changed, and their only read-site changes are the rows above. internal/mail: consolemail.go and sessend.go are new, walk.go gains the console-mail guards; the scan finds no read site in any of them (NewSESEmailSender's SDK config load is the console's default credential chain, the host role, and is in TestClaimAuthoritySourceCensus as a send-only client). |
| @coverage cmd/console 2af70021a578df584ca6cb189f65e72c821335579a1e783d947a9b476a77c0c6 |
| @coverage infra/modules/region-host 82e90b1086ae4b88cc311f7cac50074de1c1636252bf5b6acdc15cc393d856e7 |
| @coverage internal/console 18fc22e075cea71c813348099c6698d6f73b2047ff92fc5b2f2cd52408255756 |