The controller ended by its unassignment (ADR 0590): its user, its lease's cut and reads, the operator's hand path to its compositions and its writers' rows go; an enrolling node enrols through mesh-enrolment alone; the desk's prompt stays the vault's alone (DeskOnly). The golden lists are now this composer's own, taken first from the controller's with its user removed. The bus module's tools may remove the controller's leftovers by name, consumers, CONTROL, ASSIGNMENTS, its 17 buckets and each node's declaration files' bucket, under the word controller-leftovers (clause 6). The provisioner client reads placement in place of the assignments mirror. Issue 1126 B3a, B5. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
175 lines
8.3 KiB
Go
175 lines
8.3 KiB
Go
package buscompose
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/announce"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
)
|
|
|
|
// **The grants part's user list, sealed to the bus node** (novox/hq ADR 0547 clause 7, issue 1945). The grants part writes
|
|
// the list it composed as one object of its own object bucket, sealed to the key of the node holding mesh-broker, and
|
|
// names it in one key of its own state `user-list`. The node-engine of that node follows the key when the switch says
|
|
// so (clause 8), opens the object and writes the bus's user-list file.
|
|
//
|
|
// Who may do what, and nothing more:
|
|
//
|
|
// - the grants part writes its object bucket (create, put, purge of an object's old chunks, delete) and its own state
|
|
// as before;
|
|
// - the node-engine of the node holding mesh-broker reads that one key of the state, watched, and an object of the
|
|
// bucket by its name: never the bucket's list of names, never a write, never another key of the grants part's state;
|
|
// - no other principal touches either.
|
|
|
|
// GrantsUserListBucket, GrantsUserListKey, GrantsUserListObjects and GrantsUserListObjectStream are delivery's names,
|
|
// here where the grants are composed.
|
|
const (
|
|
GrantsUserListBucket = delivery.GrantsUserListBucket
|
|
GrantsUserListKey = delivery.GrantsUserListKey
|
|
GrantsUserListObjects = delivery.GrantsUserListObjects
|
|
GrantsUserListObjectStream = delivery.GrantsUserListObjectStream
|
|
)
|
|
|
|
// GrantsUserListFollows is what the node-engine of the node holding mesh-broker may publish to follow the grants part's
|
|
// list: the one key `user-list.sealed` read and watched (its own ordered consumer, filtered on that key alone), and an
|
|
// object of the bucket by its name, as a node reads its declaration's files (ADR 0486 §2: never STREAM.INFO of the object
|
|
// stream, which lists every name, and never STREAM.MSG.GET).
|
|
func GrantsUserListFollows() []string {
|
|
kv := "KV_" + GrantsUserListBucket
|
|
key := "$KV." + GrantsUserListBucket + "." + GrantsUserListKey
|
|
obj, s := GrantsUserListObjects, GrantsUserListObjectStream
|
|
return []string{
|
|
"$JS.API.DIRECT.GET." + kv + "." + key,
|
|
"$JS.API.CONSUMER.CREATE." + kv + ".*." + key,
|
|
"$JS.API.CONSUMER.DELETE." + kv + ".*",
|
|
"$JS.API.STREAM.INFO." + kv,
|
|
"$JS.FC." + kv + ".>",
|
|
"$JS.API.DIRECT.GET." + s + ".$O." + obj + ".M.*",
|
|
"$JS.API.CONSUMER.CREATE." + s + ".*.$O." + obj + ".C.*",
|
|
"$JS.API.CONSUMER.DELETE." + s + ".*",
|
|
"$JS.FC." + s + ".>",
|
|
}
|
|
}
|
|
|
|
// GrantsUserListWrites is what the grants part may publish to keep its object bucket: create it and read its info back,
|
|
// put an object (its chunks and its metadata, the old chunks of a name purged), read an object's metadata, and delete
|
|
// one. Its own state `user-list` it writes already, as every module writes its own.
|
|
func GrantsUserListWrites() []string {
|
|
obj, s := GrantsUserListObjects, GrantsUserListObjectStream
|
|
return []string{
|
|
"$JS.API.STREAM.CREATE." + s,
|
|
"$JS.API.STREAM.INFO." + s,
|
|
"$JS.API.STREAM.PURGE." + s,
|
|
"$JS.API.DIRECT.GET." + s + ".>",
|
|
"$JS.API.STREAM.MSG.GET." + s,
|
|
"$O." + obj + ".>",
|
|
}
|
|
}
|
|
|
|
// grantsOwnObjects is the grants part writing its own object bucket, and nothing of another.
|
|
func grantsOwnObjects(p Principal, pattern string) bool {
|
|
return p.Kind == KindModule && p.Module == GrantsModule &&
|
|
(pattern == "$O."+GrantsUserListObjects+".>" || pattern == "$JS.API.STREAM.CREATE."+GrantsUserListObjectStream)
|
|
}
|
|
|
|
// **Named readers** (novox/hq ADR 0547 clause 9, progressive insight of 2026-10-26; issue 1945). A state named here is
|
|
// read by the modules named and by no other, whatever another module's `reads` says: the vault's `bus-accounts` holds
|
|
// every bus account's bcrypt hash, and from take-over the grants part alone may read it. A module's announcement may name
|
|
// readers too (announce.State.Readers); both are honoured.
|
|
const VaultBusAccounts = "mesh-vault.bus-accounts"
|
|
|
|
// ArtifactStoreTools is the module holding the seat mesh-artifact-sweep (novox/hq ADR 0571 clause 2).
|
|
const ArtifactStoreTools = "artifact-store-tools"
|
|
|
|
// ConfigModule is the module that holds the seat mesh-config, a reader of the controller's lease (NamedReaders).
|
|
const ConfigModule = "mesh-config"
|
|
|
|
// NamedReaders are the states only the modules named may read, by `<owner>.<name>`: the grants part's wanted accounts
|
|
// are read by the vault alone (novox/hq ADR 0561 §2); nobody reads the healers' `would` (ADR 0564 clause 8,
|
|
// health_grants.go).
|
|
var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsAccounts: {VaultSeat}, HealersWould: {},
|
|
// ADR 0577 clause 1: the node-engine's data items are read by disk-load alone, each node's its own node's keys
|
|
// (NodeKeyedReads); the engine announces nothing of its own, so its readers are named here.
|
|
delivery.EngineDataRead: {health.DiskLoadModule},
|
|
// ADR 0583 §1 insight: every node's login is read by the grants part alone, each engine its own node's key through its
|
|
// machine principal (NodeLoginsReads), never through `reads`.
|
|
delivery.NodeLoginsRead: {GrantsModule},
|
|
// ADR 0587 §2: the open enrolments are read by their holder and, as a named reader, by the grants part.
|
|
delivery.EnrolmentsRead: {GrantsModule}}
|
|
|
|
// ReaderMay says whether module may read the state read (`<owner>.<name>`) by the named readers: always, for a state
|
|
// that names none.
|
|
func ReaderMay(module, read string) bool {
|
|
readers, named := NamedReaders[read]
|
|
return !named || slices.Contains(readers, module)
|
|
}
|
|
|
|
// readsNamed splits a module's reads by the named readers: those it may read, and those named for others.
|
|
func readsNamed(module string, reads []string) (granted, refused []string) {
|
|
for _, r := range reads {
|
|
if ReaderMay(module, strings.TrimSpace(r)) {
|
|
granted = append(granted, r)
|
|
} else {
|
|
refused = append(refused, r)
|
|
}
|
|
}
|
|
return granted, refused
|
|
}
|
|
|
|
// withAnnouncedReads is d with its reads judged for d's module (ReadsAnnouncedBy) against what the owners announced:
|
|
// every read its manifest names, granted or refused by the caller, judged again here, so a caller that split them
|
|
// without the reader (the controller's own copy of a reader-less judgement, issue 774: the vault, the assigner, the
|
|
// grants part and the artifact store's tools refused the lease they are named for) cannot refuse a read the reader is
|
|
// named for, nor grant one named for another. With no announcements read it is withNamedReaders.
|
|
func withAnnouncedReads(d Declared, announced map[string]announce.States) Declared {
|
|
if announced == nil {
|
|
return withNamedReaders(d)
|
|
}
|
|
all := append(append([]string(nil), d.Reads...), d.ReadsRefused...)
|
|
if len(all) == 0 {
|
|
return d
|
|
}
|
|
d.Reads, d.ReadsRefused = ReadsAnnouncedBy(d.Module, all, announced)
|
|
return d
|
|
}
|
|
|
|
// withNamedReaders is d with the reads named for other modules moved to its refused reads.
|
|
func withNamedReaders(d Declared) Declared {
|
|
granted, refused := readsNamed(d.Module, d.Reads)
|
|
if len(refused) == 0 {
|
|
return d
|
|
}
|
|
d.Reads = granted
|
|
d.ReadsRefused = append(append([]string(nil), d.ReadsRefused...), refused...)
|
|
return d
|
|
}
|
|
|
|
// **The hearings the grants part withholds, on the bus** (issue 1955 L7, mesh-delivery e564eaf): a hearing of a restricted
|
|
// stream a module asks in the tool runner, withheld because only an account of its own may hear it (ADR 0376 §2), kept
|
|
// in the grants part's state `withheld`, one key `<module>.<stream, lower case>.<machine>` (the controller's condition id
|
|
// of it), written on change only and deleted once heard; the grants part's tools read it (D-heard was dropped on
|
|
// 2026-10-30, so no other module reads it).
|
|
const (
|
|
GrantsWithheldState = "withheld"
|
|
GrantsWithheldBucket = GrantsModule + "_" + GrantsWithheldState
|
|
GrantsWithheldRead = GrantsModule + "." + GrantsWithheldState
|
|
WithheldVersion = 1
|
|
)
|
|
|
|
// WithheldHearing is one withheld hearing: the machine, the module, the restricted stream and its emitter, the plain
|
|
// words of it and the remedy (Withheld.Said), and when the grants part first wrote it as it stands.
|
|
type WithheldHearing struct {
|
|
V int `json:"v"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Stream string `json:"stream"`
|
|
Emitter string `json:"emitter"`
|
|
Said string `json:"said"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// WithheldKey is a withheld hearing's key: `<module>.<stream, lower case>.<machine>`.
|
|
func WithheldKey(w Withheld) string { return w.Module + "." + strings.ToLower(w.Stream) + "." + w.Node }
|