Merge branch 'feat/47-G-fix9-serves' into feat/47-G-join9

This commit is contained in:
2026-11-12 01:42:59 +01:00
2 changed files with 71 additions and 15 deletions
+48 -8
View File
@@ -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 <module>.<tool> or a role's
// seat:<seat>.<verb> (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:<seat>.<verb>: 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:<seat>.<verb>, 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:<seat>.<verb>: %w", err)
}
return list, nil
}
+23 -7
View File
@@ -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")
}
}