Files
mesh-delivery/modules/mesh-grants/internal/buscompose/composed.go
T
jschoubben 0b03b9c7c5 mesh-grants: create a module's object buckets and history streams, grant their readers read-only, and warn at 75 percent
ADR 0592 §5, §6, §9, §25 (the grants part's share by ADR 0593 §10): an object bucket and a stream that
keeps its history are created create-only with their caps and no age limit, refusing new data when full;
the owner alone writes its bucket and a module its grant names reads it; the part raises
mesh-grants.history-full once any passes 75 percent. Each engine also reads the indexer's manifest
verdicts by one key for compose-candidate.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:19 +01:00

105 lines
5.5 KiB
Go

package buscompose
import (
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4, as ADR 0593 §6 changes them): one per node,
// `composed-<node>`, which that node's engine alone writes, unsigned, with every composition it makes, and which the
// grants part creates (EngineComposedBuckets). The provisioner, the assigner and the view read every node's
// (delivery.ComposedRead, a named-reader state); no engine applies a value read from it.
// ComposedReads is what a node's node-engine publishes to keep and follow its compositions and wants (ADR 0593 §6, ADR
// 0539 clause 9): the writes of its own composition bucket's keys (`<node>.<module>.<fp>` and `<node>.node`, all under
// `<node>.`, so the bucket's whole) and their read, its own wants (`want.<node>.<module>` and `want.<node>.node`) in the
// assigner's bucket, and the indexer's candidates by one key's direct get (ADR 0593 §7: `compose-candidate` reads the
// check's candidate itself; no listing and no watch); nothing of another node's, never the bucket's creation.
func ComposedReads(node string) ([]string, error) {
bucket, err := delivery.ComposedBucketFor(node)
if err != nil {
return nil, err
}
if !safeSubject.MatchString(node) {
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
}
out := append([]string{"$KV." + bucket + ".>"}, readGrants(bucket, "")...)
out = append(out, readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...)
return append(out, CandidateReads...), nil
}
// CandidateReads is what a node's engine is granted of the indexer's candidates and manifest verdicts to answer
// `compose-candidate` (ADR 0593 §7, ADR 0578 §3): binding each bucket and a direct get of one key; no consumer, so it
// neither lists nor watches them.
var CandidateReads = []string{
"$JS.API.DIRECT.GET.KV_" + delivery.CandidatesBucket + ".>",
"$JS.API.STREAM.INFO.KV_" + delivery.CandidatesBucket,
"$JS.API.DIRECT.GET.KV_" + delivery.VerdictsBucket + ".>",
"$JS.API.STREAM.INFO.KV_" + delivery.VerdictsBucket,
}
// EngineComposedBuckets are the composition buckets of every node, as the node-engine's state (ADR 0593 §6): the grants
// part creates each, create-only, with the history ADR 0539 clause 4 keeps (delivery.ComposedHistory), as it creates a
// module's declared state. A node whose name a bucket cannot take is left out (its engine is refused its writes too).
func EngineComposedBuckets(nodes []string) []Bucket {
var out []Bucket
for _, node := range sortedCopy(nodes) {
bucket, err := delivery.ComposedBucketFor(node)
if err != nil || !safeSubject.MatchString(node) {
continue
}
out = append(out, Bucket{Module: EngineModule, Name: delivery.ComposedState, Whole: bucket, History: delivery.ComposedHistory})
}
return out
}
// composedFamily is every node's composition bucket, for a reader of delivery.ComposedRead.
func composedFamily(nodes []string) []string {
var out []string
for _, b := range EngineComposedBuckets(nodes) {
out = append(out, b.Bucket())
}
return out
}
// What a reader of a per-node state is granted beside each bucket's read (mesh-sdk process.FamilyListSubject and
// FamilyCreatedAdvisory, ADR 0593 §6): the listing of stream names, by which it finds every node's bucket, and the
// creation advisory of any stream, by which it binds a node's bucket made after it started. Both say a name only.
const (
familyListSubject = "$JS.API.STREAM.NAMES"
familyCreatedAdvisory = "$JS.EVENT.ADVISORY.STREAM.CREATED.*"
)
// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want (ADR
// 0539 clause 16, ADR 0429 §14): its backup holder backs a data module up before it moves, and says when it was. Asked
// of its own machine only, `mesh.seat.<seat>.tool.<verb>.<node>`.
var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}}
// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node.
func NodeAgentAsks(node string) ([]string, error) {
if !safeSubject.MatchString(node) {
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
}
var out []string
for _, v := range VerbsTheNodeAgentAsks {
out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node)
}
return out, nil
}
// NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a
// backup is taken (`backed-up`), from whichever module holds node-backup on its machine (restic today).
var NodeAgentHears = []string{"mesh.mod." + BackupHolderModule + ".event.backed-up"}
// BackupHolderModule is the module that holds node-backup on every node, restic: the one whose word a node-engine hears.
// A second holder of the seat is a change of this grant.
const BackupHolderModule = "restic"
// DeclarationsStream is the stream of each node's last declaration, `mesh.node.<node>.declare` (issue 107). Release 6
// sends none (ADR 0539 clauses 5 and 11): the stream is kept, with each node's consumer, only so a node away during the
// switch finds its last declaration and with it the new node-engine; the controller deletes it once every node's have
// names the new node-engine, and from then on neither asserts it nor any node's consumer on it; nor makes it again once
// it is gone from the bus for any reason, since a stream made anew would hold no node's last declaration (issue 1945).
const DeclarationsStream = "NODES"