From 8af92dedaca23fdbbbac0c065d57c012f4903edf Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:18:50 +0100 Subject: [PATCH] fix9-serves-1: mesh-sdk reads a process's serves beside its bus (ProcessServes); a serves inside bus is refused Builds: design 47 G A-115, A-127 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/catalogue/process_bus.go | 56 +++++++++++++++++++++++++++----- go/catalogue/process_bus_test.go | 30 +++++++++++++---- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/go/catalogue/process_bus.go b/go/catalogue/process_bus.go index 6559e59b..f563eaf5 100644 --- a/go/catalogue/process_bus.go +++ b/go/catalogue/process_bus.go @@ -14,7 +14,7 @@ const ProcessBusField = "bus" // ProcessBus is what one process of a module does on the bus (novox/hq ADR 0436 §1, §2, §3, §5): its own bus // account, granted exactly this and nothing of the module's other grants or the tool runner's — never a seat // to hold, never a restricted stream heard, never a warrant published (§3, §5). It answers a seat's verb only -// where it declares it in Serves (design 47 G A-107, A-109). +// where its resource declares `serves` beside this bus, never inside it (design 47 G A-115, A-127; ProcessServes). type ProcessBus struct { // Emits are the events this process emits, named locally, as the module's own Emits names them // (EmitsAll): a process emits only what its module may emit as a whole. @@ -28,10 +28,6 @@ type ProcessBus struct { // Invokes are the tools and seat verbs this process calls, each . or a role's // seat:. (as Manifest.Invokes), or * for every tool on the mesh. Invokes []string `json:"invokes,omitempty"` - // Serves are the seat verbs this process answers directly over the bus (design 47 G A-107, A-109, A-110), each - // seat:.: the grants part composes the process account's subscribe of the verb's call and tool - // subjects from it, and, for a verb whose seat definition says chunks: true, the chunk and ack subjects. - Serves []string `json:"serves,omitempty"` // Removes are what a tools bundle's account may remove from the bus, from a closed set (BusRemovals; novox/hq ADR // 0564 Limits 4): declared alone, by the module holding mesh-broker, never by a process. Removes []string `json:"removes,omitempty"` @@ -58,6 +54,8 @@ const ( type BusAccount struct { ID string Bus ProcessBus + // Serves are the seat verbs the process answers (ProcessServes): empty for a tools bundle's account. + Serves []string // Tools says it is a tools bundle's, run by the node's tool runtime, not a process the node runs. Tools bool } @@ -67,11 +65,11 @@ type BusAccount struct { // bundle's is read from the repository's build, or from the resolved manifest's bundles. func (m Manifest) BusAccounts() []BusAccount { var out []BusAccount - buses := m.ProcessBuses() + buses, serves := m.ProcessBuses(), m.ProcessServes() for _, r := range m.Resources { id := fmt.Sprint(r["id"]) if b, ok := buses[id]; ok && fmt.Sprint(r["type"]) == "process" { - out = append(out, BusAccount{ID: id, Bus: b}) + out = append(out, BusAccount{ID: id, Bus: b, Serves: serves[id]}) } } seen := map[string]bool{} @@ -101,7 +99,7 @@ func (b *ProcessBus) UnmarshalJSON(raw []byte) error { dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(&p); err != nil { - return fmt.Errorf("a process's bus is {emits, queues, state, asks, invokes, serves, keeps}: %w", typedUnknown(err)) + return fmt.Errorf("a process's bus is {emits, queues, state, asks, invokes, removes, keeps}; `serves` is a field of the process beside its bus (design 47 G A-127): %w", typedUnknown(err)) } *b = ProcessBus(p) return nil @@ -143,3 +141,45 @@ func (m Manifest) ProcessBuses() map[string]ProcessBus { } return out } + +// ProcessServesField is the key a process resource declares the seat verbs it answers on, beside its `bus` and never inside +// it (design 47 G A-115, A-127). +const ProcessServesField = "serves" + +// ProcessServes is the seat verbs each process of this module answers, by its id: each seat:., read from the +// process resource's own `serves` (design 47 G A-115), whether or not the process declares a bus. A `serves` that is not a +// list of strings is read as none; the validator names it (ProcessServesProblem). +func (m Manifest) ProcessServes() map[string][]string { + var out map[string][]string + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) != "process" { + continue + } + list, err := ProcessServesOf(r) + if err != nil || len(list) == 0 { + continue + } + if out == nil { + out = map[string][]string{} + } + out[fmt.Sprint(r["id"])] = list + } + return out +} + +// ProcessServesOf is the `serves` of one process resource, and an error where it is not a list of strings. +func ProcessServesOf(r map[string]any) ([]string, error) { + raw, present := r[ProcessServesField] + if !present { + return nil, nil + } + body, err := json.Marshal(raw) + if err != nil { + return nil, err + } + var list []string + if err := json.Unmarshal(body, &list); err != nil { + return nil, fmt.Errorf("a process's serves is a list of seat:.: %w", err) + } + return list, nil +} diff --git a/go/catalogue/process_bus_test.go b/go/catalogue/process_bus_test.go index 08a7c8a9..1f778c94 100644 --- a/go/catalogue/process_bus_test.go +++ b/go/catalogue/process_bus_test.go @@ -17,15 +17,31 @@ func strSliceEqual(a, b []string) bool { return true } -func TestProcessBusReadsServesAndRefusesUnknownKeys(t *testing.T) { - var b ProcessBus - if err := json.Unmarshal([]byte(`{"serves":["seat:mesh-broker.snapshot-get","seat:mesh-broker.list"]}`), &b); err != nil { - t.Fatal(err) - } - if !strSliceEqual(b.Serves, []string{"seat:mesh-broker.snapshot-get", "seat:mesh-broker.list"}) { - t.Errorf("serves = %v", b.Serves) +// Design 47 G A-115, A-127: `serves` is a field of the process beside its bus; inside the bus it is refused. +func TestProcessServesIsReadBesideTheBusAndRefusedInsideIt(t *testing.T) { + if err := json.Unmarshal([]byte(`{"serves":["seat:mesh-broker.list"]}`), &ProcessBus{}); err == nil { + t.Error("a serves inside bus was accepted") } if err := json.Unmarshal([]byte(`{"holds":["x"]}`), &ProcessBus{}); err == nil { t.Error("an unknown key was accepted") } + m := Manifest{Module: "nats", Resources: []map[string]any{ + {"id": "keeper", "type": "process", "serves": []any{"seat:mesh-broker.snapshot-get", "seat:mesh-broker.list"}, + "bus": map[string]any{"keeps": true}}, + {"id": "plain", "type": "process", "serves": []any{"seat:mesh-broker.snapshot-remove"}}, + {"id": "mute", "type": "process"}, + }} + got := m.ProcessServes() + if !strSliceEqual(got["keeper"], []string{"seat:mesh-broker.snapshot-get", "seat:mesh-broker.list"}) || + !strSliceEqual(got["plain"], []string{"seat:mesh-broker.snapshot-remove"}) || len(got) != 2 { + t.Errorf("process serves = %v", got) + } + accounts := m.BusAccounts() + if len(accounts) != 1 || !strSliceEqual(accounts[0].Serves, got["keeper"]) { + t.Errorf("the bus account's serves = %+v", accounts) + } + bad := Manifest{Resources: []map[string]any{{"id": "x", "type": "process", "serves": "seat:a.b"}}} + if _, err := ProcessServesOf(bad.Resources[0]); err == nil { + t.Error("a serves that is not a list was accepted") + } }