Files
mesh-sdk/go/buscompose/userlist_switch.go
T
jschoubben 8c2ce57eee Compose a node's two logins from the vault's node-logins, so a rotation can switch with both loaded
ADR 0583 §1: a node's account rotates between node.<n> and node.<n>--b, each with the node's permissions and its
inbox _INBOX.node.<n>. Records gains NodeLogins (each node's key of node-logins; the context file's NodeRotating,
renamed because a node that rotated and rotates no more logs in as its second login alone), and Users composes one
node user per login: the one in use and, while it rotates, the other. Each login calls in its own name, which is how
the vault tells the login that confirms (§3). While the vault serves rotate-node, each node reads its own node's key
of node-logins alone and may call node-account-confirm in its own name (§1 insight); the grants part is the state's
one named reader. With no rotation every list is the controller's golden one, unchanged. Tested as a subtest of
TestTheCopiedComposerComposesAsTheControllers, no new test function.

(issue 774, 774/S39)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:41:03 +01:00

185 lines
9.2 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/handacts"
"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).
// The controller's lease is read by the parts that turn on at its write (ADR 0561 §3, §6), and by the assigner, which
// reads the catalogue's placement alone from then (ADR 0549 §5, issue 774).
//
// The health holders (novox/hq ADR 0564 Limits 2 and 8, clauses 8 and 9): the condition keeper, mesh-healers and the
// watchman read the lease; the keeper alone the controller's conditions; mesh-healers and mesh-hand-acts its hand-act log
// (ADR 0565 clauses 10, 11: every reader of acts reads both logs); and nobody the healers' `would` (health_grants.go).
var NamedReaders = map[string][]string{VaultBusAccounts: {GrantsModule}, GrantsAccounts: {VaultSeat},
ControllerLease: {VaultSeat, GrantsModule, delivery.AssignerModule, health.KeeperModule, health.HealersModule, WatchmanModule,
// ADR 0571 clause 2: the artifact store's seat sweeps only once the lease names no controller.
ArtifactStoreTools,
// ADR 0561 §3: mesh-config takes over the config only once the lease is handed away (its takeover.go leaseAway).
ConfigModule},
ControllerConditions: {health.KeeperModule}, ControllerHandActs: {health.HealersModule, handacts.Module}, 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}}
// 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 }