The grants part wrote a binding into a membership on the bus module's created, but granted its subjects to no account but the ledger's, so distribution's program and the tool runner carrying it were refused their own key-value, object store and stream entries (issue 1966, ADR 0595 §1, design 25 § 12.2). The kept bindings now reach Declared in the same pass that writes them into the membership, and one function, boundOf, grants each to the module's own account and the tool runner, as its old section is granted; a process's bus.state names a key-value entry as an old state. The composed membership carries the bindings, so grants_membership's two sides agree. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
268 lines
12 KiB
Go
268 lines
12 KiB
Go
package buscompose
|
|
|
|
import (
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
|
)
|
|
|
|
// **The bus module's process has a bus account of its own** (novox/hq ADR 0595 §4, §5; design 25 § 12.2, § 12.3;
|
|
// DLV-G2): composed for the process of the module holding the seat mesh-broker (a seat, never a module's name), it
|
|
// creates, updates, reads, snapshots and deletes every module's bus state under that module's names only, each stream
|
|
// by its name (a key-value bucket's and an object store's by the stream the bus keeps it in); it hears what its module
|
|
// consumes (the indexer's manifest-accepted) on a durable consumer of its own, says its module's own stream's events,
|
|
// and raises its conditions through the operator channel. As every process account it serves nothing (ADR 0436 §3): the
|
|
// seat's four reading verbs are served by its module's tools, and the four acting verbs by nobody on the bus, a
|
|
// person's ask reaching the process through its own state. No account holds the acting verbs' subjects, the tool
|
|
// runner's included: each is denied to every principal whose publish would reach it.
|
|
//
|
|
// The names are one rule (BusStateStreams), read by the bus module where it creates them: until mesh-sdk carries it
|
|
// as one function (asked of the mesh-sdk member of the milestone), this is the grants part's side of it.
|
|
|
|
// The verbs of the seat mesh-broker (design 25 § 12.3), until its definition is generated in mesh-contracts: the four
|
|
// reading verbs any caller its role grants may ask, and the four acting verbs the bus module's process alone serves.
|
|
var (
|
|
BrokerReadingVerbs = []string{"list", "show", "check", "orphans"}
|
|
BrokerActingVerbs = []string{"create", "apply", "recreate", "drop"}
|
|
)
|
|
|
|
// BrokerVerbs are the eight.
|
|
func BrokerVerbs() []string {
|
|
return append(append([]string(nil), BrokerReadingVerbs...), BrokerActingVerbs...)
|
|
}
|
|
|
|
// brokerSeat is the seat mesh-broker as its verbs' subjects name it: held once, for the mesh.
|
|
var brokerSeat = Seat{Name: meshbroker.Seat}
|
|
|
|
// BrokerVerbSubjects are where the seat's verbs are asked: each verb's tool subject, and its caller-named form (issue
|
|
// 365).
|
|
func BrokerVerbSubjects(verbs []string) []string {
|
|
var out []string
|
|
for _, v := range verbs {
|
|
out = append(out, seatToolSubject(brokerSeat, v, ""), seatSubject(brokerSeat, CallKind, v)+".>")
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// BrokerActingOnly are the subjects of the seat's acting verbs, which no principal publishes on, however wide its
|
|
// grant: the bus module's process serves them, and a person's ask reaches it on its own tool subject instead
|
|
// (design 25 § 12.3).
|
|
func BrokerActingOnly() []string { return BrokerVerbSubjects(BrokerActingVerbs) }
|
|
|
|
// BusStateStreams are the streams one bus-state entry of module is kept in on the bus, by the mesh's names
|
|
// `<module>_<local>` (design 25 § 12.1): a stream's own; a key-value bucket's KV_ stream; an object store's OBJ_
|
|
// stream; a ledger's history stream, its snapshot stream, and its state and index buckets' KV_ streams. A name that is
|
|
// not one plain subject token names nothing.
|
|
func BusStateStreams(module string, e catalogue.BusStateEntry) []string {
|
|
base := module + "_" + e.Name
|
|
if !safeSubject.MatchString(base) || strings.Contains(e.Name, "_") || strings.Contains(module, "_") {
|
|
return nil
|
|
}
|
|
var out []string
|
|
switch e.Storage {
|
|
case catalogue.StorageKeyValue:
|
|
out = []string{"KV_" + base}
|
|
case catalogue.StorageObjectStore:
|
|
out = []string{"OBJ_" + base}
|
|
case catalogue.StorageLedger:
|
|
out = []string{base, base + "_snapshot", "KV_" + base + "_state"}
|
|
for _, t := range e.Types {
|
|
for _, ix := range t.Indexes {
|
|
if safeSubject.MatchString(ix.Name) {
|
|
out = append(out, "KV_"+base+"_index-"+ix.Name)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
out = []string{base}
|
|
}
|
|
sort.Strings(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// keeperConsumer is the durable consumer the bus module's process hears its module's consumes on, on EVENTS:
|
|
// `<node>_<module>_<process>`, as a process's own consumer is named (QueueConsumer), filtered on the consumed events.
|
|
func keeperConsumer(p Principal) (Consumer, bool) {
|
|
var filters []string
|
|
for _, c := range p.Consumes {
|
|
if s, err := consumedSubject(c); err == nil && !strings.HasPrefix(s, moduleStreamPrefix) {
|
|
filters = append(filters, s)
|
|
}
|
|
}
|
|
// And the events of the seats its module watches (its consumes naming a seat's event, such as the seat mesh-catalog's
|
|
// manifest-accepted), on the seat's own subjects.
|
|
for _, w := range p.Watches {
|
|
if w.Kinded {
|
|
continue
|
|
}
|
|
for _, e := range w.Emits {
|
|
if pattern, ok := machineEvent(w, e); ok {
|
|
e = pattern
|
|
}
|
|
filters = append(filters, seatSubject(w, "event", e))
|
|
}
|
|
}
|
|
// And the events of the seats its module uses that are said to it by name, the operator channel's `decided` for its own
|
|
// asks among them (design 25 § 12.3: the process wakes on the warrant, heard on its durable consumer, never on a timer).
|
|
for _, s := range SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe {
|
|
if strings.Contains(s, ".event.") {
|
|
filters = append(filters, s)
|
|
}
|
|
}
|
|
if len(filters) == 0 {
|
|
return Consumer{}, false
|
|
}
|
|
sort.Strings(filters)
|
|
return Consumer{Name: p.Node + "_" + p.Module + "_" + p.Process, Stream: "EVENTS", Filters: slices.Compact(filters),
|
|
AckWaitSeconds: 30, MaxDeliver: 5, FromNow: true,
|
|
Why: "what the bus module's process " + p.Process + " hears of its module's consumes (novox/hq ADR 0595 §4): the " +
|
|
"indexer's accepted verdicts, from now, its first view at start holding what came before"}, true
|
|
}
|
|
|
|
// keeperConsumerGrants are the subjects the process reads and acknowledges its consumer through.
|
|
func keeperConsumerGrants(c Consumer) []string {
|
|
return []string{"$JS.API.CONSUMER.INFO." + c.Stream + "." + c.Name, "$JS.API.CONSUMER.MSG.NEXT." + c.Stream + "." + c.Name,
|
|
"$JS.ACK." + c.Stream + "." + c.Name + ".>"}
|
|
}
|
|
|
|
// ownStreamReadGrants are what reading a module's own streams back needs (the bus module's tool history): each stream's
|
|
// information and an ordered consumer of its own, as a log's reader has (readLogGrants).
|
|
func ownStreamReadGrants(module string, streams []ModuleStream) []string {
|
|
var out []string
|
|
for _, s := range streams {
|
|
if s.Module != module || !safeSubject.MatchString(s.Name) {
|
|
continue
|
|
}
|
|
out = append(out, "$JS.API.STREAM.INFO."+s.Name, "$JS.API.CONSUMER.CREATE."+s.Name+".>",
|
|
"$JS.API.CONSUMER.MSG.NEXT."+s.Name+".*", "$JS.API.CONSUMER.INFO."+s.Name+".*", "$JS.API.CONSUMER.DELETE."+s.Name+".*")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BusStateNames are every stream the bus module's process may define: those of every entry a manifest asks for now
|
|
// (asked), and those the bus module said it created and has not dropped (bound, by module: names of streams), so an
|
|
// orphan whose manifest left the trunk can still be dropped. Sorted, once each.
|
|
func BusStateNames(asked map[string][]catalogue.BusStateEntry, bound map[string][]string) []string {
|
|
var out []string
|
|
for module, entries := range asked {
|
|
for _, e := range entries {
|
|
out = append(out, BusStateStreams(module, e)...)
|
|
}
|
|
}
|
|
for _, names := range bound {
|
|
for _, n := range names {
|
|
if safeSubject.MatchString(n) {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// busStateKeeperGrants are what the bus module's process publishes to keep the bus state named: each stream's create,
|
|
// update, information, deletion and snapshot with the acknowledgement of the snapshot's chunks (ADR 0626 §1; the server
|
|
// waits on `$JS.SNAPSHOT.ACK.<stream>.…` past its window), and the stream list it finds orphans in. Never a consumer of
|
|
// one, nor any stream's own subjects: a snapshot carries an entry's data to the keeper's inbox alone, and the keeper
|
|
// hears no message of it (ADR 0626 §1, restricted streams included). And the account's own information
|
|
// ($JS.API.INFO): the client library asks it before every key-value bucket it creates, so without it each create times
|
|
// out on a refused publish (the keeper's KV_distribution_* and KV_gitea_*, 2026-11-08); it reads the account's limits and
|
|
// usage, never a stream's data.
|
|
func busStateKeeperGrants(names []string) []string {
|
|
out := []string{"$JS.API.INFO", "$JS.API.STREAM.NAMES", "$JS.API.STREAM.LIST"}
|
|
for _, n := range names {
|
|
for _, op := range []string{"CREATE", "UPDATE", "INFO", "DELETE", "SNAPSHOT"} {
|
|
out = append(out, "$JS.API.STREAM."+op+"."+n)
|
|
}
|
|
out = append(out, "$JS.SNAPSHOT.ACK."+n+".>")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// brokerSeatEvents are the subjects of the seat mesh-broker's events, from the held seat's definition: what the bus
|
|
// module's keeper says them on, like every seat's holder (the lead's ruling, registry milestone 13:0x).
|
|
func brokerSeatEvents(holds []Seat) []string {
|
|
var out []string
|
|
for _, s := range holds {
|
|
if s.Name != meshbroker.Seat {
|
|
continue
|
|
}
|
|
for _, e := range s.Emits {
|
|
if safeSubject.MatchString(e) {
|
|
out = append(out, seatSubject(s, "event", e))
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// keepsBusState is the bus module's process defining one of the bus-state streams it keeps: the writers table's
|
|
// exemption from stream definitions being the grants part's (ADR 0595 §4).
|
|
func keepsBusState(p Principal, pattern string) bool {
|
|
if p.Kind != KindProcess || len(p.KeepsBusState) == 0 {
|
|
return false
|
|
}
|
|
for _, op := range []string{"CREATE", "UPDATE", "DELETE"} {
|
|
if name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM."+op+"."); ok {
|
|
return slices.Contains(p.KeepsBusState, name)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// withoutActing are a holder's served verbs of mesh-broker without the acting four.
|
|
func withoutActing(served []string) []string {
|
|
var out []string
|
|
for _, v := range served {
|
|
if !slices.Contains(BrokerActingVerbs, v) {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// boundGrants are a module's bus state asked in `requires`, as the grants of the account running its code take it
|
|
// (ADR 0595 §1, ADR 0596 §2, design 25 § 12.2; issue 1966): the local names of its key-value entries, granted as an old
|
|
// `state` is; the bus names of its object stores, granted as an old `objects` is; and what its streams (events, work
|
|
// queue, log, bare) are published and read on. A ledger has its own account (ledger.go) and is not among them.
|
|
type boundGrants struct {
|
|
States, Objects, Streams []string
|
|
}
|
|
|
|
// boundOf is one module's bindings as boundGrants, for its own account and the tool runner carrying it, through this one
|
|
// function. **Only the owner reaches it**: a binding is granted only where it names the module's own entry, its names on
|
|
// the bus being the mesh's `<module>_<local>` (BusStateStreams) and a stream's subjects under `mesh.bus.<module>.<local>`;
|
|
// a binding naming any other is granted nothing.
|
|
func boundOf(module string, bindings []membership.BusBinding) boundGrants {
|
|
var g boundGrants
|
|
for _, b := range bindings {
|
|
base := module + "_" + b.Local
|
|
if !safeSubject.MatchString(module) || !safeSubject.MatchString(b.Local) || strings.Contains(b.Local, "_") || strings.Contains(module, "_") {
|
|
continue
|
|
}
|
|
switch b.Storage {
|
|
case catalogue.StorageKeyValue:
|
|
if b.Bucket == base {
|
|
g.States = append(g.States, b.Local)
|
|
}
|
|
case catalogue.StorageObjectStore:
|
|
if b.ObjectStore == base {
|
|
g.Objects = append(g.Objects, base)
|
|
}
|
|
case catalogue.StorageStream:
|
|
if b.Stream != base || b.Subject != "mesh.bus."+module+"."+b.Local {
|
|
continue
|
|
}
|
|
g.Streams = append(g.Streams, b.Subject+".>",
|
|
"$JS.API.STREAM.INFO."+base, "$JS.API.STREAM.MSG.GET."+base, "$JS.API.DIRECT.GET."+base+".>",
|
|
"$JS.API.CONSUMER.CREATE."+base+".>", "$JS.API.CONSUMER.DELETE."+base+".>", "$JS.FC."+base+".>")
|
|
}
|
|
}
|
|
return g
|
|
}
|