package buscompose import ( "fmt" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/health" ) // **The health holders' grants** (novox/hq ADR 0564 clauses 2, 5 and 8, Limits 2, 4 and 8; issue 1952 S5). // // - The condition keeper reads the controller's open conditions until the lease write, for what clauses 8 and 9 carry // from them, and mesh-healers the controller's hand-act log, for S15 (Limits 2): both buckets the controller // announces readable by nobody, so each is read by its named reader alone. The keeper, mesh-healers and the // watchman read the controller's lease (Limits 8). Nobody reads mesh-healers' `would` (clause 8): what the healers // would have done reaches no condition. Every observer's `findings`, the healers' `attempts`, the wants and the // haves are read through `reads` as any announced state is; and no module writes another's state, so an observer // can never write the keeper's buckets (clause 2). // - The watchman makes its own consumer on DEAD_LETTER_NOTICES, by its own name, pulled, and reads whether the // controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5). It // lists and shows DEAD_LETTERS, which is all it does before the lease write. // - The bus module's tools run on an account of their own, declared by their bundle's `bus` with `removes` // (catalogue.BusAccounts), which may remove only the consumer `controller` on DEAD_LETTER_NOTICES and keys of the // node-engine's acts bucket (Limits 4), never the backup's account `broker`, which only reads (ADR 0235). // The states the composer names its readers for, beside the lease (NamedReaders). var ( // ControllerConditions is the controller's open conditions, read by the keeper alone (ADR 0564 clauses 8, 9). ControllerConditions = ControllerSeat + ".conditions" // ControllerHandActs is the controller's hand-act log, read by mesh-healers alone of the four health holders (ADR // 0564 Limits 2). ControllerHandActs = ControllerSeat + ".hand-acts" // HealersWould is what mesh-healers would have done while the lease names a controller: read by nobody (clause 8). HealersWould = health.HealersModule + "." + health.WouldState ) // BusRemovalGrants are the whole authority of an account whose bus declares removals (ADR 0564 Limits 4, issue 1952 // notes 6 and 7), each removal its grant: the controller's consumer on DEAD_LETTER_NOTICES read and deleted (the lease // write's step, clause 5); the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal // would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills; the ASSIGNMENTS stream read and deleted whole (the lease // write, research 072 row 33). A // removal outside the set is refused, as the catalogue refuses it (catalogue.BusRemovals). func BusRemovalGrants(removes []string) ([]string, error) { var out []string for _, r := range removes { switch r { case catalogue.RemovesControllerDeadLetterConsumer: out = append(out, "$JS.API.CONSUMER.INFO."+DeadLetterNoticesStream+"."+ControllerName, "$JS.API.CONSUMER.DELETE."+DeadLetterNoticesStream+"."+ControllerName) case catalogue.RemovesEngineActs: out = append(out, "$JS.API.STREAM.INFO.KV_"+EngineActsBucket, "$KV."+EngineActsBucket+".>") case catalogue.RemovesAssignmentsStream: out = append(out, "$JS.API.STREAM.INFO."+AssignmentsStream, "$JS.API.STREAM.DELETE."+AssignmentsStream) default: return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4)", r, strings.Join(catalogue.BusRemovals, ", ")) } } return out, nil } // removesAssignmentsStream is an account declared to remove the stream ASSIGNMENTS: the writers table's one exemption from // stream definitions being the controller's, for deleting that one stream at the lease write (research 072 row 33). func removesAssignmentsStream(p Principal, pattern string) bool { return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesAssignmentsStream) && pattern == "$JS.API.STREAM.DELETE."+AssignmentsStream } // removesEngineActs is an account declared to remove keys of the node-engine's acts bucket: the writers table's one // exemption from a node's engine being the only writer of its keys, for removal (ADR 0564 Limits 4). func removesEngineActs(p Principal, pattern string) bool { return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesEngineActs) && pattern == "$KV."+EngineActsBucket+".>" } // WatchmanDeadLetterConsumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only once the lease names no // controller and `controller` is gone (ADR 0564 clause 5). const WatchmanDeadLetterConsumer = WatchmanModule // 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{ "$JS.API.STREAM.INFO." + DeadLetterNoticesStream, "$JS.API.CONSUMER.INFO." + DeadLetterNoticesStream + "." + ControllerName, "$JS.API.CONSUMER.CREATE." + own, "$JS.API.CONSUMER.INFO." + own, "$JS.API.CONSUMER.MSG.NEXT." + own, "$JS.ACK." + own + ".>", "$JS.API.STREAM.INFO." + DeadLettersStream, "$JS.API.STREAM.MSG.GET." + DeadLettersStream, deadLetterPrefix + ">", // And the provisioner clients' reports, the newest per subject read at its start (S21, ADR 0362 ยง3). "$JS.API.STREAM.INFO." + ProvAppliedStream, "$JS.API.STREAM.MSG.GET." + ProvAppliedStream, "$JS.API.DIRECT.GET." + ProvAppliedStream + ".>", } for _, s := range DeadLetterSources(sources) { 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 and the bus module's slow consumers and closed // connections (S9), the provisioner clients' reports (S21), and the dead letters kept. Never the bus's API audit // (`$JS.EVENT.ADVISORY.API`): it carries every API call's request and answer in the account, the vault's hashes among // them (the lead, 2026-10-29). 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.>", "mesh.mod.nats.event.bus.slow-consumer", "mesh.mod.nats.event.bus.connection-closed", ProvAppliedSubjects, 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 { seen := map[string]bool{} var out []string for _, n := range names { switch { case seen[n], !safeSubject.MatchString(n), strings.HasPrefix(n, "KV_"), strings.HasPrefix(n, "OBJ_"), n == DeadLettersStream, n == DeadLetterNoticesStream: continue } seen[n] = true out = append(out, n) } sort.Strings(out) return out } // deadLetterSourcesOf are the streams the records' consumers give up on: EVENTS, the work queue of every seat that // accepts work, and every module's and process's own work queue. func deadLetterSourcesOf(r Records) []string { out := []string{EventsStream} for _, mods := range r.Assigned { for _, d := range mods { for _, s := range append(append([]Seat(nil), d.Holds...), d.Uses...) { if len(s.Accepts) > 0 { out = append(out, seatStreamName(s.Name)) } } for _, q := range d.Queues { out = append(out, q.Stream) } for _, pr := range d.Processes { for _, q := range pr.Queues { out = append(out, q.Stream) } } } } return DeadLetterSources(out) } // 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 && pattern == deadLetterPrefix+">") }