The mesh's four streams, asserted on every start

Task 1.4. The foundation set only — a seat's streams come at registration
and a module's consumers at assignment, neither of which has happened at
genesis (ADR 0118).

Asserted rather than created: a stream that was deleted, or a mesh raised
from a backup, must converge rather than run without the guarantee its
messages assume.

Two things the definitions have to get right, both tested:
- CONTROL names its subjects instead of taking mesh.control.>, because
  heartbeats live under that prefix and a stream of them competes for
  retention with the messages that matter
- EVENTS filters on the event token, which is why that token exists; a
  filter over a module's whole namespace would persist every tool call

Overlapping filters are refused where the set is written: NATS accepts two
streams matching one subject and stores the message twice under two
retentions, which nothing reports.

Adds nats.go as a dependency; it pulled golang.org/x/* forward. Full suite
green.
This commit is contained in:
2026-09-26 21:02:18 +02:00
parent c753f9d5c0
commit 1f36787d75
7 changed files with 335 additions and 31 deletions
+31 -13
View File
@@ -41,6 +41,7 @@ type Seat struct {
Name string
Accepts []string
Emits []string
Serves []string
Versions []string // protocol versions served beside the current one; empty for v1 only
}
@@ -149,10 +150,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
// else may publish into it, so an event's source is a fact the server enforces rather
// than a claim in the body (design 29 §2).
own := "mesh.mod." + p.Module
if len(p.Emits) > 0 {
for _, e := range p.Emits {
pub = append(pub, own+"."+e)
}
for _, e := range p.Emits {
pub = append(pub, own+".event."+e)
}
for _, t := range p.Serves {
sub = append(sub, own+".tool."+t)
@@ -161,16 +160,24 @@ func PermissionsFor(p Principal) (Permissions, error) {
// 2. What it consumes, by the emitter's own subject — an event is addressed to its
// emitter, because the emitter's identity is the meaning (ADR 0118).
for _, c := range p.Consumes {
sub = append(sub, "mesh.mod."+c)
emitter, event, ok := strings.Cut(c, ".")
if !ok {
return Permissions{}, fmt.Errorf(
"%q does not name an emitter and an event: a consumed event is <module>.<event>", c)
}
sub = append(sub, "mesh.mod."+emitter+".event."+event)
}
// 3. Seats it holds: full participation.
for _, s := range p.Holds {
for _, a := range s.Accepts {
sub = append(sub, seatSubject(s, a))
sub = append(sub, seatSubject(s, "accept", a))
}
for _, e := range s.Emits {
pub = append(pub, seatSubject(s, e))
pub = append(pub, seatSubject(s, "event", e))
}
for _, t := range s.Serves {
sub = append(sub, seatSubject(s, "tool", t))
}
}
@@ -179,7 +186,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
// events and lie about outcomes (design 29 §2).
for _, s := range p.Uses {
for _, a := range s.Accepts {
pub = append(pub, seatSubject(s, a))
pub = append(pub, seatSubject(s, "accept", a))
}
for _, t := range s.Serves {
pub = append(pub, seatSubject(s, "tool", t))
}
}
}
@@ -207,11 +217,19 @@ func PermissionsFor(p Principal) (Permissions, error) {
}, nil
}
// seatSubject places a seat's verb. A seat serving more than its current protocol version carries
// the version as a token (design 29 §8): the seat stays one role, and v1 and v2 run beside each
// other until nothing is bound to the old one.
func seatSubject(s Seat, verb string) string {
return "mesh.seat." + s.Name + "." + verb
// seatSubject places a seat's verb under the kind of traffic it is.
//
// **The kind token is load-bearing, not decoration.** A stream is defined by a subject filter, so
// without it a stream over a seat or a module's namespace would capture that namespace's *tool*
// traffic too — and a tool call must never be persisted (design 25 §3: tools stay on core NATS).
// Found while defining the streams: the first draft of design 29 had one namespace per module
// with no kind, which reads well and cannot be filtered.
//
// A seat serving more than its current protocol version carries the version as a token
// (design 29 §8): the seat stays one role, and v1 and v2 run beside each other until nothing is
// bound to the old one.
func seatSubject(s Seat, kind, verb string) string {
return "mesh.seat." + s.Name + "." + kind + "." + verb
}
// consumerName is the durable consumer the controller derives for this principal. It is here