From 2130f99612915cca6b2b9be183937ed71902fa0d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 14 Oct 2026 22:17:09 +0200 Subject: [PATCH] mesh-mail: say mail events on its own stream MAIL, granted per account by grants (hq ADR 0363) ADR 0359 asked for a stream of its own and a bus grant from the module's grants; ADR 0363 built both. The events leave the shared EVENTS stream, and one family pattern per change replaces the literal per-account emits, so a new account changes no manifest. --- modules/mesh-mail/README.md | 7 +++-- .../mesh-mail/cmd/mesh-mail/manifest_test.go | 30 +++++++++++++------ modules/mesh-mail/module.json | 18 ++++++++--- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/modules/mesh-mail/README.md b/modules/mesh-mail/README.md index 0e121686..be92c413 100644 --- a/modules/mesh-mail/README.md +++ b/modules/mesh-mail/README.md @@ -14,7 +14,9 @@ This is the first stage: accounts of the kind `imap`, read-only. Nothing here fl (`s3-bucket`, from `minio`). - **Says each change.** A message added, its flags changed, or removed is said as the event `mail..message.`: account, folder, uid, message id, date, sender, recipients, - subject, size, flags and attachment references; never the body, never an attachment's bytes. The first look at a + subject, size, flags and attachment references; never the body, never an attachment's bytes. The events go to the + module's own stream `MAIL`, kept seven days, never to the shared EVENTS stream (novox/hq ADR 0363); a module hears an + account's part only when its `consumes` names mesh-mail and the setting `grants` names it for that account. The first look at a folder indexes what is there and says nothing: the bus hears what changed since the watch began. The keeper writes each event in the same step as the change it says; the bundle says it through the runtime and removes it once the bus took it. @@ -34,7 +36,8 @@ This is the first stage: accounts of the kind `imap`, read-only. Nothing here fl more password; no code changes. `user` signs in under another name than the address. `grants` — per account, who may read its mail: a module's name (on every machine), `.`, or -`person.`. Nobody by default. +`person.`. Nobody by default. It restricts the stream `MAIL` on the bus (a person's entry grants nothing there) +and the tools `mail_search` and `mail_read`. The password of an account is the own secret `mail-credential-`, given at the desk; the mesh never makes one. diff --git a/modules/mesh-mail/cmd/mesh-mail/manifest_test.go b/modules/mesh-mail/cmd/mesh-mail/manifest_test.go index 5b9bd5b5..128300cb 100644 --- a/modules/mesh-mail/cmd/mesh-mail/manifest_test.go +++ b/modules/mesh-mail/cmd/mesh-mail/manifest_test.go @@ -21,11 +21,20 @@ type manifest struct { OwnSecrets map[string]map[string]string `json:"own-secrets"` Tools []string `json:"tools"` Emits []string `json:"emits"` - Consumes []string `json:"consumes"` - Invokes []string `json:"invokes"` - Claims []json.RawMessage `json:"claims"` - Resources []map[string]any `json:"resources"` - Build struct { + Streams []struct { + Name string `json:"name"` + Events []string `json:"events"` + MaxAgeDays int `json:"max-age-days"` + Grant struct { + Config string `json:"config"` + Events string `json:"events"` + } `json:"grant"` + } `json:"streams"` + Consumes []string `json:"consumes"` + Invokes []string `json:"invokes"` + Claims []json.RawMessage `json:"claims"` + Resources []map[string]any `json:"resources"` + Build struct { Artifacts []struct { From string `json:"from"` Binary string `json:"binary"` @@ -72,12 +81,15 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { if !reflect.DeepEqual(m.Tools, names) || !reflect.DeepEqual(names, []string{"mail_accounts", "mail_search", "mail_read", "mail_health"}) { t.Fatalf("tools %v, the code serves %v", m.Tools, names) } - var emits []string + // Its events go to its own stream MAIL, kept a week, each account's part heard only by the consumers the trusted + // config value grants names for it (ADR 0359 §2, §4; ADR 0363): none on the shared EVENTS stream. + var events []string for _, c := range Changes { - emits = append(emits, EventKey("mesh", c)) + events = append(events, EventKey("*", c)) } - if !reflect.DeepEqual(m.Emits, emits) { - t.Fatalf("emits %v, the code says %v for the account mesh", m.Emits, emits) + if len(m.Emits) != 0 || len(m.Streams) != 1 || m.Streams[0].Name != "mail" || !reflect.DeepEqual(m.Streams[0].Events, events) || + m.Streams[0].MaxAgeDays != 7 || m.Streams[0].Grant.Config != "grants" || m.Streams[0].Grant.Events != "mail.{key}.**" { + t.Fatalf("emits %v, streams %+v; the code says %v", m.Emits, m.Streams, events) } // It reads its own mail and calls nothing, consumes nothing and claims no seat. if len(m.Consumes)+len(m.Invokes)+len(m.Claims) != 0 { diff --git a/modules/mesh-mail/module.json b/modules/mesh-mail/module.json index fab0a98e..8a40df5d 100644 --- a/modules/mesh-mail/module.json +++ b/modules/mesh-mail/module.json @@ -31,10 +31,20 @@ "mail_read", "mail_health" ], - "emits": [ - "mail.mesh.message.added", - "mail.mesh.message.changed", - "mail.mesh.message.removed" + "streams": [ + { + "name": "mail", + "events": [ + "mail.*.message.added", + "mail.*.message.changed", + "mail.*.message.removed" + ], + "max-age-days": 7, + "grant": { + "config": "grants", + "events": "mail.{key}.**" + } + } ], "resources": [ {