Grant a bar one key of the state it shows, and per-machine state its own machine's key (hq ADR 0260)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

A derived read reached the whole bucket, and the power module could write every
machine's draw. A read granted for a block now reaches that block's key alone, by the
direct get of its subject and a consumer filtered to it, and state declared per-machine
is written and read at the machine's own key only.
This commit is contained in:
jochen
2026-10-08 15:51:20 +02:00
parent 41d6019fa0
commit b74268fb08
12 changed files with 242 additions and 51 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
}
}
m.State = stateIssuedFor(d)
m.State = stateIssuedFor(d, node)
if len(d.Invokes) > 0 {
m.Reaches = map[string][]string{}
for _, t := range d.Invokes {
+8 -2
View File
@@ -111,6 +111,10 @@ type Principal struct {
// instances and read by whoever declares it.
State []string
Reads []string
// PerMachine is the local names of its state keyed by machine, reaching the key named for Node and
// no other; KeyedReads the keys of others' state it reads one key at a time (novox/hq ADR 0260).
PerMachine []string
KeyedReads []KeyedRead
// SnapshotsTheBus is the bus's own module, the one holding mesh-broker (novox/hq ADR 0235). Its
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
@@ -591,7 +595,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
// 5. Its state, and the state of others it reads (novox/hq ADR 0201): every one read and
// watched, its own written too.
pub = append(pub, stateGrants(p.Module, p.State, p.Reads)...)
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State,
PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...)
case KindNodeTools:
// **One process serves what every module on the machine would have served for itself**
@@ -667,7 +672,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
// does not write another's bucket through it is the runtime's to keep, from the membership
// each assignment is issued, as it keeps each module's events under that module's own name.
for _, d := range p.Carries {
pub = append(pub, stateGrants(d.Module, stateNames(d.State), d.Reads)...)
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...)
}
sub = unique(sub)
pub = unique(pub)
+89 -14
View File
@@ -32,6 +32,9 @@ type Bucket struct {
History int
// TTLSeconds is how long a value lives; zero is until replaced or deleted.
TTLSeconds int
// PerMachine says each machine's instance writes only the key named for its machine (novox/hq ADR
// 0260), and is granted that key and no other.
PerMachine bool
}
// BucketName is the bucket a module's state lives in: the module and the local name joined by an
@@ -72,52 +75,124 @@ func bucketOfRead(read string) (string, bool) {
// read; without CONSUMER.CREATE no key is listed and nothing is watched; without CONSUMER.DELETE a
// watch cannot be stopped and lingers on the server. A write outside these is refused by the server
// — and reaches the writer as a timeout, not a refusal, which is why the runtime refuses first.
func stateGrants(module string, keeps []string, reads []string) []string {
//
// **One key, where the state is per machine** (novox/hq ADR 0260). The owner's instance on a machine
// writes and reads the key named for that machine, and a holder granted a read for its machine reads
// that key: a direct get of the key's own subject, and a consumer whose filter is that subject — the
// server's create-with-filter form, which a client uses for a watch of one key and which the server
// holds to the filter it names. What cannot be narrowed by key: binding (STREAM.INFO), deleting a
// consumer (named at random by the client) and flow control; none of them reads or writes a value.
func stateGrants(a stateAccess) []string {
var out []string
read := func(bucket string) {
read := func(bucket, key string) {
stream := "KV_" + bucket
out = append(out, "$JS.API.STREAM.INFO."+stream)
if key == "" {
out = append(out,
"$JS.API.DIRECT.GET."+stream+".>",
"$JS.API.CONSUMER.CREATE."+stream+".>")
} else {
out = append(out,
"$JS.API.DIRECT.GET."+stream+".$KV."+bucket+"."+key,
"$JS.API.CONSUMER.CREATE."+stream+".*.$KV."+bucket+"."+key)
}
out = append(out,
"$JS.API.STREAM.INFO."+stream,
"$JS.API.DIRECT.GET."+stream+".>",
"$JS.API.CONSUMER.CREATE."+stream+".>",
"$JS.API.CONSUMER.DELETE."+stream+".>",
"$JS.FC."+stream+".>")
}
for _, name := range keeps {
machineKey := ""
if safeSubject.MatchString(a.Node) {
machineKey = a.Node
}
perMachine := map[string]bool{}
for _, n := range a.PerMachine {
perMachine[n] = true
}
for _, name := range a.Keeps {
if !safeSubject.MatchString(name) {
continue
}
bucket := BucketName(module, name)
read(bucket)
bucket := BucketName(a.Module, name)
if perMachine[name] {
if machineKey == "" {
continue // a machine with no usable name reaches none of it
}
read(bucket, machineKey)
out = append(out, "$KV."+bucket+"."+machineKey)
continue
}
read(bucket, "")
out = append(out, "$KV."+bucket+".>")
}
for _, r := range reads {
for _, r := range a.Reads {
if bucket, ok := bucketOfRead(r); ok {
read(bucket)
read(bucket, "")
}
}
for _, k := range a.KeyedReads {
if bucket, ok := bucketOfRead(k.Read); ok && safeSubject.MatchString(k.Key) {
read(bucket, k.Key)
}
}
return out
}
// KeyedRead is one key of another module's state a principal reads, and no other key of it.
type KeyedRead struct {
Read string `json:"read"`
Key string `json:"key"`
}
// stateAccess is what one principal may do with state: the module's own buckets (some per machine),
// what it reads whole, and what it reads for its machine's key alone.
type stateAccess struct {
Module, Node string
Keeps, PerMachine []string
Reads []string
KeyedReads []KeyedRead
}
// perMachineNames is the local names of a module's buckets keyed by machine.
func perMachineNames(buckets []Bucket) []string {
var out []string
for _, b := range buckets {
if b.PerMachine {
out = append(out, b.Name)
}
}
return out
}
// StateIssued is one bucket an assignment may reach, by the name its module uses for it: its own
// state by the local name, another's as `<module>.<name>` (novox/hq ADR 0201).
// state by the local name, another's as `<module>.<name>` (novox/hq ADR 0201). Key, when set, is the
// one key it may reach (novox/hq ADR 0260): this machine's.
type StateIssued struct {
Name string `json:"name"`
Bucket string `json:"bucket"`
Writes bool `json:"writes,omitempty"`
Key string `json:"key,omitempty"`
}
// stateIssuedFor is every bucket a module's code may reach, as its membership lists them.
func stateIssuedFor(d Declared) []StateIssued {
// stateIssuedFor is every bucket a module's code may reach on a machine, as its membership lists them.
func stateIssuedFor(d Declared, node string) []StateIssued {
var out []StateIssued
for _, b := range d.State {
out = append(out, StateIssued{Name: b.Name, Bucket: BucketName(d.Module, b.Name), Writes: true})
issued := StateIssued{Name: b.Name, Bucket: BucketName(d.Module, b.Name), Writes: true}
if b.PerMachine {
issued.Key = node
}
out = append(out, issued)
}
for _, r := range d.Reads {
if bucket, ok := bucketOfRead(r); ok {
out = append(out, StateIssued{Name: r, Bucket: bucket})
}
}
for _, k := range d.KeyedReads {
if bucket, ok := bucketOfRead(k.Read); ok {
out = append(out, StateIssued{Name: k.Read, Bucket: bucket, Key: k.Key})
}
}
return out
}
+57 -1
View File
@@ -78,7 +78,8 @@ func TestAModuleWithNoStateReachesNoBucket(t *testing.T) {
// A read that names no bucket grants nothing rather than something that happens to parse.
func TestAReadThatNamesNoBucketGrantsNothing(t *testing.T) {
if got := stateGrants("a", nil, []string{"nodot", "x.", ".y", "a.b>"}); len(got) != 0 {
if got := stateGrants(stateAccess{Module: "a", Node: "one", Reads: []string{"nodot", "x.", ".y", "a.b>"},
KeyedReads: []KeyedRead{{Read: "nodot", Key: "one"}, {Read: "a.b", Key: "x>"}}}); len(got) != 0 {
t.Fatalf("granted %v for reads that name no bucket", got)
}
}
@@ -178,3 +179,58 @@ func TestABucketIsAssertedOverAHeldConnection(t *testing.T) {
t.Fatalf("the bucket is not there: %v", err)
}
}
// **State keyed by machine reaches its machine's key alone** (novox/hq ADR 0260): the owner's instance on
// a machine writes and reads that key, and a bar granted a key reads that key — a direct get of its
// subject and a consumer filtered to it — and no other key of the bucket.
func TestPerMachineStateAndAKeyedReadReachOneKey(t *testing.T) {
owner, err := PermissionsFor(Principal{Kind: KindModule, Node: "laptop", Module: "power", PasswordHash: "x",
State: []string{"draw"}, PerMachine: []string{"draw"}})
if err != nil {
t.Fatal(err)
}
has(t, owner.Publish, "$KV.power_draw.laptop")
has(t, owner.Publish, "$JS.API.DIRECT.GET.KV_power_draw.$KV.power_draw.laptop")
has(t, owner.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.*.$KV.power_draw.laptop")
hasNot(t, owner.Publish, "$KV.power_draw.>")
hasNot(t, owner.Publish, "$JS.API.DIRECT.GET.KV_power_draw.>")
hasNot(t, owner.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.>")
bar, err := PermissionsFor(Principal{Kind: KindModule, Node: "laptop", Module: "bar", PasswordHash: "x",
KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}})
if err != nil {
t.Fatal(err)
}
has(t, bar.Publish, "$JS.API.STREAM.INFO.KV_power_draw")
has(t, bar.Publish, "$JS.API.DIRECT.GET.KV_power_draw.$KV.power_draw.laptop")
has(t, bar.Publish, "$JS.API.CONSUMER.CREATE.KV_power_draw.*.$KV.power_draw.laptop")
for _, s := range bar.Publish {
if strings.HasPrefix(s, "$KV.") || strings.HasSuffix(s, "KV_power_draw.>") && !strings.Contains(s, "DELETE") && !strings.HasPrefix(s, "$JS.FC.") {
t.Fatalf("a keyed read was granted %q", s)
}
}
// And the machine's runtime, which carries both, is granted the union: still the one key.
runtime, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule, PasswordHash: "x",
Carries: []Declared{
{Module: "power", State: []Bucket{{Module: "power", Name: "draw", PerMachine: true}}},
{Module: "bar", KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}},
}})
if err != nil {
t.Fatal(err)
}
has(t, runtime.Publish, "$KV.power_draw.laptop")
hasNot(t, runtime.Publish, "$KV.power_draw.>")
hasNot(t, runtime.Publish, "$JS.API.DIRECT.GET.KV_power_draw.>")
}
func TestAMembershipNamesTheOneKeyOfKeyedState(t *testing.T) {
m := MembershipFor("laptop", Declared{Module: "bar", KeyedReads: []KeyedRead{{Read: "power.draw", Key: "laptop"}}}, Placements{})
if len(m.State) != 1 || m.State[0] != (StateIssued{Name: "power.draw", Bucket: "power_draw", Key: "laptop"}) {
t.Fatalf("%+v", m.State)
}
o := MembershipFor("laptop", Declared{Module: "power", State: []Bucket{{Module: "power", Name: "draw", PerMachine: true}}}, Placements{})
if len(o.State) != 1 || o.State[0] != (StateIssued{Name: "draw", Bucket: "power_draw", Writes: true, Key: "laptop"}) {
t.Fatalf("%+v", o.State)
}
}
+4
View File
@@ -37,6 +37,9 @@ type Declared struct {
State []Bucket
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0201).
Reads []string
// KeyedReads are keys of other modules' state it reads, each one key alone: what a seat's holder is
// granted for the pieces the modules beside it offer (novox/hq ADR 0260).
KeyedReads []KeyedRead
// NoAccount says the module declares no own secret named broker, so no account could ever be
// delivered to it and nothing can connect as it (novox/hq issue 195). Said in the negative so a
// record that does not say is composed as it always was.
@@ -113,6 +116,7 @@ func Users(r Records) ([]Principal, error) {
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
State: stateNames(d.State), Reads: d.Reads, SnapshotsTheBus: d.SnapshotsTheBus,
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
})
}
if runtimeHere {