A-163: the asker in `accept.<queue>.<asker>` is the asking seat's name (mesh-pipeline-runner, mesh-catalog, mesh-wants, mesh-builds), never a nickname: the builder asks as mesh-builds (was "builder"), the indexer's validation as mesh-catalog (was "indexer"), the assigner's runs as mesh-wants (was "assigner"), the runner's steps on `accept.check.mesh-pipeline-runner` and `accept.delivery.mesh-pipeline-runner` (were `accept.check.step` and `accept.delivery.step`, where the holder read `step` as the asker). An asker naming none (the checker) asks on main's subject (A-184). A build seat's queues are delivery, check and recheck alone (A-170, A-184): buildstep's AskKinds holds no step, a holder takes a step from check or delivery, and the grants part makes a worker per queue, never per kind. A holder keys a named ask's step `<asker>.<node>.<id>` and main's `<node>.<id>` (§481, §483); readers watch their own name's keys. The composition step's step-progress names the one caller mesh-wants' definition lists for step, read from the seat list (A-294). Builds: design 47 G §481, §483, §543, §600, A-163, A-170, A-184, A-248, A-294 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
779 lines
32 KiB
Go
779 lines
32 KiB
Go
package grants
|
|
|
|
import (
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
|
sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"mesh-grants/internal/buscompose"
|
|
)
|
|
|
|
// Records is what the bus composer composes from, rebuilt from the bus (mesh-controller internal/inventory busrecords.go,
|
|
// BusRecords, read the same way from Inputs instead of the controller's store). Pure. An assigned module whose manifest
|
|
// the indexer does not hold is left out and named, where the controller refuses the whole composition: the shadow
|
|
// composes what it can and says what it could not.
|
|
func Records(in Inputs) (buscompose.Records, []Gap) { return records(in, false) }
|
|
|
|
// ListRecords is Records for the user list and the grants (issue 774): a module whose wanted build's manifest is not the
|
|
// trunk's is declared with what both grant (unionDeclared), so the list carries the trunk manifest's subjects before the
|
|
// assigner moves the want to it, and a grant at the trunk's fingerprint waits for nothing the want's move would bring.
|
|
// Memberships are composed from it too, and from the build each machine runs now (Inputs.Had): while a machine's build
|
|
// differs from the want's or the trunk's, neither loses a read, call or consume (ADR 0660 §2 applied to memberships,
|
|
// issue 1966). Consumers stay with Records. Pure.
|
|
func ListRecords(in Inputs) (buscompose.Records, []Gap) { return records(in, true) }
|
|
|
|
func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
|
var gaps []Gap
|
|
seats := map[string]catalogue.SeatDeclaration{}
|
|
declarers := map[string]string{}
|
|
declared := in.declared()
|
|
for _, m := range declared {
|
|
for _, s := range m.DefinesSeats {
|
|
seats[s.Name] = s
|
|
declarers[s.Name] = 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}
|
|
}
|
|
// The enrolment users, from the seat mesh-enrolment's keys, enrolling through its redeem alone (ADR 0587 §4, §6).
|
|
out := buscompose.Records{Assigned: map[string][]buscompose.Declared{}, People: map[string][]string{},
|
|
Interchangeable: map[string]bool{}, Enrolling: slices.Clone(in.Enrolling),
|
|
// And each node's logins (ADR 0583 §1): both while it rotates.
|
|
NodeLogins: in.NodeLogins, Kept: in.Kept, Chunked: in.Chunked, EngineEvents: in.EventSubjects[buscompose.EngineSeat],
|
|
// What the bus module's process may define: every manifest's bus state, and what it said it created (DLV-G2).
|
|
BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound,
|
|
// Each node's capability reading and what each node-engine said running (design 46 approval mark §6).
|
|
Readings: in.Readings, Running: map[string]string{}}
|
|
for key, h := range in.Heard {
|
|
if !h.Stopped {
|
|
out.Running[key] = h.Fingerprint
|
|
}
|
|
}
|
|
for module, m := range in.Manifests {
|
|
if len(m.BusState) > 0 {
|
|
out.BusState[module] = m.BusState
|
|
}
|
|
}
|
|
// The bus names of kept data the recovery module's account is granted (design 47 G §554, §565): those of every manifest of the
|
|
// catalog graph the part holds (the trunk's, each node's wanted, ran and had builds), and those restore points name.
|
|
out.RecoveryNames = recoveryNamesOf(in)
|
|
// The holders on record, after the seats whose holder has left are passed (ADR 0660): the one rule of mesh-sdk, the
|
|
// node-engine's too, read with the manifest each node runs, else the trunk's. Composed, never recorded.
|
|
holders, passed := holdersAfterPass(in)
|
|
gaps = append(gaps, passed...)
|
|
values := func(provider, setting string) []any {
|
|
var vs []any
|
|
for _, node := range in.Nodes {
|
|
if v, ok := in.Values[node+"/"+provider+"/"+setting]; ok && slices.Contains(in.Assigned[node], provider) {
|
|
vs = append(vs, v)
|
|
}
|
|
}
|
|
return vs
|
|
}
|
|
for _, node := range in.Nodes {
|
|
out.Nodes = append(out.Nodes, node)
|
|
manifestOf := func(module string) (catalogue.Manifest, bool) {
|
|
if m, ok := in.Running[node+"."+module]; ok {
|
|
return m, true
|
|
}
|
|
m, ok := in.Manifests[module]
|
|
return m, ok
|
|
}
|
|
var here []catalogue.Manifest
|
|
for _, module := range in.Assigned[node] {
|
|
if m, ok := manifestOf(module); ok {
|
|
here = append(here, m)
|
|
}
|
|
}
|
|
for _, module := range in.Assigned[node] {
|
|
m, known := manifestOf(module)
|
|
if !known {
|
|
gaps = append(gaps, Gap{Input: module + " on " + node, Why: "left out of the shadow list: no manifest to derive what it may say from"})
|
|
continue
|
|
}
|
|
declare := func(m catalogue.Manifest) buscompose.Declared {
|
|
m.Consumes = resolveSeatViews(m.Consumes, holders)
|
|
m.Reads = resolveSeatReads(m.Reads, holders)
|
|
d := declaredFor(m, seats, declarers)
|
|
if len(in.Placed) > 0 {
|
|
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
|
|
}
|
|
for _, sr := range catalogue.ReadsGranted(m, here, node) {
|
|
d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key})
|
|
}
|
|
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes, func(event string) bool { return in.Defined[event] })
|
|
var keptHeard bool
|
|
d.Watches, d.Consumes, d.Hears, keptHeard = keptOutOfHearing(in.Kept, d.Watches, d.Consumes, d.Hears)
|
|
d.HearsAStream = d.HearsAStream || keptHeard
|
|
d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, declared, values)
|
|
// The sent stores of the modules it is for (design 47 G §59, §124), read from the catalog graph's manifests and the
|
|
// definitions' callers.
|
|
d.ReadsObjects = append(d.ReadsObjects, sentStoresRead(m, in.Manifests, in.VerbCallers)...)
|
|
// Its bindings as kept (bindings.go): the same source its membership's `bindings` is written from in this pass,
|
|
// so a grant and its binding are written together, never one before the other (issue 1966, the kind of issue 249).
|
|
d.Bindings = in.Bindings[m.Module]
|
|
withEventSubjects(in.EventSubjects, &d)
|
|
withAcceptCallers(in.AcceptCallers, &d)
|
|
withVerbCallers(in.VerbCallers, &d)
|
|
withNoNamedAsks(NamedAsksUnused(in), &d)
|
|
return d
|
|
}
|
|
d := declare(m)
|
|
trunk, onTrunk := in.Manifests[module]
|
|
// What a node composing a consumer reads of it is the trunk manifest's served keys (frombus reads the indexer's
|
|
// manifest of another node's module), and what runs may still be another build: both are granted (issue 1126).
|
|
if onTrunk {
|
|
d.Served = slices.Compact(slices.Sorted(slices.Values(append(slices.Clone(d.Served), catalogue.ServedSettings(trunk)...))))
|
|
}
|
|
if union && onTrunk && !reflect.DeepEqual(m, trunk) {
|
|
d = unionDeclared(d, declare(trunk))
|
|
}
|
|
if had, ok := in.Had[node+"."+module]; union && ok && !reflect.DeepEqual(m, had) {
|
|
d = unionDeclared(d, declare(had))
|
|
}
|
|
out.Assigned[node] = append(out.Assigned[node], d)
|
|
if m.Instances == catalogue.InstancesInterchangeable || union && onTrunk && trunk.Instances == catalogue.InstancesInterchangeable {
|
|
out.Interchangeable[m.Module] = true
|
|
}
|
|
}
|
|
}
|
|
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
|
|
// part; it adds no principal, since each node's engine speaks as the node's account.
|
|
if m, ok := declared[buscompose.EngineModule]; ok {
|
|
// What it consumes and invokes are its node account's: a seat's event also on the seat's own subject, as a module's
|
|
// consumes are split (consumedOf), every consume still on its emitter's as before, and each verb it invokes in the
|
|
// node's own name (issue 1966: the seat list's verb and events).
|
|
_, watches := consumedOf(m, seats, declarers)
|
|
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, Watches: watches, Invokes: m.Invokes,
|
|
State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
|
|
withEventSubjects(in.EventSubjects, out.Engine)
|
|
withAcceptCallers(in.AcceptCallers, out.Engine)
|
|
withVerbCallers(in.VerbCallers, out.Engine)
|
|
}
|
|
return out, gaps
|
|
}
|
|
|
|
// declaredFor is mesh-controller's declaredFor (internal/inventory/busrecords.go), copied: one module's manifest as the
|
|
// composer needs it.
|
|
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) buscompose.Declared {
|
|
fromModules, watches := consumedOf(m, seats, declarers)
|
|
d := buscompose.Declared{
|
|
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
|
Invokes: m.Invokes, State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
|
|
Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), ServesOwn: ownServesOf(m), Checks: catalogue.HealthChecks(m),
|
|
RunsAs: m.RunsAs, Issued: issuedTools(m),
|
|
// The keys its served values fill, which every node may read where it runs (issue 1126).
|
|
Served: catalogue.ServedSettings(m),
|
|
// Its own object buckets (ADR 0592 §5).
|
|
Objects: objectsOf(m),
|
|
// The ledgers it asks for in `requires` (issue 1966 D5): each assignment gets a ledger account.
|
|
Ledgers: ledgersOf(m),
|
|
// The events of its finding stream asked for in `requires` (lead answer A-280).
|
|
FindingEvents: findingEventsOf(m),
|
|
}
|
|
d.Loads = loadsABundle(m)
|
|
// Its marked tools (design 46 approval mark §1, §6): a name its `needs-approval` gives that it serves as a tool.
|
|
for _, mark := range m.NeedsApproval {
|
|
if slices.Contains(m.Tools, mark) {
|
|
d.Marked = append(d.Marked, mark)
|
|
}
|
|
}
|
|
d.MarkedVerbs = markedNamed(d, seats)
|
|
d.CatchUps = catchUpsOf(m)
|
|
if _, reads := m.OwnSecrets["broker"]; !reads {
|
|
d.NoAccount = true
|
|
}
|
|
if _, reads := m.OwnSecrets[buscompose.ProvisionerSecret]; reads && m.ClaimsSeat(buscompose.NodeProvisionerSeat) {
|
|
d.ProvisionsNode = true
|
|
}
|
|
d.SnapshotsTheBus = m.ClaimsSeat(meshbroker.Seat)
|
|
d.HoldsRecovery = m.ClaimsSeat(buscompose.RecoverySeat)
|
|
for _, c := range m.Claims {
|
|
if s, hasAProtocol := seats[c.Name]; hasAProtocol {
|
|
held := asSeat(s, declarers[s.Name])
|
|
held.Kind = c.Kind
|
|
held.Capabilities = c.Capabilities
|
|
held.Marked = markedVerbs(m, s)
|
|
d.Holds = append(d.Holds, held)
|
|
}
|
|
}
|
|
for _, name := range m.Uses {
|
|
if s, declaredSomewhere := seats[name]; declaredSomewhere {
|
|
d.Uses = append(d.Uses, asSeat(s, declarers[s.Name]))
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
// seatViewConsume is a `consumes` entry naming a seat's view as `seat:<seat>.views.<state>` (design 47 G §616).
|
|
const seatViewConsume = "seat:"
|
|
|
|
// resolveSeatViews resolves each `seat:<seat>.views.<state>` of a module's consumes to `<holder>.views.<state>`, the module
|
|
// holding the seat on record, as it resolves a seat's events: a new holder is granted with no change of the manifest (design
|
|
// 47 G §616). An entry whose seat has no holder on record stays as written, and grants nothing. Pure.
|
|
func resolveSeatViews(consumes []string, holders []catalogue.Held) []string {
|
|
var out []string
|
|
for i, c := range consumes {
|
|
rest, isSeat := strings.CutPrefix(c, seatViewConsume)
|
|
seatName, view, named := strings.Cut(rest, ".views.")
|
|
if !isSeat || !named || view == "" {
|
|
if out != nil {
|
|
out = append(out, c)
|
|
}
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = slices.Clone(consumes[:i])
|
|
}
|
|
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
|
|
if holder < 0 {
|
|
out = append(out, c)
|
|
continue
|
|
}
|
|
out = append(out, holders[holder].Module+".views."+view)
|
|
}
|
|
if out == nil {
|
|
return consumes
|
|
}
|
|
return out
|
|
}
|
|
|
|
// resolveSeatReads resolves each `seat:<seat>.<state>` of a module's reads to `<holder>.<state>`, the module holding the seat
|
|
// on record, as resolveSeatViews does a view: a new holder is read with no change of the manifest (design 47 G A-116). An entry
|
|
// whose seat has no holder on record stays as written, and grants nothing. Pure.
|
|
func resolveSeatReads(reads []string, holders []catalogue.Held) []string {
|
|
var out []string
|
|
for i, r := range reads {
|
|
rest, isSeat := strings.CutPrefix(r, seatViewConsume)
|
|
seatName, state, named := strings.Cut(rest, ".")
|
|
if !isSeat || !named || state == "" || strings.Contains(state, ".") {
|
|
if out != nil {
|
|
out = append(out, r)
|
|
}
|
|
continue
|
|
}
|
|
if out == nil {
|
|
out = slices.Clone(reads[:i])
|
|
}
|
|
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
|
|
if holder < 0 {
|
|
out = append(out, r)
|
|
continue
|
|
}
|
|
out = append(out, holders[holder].Module+"."+state)
|
|
}
|
|
if out == nil {
|
|
return reads
|
|
}
|
|
return out
|
|
}
|
|
|
|
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
|
|
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
|
|
// alike.
|
|
func consumedOf(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration, declarers map[string]string) ([]string, []buscompose.Seat) {
|
|
var fromModules []string
|
|
var watches []buscompose.Seat
|
|
for _, c := range m.Consumes {
|
|
emitter, event, named := strings.Cut(c, ".")
|
|
if named {
|
|
if s, isASeat := seats[emitter]; isASeat && s.Kinded {
|
|
ev := strings.TrimSuffix(event, ".*")
|
|
if catalogue.SeatSays(s.Emits, ev) {
|
|
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
|
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
|
continue
|
|
}
|
|
}
|
|
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
|
|
// Its scope, and whether the node-engine holds it, place an event that names its holder's machine (ADR 0618 §4).
|
|
compiled, _ := catalogue.SeatNamed(s.Name)
|
|
watches = append(watches, buscompose.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{event},
|
|
HeldByTheEngine: compiled.HeldByTheEngine})
|
|
continue
|
|
}
|
|
}
|
|
fromModules = append(fromModules, c)
|
|
}
|
|
return fromModules, watches
|
|
}
|
|
|
|
// loadsABundle says the node's tool runtime loads a bundle of the module. The controller reads it from the resolved
|
|
// manifest's Bundles, which a build derives and the indexer's manifest does not carry (the builder's build record names
|
|
// the repository's manifest digest), so the same answer is derived here from `build.artifacts` by mesh-sdk's rule
|
|
// (the build's own rule for what a bundle loads): a bundle that says `loads`, else every bundle of a module that declares tools and that no
|
|
// resource of the module runs. Without it a module whose only runtime-loaded part is a helper (route-proxy's
|
|
// route-provider) was left out of node-tools' grants (issue 1945).
|
|
func loadsABundle(m catalogue.Manifest) bool {
|
|
for _, b := range m.Bundles {
|
|
if len(b.Loads) > 0 {
|
|
return true
|
|
}
|
|
}
|
|
if m.Build == nil {
|
|
return false
|
|
}
|
|
run := map[string]bool{}
|
|
for _, r := range m.Resources {
|
|
if a, ok := r["artifact"].(string); ok && a != "" {
|
|
run[a] = true
|
|
}
|
|
}
|
|
for _, a := range m.Build.Artifacts {
|
|
if a.Kind != catalogue.ArtifactBundle {
|
|
continue
|
|
}
|
|
if len(a.Loads) > 0 || (a.Loads == nil && len(m.Tools) > 0 && !run[a.Name]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func issuedTools(m catalogue.Manifest) []string {
|
|
var out []string
|
|
for _, o := range m.Provides {
|
|
if tool, issued := m.IssuedCredentialOf(o.Name); issued && !slices.Contains(out, tool) {
|
|
out = append(out, tool)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func bucketsOf(m catalogue.Manifest) []buscompose.Bucket {
|
|
var out []buscompose.Bucket
|
|
for _, s := range m.State {
|
|
out = append(out, buscompose.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds, MaxMiB: s.MaxMiB,
|
|
PerMachine: s.PerMachine, KeyTTL: s.KeyTTL, ReadOnly: s.ReadOnly})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// bucketsFor is a manifest's buckets as this part creates them: bucketsOf, and for the node-engine every node's bucket of
|
|
// compositions, `composed-<node>`, in place of a state it names so (novox/hq ADR 0593 §6): the engine writes its own node's
|
|
// and never creates it.
|
|
func bucketsFor(m catalogue.Manifest, nodes []string) []buscompose.Bucket {
|
|
out := bucketsOf(m)
|
|
if m.Module != buscompose.EngineModule {
|
|
return out
|
|
}
|
|
out = slices.DeleteFunc(out, func(b buscompose.Bucket) bool { return b.Name == buscompose.ComposedState })
|
|
return append(out, buscompose.EngineComposedBuckets(nodes)...)
|
|
}
|
|
|
|
// objectsOf is a manifest's own object buckets (ADR 0592 §5).
|
|
func objectsOf(m catalogue.Manifest) []buscompose.ObjectBucket {
|
|
var out []buscompose.ObjectBucket
|
|
for _, o := range m.Objects {
|
|
out = append(out, buscompose.ObjectBucket{Module: m.Module, Name: o.Name, MaxMiB: o.MaxMiB, Restricted: o.Grant != nil})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// readsOfObjects takes out of a module's reads each that names another module's object bucket (ADR 0592 §9): read, by
|
|
// its bus name, where the owner's trusted config value of the bucket's grant names the module on this node, and refused
|
|
// otherwise, a bucket with no grant included (its owner alone reads it). Every other read stays as it was: a state's.
|
|
func readsOfObjects(module, node string, reads, refused []string, manifests map[string]catalogue.Manifest,
|
|
values func(provider, setting string) []any) (states, objects, refusedOut []string) {
|
|
refusedOut = refused
|
|
for _, r := range reads {
|
|
owner, name, ok := strings.Cut(strings.TrimSpace(r), ".")
|
|
o, isObjects := catalogue.ObjectsDeclaration{}, false
|
|
if ok {
|
|
o, isObjects = manifests[owner].ObjectsNamed(name)
|
|
}
|
|
if !isObjects {
|
|
states = append(states, r)
|
|
continue
|
|
}
|
|
if o.Grant != nil && o.ObjectsReader(values(owner, o.Grant.Config), module, node) {
|
|
objects = append(objects, o.Bucket(owner))
|
|
} else {
|
|
refusedOut = append(refusedOut, r)
|
|
}
|
|
}
|
|
return states, objects, refusedOut
|
|
}
|
|
|
|
// ledgersOf are the local names of the ledgers a manifest asks for in `requires`.
|
|
func ledgersOf(m catalogue.Manifest) []string {
|
|
var out []string
|
|
for _, e := range m.BusState {
|
|
if e.Storage == catalogue.StorageLedger {
|
|
out = append(out, e.Name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func logsOf(m catalogue.Manifest) []buscompose.Log {
|
|
var out []buscompose.Log
|
|
for _, l := range m.Logs {
|
|
out = append(out, buscompose.Log{Module: m.Module, Name: l.Name, MaxMiB: l.Cap(), MaxAgeDays: l.MaxAgeDays})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// markedVerbs are the verbs of a seat the module holds that need the operator's approval: those its definition marks for every
|
|
// holder, and those the module's `needs-approval` names as `<seat>.<verb>` (design 46 approval mark §2, §6).
|
|
func markedVerbs(m catalogue.Manifest, s catalogue.SeatDeclaration) []string {
|
|
var out []string
|
|
for _, v := range s.Serves {
|
|
if v.NeedsApproval || slices.Contains(m.NeedsApproval, s.Name+"."+v.Name) {
|
|
out = append(out, v.Name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// markedNamed are the verbs a seat definition marks (design 46 approval mark §2; design 47 G §677) that the declaration's module
|
|
// or one of its processes invokes by name or answers, each `seat:<seat>.<verb>`, sorted.
|
|
func markedNamed(d buscompose.Declared, seats map[string]catalogue.SeatDeclaration) []string {
|
|
named := slices.Concat(d.Invokes, d.ServesOwn)
|
|
for _, pr := range d.Processes {
|
|
named = append(named, slices.Concat(pr.Invokes, pr.Serves)...)
|
|
}
|
|
var out []string
|
|
for _, v := range named {
|
|
seat, verb, ok := catalogue.SeatInvoke(v)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if i := slices.IndexFunc(seats[seat].Serves, func(s catalogue.Verb) bool { return s.Name == verb }); i >= 0 && seats[seat].Serves[i].NeedsApproval {
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
slices.Sort(out)
|
|
return slices.Compact(out)
|
|
}
|
|
|
|
// queuesOf are a pulled-by-kind seat's queues, in the order of its accepted kinds: a kind's stream, or its own name where it
|
|
// rides none (step and run ride check or delivery, so a build seat's are delivery, check and recheck). Each is one subject
|
|
// and one worker of each form (design 47 G §600, §544, A-170, A-184).
|
|
func queuesOf(own catalogue.Seat) []string {
|
|
streamed := map[string]bool{}
|
|
var out []string
|
|
for _, on := range own.AcceptedOn {
|
|
if stream, kind, ok := strings.Cut(on, "."); ok {
|
|
streamed[kind] = true
|
|
if !slices.Contains(out, stream) {
|
|
out = append(out, stream)
|
|
}
|
|
}
|
|
}
|
|
var queues []string
|
|
for _, a := range own.Accepts {
|
|
if !streamed[a] && !slices.Contains(queues, a) {
|
|
queues = append(queues, a)
|
|
}
|
|
}
|
|
for _, q := range out {
|
|
if !slices.Contains(queues, q) {
|
|
queues = append(queues, q)
|
|
}
|
|
}
|
|
return queues
|
|
}
|
|
|
|
func asSeat(s catalogue.SeatDeclaration, declarer string) buscompose.Seat {
|
|
seat := buscompose.Seat{Name: s.Name, Scope: s.Scope, Accepts: s.Accepts, Emits: s.Emits,
|
|
Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs, DeclaredBy: declarer}
|
|
if own, ok := catalogue.SeatNamed(s.Name); ok && own.PulledByKind {
|
|
seat.PulledByKind = true
|
|
seat.Accepts = queuesOf(own)
|
|
}
|
|
for _, r := range s.Records {
|
|
if declarer != "" {
|
|
seat.Records = append(seat.Records, buscompose.BucketName(declarer, r))
|
|
}
|
|
}
|
|
return seat
|
|
}
|
|
|
|
func streamsOf(m catalogue.Manifest) []buscompose.ModuleStream {
|
|
var out []buscompose.ModuleStream
|
|
for _, s := range m.Streams {
|
|
events := make([]string, 0, len(s.Events))
|
|
for _, e := range s.Events {
|
|
events = append(events, sdkbc.WirePattern(e))
|
|
}
|
|
out = append(out, buscompose.ModuleStream{Module: m.Module, Name: s.BusName(), Events: events,
|
|
MaxAgeDays: s.Days(), MaxMiB: s.Cap(), Restricted: s.Grant != nil,
|
|
WorkQueue: s.Retention == catalogue.StreamRetentionWorkQueue, Duplicates: s.Duplicates, History: s.KeepsHistory(), LastPerSubject: s.LastPerSubject})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func queueOwnersOf(m catalogue.Manifest) []buscompose.QueueOwner {
|
|
buses := m.ProcessBuses()
|
|
if len(buses) == 0 {
|
|
return nil
|
|
}
|
|
byName := map[string]catalogue.StreamDeclaration{}
|
|
for _, s := range m.Streams {
|
|
byName[s.Name] = s
|
|
}
|
|
var out []buscompose.QueueOwner
|
|
for _, r := range m.Resources {
|
|
id := fmt.Sprint(r["id"])
|
|
b, declared := buses[id]
|
|
if !declared {
|
|
continue
|
|
}
|
|
for _, q := range b.Queues {
|
|
s, known := byName[q]
|
|
if !known || s.Retention != catalogue.StreamRetentionWorkQueue {
|
|
continue
|
|
}
|
|
out = append(out, buscompose.QueueOwner{Process: id, Stream: s.BusName(), MaxDeliver: s.MaxDeliveries})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ownServesOf is the seat verbs of the module's processes that declare no bus, which run on the module's own account (design
|
|
// 47 G A-115, A-127); a process with a bus carries its own serves (processesOf).
|
|
func ownServesOf(m catalogue.Manifest) []string {
|
|
buses := m.ProcessBuses()
|
|
var out []string
|
|
for _, r := range m.Resources {
|
|
id := fmt.Sprint(r["id"])
|
|
if _, bus := buses[id]; bus {
|
|
continue
|
|
}
|
|
out = append(out, m.ProcessServes()[id]...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal {
|
|
accounts := m.BusAccounts()
|
|
if len(accounts) == 0 {
|
|
return nil
|
|
}
|
|
queues := queueOwnersOf(m)
|
|
var out []buscompose.ProcessPrincipal
|
|
// Every own bus account, processes first and then tools bundles (catalogue.BusAccounts): a tools bundle's account carries
|
|
// what it may remove from the bus (ADR 0564 Limits 4, issue 1952 S5).
|
|
for _, a := range accounts {
|
|
pr := buscompose.ProcessPrincipal{Process: a.ID, Emits: a.Bus.Emits, State: a.Bus.State, Asks: a.Bus.Asks,
|
|
Invokes: a.Bus.Invokes, Serves: a.Serves, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
|
|
for _, q := range queues {
|
|
if q.Process == a.ID {
|
|
pr.Queues = append(pr.Queues, q)
|
|
}
|
|
}
|
|
out = append(out, pr)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node string, values func(provider, setting string) []any,
|
|
consumes []string, defined func(event string) bool) ([]buscompose.Heard, bool, []string) {
|
|
reading := m
|
|
reading.Consumes = consumes
|
|
parts, touches, onEvents := catalogue.Hearing(reading, declared, node, values, defined)
|
|
var out []buscompose.Heard
|
|
for _, p := range parts {
|
|
h := buscompose.Heard{Stream: p.Stream.BusName(), Emitter: p.Emitter, Restricted: p.Stream.Grant != nil, LastPerSubject: p.Stream.LastPerSubject}
|
|
for _, pattern := range p.Patterns {
|
|
h.Filters = append(h.Filters, buscompose.ModuleStreamSubject(p.Emitter, sdkbc.WirePattern(pattern)))
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
// **Either form of a part's finding stream** (design 47 G §386, §529): a `*` emitter before an event reaches, beside each
|
|
// module's old stream, the stream a part asked for in `requires` by the template events (its `finding-events`) that carries
|
|
// the event, one durable consumer on it, heard on the binding's subject `mesh.bus.<module>.<local>.<event>`.
|
|
for _, c := range reading.Consumes {
|
|
emitter, event, named := strings.Cut(c, ".")
|
|
if !named || emitter != catalogue.OneName || event == "" || strings.ContainsAny(event, "*>") || defined == nil || !defined(event) {
|
|
continue
|
|
}
|
|
for _, module := range slices.Sorted(maps.Keys(declared)) {
|
|
for _, e := range declared[module].BusState {
|
|
if e.Storage != catalogue.StorageStream || e.Template != catalogue.TemplateEvents {
|
|
continue
|
|
}
|
|
h := buscompose.Heard{Stream: catalogue.BusName(module, e.Name), Emitter: module}
|
|
for _, ev := range e.Events {
|
|
// An event is said on its binding's subject with the machine last (§529): `<event>.<node>`.
|
|
if ev == event || ev == event+".*" {
|
|
h.Filters = append(h.Filters, "mesh.bus."+module+"."+e.Name+"."+event+".*")
|
|
}
|
|
}
|
|
if len(h.Filters) > 0 {
|
|
out = append(out, h)
|
|
touches = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out, touches, onEvents
|
|
}
|
|
|
|
// holdersAfterPass is the holders on record (each node's assignments, as the seat mesh-module-assigner holds them) as they
|
|
// stand after catalogue.SeatsToPassOn (novox/hq ADR 0660): a holder whose every assignment's manifest no longer claims
|
|
// its seat has left it, and the seat is the only claimant's. Each assignment is read with the manifest its node runs
|
|
// (Inputs.Ran: the digest its node-engine says it runs), else the trunk's, else the running build of a held module; one with none is unread and keeps what it
|
|
// holds. A pass is said as a gap and recorded nowhere (ADR 0660 §4). Pure.
|
|
func holdersAfterPass(in Inputs) ([]catalogue.Held, []Gap) {
|
|
var held []catalogue.Held
|
|
for _, node := range slices.Sorted(maps.Keys(in.Placed)) {
|
|
for _, x := range in.Placed[node].Holds {
|
|
if x.Scope != "" && x.Scope != catalogue.ScopeMesh {
|
|
continue
|
|
}
|
|
held = append(held, catalogue.Held{Claim: x.Claim, Scope: catalogue.ScopeMesh, Node: node, Module: x.Module, Site: x.Site, Kind: x.Kind})
|
|
}
|
|
}
|
|
var assignments []catalogue.Assignment
|
|
for _, node := range slices.Sorted(maps.Keys(in.Assigned)) {
|
|
for _, module := range in.Assigned[node] {
|
|
assignments = append(assignments, catalogue.Assignment{Node: node, Module: module})
|
|
}
|
|
}
|
|
heard := func(node, module string) (catalogue.Heard, bool) {
|
|
h, ok := in.Heard[node+"."+module]
|
|
return h, ok
|
|
}
|
|
passes, said := catalogue.SeatsToPassOn(held, assignments, heard, ranEntries{in})
|
|
var gaps []Gap
|
|
for _, p := range passes {
|
|
held = slices.DeleteFunc(held, func(h catalogue.Held) bool { return h.Kind == p.From.Kind && sameSeat(h.Claim, p.Seat.Name) })
|
|
p.To.Site = p.From.Site
|
|
held = append(held, p.To)
|
|
gaps = append(gaps, Gap{Input: "seat " + p.Seat.Name, Why: fmt.Sprintf(
|
|
"%s on %s is on record as its holder and its manifest no longer claims it: composed as held by %s on %s, not recorded (ADR 0660)",
|
|
p.From.Module, p.From.Node, p.To.Module, p.To.Node)})
|
|
}
|
|
for _, w := range said {
|
|
gaps = append(gaps, Gap{Input: "seat passes", Why: w})
|
|
}
|
|
return held, gaps
|
|
}
|
|
|
|
// ranEntries is the reader of entries SeatsToPassOn takes (design 47 G §459): the manifest of a fingerprint a node-engine
|
|
// said it runs, and a module's manifest where none was heard: the trunk's, else the running build of a held module.
|
|
type ranEntries struct{ in Inputs }
|
|
|
|
func (r ranEntries) ByFingerprint(fingerprint string) (catalogue.Manifest, bool) {
|
|
m, ok := r.in.RanByFingerprint[fingerprint]
|
|
return m, ok
|
|
}
|
|
|
|
func (r ranEntries) OnBranch(module, _ string) (catalogue.Manifest, bool) {
|
|
if m, ok := r.in.Manifests[module]; ok {
|
|
return m, true
|
|
}
|
|
m, ok := r.in.Held[module]
|
|
return m, ok
|
|
}
|
|
|
|
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
|
|
// holds are the holders on record, as catalogue.World's Holdings are, after the passes of holdersAfterPass: a seat held
|
|
// once for the mesh that some node's assignments holds is held only where assignments names it (this node's naming this
|
|
// module), so a module claiming it on two machines holds it on the one assignments names, not on both. A seat no
|
|
// assignments holds is held by derivation, by the module claiming it wherever it is placed, as it was before assignments:
|
|
// the seeded assignments hold only the seats the controller had on record (twelve on the control-node), and treating their
|
|
// holds as every holder left nine seats with no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every
|
|
// node is held wherever the module runs.
|
|
func heldWherePlaced(holds []buscompose.Seat, holders []catalogue.Held, node, module string) []buscompose.Seat {
|
|
same := func(x catalogue.Held, h buscompose.Seat) bool { return sameSeat(x.Claim, h.Name) && x.Kind == h.Kind }
|
|
var out []buscompose.Seat
|
|
for _, h := range holds {
|
|
if h.Scope != "" && h.Scope != catalogue.ScopeMesh {
|
|
out = append(out, h)
|
|
continue
|
|
}
|
|
onRecord := slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) })
|
|
if !onRecord || slices.ContainsFunc(holders, func(x catalogue.Held) bool { return same(x, h) && x.Node == node && x.Module == module }) {
|
|
out = append(out, h)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sameSeat says two claims name one seat, under either of its names (catalogue.SeatNamed, ADR 0122).
|
|
func sameSeat(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
sa, oka := catalogue.SeatNamed(a)
|
|
sb, okb := catalogue.SeatNamed(b)
|
|
return oka && okb && sa.Name == sb.Name
|
|
}
|
|
|
|
// unionDeclared is what either declaration grants (issue 774): every list of b's entries a lacks appended to a's, a
|
|
// process of both merged by its name, a flag that adds a grant set where either sets it, and an account kept where
|
|
// either has one (NoAccount only where both say none). a's names and RunsAs stand. Pure.
|
|
func unionDeclared(a, b buscompose.Declared) buscompose.Declared {
|
|
out := a
|
|
unionFields(reflect.ValueOf(&out).Elem(), reflect.ValueOf(b))
|
|
out.NoAccount = a.NoAccount && b.NoAccount
|
|
procs := slices.Clone(a.Processes)
|
|
for _, bp := range b.Processes {
|
|
i := slices.IndexFunc(procs, func(ap buscompose.ProcessPrincipal) bool { return ap.Process == bp.Process })
|
|
if i < 0 {
|
|
procs = append(procs, bp)
|
|
continue
|
|
}
|
|
merged := procs[i]
|
|
unionFields(reflect.ValueOf(&merged).Elem(), reflect.ValueOf(bp))
|
|
procs[i] = merged
|
|
}
|
|
out.Processes = procs
|
|
return out
|
|
}
|
|
|
|
// unionFields appends to each slice field of dst the elements of src's it lacks, and sets each bool field src sets.
|
|
func unionFields(dst, src reflect.Value) {
|
|
for i := 0; i < dst.NumField(); i++ {
|
|
f, g := dst.Field(i), src.Field(i)
|
|
switch f.Kind() {
|
|
case reflect.Slice:
|
|
merged := reflect.AppendSlice(reflect.MakeSlice(f.Type(), 0, f.Len()+g.Len()), f)
|
|
for j := 0; j < g.Len(); j++ {
|
|
held := false
|
|
for k := 0; k < merged.Len() && !held; k++ {
|
|
held = reflect.DeepEqual(merged.Index(k).Interface(), g.Index(j).Interface())
|
|
}
|
|
if !held {
|
|
merged = reflect.Append(merged, g.Index(j))
|
|
}
|
|
}
|
|
f.Set(merged)
|
|
case reflect.Bool:
|
|
f.SetBool(f.Bool() || g.Bool())
|
|
}
|
|
}
|
|
}
|
|
|
|
// catchUpsOf are the catch-up tools a manifest's kept data name, each once (design 47 G §406, §631): the module's own tools
|
|
// the node-engine calls after a restore of that data, which it is granted to call on its own node (A-108).
|
|
func catchUpsOf(m catalogue.Manifest) []string {
|
|
var tools []string
|
|
for _, k := range catalogue.KeptDeclarations(m) {
|
|
if k.CatchUp != nil && k.CatchUp.Tool != "" && !slices.Contains(tools, k.CatchUp.Tool) {
|
|
tools = append(tools, k.CatchUp.Tool)
|
|
}
|
|
}
|
|
return tools
|
|
}
|
|
|
|
// findingEventsOf are the events of m's `finding-events` entry, an `events` template it requires (design 47 G §529; lead
|
|
// answer A-280), none where it asks for none.
|
|
func findingEventsOf(m catalogue.Manifest) []string {
|
|
for _, e := range m.BusState {
|
|
if e.Name == buscompose.FindingEventsLocal && e.Storage == catalogue.StorageStream && e.Template == catalogue.TemplateEvents {
|
|
return append([]string(nil), e.Events...)
|
|
}
|
|
}
|
|
return nil
|
|
}
|