Let the tool runner's account hear the event families its manifest names (issue 745, ADR 0367) #342

Merged
mesh-tools merged 1 commits from 745-mesh-wait into main 2026-10-14 23:14:42 +00:00
9 changed files with 537 additions and 0 deletions
Showing only changes of commit 755457cc37 - Show all commits
+5
View File
@@ -66,6 +66,10 @@ type Membership struct {
// consumer on that stream the runtime binds beside the one on EVENTS (§8).
Streams []StreamIssued `json:"streams,omitempty"`
Hears []HeardIssued `json:"hears,omitempty"`
// Waits are the event families a session may wait on through the mesh MCP server, the tool runner's
// alone (novox/hq ADR 0367 §2): the subjects its account is granted, under the names a session asks for.
// The tool runner refuses any family not listed here.
Waits []WaitIssued `json:"waits,omitempty"`
}
// Served is one address a tool is answered on.
@@ -133,6 +137,7 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
m.Logs = logsIssuedFor(d)
m.Streams = streamsIssuedFor(d)
m.Hears = HeardIssuedFor(node, d)
m.Waits = waitsIssuedFor(d)
t := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches)
for _, s := range append(append([]Seat{}, d.Uses...), d.Watches...) {
if !s.Kinded {
+8
View File
@@ -883,6 +883,14 @@ func PermissionsFor(p Principal) (Permissions, error) {
for _, d := range p.Carries {
pub = append(pub, logGrants(d.Module, logNames(d.Logs))...)
}
// **And the event families a session may wait on** (novox/hq ADR 0367 §2): the tool runner's module's own,
// named one by one in its manifest, as a plain subscription — no consumer is made for them, and no
// call or setting adds one.
waits, err := waitGrants(p.Carries)
if err != nil {
return Permissions{}, err
}
sub = append(sub, waits...)
// **Never the traffic of a trusted holder** (novox/hq ADR 0259 §8): the machine's runtime runs as the
// operator's account, which every agent runs as, so a module saying warrants or speaking for a kind
// that proves its sender is never composed into it — refused here, naming it, whatever registration
+3
View File
@@ -36,6 +36,9 @@ type Declared struct {
Watches []Seat
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
Invokes []string
// Waits are the event families a session may wait on through the tool runner, the tool runner's module's
// alone (novox/hq ADR 0367 §2): its account subscribes them for a call, through no consumer.
Waits []Wait
// State is the state it keeps, each a bucket its instances write (novox/hq ADR 0201).
State []Bucket
// Logs are the logs it keeps, each a stream its instances append to (novox/hq ADR 0297).
+100
View File
@@ -0,0 +1,100 @@
package broker
import (
"fmt"
"strings"
)
// What a session may wait on through the mesh MCP server (novox/hq ADR 0367).
//
// **A grant apart from `consumes`, and the tool runner's alone.** The tool runner's module names in its
// manifest the event families a session may wait on, one by one; the runtime's account is granted a plain
// subscription on those subjects and nothing wider — no durable consumer, no queue of its own — and its
// membership names the same list, from which `mesh_wait` refuses any other. Both are read from this one
// composition, so the grant and what the tool runner offers cannot disagree, and nothing but the manifest
// adds a family (§6).
// Wait is one event family the tool runner's module declares: its name as the manifest gives it, and the
// emitter — a seat or a module — and the event, as the inventory resolved them.
type Wait struct {
// Name is the family as the manifest names it, `<emitter>.<event>`: what a session asks for.
Name string
Emitter string
// Seat says the emitter is a seat, whose events live under the seat's namespace (novox/hq ADR 0121).
Seat bool
// Event is the event, with any further token the subject carries: `decided.claude-code`, the asker of a
// seat that names it last (ADR 0259 §3).
Event string
// Last names what the seat's event subject ends in — "kind" for a kinded bench, "asker" for a seat naming
// its caller (ADR 0259 §3) — and is empty for an event that ends at its name. A family needing it is waited
// on with it given, and refused without.
Last string
}
// WaitIssued is one family as a membership issues it: the name a session asks for, and its subject.
type WaitIssued struct {
Event string `json:"event"`
Subject string `json:"subject"`
}
// WaitSubject is where a family's events land. Each token is checked before it becomes a subject: a
// wildcard, a dot inside a name or an empty token would widen the grant.
func WaitSubject(w Wait) (string, error) {
if !safeSubject.MatchString(w.Emitter) {
return "", fmt.Errorf("%q cannot name an emitter of a waited event: it would widen the subject", w.Emitter)
}
for _, t := range strings.Split(w.Event, ".") {
if !safeSubject.MatchString(t) {
return "", fmt.Errorf("%q cannot name a waited event: each of its names is plain, never a wildcard "+
"(novox/hq ADR 0367 §2)", w.Event)
}
}
if w.Last != "" && !strings.Contains(w.Event, ".") {
return "", fmt.Errorf("%s names no %s: %s's events are said for each %s, so a family is %s.%s.<%s>; "+
"without it no event would ever be heard (novox/hq ADR 0367 §5)",
w.Name, w.Last, w.Emitter, w.Last, w.Emitter, w.Event, w.Last)
}
if w.Seat {
return "mesh.seat." + w.Emitter + ".event." + w.Event, nil
}
return "mesh.mod." + w.Emitter + ".event." + w.Event, nil
}
// waitGrants is the subscription the tool runner's account is granted for its module's families, and an
// error for a carried module other than the tool runner's that declares any (ADR 0367 §2).
func waitGrants(carries []Declared) ([]string, error) {
var sub []string
for _, d := range carries {
if len(d.Waits) == 0 {
continue
}
if d.Module != RuntimeModule {
return nil, fmt.Errorf("%s declares what a session waits on; only %s, the tool runner's module, "+
"does (novox/hq ADR 0367 §2)", d.Module, RuntimeModule)
}
for _, w := range d.Waits {
s, err := WaitSubject(w)
if err != nil {
return nil, err
}
sub = append(sub, s)
}
}
return sub, nil
}
// waitsIssuedFor is the families a membership names: the tool runner's module's, and nobody else's.
func waitsIssuedFor(d Declared) []WaitIssued {
if d.Module != RuntimeModule {
return nil
}
var out []WaitIssued
for _, w := range d.Waits {
s, err := WaitSubject(w)
if err != nil {
continue // refused where the account is composed, which names it
}
out = append(out, WaitIssued{Event: w.Name, Subject: s})
}
return out
}
+135
View File
@@ -0,0 +1,135 @@
package broker
import (
"reflect"
"sort"
"strings"
"testing"
)
// The families ADR 0367 §5 starts the grant with, resolved as the inventory resolves a manifest: a seat's
// event under the seat's namespace, a module's under the module's.
var firstFamilies = []Wait{
{Name: "mesh-knowledge.reviewed", Emitter: "mesh-knowledge", Event: "reviewed"},
{Name: "mesh-delivery.transition", Emitter: "mesh-delivery", Event: "transition"},
{Name: "gitea.forge.status", Emitter: "gitea", Event: "forge.status"},
{Name: "mesh-controller.checked", Emitter: "mesh-controller", Seat: true, Event: "checked"},
{Name: "operator-channel.decided.claude-code", Emitter: "operator-channel", Seat: true, Event: "decided.claude-code"},
}
var firstSubjects = []string{
"mesh.mod.gitea.event.forge.status",
"mesh.mod.mesh-delivery.event.transition",
"mesh.mod.mesh-knowledge.event.reviewed",
"mesh.seat.mesh-controller.event.checked",
"mesh.seat.operator-channel.event.decided.claude-code",
}
func runtimeCarrying(waits []Wait) Principal {
return Principal{Kind: KindNodeTools, Node: "desk", Module: RuntimeModule, PasswordHash: "x", Carries: []Declared{
{Module: "shop", Emits: []string{"order.placed"}, Consumes: []string{"billing.paid"}, Serves: []string{"price"}},
{Module: RuntimeModule, Invokes: []string{"*"}, Waits: waits},
}}
}
func without(all, some []string) []string {
drop := map[string]bool{}
for _, s := range some {
drop[s] = true
}
var out []string
for _, s := range all {
if !drop[s] {
out = append(out, s)
}
}
return out
}
// novox/hq ADR 0367 "How it is checked": the account's wait grant equals the manifest's families. The tool
// runner's account subscribes exactly the families' subjects beyond what it had without them — nothing
// published, nothing wider — and the membership it is issued names the same subjects.
func TestTheToolRunnersWaitGrantIsTheManifestsFamiliesAlone(t *testing.T) {
before, err := PermissionsFor(runtimeCarrying(nil))
if err != nil {
t.Fatal(err)
}
after, err := PermissionsFor(runtimeCarrying(firstFamilies))
if err != nil {
t.Fatal(err)
}
added := without(after.Subscribe, before.Subscribe)
sort.Strings(added)
if !reflect.DeepEqual(added, firstSubjects) {
t.Errorf("the wait grant is %v, want the families' subjects %v", added, firstSubjects)
}
if extra := without(after.Publish, before.Publish); len(extra) > 0 {
t.Errorf("waiting granted a publish: %v", extra)
}
for _, s := range after.Subscribe {
if strings.HasPrefix(s, "mesh.mod.*.event") || s == "mesh.>" || s == ">" || strings.HasSuffix(s, ".event.>") {
t.Errorf("the runtime may subscribe every event: %s", s)
}
}
for _, other := range []string{"mesh.seat.operator-channel.event.decided.shop",
"mesh.mod.mesh-knowledge.event.proposed", "mesh.mod.gitea.event.forge.push"} {
if MaySubscribe(after, other) {
t.Errorf("the runtime may subscribe %s, which no family names", other)
}
}
issued := MembershipFor("desk", runtimeCarrying(firstFamilies).Carries[1], Placements{})
var subjects []string
for _, w := range issued.Waits {
subjects = append(subjects, w.Subject)
}
sort.Strings(subjects)
if !reflect.DeepEqual(subjects, firstSubjects) {
t.Errorf("the membership names %v, want the grant %v", subjects, firstSubjects)
}
if issued.Waits[0].Event != "mesh-knowledge.reviewed" {
t.Errorf("a family is issued under its manifest name: %+v", issued.Waits[0])
}
if shop := MembershipFor("desk", runtimeCarrying(firstFamilies).Carries[0], Placements{}); len(shop.Waits) > 0 {
t.Errorf("a module beside the runtime is issued waits: %+v", shop.Waits)
}
}
// **A subscription for the call, never a durable consumer** (ADR 0367 §2): composing the users makes no
// consumer that filters a family, and none for the runtime's module at all.
func TestAWaitMakesNoDurableConsumer(t *testing.T) {
for _, c := range ConsumersOf([]Principal{runtimeCarrying(firstFamilies)}) {
if c.Module == RuntimeModule {
t.Errorf("the runtime's module has a durable consumer: %+v", c.Consumer)
}
for _, f := range c.Consumer.Filters {
for _, s := range firstSubjects {
if f == s {
t.Errorf("%s's consumer filters the family %s", c.Module, s)
}
}
}
}
}
// Only the tool runner's module waits (ADR 0367 §2): a carried module declaring waits is refused at
// composition, naming it, and no name that would widen a subject is composed.
func TestAWaitIsComposedOnlyForTheToolRunnerAndOnlyWhole(t *testing.T) {
p := runtimeCarrying(nil)
p.Carries[0].Waits = []Wait{{Name: "mesh-knowledge.reviewed", Emitter: "mesh-knowledge", Event: "reviewed"}}
if _, err := PermissionsFor(p); err == nil || !strings.Contains(err.Error(), "shop") {
t.Errorf("a carried module's waits were composed: %v", err)
}
for _, w := range []Wait{
{Name: "x", Emitter: "mesh-knowledge", Event: "*"},
{Name: "x", Emitter: "mesh-knowledge", Event: "a.>"},
{Name: "x", Emitter: "*", Event: "reviewed"},
{Name: "x", Emitter: "mesh.knowledge", Event: "reviewed"},
{Name: "x", Emitter: "mesh-knowledge", Event: ""},
{Name: "x", Emitter: "mesh-knowledge", Event: "a..b"},
} {
if _, err := PermissionsFor(runtimeCarrying([]Wait{w})); err == nil {
t.Errorf("%+v was composed", w)
}
}
}
+48
View File
@@ -513,6 +513,13 @@ type Manifest struct {
// person's account (design 25 §7) already had the same shape.
Invokes []string `json:"invokes,omitempty"`
// Waits are the event families a session may wait on through the mesh MCP server's `mesh_wait`, each
// `<emitter>.<event>` as `consumes` names one, a seat's or a module's (novox/hq ADR 0367 §2). Only the tool
// runner's module declares them, one by one and never by a wildcard. **A grant apart from `consumes`**: the
// controller grants the tool runner's account a plain subscription on those subjects alone, with no
// durable consumer, and issues the same list in its membership; no call or setting adds one (§6).
Waits []string `json:"waits,omitempty"`
// State is the current state this module keeps on the bus, by local name: each a key-value
// bucket the controller creates, which every instance of the module writes and reads
// (novox/hq ADR 0201). Not history — that is an event — and never a secret, sealed or not.
@@ -1971,6 +1978,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
}
}
problems = append(problems, invokeProblems(m)...)
problems = append(problems, waitProblems(m)...)
problems = append(problems, replacesProblems(m)...)
problems = append(problems, UnitSettingProblems(m)...)
problems = append(problems, OwnerSettingProblems(m)...)
@@ -2694,6 +2702,46 @@ func invokeProblems(m Manifest) []string {
return problems
}
// waitToken is one name of an event family's subject: never a wildcard, never a dot, never empty.
var waitToken = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)
// waitProblems judges the event families a module says a session may wait on (novox/hq ADR 0367 §2, §6):
// the tool runner's alone, each named whole — emitter and event, with no wildcard — and each once. Refused
// here, in the manifest's words, rather than at the composition of the bus's users.
func waitProblems(m Manifest) []string {
if len(m.Waits) == 0 {
return nil
}
if m.Module != RuntimeModule {
return []string{fmt.Sprintf("%s says what a session waits on; only %s, the tool runner's module, "+
"declares waits (novox/hq ADR 0367 §2)", m.Module, RuntimeModule)}
}
var problems []string
seen := map[string]bool{}
for _, w := range m.Waits {
if strings.ContainsAny(w, "*>") {
problems = append(problems, fmt.Sprintf("%s waits on %q: a family is named one by one, never a "+
"wildcard (novox/hq ADR 0367 §2)", m.Module, w))
continue
}
tokens := strings.Split(w, ".")
whole := len(tokens) >= 2 && name.MatchString(tokens[0])
for _, t := range tokens[1:] {
whole = whole && waitToken.MatchString(t)
}
if !whole {
problems = append(problems, fmt.Sprintf("%s waits on %q, which does not name an emitter and an "+
"event: a family is <emitter>.<event>, as consumes names one", m.Module, w))
continue
}
if seen[w] {
problems = append(problems, fmt.Sprintf("%s waits on %q twice", m.Module, w))
}
seen[w] = true
}
return problems
}
// How a module takes one of its own secrets (ADR 0114): read from the file when it starts, or
// applied by its own code to a backend that takes it once.
const (
+43
View File
@@ -0,0 +1,43 @@
package catalogue
import (
"strings"
"testing"
)
// novox/hq ADR 0367 §2: the tool runner's module names, one by one, the event families a session may wait on.
func TestTheToolRunnerMayNameTheEventsASessionWaitsOn(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"node-tools","version":"1",` +
`"waits":["mesh-knowledge.reviewed","operator-channel.decided.claude-code"]}`))
if err != nil {
t.Fatal(err)
}
if len(m.Waits) != 2 || m.Waits[1] != "operator-channel.decided.claude-code" {
t.Fatalf("waits not read: %v", m.Waits)
}
}
// A family is named one by one, never by a wildcard, and only by the tool runner's module (ADR 0367 §2, §6):
// each refusal is said in the manifest's words, at parse, rather than at the composition of the bus's users.
func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) {
for manifest, want := range map[string]string{
`{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only node-tools",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge.*"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["**"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge.>"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["*.reviewed"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["reviewed"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["a.b","a.b"]}`: "twice",
} {
_, err := ParseManifest([]byte(manifest))
if err == nil {
t.Errorf("accepted: %s", manifest)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("%s refused for the wrong reason (want %q): %v", manifest, want, err)
}
}
}
+28
View File
@@ -178,6 +178,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
Serves: m.Tools,
// And what it calls (novox/hq ADR 0152) — the console's `*`, nothing else's.
Invokes: m.Invokes,
// And the event families a session may wait on, the tool runner's alone (novox/hq ADR 0367 §2).
Waits: waitsOf(m, seats),
// And the state it keeps and reads (novox/hq ADR 0201).
State: bucketsOf(m),
Reads: m.Reads,
@@ -219,6 +221,32 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
return d
}
// waitsOf is the event families a manifest says a session may wait on, each resolved as a consumed name is
// (novox/hq ADR 0121, ADR 0239): a seat's event when the seat says it, else the event of the module of that
// name. A seat whose event subjects end in a kind (a kinded bench) or in the asker (ADR 0259 §3) is waited on
// with that last name given — `decided.claude-code` — and the family says which it needs, so one given without
// it is refused where it is composed rather than granted a subject nothing publishes on.
func waitsOf(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration) []broker.Wait {
var out []broker.Wait
for _, w := range m.Waits {
emitter, event, _ := strings.Cut(w, ".")
first, _, _ := strings.Cut(event, ".")
wait := broker.Wait{Name: w, Emitter: emitter, Event: event}
if s, isASeat := seats[emitter]; isASeat {
switch {
case s.Kinded && catalogue.SeatSays(s.Emits, first):
wait.Seat, wait.Last = true, "kind"
case s.NamedByCaller(first) && catalogue.SeatSays(s.Emits, first):
wait.Seat, wait.Last = true, "asker"
case catalogue.SeatSays(s.Emits, event):
wait.Seat = true
}
}
out = append(out, wait)
}
return out
}
// issuedTools are the tools through which a module answers the credentials its software issues (novox/hq ADR 0305).
func issuedTools(m catalogue.Manifest) []string {
var out []string
+167
View File
@@ -0,0 +1,167 @@
package inventory
import (
"reflect"
"sort"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
)
// The tool runner's manifest with the families novox/hq ADR 0367 §5 starts the grant with, and one more.
const runtimeWaiting = `{"module": "node-tools", "version": "1", "invokes": ["*"], "waits": [%s]}`
var adr0367Families = []string{`"mesh-knowledge.reviewed"`, `"mesh-delivery.transition"`, `"gitea.forge.status"`,
`"mesh-controller.checked"`, `"operator-channel.decided.claude-code"`}
// composeWaits composes the bus's users from manifests as BusRecords does — the router declaring the operator
// channel, the mesh's own seats, a person granted every tool — and answers the tool runner's account's
// subscriptions on events, and the subjects its membership names.
func composeWaits(t *testing.T, families []string, people map[string][]string) (granted, issued []string) {
t.Helper()
parse := func(s string) catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(s))
if err != nil {
t.Fatal(err)
}
return m
}
router := parse(`{"module": "messenger", "version": "1", "runs-as": "messenger",
"seats": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"], "accepts": ["ask"],
"emits": ["decided"], "by-caller": ["ask", "decided"]}],
"claims": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"]}],
"own-secrets": {"broker": "${dir:state}/broker"}, "secrets-owner": "messenger",
"resources": [{"id": "account", "type": "user", "name": "messenger", "shell": "/usr/bin/nologin", "home": "/var/lib/messenger"},
{"id": "state", "type": "directory", "mode": "0700", "place": ".", "owner": "messenger"}]}`)
asker := parse(`{"module": "claude-code", "version": "1", "consumes": ["mesh-knowledge.reviewed"],
"own-secrets": {"broker": "${dir:state}/broker"}, "resources": [{"id": "state", "type": "directory", "mode": "0700", "place": "."}]}`)
runtime := parse(strings.Replace(runtimeWaiting, "%s", strings.Join(families, ", "), 1))
manifests := []catalogue.Manifest{router, asker, runtime}
seats := map[string]catalogue.SeatDeclaration{}
declarers := map[string]string{}
for _, m := range manifests {
for _, s := range m.DefinesSeats {
seats[s.Name], declarers[s.Name] = s, m.Module
}
}
for _, own := range catalogue.SeatsWithAProtocol() {
seats[own.Name] = catalogue.SeatDeclaration{Name: own.Name, Scope: own.Scope, Accepts: own.Accepts,
Emits: own.Emits, Serves: own.Serves}
}
records := broker.Records{Nodes: []string{"laptop"}, Assigned: map[string][]broker.Declared{},
People: people, Interchangeable: map[string]bool{}}
for _, m := range manifests {
records.Assigned["laptop"] = append(records.Assigned["laptop"], declaredFor(m, seats, declarers))
}
users, err := broker.Users(records)
if err != nil {
t.Fatal(err)
}
for _, u := range users {
if u.Kind != broker.KindNodeTools {
continue
}
p, err := broker.PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
for _, s := range p.Subscribe {
if strings.Contains(s, ".event.") {
granted = append(granted, s)
}
}
}
for _, d := range records.Assigned["laptop"] {
for _, w := range broker.MembershipFor("laptop", d, broker.PlacementsOf(records, nil)).Waits {
issued = append(issued, w.Subject)
}
}
sort.Strings(granted)
sort.Strings(issued)
return granted, issued
}
// novox/hq ADR 0367 "How it is checked": the account's wait grant equals the manifest's families — a seat's
// event under the seat, the asker's own `decided` alone, a module's under the module — and the membership
// names the same; no person's grant, however wide, widens it; a family is granted only once the manifest
// that adds it is composed.
func TestTheWaitGrantIsTheToolRunnersManifestAndNothingElse(t *testing.T) {
want := []string{
"mesh.mod.gitea.event.forge.status",
"mesh.mod.mesh-delivery.event.transition",
"mesh.mod.mesh-knowledge.event.reviewed",
"mesh.seat.mesh-controller.event.checked",
"mesh.seat.operator-channel.event.decided.claude-code",
}
granted, issued := composeWaits(t, adr0367Families, map[string][]string{})
if !reflect.DeepEqual(granted, want) {
t.Errorf("the tool runner hears %v, want the manifest's families %v", granted, want)
}
if !reflect.DeepEqual(issued, want) {
t.Errorf("its membership names %v, want the grant %v", issued, want)
}
// No call or setting widens it: a person granted every tool, an operator's account, changes nothing.
if wide, _ := composeWaits(t, adr0367Families, map[string][]string{"operator": {"*"}}); !reflect.DeepEqual(wide, want) {
t.Errorf("a person's grant widened the wait grant: %v", wide)
}
// And without the families it hears no event at all (ADR 0043 §3): a module beside it consuming one
// lends it nothing.
if none, issuedNone := composeWaits(t, nil, map[string][]string{}); len(none)+len(issuedNone) > 0 {
t.Errorf("a tool runner declaring no waits hears %v, issued %v", none, issuedNone)
}
// A family is granted only after the manifest change that adds it is composed.
more := append(append([]string{}, adr0367Families...), `"mesh-knowledge.accepted"`)
grown, _ := composeWaits(t, more, map[string][]string{})
if added := without(grown, want); !reflect.DeepEqual(added, []string{"mesh.mod.mesh-knowledge.event.accepted"}) {
t.Errorf("adding one family to the manifest granted %v", added)
}
}
func without(all, some []string) []string {
drop := map[string]bool{}
for _, s := range some {
drop[s] = true
}
var out []string
for _, s := range all {
if !drop[s] {
out = append(out, s)
}
}
return out
}
// A family of a seat whose event subjects end in a kind (a kinded bench) or in the asker (a seat naming its
// caller) is composed with that last token, and refused without it, naming the family: a subject without it
// is one nothing publishes on, and a wait on it would never hear (the review of issue 745).
func TestAWaitOnASeatNamingAKindOrAnAskerNeedsThatName(t *testing.T) {
seats := map[string]catalogue.SeatDeclaration{
"intake": {Name: "intake", Kinded: true, Emits: []string{"choice", "failed"}},
"operator-channel": {Name: "operator-channel", Emits: []string{"decided"}, ByCaller: []string{"ask", "decided"}},
}
compose := func(family string) (string, error) {
m := catalogue.Manifest{Module: broker.RuntimeModule, Waits: []string{family}}
waits := waitsOf(m, seats)
return broker.WaitSubject(waits[0])
}
if s, err := compose("intake.choice.telegram"); err != nil || s != "mesh.seat.intake.event.choice.telegram" {
t.Errorf("a kind's event: %q, %v", s, err)
}
if s, err := compose("operator-channel.decided.claude-code"); err != nil || s != "mesh.seat.operator-channel.event.decided.claude-code" {
t.Errorf("an asker's event: %q, %v", s, err)
}
for family, want := range map[string]string{"intake.choice": "kind", "operator-channel.decided": "asker"} {
s, err := compose(family)
if err == nil || !strings.Contains(err.Error(), family) || !strings.Contains(err.Error(), want) {
t.Errorf("%s composed as %q (%v), want it refused for want of its %s", family, s, err, want)
}
}
// Refused where the bus's users are composed, naming it, so the merge gate is red before it lands.
p := broker.Principal{Kind: broker.KindNodeTools, Node: "laptop", Module: broker.RuntimeModule, Carries: []broker.Declared{
{Module: broker.RuntimeModule, Waits: waitsOf(catalogue.Manifest{Module: broker.RuntimeModule, Waits: []string{"intake.choice"}}, seats)}}}
if _, err := broker.PermissionsFor(p); err == nil || !strings.Contains(err.Error(), "intake.choice") {
t.Errorf("the runtime was composed with a family no subject carries: %v", err)
}
}