ADR 0593 §1: buscompose's engine moves from mesh-sdk into mesh-grants/internal/buscompose, its golden user lists unchanged; the controller's raise and assert code is left behind, and the merged switch test runs inside the part's whole integration test. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
283 lines
15 KiB
Go
283 lines
15 KiB
Go
package buscompose
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The writers table (novox/hq to-be 45 §1, ADR 0227 rule 1), compiled in.
|
|
//
|
|
// **Every kind of state the core keeps has one writer; anyone else asks it.** The table is the design's,
|
|
// row for row, with what each row writes on the bus where it writes there. Two things read it: the
|
|
// composition of every principal's grants (PermissionsFor), which **refuses a grant that lets a
|
|
// principal publish on a subject another writes** — so a second writer cannot be granted by accident,
|
|
// and the composition says which state and whose — and the test beside it, which walks the rows
|
|
// against the design's list and the composition of a whole mesh. Changing a writer is a change to
|
|
// this table, through a decision.
|
|
|
|
// WriterRow is one row of the writers table.
|
|
type WriterRow struct {
|
|
State string
|
|
Writer string
|
|
KeptIn string
|
|
Others string
|
|
// Subjects are the bus subjects a write of this state is a publish to; none for state kept off the
|
|
// bus (a module's own store) or not built yet.
|
|
Subjects []string
|
|
// Writes says a principal granted a publish pattern overlapping Subjects is this state's writer —
|
|
// given the pattern, because a machine writes its own report and no other's.
|
|
Writes func(p Principal, pattern string) bool
|
|
// Shared says why more than one principal may publish here; empty for one writer.
|
|
Shared string
|
|
}
|
|
|
|
// createsDeclared, and the other writers' tests, are who a row's writer is as a principal.
|
|
//
|
|
// createsDeclared is the grants part creating one stream a manifest declares, by its own name: the create alone, never an
|
|
// update or a delete (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); or the bus module's tools deleting one of
|
|
// the ended controller's leftover streams (ADR 0590 clause 6).
|
|
func createsDeclared(p Principal, pattern string) bool {
|
|
if removesLeftover(p, pattern) {
|
|
return true
|
|
}
|
|
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
|
|
if ok && p.Kind == KindModule && p.Module == GrantsModule && name == GrantsUserListObjectStream {
|
|
return true
|
|
}
|
|
return ok && p.Kind == KindModule && p.Module == GrantsModule && slices.Contains(p.CreatesStreams, name) &&
|
|
safeSubject.MatchString(name)
|
|
}
|
|
|
|
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
|
func ownMachine(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
return p.Kind == KindNode && len(tokens) > 2 && tokens[2] == p.Node
|
|
}
|
|
|
|
// holdsSeatOf is a principal holding the seat a `mesh.seat.<seat>.…` pattern names: its module's own
|
|
// principal, or the node tools carrying a module that holds it (ADR 0175, the runtime is its modules).
|
|
func holdsSeatOf(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
if len(tokens) < 3 {
|
|
return false
|
|
}
|
|
seat := tokens[2]
|
|
holds := func(seats []Seat) bool {
|
|
return slices.ContainsFunc(seats, func(s Seat) bool { return s.Name == seat })
|
|
}
|
|
switch p.Kind {
|
|
case KindModule:
|
|
return holds(p.Holds)
|
|
case KindNodeTools:
|
|
return slices.ContainsFunc(p.Carries, func(d Declared) bool { return holds(d.Holds) })
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ownModule is a module publishing under its own name, or the node tools carrying it.
|
|
func ownModule(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
if len(tokens) < 3 {
|
|
return false
|
|
}
|
|
module := tokens[2]
|
|
switch p.Kind {
|
|
case KindModule:
|
|
return p.Module == module
|
|
case KindNodeTools:
|
|
return slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.Module == module })
|
|
}
|
|
return false
|
|
}
|
|
|
|
// moduleNamed is the writer that is module: its own principal, a process of it, or the node tools carrying it.
|
|
func moduleNamed(module string) func(Principal, string) bool {
|
|
return func(p Principal, _ string) bool {
|
|
switch p.Kind {
|
|
case KindModule, KindProcess:
|
|
return p.Module == module
|
|
case KindNodeTools:
|
|
return slices.ContainsFunc(p.Carries, func(d Declared) bool { return d.Module == module })
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ownAnnouncer is a module, a process of it or the node tools carrying it saying what that module keeps (novox/hq ADR
|
|
// 0495 clause 2): each under its own name alone.
|
|
func ownAnnouncer(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
if len(tokens) < 3 {
|
|
return false
|
|
}
|
|
if p.Kind == KindProcess {
|
|
return p.Module == tokens[2]
|
|
}
|
|
return ownModule(p, pattern)
|
|
}
|
|
|
|
// ownProvisioner is a machine's provisioner client saying a report about its own machine: the third token of
|
|
// `prov.applied.<node>.…` is its machine (novox/hq ADR 0349 §4, design 57 P3).
|
|
func ownProvisioner(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
return p.Kind == KindProvisioner && len(tokens) > 3 && tokens[2] == p.Node
|
|
}
|
|
|
|
// ownNodeKeys is a node writing the have keys of its own node: `$KV.<bucket>.<node>.…`, the node in the third token
|
|
// and no wildcard there (novox/hq ADR 0429 §1).
|
|
func ownNodeKeys(p Principal, pattern string) bool {
|
|
tokens := strings.Split(pattern, ".")
|
|
return p.Kind == KindNode && len(tokens) > 2 && tokens[2] == p.Node
|
|
}
|
|
|
|
// kvOf is a bucket's write subjects.
|
|
func kvOf(bucket string) []string { return []string{"$KV." + bucket + ".>"} }
|
|
|
|
// WritersTable is to-be 45 §1, in its order.
|
|
var WritersTable = []WriterRow{
|
|
{State: "a machine's applied state and its report", Writer: "the node-engine's apply queue",
|
|
KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply",
|
|
// And its health statement between reports (novox/hq ADR 0240): the same writer stating the same
|
|
// machine, inside the grant it already had (`mesh.control.<its own>.>`). And a line mesh-cli was given
|
|
// on the machine (novox/hq ADR 0272): that subject is this machine's engine's alone.
|
|
Subjects: []string{"mesh.control.*.report", "mesh.control.*.health", "mesh.control.*.cli"}, Writes: ownMachine},
|
|
{State: "stream definitions and bus permissions", Writer: "the grants part", KeptIn: "the bus", Others: "—",
|
|
// A stream's definition, and a durable consumer's by the API that names it so. Not every
|
|
// consumer create: a module watching its own bucket makes and deletes an ordered consumer on the
|
|
// bucket's stream (ADR 0201), which defines nothing the mesh keeps.
|
|
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
|
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
|
Writes: createsDeclared,
|
|
Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the ended controller's leftover streams, each by its name, and no other (novox/hq ADR 0590 clause 6)"},
|
|
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
|
Others: "the builder asks",
|
|
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
|
Writes: holdsSeatOf,
|
|
Shared: "every machine holding the build seat answers the asks it took; each outcome names its ask"},
|
|
// What a module keeps and who may read it (novox/hq ADR 0495 clauses 2 and 3, issue 1790): said by the module.
|
|
{State: "a module's states announced", Writer: "the module itself, from its code, under its own name",
|
|
KeptIn: "the bus (EVENTS)", Others: "the grants part grants readers from it",
|
|
Subjects: []string{"mesh.mod.*.event.states"}, Writes: ownAnnouncer},
|
|
{State: "a merge announced", Writer: "one announcer per forge (the hook, or the poll when the hook is absent — never both)",
|
|
KeptIn: "the bus", Others: "—", Subjects: []string{"mesh.mod.*.event.pull.merged"}, Writes: ownModule},
|
|
// A pull request's head, before it merges (novox/hq to-be 45 §9): the same one announcer.
|
|
{State: "a pull request's head announced", Writer: "the forge's announcer, the one that announces its merges",
|
|
KeptIn: "the bus", Others: "the checker checks it", Subjects: []string{"mesh.mod.*.event.pull.updated"},
|
|
Writes: ownModule},
|
|
{State: "a pull request's merge check", Writer: "the checker that ran it",
|
|
KeptIn: "the bus", Others: "the forge's holder sets it as the pull request's status"},
|
|
{State: "a provider's standing", Writer: "the provider", KeptIn: "the provider's events",
|
|
Others: "the watchman keeps the newest word as a condition",
|
|
Subjects: []string{"mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered",
|
|
"mesh.mod.*.event.provisioner.retirement"},
|
|
Writes: ownModule},
|
|
{State: "the operator-channel's open messages", Writer: "the seat's holder", KeptIn: "its own key-value state",
|
|
Others: "—"},
|
|
// The seat mesh-enrolment's open enrolments (novox/hq ADR 0587 §2, issue 774): its holder alone writes them, each a
|
|
// compare-and-set at the revision read; the grants part reads them as a named reader.
|
|
{State: "the open enrolments", Writer: "the seat mesh-enrolment's holder, compare-and-set",
|
|
KeptIn: "key-value " + delivery.EnrolmentsBucket + ", with per-key expiry",
|
|
Others: "the grants part reads them (a named reader) and composes each enrol.<node> user from them",
|
|
Subjects: kvOf(delivery.EnrolmentsBucket), Writes: moduleNamed(delivery.EnrolmentModule)},
|
|
// Which login each node uses (novox/hq ADR 0583 §1 insight): the vault alone writes it.
|
|
{State: "a node's logins", Writer: "the vault", KeptIn: "key-value " + delivery.NodeLoginsBucket,
|
|
Others: "the grants part reads every key, each node's engine its own node's key alone",
|
|
Subjects: kvOf(delivery.NodeLoginsBucket), Writes: moduleNamed(delivery.VaultModule)},
|
|
// What a consumer gave up on (novox/hq issue 330): copied by the watchman from the server's notice (ADR 0564
|
|
// clause 5, ADR 0590 clause 2).
|
|
{State: "a message a consumer gave up on", Writer: "the watchman", KeptIn: "the bus, the stream " + DeadLettersStream,
|
|
Others: "read through the watchman's dead-letters; delivered again by nobody until the hand-act log's home is delivered",
|
|
Subjects: []string{deadLetterPrefix + ">", againPrefix + ">"}, Writes: keepsDeadLetters},
|
|
// What each machine has of each module (novox/hq ADR 0429 §1, design 47): written by that machine's node-engine under
|
|
// its own node's keys; the view and the watchman read it.
|
|
{State: "a machine's have", Writer: "the node-engine of the machine, under its own keys `<node>.>`",
|
|
KeptIn: "key-value " + delivery.HaveBucket + ", twenty revisions of each key",
|
|
Others: "read through `reads` as node-engine.have", Subjects: kvOf(delivery.HaveBucket), Writes: ownNodeKeys},
|
|
// The acts a node's engine was asked through its node seat, and its own asks (novox/hq ADR 0563 §2, §3): the
|
|
// node-engine's declared state, created by the grants part, written by each node's engine under its own keys.
|
|
// A node's own health report (issue 774): its engine's own key, read by the watchman.
|
|
{State: "a machine's health report", Writer: "the node-engine of the machine, its key `<node>` alone",
|
|
KeptIn: "key-value " + delivery.NodeHealthBucket, Others: "read by the watchman through `reads`",
|
|
Subjects: kvOf(delivery.NodeHealthBucket), Writes: ownNodeKeys},
|
|
// A node's declared packages (novox/hq ADR 0565 clause 6): its engine's own key, read by pacman's verbs.
|
|
{State: "a machine's declared packages", Writer: "the node-engine of the machine, its key `<node>` alone",
|
|
KeptIn: "key-value " + EnginePackagesBucket, Others: "read by pacman through `reads`",
|
|
Subjects: kvOf(EnginePackagesBucket), Writes: ownNodeKeys},
|
|
// A node's placed modules' data items (novox/hq ADR 0577 clause 1): its engine's own keys, read by that node's disk-load.
|
|
{State: "a machine's data items", Writer: "the node-engine of the machine, under its own keys `<node>.>`",
|
|
KeptIn: "key-value " + EngineDataBucket, Others: "read by the node's disk-load through `reads`, its own node's keys alone",
|
|
Subjects: kvOf(EngineDataBucket), Writes: ownNodeKeys},
|
|
{State: "a machine's hand acts", Writer: "the node-engine of the machine, under its own keys `<node>.>`, compare-and-set",
|
|
KeptIn: "key-value " + EngineActsBucket + ", the node-engine's declared state acts",
|
|
Others: "read through the node seat's verb acts", Subjects: kvOf(EngineActsBucket),
|
|
Writes: func(p Principal, pattern string) bool {
|
|
return ownNodeKeys(p, pattern) || removesEngineActs(p, pattern)
|
|
},
|
|
Shared: "the bus module's tools account removes keys of a full bucket, for removal only (novox/hq ADR 0564 Limits 4)"},
|
|
// The object buckets (novox/hq ADR 0486 §2, ADR 0547 clause 7): the declaration files' buckets the ended controller
|
|
// wrote are written by nobody (ADR 0590); the grants part writes its own.
|
|
{State: "an object bucket", Writer: "the grants part, its own bucket alone",
|
|
KeptIn: "the object bucket " + DeclarationFilesBucket + "-<node> of each node, an object removed a day after the " +
|
|
"node's latest declaration no longer names it",
|
|
Others: "a node-engine reads an object of its own bucket by its name and may neither list the bucket nor write it",
|
|
Subjects: []string{"$O.*.>"}, Writes: func(p Principal, pattern string) bool {
|
|
return grantsOwnObjects(p, pattern)
|
|
},
|
|
Shared: "nobody else: the grants part writes its own object bucket " + GrantsUserListObjects + ", the user list sealed to the bus node, and no other (novox/hq ADR 0547 clause 7)"},
|
|
// What a machine's provisioner client placed of a module's files (novox/hq ADR 0349 §4, design 57 P6): its own
|
|
// machine's word alone, which a walk's end and a gate wait for (ADR 0362, ADR 0349 §15). Kept newest per module
|
|
// in the stream ProvAppliedStream, and by the client in the provisioner server's `applied` bucket.
|
|
{State: "a module's files placed on a machine", Writer: "that machine's provisioner client",
|
|
KeptIn: "the bus, the stream " + ProvAppliedStream + ", newest per module; and key-value " +
|
|
ProvisionerAppliedBucket,
|
|
Others: "the watchman reads it; a failure is that machine's failed apply",
|
|
Subjects: []string{"prov.applied.*.>"}, Writes: ownProvisioner},
|
|
}
|
|
|
|
// CheckWriters refuses a grant that lets a principal publish on a subject the writers table gives
|
|
// another writer (to-be 45 §1): which state, whose, and the pattern that would make a second writer.
|
|
func CheckWriters(p Principal, publish []string) error {
|
|
var problems []string
|
|
for _, pattern := range publish {
|
|
for _, row := range WritersTable {
|
|
if row.Writes == nil {
|
|
continue
|
|
}
|
|
for _, subject := range row.Subjects {
|
|
if !SubjectsOverlap(pattern, subject) || row.Writes(p, pattern) {
|
|
continue
|
|
}
|
|
problems = append(problems, fmt.Sprintf("%s may publish %s, which writes %s (%s), whose writer is %s",
|
|
p.Username(), pattern, row.State, subject, row.Writer))
|
|
}
|
|
}
|
|
}
|
|
if len(problems) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("a second writer would be granted (novox/hq to-be 45 §1, one writer per piece of state):\n %s",
|
|
strings.Join(problems, "\n "))
|
|
}
|
|
|
|
// SubjectsOverlap says some subject matches both patterns: `*` is one token, `>` one or more to the end.
|
|
func SubjectsOverlap(a, b string) bool {
|
|
x, y := strings.Split(a, "."), strings.Split(b, ".")
|
|
for i := 0; ; i++ {
|
|
switch {
|
|
case i == len(x) && i == len(y):
|
|
return true
|
|
case i == len(x) || i == len(y):
|
|
return false
|
|
case x[i] == ">" || y[i] == ">":
|
|
return true
|
|
case x[i] == "*" || y[i] == "*" || x[i] == y[i]:
|
|
continue
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|