diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 734c5730..46931311 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -2490,25 +2490,30 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M // // **But its tools bundle may say and ask what it declares** (novox/hq ADR 0357): the node's runtime carries // the bundle, is granted the events the module emits under the module's name, as for any module it carries, - // and asks every tool and seat verb on the bundle's behalf (mesh-sdk go/buscompose, the runtime's invoke of - // `*`) — the bus's account is not who publishes or asks. So emits and invokes are refused only for a bus - // module with no bundle to say or ask them (issue 774: nats's bundle records its hand act). + // hears what it consumes and asks every tool and seat verb on the bundle's behalf (mesh-sdk go/buscompose, the + // runtime's invoke of `*`) — the bus's account is not who publishes or asks. So emits, invokes, consumes and uses + // are refused only for a bus module with no bundle to say, hear or ask them (issue 774: nats's bundle records its + // hand act). + // + // **And its keeper has a process account of its own** (novox/hq ADR 0626 §1, §3, changing ADR 0235 §8): exactly + // one process declares `bus`, the keeper, whose declaration alone may name `keeps` (the bus's create and update + // subjects under modules' names), and names no subject on which a tool or a verb is answered: neither + // `*`, nor one of the module's own tools, nor a verb of a seat it holds (its seat's acting verbs reach the keeper + // through its state, ADR 0626 §4). The bus state the foundation keeps on the old sections (ADR 0595 §10, ADR 0619) + // is the keeper's state and the bus's own event stream, so a bus module of the foundation may declare it. if _, account := m.OwnSecrets["broker"]; account && m.ClaimsSeat(catalogue.BrokerSeat) { var said []string bundled := manifestShipsABundle(m) - if len(m.EmitsAll()) > 0 && !bundled { - said = append(said, "emits") - } - if len(m.Invokes) > 0 && !bundled { - said = append(said, "invokes") - } + foundation := slices.Contains(Foundation, m.Module) for _, f := range []struct { name string n int - }{{"consumes", len(m.Consumes)}, {"uses", len(m.Uses)}, - {"state", len(m.State)}, {"logs", len(m.Logs)}, {"reads", len(m.Reads)}, - {"streams", len(m.Streams)}} { - if f.n > 0 { + ok bool + }{{"emits", len(m.EmitsAll()), bundled}, {"invokes", len(m.Invokes), bundled}, + {"consumes", len(m.Consumes), bundled}, {"uses", len(m.Uses), bundled}, + {"state", len(m.State), foundation}, {"logs", len(m.Logs), foundation}, {"reads", len(m.Reads), false}, + {"streams", len(m.Streams), foundation}} { + if f.n > 0 && !f.ok { said = append(said, f.name) } } @@ -2517,6 +2522,15 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M "%s holds %s and declares a bus account, which is granted the bus's snapshot API and nothing "+ "else (novox/hq ADR 0235); its %s would be granted nothing", m.Module, catalogue.BrokerSeat, strings.Join(said, ", "))) } + problems = append(problems, keeperBusProblems(m)...) + } else { + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) == "process" && busKeeps(r) { + problems = append(problems, fmt.Sprintf("%s's process %v's bus keeps the bus's entries; only the keeper "+ + "of the module holding %s with its own bus account may (novox/hq ADR 0626 §1, §3)", m.Module, r["id"], + catalogue.BrokerSeat)) + } + } } for _, r := range m.Requires { if r == "amqp" { @@ -3969,6 +3983,50 @@ func processAccountProblems(m catalogue.Manifest, processID, user string) []stri "§2)", m.Module, processID, user, user)} } +// busKeeps says the process resource r's bus declares `keeps` (novox/hq ADR 0626 §1): read from the resource as +// written, since catalogue.ProcessBus does not carry the key yet (a request to mesh-sdk). Any value but false or absent +// counts, so a refusal is never dodged by its spelling. +func busKeeps(r map[string]any) bool { + b, _ := r["bus"].(map[string]any) + v, present := b["keeps"] + return present && v != false +} + +// keeperBusProblems is what is wrong with the bus module's process accounts (novox/hq ADR 0626 §1, §3): a second +// process declaring `bus` beside its keeper, or a keeper's bus naming a subject on which a tool or a verb is answered: +// `*`, one of the module's own tools, or a verb of a seat the module holds. +func keeperBusProblems(m catalogue.Manifest) []string { + var problems, keepers []string + held := map[string]bool{} + for _, c := range m.Claims { + held[c.Name] = true + } + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) != "process" { + continue + } + b, present, err := processBusOf(r) + if !present || err != nil { + continue + } + id := fmt.Sprint(r["id"]) + keepers = append(keepers, id) + for _, t := range b.Invokes { + seat, verb, isSeat := strings.Cut(strings.TrimPrefix(t, "seat:"), ".") + own := !strings.HasPrefix(t, "seat:") && seat == m.Module && slices.Contains(m.Tools, verb) + if t == "*" || own || (strings.HasPrefix(t, "seat:") && isSeat && held[seat]) { + problems = append(problems, fmt.Sprintf("%s's keeper %s's bus invokes %q, a subject on which a tool or "+ + "a verb is answered; the keeper is granted none (novox/hq ADR 0626 §1, §3, ADR 0436 §3)", m.Module, id, t)) + } + } + } + if len(keepers) > 1 { + problems = append(problems, fmt.Sprintf("%s holds %s and its processes %s declare bus; only its keeper has a "+ + "process account of its own (novox/hq ADR 0626 §1, §3)", m.Module, catalogue.BrokerSeat, strings.Join(keepers, ", "))) + } + return problems +} + // toolsBusProblems is what is wrong with a tools bundle's bus (ADR 0564 Limits 4): a bus on anything but a bundle // compiled to a binary the runtime loads, a removal outside the closed set, or a removal beside any other grant or by a // module that does not hold mesh-broker. Every other field is judged as a process's. diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 5e3c4325..b58c5b5c 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -4271,6 +4271,47 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) { t.Fatalf("the built manifest of a bus module whose tools bundle declares what it emits was refused: %v", err) } + // ADR 0626 §1, §3: its keeper has a process account of its own, and alone may say `keeps`; the foundation's old + // sections are the keeper's state and the bus's own stream. The shape of A5's nats manifest (issue 1966), cut to the + // parts the rule reads. + keeper := func(bus string, more string) []byte { + return []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], + "claims":[{"name":"mesh-broker","scope":"mesh","serves":["list"]}],"own-secrets":{"broker":"/run/broker"}, + "tools":["nats_server"],"consumes":["mesh-delivery-indexer.manifest-accepted"],"uses":["operator-channel"], + "state":[{"name":"requests","history":5}],"streams":[{"name":"broker-events","events":["created"]}], + "emits":["consumer-removed"], + "resources":[{"id":"nats-keeper","type":"user","name":"nats-keeper"}, + {"id":"keeper","type":"process","user":"nats-keeper","run":["./t","keep"],"bus":` + bus + `}` + more + `], + "build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/t","binary":"t"}]}}`) + } + for _, c := range []struct { + name, bus, more, refused string + }{ + {"the keeper keeps", `{"keeps":true,"state":["requests"],"invokes":["seat:mesh-hand-acts.hand-act"]}`, "", ""}, + {"a second process declares bus", `{"keeps":true}`, + `,{"id":"other","type":"process","user":"nats-keeper","run":["./t"],"bus":{"state":["requests"]}}`, "only its keeper"}, + {"the keeper invokes its own tool", `{"keeps":true,"invokes":["nats.nats_server"]}`, "", "a subject on which a tool"}, + {"the keeper invokes every tool", `{"keeps":true,"invokes":["*"]}`, "", "a subject on which a tool"}, + {"the keeper invokes its own seat's verb", `{"keeps":true,"invokes":["seat:mesh-broker.list"]}`, "", "a subject on which a tool"}, + } { + _, err := ParseManifest(keeper(c.bus, c.more)) + if c.refused == "" && err != nil || c.refused != "" && (err == nil || !strings.Contains(err.Error(), c.refused)) { + t.Errorf("%s: want refused %q, got %v", c.name, c.refused, err) + } + } + if _, err := ParseManifest(keeper(`{}`, "")); err != nil { + t.Errorf("the bus module's keeper without keeps: %v", err) + } + reads := []byte(strings.Replace(string(keeper(`{"keeps":true}`, "")), `"tools":`, `"reads":["m.s"],"tools":`, 1)) + if _, err := ParseManifest(reads); err == nil || !strings.Contains(err.Error(), "its reads would be granted nothing") { + t.Errorf("a bus module reading another's state: %v", err) + } + other := []byte(`{"module":"app","version":"1","resources":[{"id":"u","type":"user","name":"app"}, + {"id":"p","type":"process","user":"app","run":["./a"],"bus":{"keeps":true}}]}`) + if _, err := ParseManifest(other); err == nil || !strings.Contains(err.Error(), "only the keeper") { + t.Errorf("keeps on another module's process: %v", err) + } + quiet := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}], "claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"}, "tools":["bus_streams"]}`)