DLV-J4: the judge allows the bus module's keeper its own process account, and keeps on it alone (ADR 0626 §1, §3)

The new nats manifest has a keeper process with a bus account of its own; the judge refused it under ADR 0235 §8.
Needs mesh-sdk's ProcessBus to carry keeps (requested of A1); until then the strict decoder refuses the key.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 12:40:49 +01:00
parent 3a789b7a6b
commit a7d490b0e5
2 changed files with 112 additions and 13 deletions
+71 -13
View File
@@ -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.
@@ -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"]}`)