Fit the watchman's grant to what L4 does: hear its watchdogs' words and read a source's age and message, never deliver again or drop, so it holds no more than its job (issue 1952 S5)

This commit is contained in:
2026-10-29 21:44:53 +01:00
parent 2a04bf0ac5
commit 69fcb6c1c1
4 changed files with 51 additions and 24 deletions
+19 -6
View File
@@ -34,12 +34,13 @@ var createdAdvisory = regexp.MustCompile(`"\$JS\.EVENT\.ADVISORY\.STREAM\.CREATE
// keep applying without composing their own until the next take-over. Each principal's hearing of its buckets' creation
// (issue 1790) is the other row beyond, matched by createdAdvisory.
//
// And the watchman's dead letters (novox/hq ADR 0564 clause 5, issue 1952 S5): its own consumer on DEAD_LETTER_NOTICES and
// a read of DEAD_LETTERS, the controller's job until the lease write.
// And the watchman's dead letters and watchdogs (novox/hq ADR 0564 clauses 4, 5, issue 1952 S5): its own consumer on
// DEAD_LETTER_NOTICES, a read of DEAD_LETTERS, and the words its watchdogs hear, the controller's jobs until the lease
// write.
func beyondTheControllers(user string) map[string]bool {
out := map[string]bool{}
if strings.HasSuffix(user, "."+WatchmanModule) {
for _, s := range WatchmanDeadLetterGrants(nil) {
for _, s := range append(WatchmanDeadLetterGrants(nil), WatchmanHears()...) {
out[s] = true
}
}
@@ -605,8 +606,8 @@ func healthChecks(t *testing.T) {
"a.mesh-conditions": {"$KV.mesh-conditions_conditions.machine.g14.silent"},
WatchmanModule: {"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES." + WatchmanModule, "$JS.API.CONSUMER.MSG.NEXT.DEAD_LETTER_NOTICES." + WatchmanModule,
"$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", "$JS.API.STREAM.MSG.GET.DEAD_LETTERS"},
WatchmanModule + " sources": {"$JS.API.STREAM.MSG.GET.EVENTS", "$JS.API.STREAM.MSG.DELETE.SEAT_TELEGRAM_SENDER",
"$JS.API.STREAM.MSG.GET.M_WORK", "$JS.API.STREAM.MSG.DELETE.DEAD_LETTERS", "mesh.events.dead.EVENTS.a_m", "mesh.again.a_m.x"},
WatchmanModule + " sources": {"$JS.API.STREAM.MSG.GET.EVENTS", "$JS.API.STREAM.INFO.SEAT_TELEGRAM_SENDER",
"$JS.API.STREAM.MSG.GET.M_WORK", "$JS.API.STREAM.INFO.EVENTS", "mesh.events.dead.EVENTS.a_m"},
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller",
"$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts"},
}
@@ -619,7 +620,10 @@ func healthChecks(t *testing.T) {
// Its reads and deletes of a message are on the streams a dead letter comes from, named, never a bucket's
// (the lead, 2026-10-29).
WatchmanModule + " sources": {"$JS.API.STREAM.MSG.GET.KV_mesh-vault_bus-accounts", "$JS.API.STREAM.MSG.GET.KV_x",
"$JS.API.STREAM.MSG.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list", "$JS.API.STREAM.MSG.GET.OTHER"},
"$JS.API.STREAM.MSG.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.MSG.GET.OBJ_mesh-grants-user-list", "$JS.API.STREAM.MSG.GET.OTHER",
// In L4 it neither delivers again nor drops (clause 5, the lead 2026-10-29).
"$JS.API.STREAM.MSG.DELETE.EVENTS", "$JS.API.STREAM.MSG.DELETE.DEAD_LETTERS", "$JS.API.CONSUMER.CREATE.DEAD_LETTERS.x",
"mesh.again.a_m.x"},
WatchmanModule: {"$KV.mesh-conditions_conditions.x", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller",
"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES." + WatchmanModule},
// The bus module's tools remove the one consumer and the acts' keys, nothing else (Limits 4); its other accounts
@@ -640,6 +644,15 @@ func healthChecks(t *testing.T) {
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if name == WatchmanModule {
// It hears its watchdogs' words on core subjects (Limits 3, 11).
for _, subject := range []string{"mesh.mod.power.event.sleeping", "mesh.control.g14.tools-alive",
"mesh.seat.mesh-self-check.event.checked", "mesh.mod.keycloak.event.provisioner.failing"} {
if !MaySubscribe(perms, subject) {
t.Errorf("the watchman may not hear %s", subject)
}
}
}
subjects := append(append([]string(nil), may[name]...), may[name+" sources"]...)
denied := append(append([]string(nil), mayNot[name]...), mayNot[name+" sources"]...)
if len(may[name+" sources"]) > 0 {
+29 -17
View File
@@ -62,14 +62,14 @@ func removesEngineActs(p Principal, pattern string) bool {
// controller and `controller` is gone (ADR 0564 clause 5).
const WatchmanDeadLetterConsumer = WatchmanModule
// WatchmanDeadLetterGrants are what the watchman may publish for the dead letters (ADR 0564 clause 5): its own pulled
// consumer on DEAD_LETTER_NOTICES (made, read, pulled, acknowledged), whether `controller` still stands (read only), a
// read of DEAD_LETTERS (its list by subject, one kept message, an ordered consumer of its own to page them, a drop), the
// copies kept and the messages delivered again (`mesh.events.dead.>`, `mesh.again.>`), and on each stream a notice can
// name (sources: EVENTS, the seats' work queues and the modules' own) the message given up on read, its consumer read,
// and its message deleted from the work queue when delivered again, each stream by its name. Never a wildcard over the
// streams and never a bucket's or an object store's stream: a read of any message would read every bucket (the lead,
// 2026-10-29, on issue 1952 S5).
// WatchmanDeadLetterGrants are what the watchman may publish for the dead letters in L4 (ADR 0564 clause 5), at least
// privilege: its own pulled consumer on DEAD_LETTER_NOTICES (made, read, pulled, acknowledged); whether `controller`
// still stands (read only); a read of DEAD_LETTERS (its information and one kept message); a copy kept
// (`mesh.events.dead.>`); and on each stream a notice can name (sources: EVENTS, the seats' work queues and the
// modules' own), its information, for a source's age (ADR 0376), and the message given up on, each stream by its name.
// Never a wildcard over the streams and never a bucket's or an object store's stream: a read of any message would read
// every bucket (the lead, 2026-10-29, on issue 1952 S5). Delivering again and dropping stay refused in L4, so it may
// neither delete a message nor publish `mesh.again.>`.
func WatchmanDeadLetterGrants(sources []string) []string {
own := DeadLetterNoticesStream + "." + WatchmanDeadLetterConsumer
out := []string{
@@ -81,19 +81,29 @@ func WatchmanDeadLetterGrants(sources []string) []string {
"$JS.ACK." + own + ".>",
"$JS.API.STREAM.INFO." + DeadLettersStream,
"$JS.API.STREAM.MSG.GET." + DeadLettersStream,
"$JS.API.STREAM.MSG.DELETE." + DeadLettersStream,
"$JS.API.CONSUMER.CREATE." + DeadLettersStream + ".>",
"$JS.API.CONSUMER.DELETE." + DeadLettersStream + ".*",
"$JS.FC." + DeadLettersStream + ".>",
deadLetterPrefix + ">",
againPrefix + ">",
}
for _, s := range DeadLetterSources(sources) {
out = append(out, "$JS.API.STREAM.MSG.GET."+s, "$JS.API.STREAM.MSG.DELETE."+s, "$JS.API.CONSUMER.INFO."+s+".*")
out = append(out, "$JS.API.STREAM.INFO."+s, "$JS.API.STREAM.MSG.GET."+s)
}
return out
}
// WatchmanHears are the core subjects the watchman subscribes, with no durable consumer (ADR 0564 clauses 4, 5 and
// Limits 3, 11): the power seat's words (S1, ADR 0211), the providers' failing and recovered words (S8), each tool
// runner's beat (S11), the self-check's checked (S10), a consumer deleted (S9), and the dead letters kept.
func WatchmanHears() []string {
return []string{
"mesh.mod.power.event.*",
"mesh.mod.*.event.provisioner.failing",
"mesh.mod.*.event.provisioner.recovered",
"mesh.control.*.tools-alive",
"mesh.seat.mesh-self-check.event." + health.EventChecked,
"$JS.EVENT.ADVISORY.CONSUMER.DELETED.>",
deadLetterPrefix + ">",
}
}
// DeadLetterSources are the streams of names a dead letter may come from, sorted, none twice: each a plain name, never a
// bucket's (KV_) or an object store's (OBJ_) stream, the dead letters' own two streams, or a pattern.
func DeadLetterSources(names []string) []string {
@@ -136,8 +146,10 @@ func deadLetterSourcesOf(r Records) []string {
return DeadLetterSources(out)
}
// keepsDeadLetters is the watchman keeping the dead letters: a second writer of the copies and the deliveries again
// beside the controller, which acts until its lease is handed away and the watchman after (ADR 0564 clause 5).
// keepsDeadLetters is the watchman keeping the dead letters: a second writer of the copies beside the controller, which
// keeps them until its lease is handed away and the watchman after (ADR 0564 clause 5). Delivering again stays the
// controller's in L4.
func keepsDeadLetters(p Principal, pattern string) bool {
return isController(p, pattern) || (p.Kind == KindModule && p.Module == WatchmanModule)
return isController(p, pattern) ||
(p.Kind == KindModule && p.Module == WatchmanModule && pattern == deadLetterPrefix+">")
}
+2
View File
@@ -1006,6 +1006,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, BusConnzSubject)
// And the dead letters, from its own consumer, never the controller's (novox/hq ADR 0564 clause 5).
pub = append(pub, WatchmanDeadLetterGrants(p.DeadLetterSources)...)
// And the words its watchdogs hear (ADR 0564 clause 4, Limits 3, 11).
sub = append(sub, WatchmanHears()...)
}
// The grants part alone publishes the modules' memberships into the assignments stream (novox/hq ADR 0547 §5;
// the operator, 2026-10-25 22:31): one per subject, `mesh.assignment.<node>.<module>`, reading the last one of each
+1 -1
View File
@@ -221,7 +221,7 @@ var WritersTable = []WriterRow{
{State: "a message a consumer gave up on", Writer: "controller", KeptIn: "the bus, the stream " + DeadLettersStream,
Others: "read, delivered again or dropped through the controller's dead-letters verb",
Subjects: []string{deadLetterPrefix + ">", againPrefix + ">"}, Writes: keepsDeadLetters,
Shared: "the watchman keeps them from the controller's lease write, the controller before it (novox/hq ADR 0564 clause 5)"},
Shared: "the watchman keeps the copies from the controller's lease write, the controller before it; delivering again stays the controller's (novox/hq ADR 0564 clause 5)"},
// The record mirrors (novox/hq ADR 0289 §9, design 54 §2): rows of this table, so a grant that lets
// anything else write them is refused at composition. Built so far: the assignments and each registered
// module's declared config (design 57 F3), and the list of predefined keys kept (design 57 K3).