GR16 §554: the recovery module's account is granted the bus names of kept data
The account of the module claiming the seat mesh-recovery is granted the stream snapshot and restore subjects with their chunk subjects, a stream's deletion and a durable consumer's creation, only for the bus names of every kept data of every manifest the part holds and of every restore point or aside kept-points names; never a stream of the mesh's own (CONTROL, NODES, EVENTS, DEAD_LETTER*, PROV_APPLIED, CHECKS_*, SEAT_*, a seat's KV_). The writers table names the account as a deleter of kept data's streams (ADR 0672 §4). Builds: design 47 G §554, §565, A-230 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -158,6 +158,8 @@ type Principal struct {
|
||||
// PurgesNamed are the named asks of every queue of a build seat a composed user holds, for the grants part alone: the
|
||||
// purge of their subjects on the seat's stream and the delete of their named workers (design 47 G §544).
|
||||
PurgesNamed []NamedQueue
|
||||
// Recovers are the bus names of kept data a module holding the seat mesh-recovery is granted (RecoveryGrants), for a KindModule principal.
|
||||
Recovers []string
|
||||
// Process is the process resource's id, for a KindProcess principal alone (novox/hq ADR 0436 §2):
|
||||
// Username and inbox are <node>.<module>.<process id>.
|
||||
Process string
|
||||
@@ -966,6 +968,8 @@ func permissionsFor(p Principal) (Permissions, error) {
|
||||
// away — no other principal may subscribe this namespace, and a caller's authority is
|
||||
// still granted per tool, by name, on the publish side.
|
||||
sub = append(sub, own+".tool.>")
|
||||
// The holder of the seat mesh-recovery: the bus names of kept data it takes, restores and deletes (design 47 G §554).
|
||||
pub = append(pub, RecoveryGrants(p.Recovers)...)
|
||||
// It says what it serves (novox/hq ADR 0197): discovery for its own name and every seat it
|
||||
// holds a verb of, answered by the runtime that serves them.
|
||||
announced := []string{p.Module}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshrecovery"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// **The recovery module's account** (design 47 G §554, §565; ADR 0672 §4): the account of the module holding the seat mesh-recovery
|
||||
// (the seat, never a module's name) is granted the bus's stream snapshot and restore subjects with their chunk subjects, a stream's
|
||||
// deletion and a durable consumer's creation, only for the bus names of kept data: those of every kept data of every catalog graph
|
||||
// entry and of every entry a restore point names (an old section's name before a form change, a template's binding after), the
|
||||
// second read by the grants part through `kept-points`. The subjects its seat says, its hand-act log write, its consumes of
|
||||
// `node-backup` and its calls come from its manifest as any module's. No stream of the mesh's own is ever granted.
|
||||
|
||||
// RecoverySeat is the seat whose holder's account is granted the bus names of kept data.
|
||||
const RecoverySeat = meshrecovery.Seat
|
||||
|
||||
// meshStreams are the streams of the mesh's own, never granted to the recovery module's account (§554); every `SEAT_` stream and
|
||||
// every `KV_` bucket of a seat (KV_SEAT_…) are refused by their prefix.
|
||||
var meshStreams = []string{"CONTROL", "NODES", "EVENTS", "DEAD_LETTER_NOTICES", "DEAD_LETTERS", "PROV_APPLIED", "CHECKS_READY", "CHECKS_OTHER"}
|
||||
|
||||
// RecoveryNameRefused says a bus name is one of the mesh's own streams, or no plain subject token.
|
||||
func RecoveryNameRefused(name string) bool {
|
||||
return !safeSubject.MatchString(name) || strings.Contains(name, ".") || slices.Contains(meshStreams, name) ||
|
||||
strings.HasPrefix(name, "SEAT_") || strings.HasPrefix(name, "KV_SEAT_") || name == "KV_CONTROL" || name == "KV_NODES"
|
||||
}
|
||||
|
||||
// RecoveryGrants are what the account publishes for each bus name of kept data: the stream's snapshot and its chunks'
|
||||
// acknowledgements, its restore and the chunks sent to it, its deletion, and the creation of a durable consumer on it.
|
||||
func RecoveryGrants(names []string) []string {
|
||||
var out []string
|
||||
for _, n := range names {
|
||||
if RecoveryNameRefused(n) {
|
||||
continue
|
||||
}
|
||||
out = append(out, "$JS.API.STREAM.SNAPSHOT."+n, "$JS.SNAPSHOT.ACK."+n+".>", "$JS.API.STREAM.RESTORE."+n,
|
||||
"$JS.SNAPSHOT.RESTORE."+n+".>", "$JS.API.STREAM.DELETE."+n, "$JS.API.CONSUMER.DURABLE.CREATE."+n+".*")
|
||||
}
|
||||
return unique(out)
|
||||
}
|
||||
|
||||
// KeptBusNames are the bus names of every kept data a manifest declares: the old sections' (a state's `KV_<module>_<name>`, a log's,
|
||||
// a stream's, an object store's) and each entry of `requires` by `<module>_<local>` (BusStateStreams). Sorted, once each. Pure.
|
||||
func KeptBusNames(m catalogue.Manifest) []string {
|
||||
var out []string
|
||||
for _, s := range m.State {
|
||||
out = append(out, "KV_"+m.Module+"_"+s.Name)
|
||||
}
|
||||
for _, l := range m.Logs {
|
||||
out = append(out, "LOG_"+m.Module+"_"+l.Name)
|
||||
}
|
||||
for _, s := range m.Streams {
|
||||
out = append(out, s.BusName())
|
||||
}
|
||||
for _, o := range m.Objects {
|
||||
out = append(out, "OBJ_"+catalogue.ObjectsBucket(m.Module, o.Name))
|
||||
}
|
||||
for _, e := range m.BusState {
|
||||
out = append(out, BusStateStreams(m.Module, e)...)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return slices.Compact(out)
|
||||
}
|
||||
@@ -47,3 +47,40 @@ func TestTheNodeEngineIsGrantedWhatItNeedsOfTheRecoverySeat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §554: the account of the module holding the seat mesh-recovery is granted the snapshot, the restore with its
|
||||
// chunks, the deletion and a durable consumer's creation for the bus names of kept data, and no stream of the mesh's own; no other
|
||||
// module is granted them.
|
||||
func TestTheRecoveryAccountIsGrantedTheBusNamesOfKeptDataAlone(t *testing.T) {
|
||||
names := []string{"KV_a_x", "OBJ_b_y", "EVENTS", "SEAT_node-build-go", "KV_SEAT_x", "CONTROL", "bad.name"}
|
||||
records := Records{Nodes: []string{"one"}, RecoveryNames: names,
|
||||
Assigned: map[string][]Declared{"one": {{Module: "recovery", HoldsRecovery: true}, {Module: "plain"}}}}
|
||||
users, err := Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perms := map[string]Permissions{}
|
||||
for _, u := range users {
|
||||
if u.Kind == KindModule {
|
||||
if perms[u.Module], err = PermissionsFor(u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"KV_a_x", "OBJ_b_y"} {
|
||||
for _, want := range []string{"$JS.API.STREAM.SNAPSHOT." + n, "$JS.SNAPSHOT.ACK." + n + ".abc", "$JS.API.STREAM.RESTORE." + n,
|
||||
"$JS.SNAPSHOT.RESTORE." + n + ".abc", "$JS.API.STREAM.DELETE." + n, "$JS.API.CONSUMER.DURABLE.CREATE." + n + ".keeper"} {
|
||||
if !MayPublish(perms["recovery"], want) {
|
||||
t.Errorf("the recovery account may not publish %s", want)
|
||||
}
|
||||
if MayPublish(perms["plain"], want) {
|
||||
t.Errorf("a module that holds no recovery seat may publish %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, n := range []string{"EVENTS", "SEAT_node-build-go", "KV_SEAT_x", "CONTROL"} {
|
||||
if MayPublish(perms["recovery"], "$JS.API.STREAM.DELETE."+n) || MayPublish(perms["recovery"], "$JS.API.STREAM.SNAPSHOT."+n) {
|
||||
t.Errorf("the recovery account may touch %s, a stream of the mesh's own", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ type Declared struct {
|
||||
// SnapshotsTheBus says the module holds mesh-broker — it is the bus — and so is the one module
|
||||
// granted the snapshot API, to copy the bus's streams for the night's backup (novox/hq ADR 0235).
|
||||
SnapshotsTheBus bool
|
||||
// HoldsRecovery says the module claims the seat mesh-recovery: its account is granted the bus names of kept data (recovery.go).
|
||||
HoldsRecovery bool
|
||||
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
|
||||
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
|
||||
Checks []string
|
||||
@@ -151,6 +153,8 @@ type ProcessPrincipal struct {
|
||||
|
||||
// Records is what composing a user list needs to know about the mesh, and nothing more.
|
||||
type Records struct {
|
||||
// RecoveryNames are the bus names of kept data the holder of the seat mesh-recovery is granted (recovery.go, design 47 G §554, §565).
|
||||
RecoveryNames []string
|
||||
// Kept is every seat event kept in a stream of its own (kept.go): the grants part makes each stream, so its principal is
|
||||
// granted the create of each by name.
|
||||
Kept []KeptEvent
|
||||
@@ -328,6 +332,9 @@ func Users(r Records) ([]Principal, error) {
|
||||
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
||||
Objects: objectNames(d.Objects), ReadsObjects: d.ReadsObjects, Bound: d.Bindings, AnswersSeat: d.ServesOwn,
|
||||
})
|
||||
if d.HoldsRecovery {
|
||||
out[len(out)-1].Recovers = r.RecoveryNames
|
||||
}
|
||||
// **The grants part creates what a manifest declares** (novox/hq ADR 0547 §5; the operator, 2026-10-26
|
||||
// 08:54): every placed module's buckets, logs and streams, each by its stream's name.
|
||||
if d.Module == WatchmanModule {
|
||||
|
||||
@@ -38,7 +38,7 @@ type WriterRow struct {
|
||||
// that only adds subjects, never a delete (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54, 2026-11-05); or the bus module's tools deleting one of
|
||||
// the ended controller's leftover streams (ADR 0590 clause 6).
|
||||
func createsDeclared(p Principal, pattern string) bool {
|
||||
if removesLeftover(p, pattern) || keepsBusState(p, pattern) {
|
||||
if removesLeftover(p, pattern) || keepsBusState(p, pattern) || recoversKept(p, pattern) {
|
||||
return true
|
||||
}
|
||||
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
|
||||
@@ -54,6 +54,21 @@ func createsDeclared(p Principal, pattern string) bool {
|
||||
safeSubject.MatchString(name)
|
||||
}
|
||||
|
||||
// recoversKept is the account of the holder of the seat mesh-recovery deleting a stream of a kept data, or creating a durable
|
||||
// consumer on one, by its name alone (design 47 G §554, ADR 0672 §4: this changes ADR 0436 §3 and §5 for this account alone).
|
||||
func recoversKept(p Principal, pattern string) bool {
|
||||
if p.Kind != KindModule || len(p.Recovers) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, prefix := range []string{"$JS.API.STREAM.DELETE.", "$JS.API.CONSUMER.DURABLE.CREATE."} {
|
||||
if rest, ok := strings.CutPrefix(pattern, prefix); ok {
|
||||
name, _, _ := strings.Cut(rest, ".")
|
||||
return slices.Contains(p.Recovers, name) && !RecoveryNameRefused(name)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ownMachine is a node writing a subject whose third token is its own name, and no wildcard there.
|
||||
func ownMachine(p Principal, pattern string) bool {
|
||||
tokens := strings.Split(pattern, ".")
|
||||
@@ -154,7 +169,7 @@ var WritersTable = []WriterRow{
|
||||
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
|
||||
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
||||
Writes: createsDeclared,
|
||||
Shared: "the grants part creates a stream a manifest declares, by its name, adds to a held one the subjects its manifest names, and never removes a subject or deletes a stream (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54, 2026-11-05); the bus module's tools account deletes the ended controller's leftover streams, each by its name, and no other (novox/hq ADR 0590 clause 6); the bus module's process creates, updates and deletes every module's bus state, each stream by its name (novox/hq ADR 0595 §4)"},
|
||||
Shared: "the grants part creates a stream a manifest declares, by its name, adds to a held one the subjects its manifest names, and never removes a subject or deletes a stream (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54, 2026-11-05); the bus module's tools account deletes the ended controller's leftover streams, each by its name, and no other (novox/hq ADR 0590 clause 6); the bus module's process creates, updates and deletes every module's bus state, each stream by its name (novox/hq ADR 0595 §4); the account of the holder of the seat mesh-recovery deletes the stream of a kept data and creates a durable consumer on it, each by its name (design 47 G §554, ADR 0672 §4)"},
|
||||
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
|
||||
Others: "the builder asks",
|
||||
Subjects: []string{"mesh.seat.mesh-build-machine.event.built"},
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshrecovery"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"mesh-grants/internal/buscompose"
|
||||
)
|
||||
|
||||
// **The recovery module's restore points' names, read through `kept-points`** (design 47 G §565, A-230). The bus names of the
|
||||
@@ -298,3 +300,16 @@ func (p *Part) withKeptNames(in *Inputs) error {
|
||||
in.KeptNames = names
|
||||
return nil
|
||||
}
|
||||
|
||||
// recoveryNamesOf are the bus names of kept data of every manifest of the catalog graph the part holds, and the names a restore
|
||||
// point or an aside names (Inputs.KeptNames): a name a manifest left stays while a point names it. Sorted, once each. Pure.
|
||||
func recoveryNamesOf(in Inputs) []string {
|
||||
names := slices.Clone(in.KeptNames)
|
||||
for _, group := range []map[string]catalogue.Manifest{in.Manifests, in.Running, in.Ran, in.RanByFingerprint, in.Had, in.Held} {
|
||||
for _, m := range group {
|
||||
names = append(names, buscompose.KeptBusNames(m)...)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
return slices.Compact(names)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Design 47 G §565, A-230: the part reads points[].names and asides[].names of kept-points, keeps them in its state memberships,
|
||||
@@ -60,3 +62,17 @@ func TestKeptPointsNamesAreKeptUntilTheCallAnswers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §554, §565: the names the recovery account is granted are those of every manifest of the catalog graph (an old
|
||||
// section's and a template entry's) and those a point names, once each; a name a manifest left stays while a point names it.
|
||||
func TestTheRecoveryNamesAreTheCatalogGraphsAndThePointsNames(t *testing.T) {
|
||||
trunk := catalogue.Manifest{Module: "a", State: []catalogue.StateDeclaration{{Name: "x"}}}
|
||||
old := catalogue.Manifest{Module: "a", State: []catalogue.StateDeclaration{{Name: "old"}},
|
||||
BusState: []catalogue.BusStateEntry{{Name: "tpl", Storage: catalogue.StorageKeyValue}}}
|
||||
got := recoveryNamesOf(Inputs{Manifests: map[string]catalogue.Manifest{"a": trunk}, RanByFingerprint: map[string]catalogue.Manifest{"fp": old},
|
||||
KeptNames: []string{"KV_a_gone", "KV_a_x"}})
|
||||
want := []string{"KV_a_gone", "KV_a_old", "KV_a_tpl", "KV_a_x"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("names %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
|
||||
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)
|
||||
@@ -174,6 +177,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
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])
|
||||
|
||||
Reference in New Issue
Block a user