From d87facbb95ba4486a46c00c45a36039072ed1790 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 04:55:13 +0100 Subject: [PATCH] EE-S1: name the mesh's event subjects in one place (issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buscompose gains EveryEvent, EventSubjects, EventName and Overlaps, so the grants part and the tool runner never spell the event namespace themselves. Builds: design 34 every-event §1, §25, §26, §34, §37 (Overlaps rows) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/buscompose/events.go | 106 +++++++++++++++++++++++++++++++++++ go/buscompose/events_test.go | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 go/buscompose/events.go create mode 100644 go/buscompose/events_test.go diff --git a/go/buscompose/events.go b/go/buscompose/events.go new file mode 100644 index 00000000..49302145 --- /dev/null +++ b/go/buscompose/events.go @@ -0,0 +1,106 @@ +package buscompose + +import ( + "fmt" + "regexp" + "strings" +) + +// The one place the mesh's event subjects are named (design 34 every-event §1, §25, §26), so neither the grants part +// nor the tool runner spells the event namespace itself. + +// The namespaces an event lands in, as `from` names them (design 34 every-event §13, §26). +const ( + fromSeat = "seat" + fromModule = "module" + fromStream = "stream" +) + +// eventNamespaces are the three, each with the prefix its subjects start with, in EveryEvent's order. +var eventNamespaces = []struct{ from, prefix string }{ + {fromSeat, "mesh.seat."}, + {fromModule, "mesh.mod."}, + {fromStream, moduleStreamPrefix}, +} + +// EveryEvent is the subjects the tool runner's account hears: every seat's events, every module's, and every module +// stream's, a restricted one included, in that order (novox/hq ADR 0670 §1, ADR 0671 §1). +func EveryEvent() []string { + out := make([]string, 0, len(eventNamespaces)) + for _, ns := range eventNamespaces { + out = append(out, ns.prefix+"*.event.>") + } + return out +} + +// eventToken is the form of each token of an event name. +var eventToken = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// EventSubjects is the subjects one event name lands on. A name is `.`, optionally followed by further +// tokens (`operator-channel.decided.claude-code`), each token of the form `[A-Za-z0-9_-]+`. With from empty it answers +// six subjects, per namespace (seat, module, stream) `mesh...event.` and the same followed by `.>`; +// with from `seat`, `module` or `stream` only that namespace's two. It refuses, naming the rule, a name of fewer than two +// tokens, an empty token or a token outside that form (so `*` and `>` are refused), and a from of any other word. +func EventSubjects(name, from string) ([]string, error) { + tokens := strings.Split(name, ".") + if len(tokens) < 2 { + return nil, fmt.Errorf("event name %q has fewer than two tokens: a name is .", name) + } + for _, t := range tokens { + if !eventToken.MatchString(t) { + return nil, fmt.Errorf("event name %q has the token %q: each token of a name matches [A-Za-z0-9_-]+, "+ + "so no token is empty and none is * or >", name, t) + } + } + emitter, rest := tokens[0], strings.Join(tokens[1:], ".") + var out []string + for _, ns := range eventNamespaces { + if from != "" && from != ns.from { + continue + } + subject := ns.prefix + emitter + ".event." + rest + out = append(out, subject, subject+".>") + } + if out == nil { + return nil, fmt.Errorf("from %q is none of %s, %s or %s", from, fromSeat, fromModule, fromStream) + } + return out, nil +} + +// EventName is the inverse of EventSubjects for a heard subject: for `mesh.seat..event.`, +// `mesh.mod..event.` or `mesh.stream..event.` it answers the name `.` and +// its namespace (`seat`, `module` or `stream`); any other subject is answered as not an event (ok false). +func EventName(subject string) (name, from string, ok bool) { + for _, ns := range eventNamespaces { + after, found := strings.CutPrefix(subject, ns.prefix) + if !found { + continue + } + emitter, rest, found := strings.Cut(after, ".event.") + if !found || emitter == "" || rest == "" || strings.Contains(emitter, ".") { + return "", "", false + } + return emitter + "." + rest, ns.from, true + } + return "", "", false +} + +// Overlaps says whether some subject matches both a and b, a wildcard on either side: `*` stands for one token and +// `>`, last, for one token or more (design 34 every-event §34). +func Overlaps(a, b string) bool { + at, bt := strings.Split(a, "."), strings.Split(b, ".") + for i := 0; ; i++ { + switch { + case i == len(at) && i == len(bt): + return true + case i == len(at) || i == len(bt): + return false + case at[i] == ">" || bt[i] == ">": + return true + case at[i] == "*" || bt[i] == "*" || at[i] == bt[i]: + continue + default: + return false + } + } +} diff --git a/go/buscompose/events_test.go b/go/buscompose/events_test.go new file mode 100644 index 00000000..7c6c962e --- /dev/null +++ b/go/buscompose/events_test.go @@ -0,0 +1,98 @@ +package buscompose + +import ( + "reflect" + "strings" + "testing" +) + +// The mesh's event subjects are named in one place: EveryEvent, EventSubjects, EventName and Overlaps (design 34 +// every-event §1, §25, §26, §34, §37). +func TestTheEventSubjectsAreNamedInOnePlace(t *testing.T) { + // EE-S1/AC1 (E §1, §25): the three subjects, in order. + if got, want := EveryEvent(), []string{"mesh.seat.*.event.>", "mesh.mod.*.event.>", "mesh.stream.*.event.>"}; !reflect.DeepEqual(got, want) { + t.Fatalf("EveryEvent is %v, want %v", got, want) + } + + // EE-S1/AC2, AC3 (E §1, §26): a name's subjects, all six or one namespace's two, and the refusals naming the rule. + subjects := []struct { + name, from string + want []string + refused string + }{ + {name: "operator-channel.decided.claude-code", want: []string{ + "mesh.seat.operator-channel.event.decided.claude-code", "mesh.seat.operator-channel.event.decided.claude-code.>", + "mesh.mod.operator-channel.event.decided.claude-code", "mesh.mod.operator-channel.event.decided.claude-code.>", + "mesh.stream.operator-channel.event.decided.claude-code", "mesh.stream.operator-channel.event.decided.claude-code.>", + }}, + {name: "mesh-delivery-view.delivered", from: "seat", want: []string{ + "mesh.seat.mesh-delivery-view.event.delivered", "mesh.seat.mesh-delivery-view.event.delivered.>"}}, + {name: "mesh-delivery-view.delivered", from: "module", want: []string{ + "mesh.mod.mesh-delivery-view.event.delivered", "mesh.mod.mesh-delivery-view.event.delivered.>"}}, + {name: "mesh-mail.mail", from: "stream", want: []string{ + "mesh.stream.mesh-mail.event.mail", "mesh.stream.mesh-mail.event.mail.>"}}, + {name: "mesh-delivery-view", refused: "fewer than two tokens"}, + {name: "mesh-delivery-view..delivered", refused: "[A-Za-z0-9_-]+"}, + {name: "mesh-delivery-view.*", refused: "[A-Za-z0-9_-]+"}, + {name: "mesh-delivery-view.>", refused: "[A-Za-z0-9_-]+"}, + {name: "mesh-delivery-view.delivered", from: "bench", refused: "none of seat, module or stream"}, + } + for _, c := range subjects { + got, err := EventSubjects(c.name, c.from) + if c.refused != "" { + if err == nil || !strings.Contains(err.Error(), c.refused) { + t.Errorf("EventSubjects(%q, %q) answered %v, %v; want refused naming %q", c.name, c.from, got, err, c.refused) + } + continue + } + if err != nil || !reflect.DeepEqual(got, c.want) { + t.Errorf("EventSubjects(%q, %q) answered %v, %v; want %v", c.name, c.from, got, err, c.want) + } + // EE-S1/AC5 (E §1): every subject it answers is covered by one of EveryEvent's. + for _, s := range got { + covered := false + for _, e := range EveryEvent() { + covered = covered || Overlaps(e, s) + } + if !covered { + t.Errorf("%s is covered by none of EveryEvent", s) + } + } + } + + // EE-S1/AC4 (E §1, §26): a heard subject's name and namespace, and any other subject not an event. + names := []struct{ subject, name, from string }{ + {"mesh.seat.operator-channel.event.decided.claude-code", "operator-channel.decided.claude-code", "seat"}, + {"mesh.mod.mesh-delivery-view.event.delivered", "mesh-delivery-view.delivered", "module"}, + {"mesh.stream.mesh-delivery-checker.event.verdict", "mesh-delivery-checker.verdict", "stream"}, + {"mesh.seat.intake.call.accept.ace", "", ""}, + {"mesh.mod.netcheck.call.netcheck_dns", "", ""}, + {"$KV.views.x", "", ""}, + {"mesh.seat..event.x", "", ""}, + } + for _, c := range names { + name, from, ok := EventName(c.subject) + if name != c.name || from != c.from || ok != (c.name != "") { + t.Errorf("EventName(%q) answered %q, %q, %v; want %q, %q", c.subject, name, from, ok, c.name, c.from) + } + } + + // EE-S1/AC6 (E §34, §37): overlapping subjects, a wildcard on either side. + overlaps := []struct { + a, b string + want bool + }{ + {"mesh.stream.mail.event.mail.>", "mesh.stream.mail.event.mail.*.message", true}, + {"mesh.mod.mail.event.x", "mesh.stream.mail.event.x", false}, + {"mesh.mod.mail.event.x.>", "mesh.mod.mail.event.x", false}, + {"mesh.mod.mail.event.*", "mesh.mod.*.event.x", true}, + {"mesh.mod.mail.event.x", "mesh.mod.mail.event.x", true}, + {"mesh.mod.*.event.>", "mesh.mod.mail.event.x.ace", true}, + {"mesh.mod.mail.event.x.ace", "mesh.mod.mail.event.x", false}, + } + for _, c := range overlaps { + if Overlaps(c.a, c.b) != c.want || Overlaps(c.b, c.a) != c.want { + t.Errorf("Overlaps(%q, %q) is not %v both ways", c.a, c.b, c.want) + } + } +}