diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 0a76bcf..378c10b 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -686,7 +686,7 @@ func healthChecks(t *testing.T) { {Module: "telegram", Holds: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}}, Processes: []ProcessPrincipal{{Process: "w", Queues: []QueueOwner{{Process: "w", Stream: "M_WORK"}, {Process: "w", Stream: "KV_x"}}}}}, {Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: "nats-tools", - Removes: []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs}}, {Process: "other"}}}, + Removes: []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs, catalogue.RemovesAssignmentsStream}}, {Process: "other"}}}, {Module: "shop", Processes: []ProcessPrincipal{{Process: "nats-tools", Removes: []string{catalogue.RemovesEngineActs}}}}, }}}) if err != nil { @@ -722,7 +722,9 @@ func healthChecks(t *testing.T) { 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"}, + "$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts", + // Research 072 row 33: the ASSIGNMENTS stream is read and removed at the lease write. + "$JS.API.STREAM.INFO.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS"}, } mayNot := map[string][]string{ // The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2). @@ -763,10 +765,11 @@ func healthChecks(t *testing.T) { // remove nothing; an account is granted the removals its bus declares and no other (the catalogue refuses them // outside the bus module). "a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m", - "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts"}, + "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts", + "$JS.API.STREAM.DELETE.EVENTS", "$JS.API.STREAM.PURGE.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS_X"}, "a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"}, "a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"}, - "a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x"}, + "a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"}, } // ADR 0571 clause 1: a read naming another module's log is issued to the reader as a log it reads, never writes. if m := MembershipFor("a", Declared{Module: delivery.IndexerModule, Reads: []string{delivery.BuildLogRead, "x.y"}}, diff --git a/go/buscompose/health_grants.go b/go/buscompose/health_grants.go index e31a935..f997e60 100644 --- a/go/buscompose/health_grants.go +++ b/go/buscompose/health_grants.go @@ -40,7 +40,8 @@ var ( // 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. A +// 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 @@ -51,6 +52,8 @@ func BusRemovalGrants(removes []string) ([]string, error) { "$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, ", ")) @@ -59,6 +62,13 @@ func BusRemovalGrants(removes []string) ([]string, error) { 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 { diff --git a/go/buscompose/writers.go b/go/buscompose/writers.go index 6da9693..1239564 100644 --- a/go/buscompose/writers.go +++ b/go/buscompose/writers.go @@ -43,6 +43,9 @@ func createsDeclared(p Principal, pattern string) bool { if isController(p, pattern) { return true } + if removesAssignmentsStream(p, pattern) { + return true + } name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") if ok && p.Kind == KindModule && p.Module == GrantsModule && name == GrantsUserListObjectStream { return true @@ -185,7 +188,7 @@ var WritersTable = []WriterRow{ Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>", "$JS.API.CONSUMER.DURABLE.CREATE.>"}, Writes: createsDeclared, - Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54)"}, + Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the stream ASSIGNMENTS at the lease write, and no other (research 072 row 33)"}, {State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state", Others: "the controller asks", Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"}, diff --git a/go/catalogue/process_bus.go b/go/catalogue/process_bus.go index 25dea20..cdfa936 100644 --- a/go/catalogue/process_bus.go +++ b/go/catalogue/process_bus.go @@ -41,10 +41,13 @@ type ProcessBus struct { const ( RemovesControllerDeadLetterConsumer = "DEAD_LETTER_NOTICES.controller" RemovesEngineActs = "mesh-host_acts" + // RemovesAssignmentsStream is the stream ASSIGNMENTS, removed whole at the lease write (novox/hq research 072 row 33): + // its 350 messages have no consumer, and the placement is the provisioner's from then on. + RemovesAssignmentsStream = "ASSIGNMENTS" ) // BusRemovals is the closed set of what a bus account may remove. -var BusRemovals = []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs} +var BusRemovals = []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs, RemovesAssignmentsStream} // BusAccount is one account of a module's own beside its `broker`: a process's (ADR 0436) or a tools bundle's (ADR 0564 // Limits 4), by its id (the process resource's, or the bundle's binary), with what its bus says. diff --git a/go/catalogue/process_bus_test.go b/go/catalogue/process_bus_test.go index 7b3f8f3..3c1977c 100644 --- a/go/catalogue/process_bus_test.go +++ b/go/catalogue/process_bus_test.go @@ -63,7 +63,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { for _, c := range []struct { name, manifest, refused string }{ - {name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts"]}`)}, + {name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts","ASSIGNMENTS"]}`)}, {name: "a removal outside the set", manifest: tools(broker, `{"removes":["EVENTS.a_m"]}`), refused: "not one of"}, {name: "a removal beside a grant", manifest: tools(broker, `{"removes":["mesh-host_acts"],"invokes":["nats.nats_streams"]}`), refused: "with nothing else"}, {name: "another module's tools removing", manifest: tools(`[]`, `{"removes":["mesh-host_acts"]}`), refused: "only the bus module"}, @@ -88,7 +88,7 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { } accounts := m.BusAccounts() if len(accounts) != 1 || accounts[0].ID != "nats-tools" || !accounts[0].Tools || - !strSliceEqual(accounts[0].Bus.Removes, []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs}) { + !strSliceEqual(accounts[0].Bus.Removes, []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs, RemovesAssignmentsStream}) { t.Errorf("%s: the accounts are %+v", c.name, accounts) } // Its secret's file is the runtime account's, as the runtime's own credential is; the module's other secrets stay