AM-D5: a per-machine state reaches its machine's keys below it

A per-machine state's account reaches its machine's key and the keys
below it (mesh-sdk PerMachineKeys), no other machine's, so the tool
runner's approvals keep <node>.ask.<id> and <node>.act.<digest>; the
membership names the machine's key in the same form.

Builds: design 46 7-approval-mark §13

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:48:17 +01:00
parent 558a93c25b
commit 58e4a737d5
3 changed files with 51 additions and 5 deletions
@@ -62,6 +62,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Run("the bus module's process alone keeps every module's bus state and serves the broker's verbs", theBusModulesProcessKeepsBusState)
t.Run("a process serving a verb is granted its calls and chunk subjects, and the reverse goes to an account naming it", chunkSubjectsFollowTheAnswerer)
t.Run("the tool runner's runtime hears every event subject and nothing wider", theToolRunnerHearsEveryEvent)
t.Run("a per-machine state reaches its machine's keys below it and no other machine's", aPerMachineStateReachesItsMachinesKeys)
// ADR 0567 §3, §9: a node reads every key of the seat's assignments and its candidates (it composes with the whole
// proposal laid over), and none of its open proposals; it writes neither.
reads := strings.Join(NodeComposeReads("laptop"), " ")
@@ -1516,3 +1517,44 @@ func theToolRunnerHearsEveryEvent(t *testing.T) {
t.Errorf("%d runtimes carry the tool runner, want 1", runners)
}
}
// AM-D5 (design 46 approval mark §13): a per-machine state reaches its machine's key and the keys below it, `<node>.>`,
// and no other machine's, on the account and in the membership alike.
func aPerMachineStateReachesItsMachinesKeys(t *testing.T) {
runner := func() Declared {
return Declared{Module: "node-tools", ToolRunner: true,
State: []Bucket{{Module: "node-tools", Name: "approvals", PerMachine: true, KeyTTL: true, TTLSeconds: 172800}}}
}
r := Records{Nodes: []string{"a", "b"}, Assigned: map[string][]Declared{"a": {runner()}, "b": {runner()}}}
users, err := Users(r)
if err != nil {
t.Fatal(err)
}
const bucket = "node-tools_approvals"
found := false
for _, u := range users {
if u.Username() != "a.node-tools" {
continue
}
found = true
p, err := PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
for key, reach := range map[string]bool{"a": true, "a.ask.1": true, "a.act.d": true, "b": false, "b.ask.1": false, "ab.ask.1": false} {
if MayPublish(p, "$KV."+bucket+"."+key) != reach {
t.Errorf("a's runner writes %s's key %s: %v", bucket, key, !reach)
}
if MayPublish(p, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+key) != reach {
t.Errorf("a's runner reads %s's key %s: %v", bucket, key, !reach)
}
}
}
if !found {
t.Fatal("no account a.node-tools composed")
}
// The membership names the machine's key, which reaches the keys below it in the same form (mesh-sdk membership).
if issued := stateIssuedFor(runner(), "a"); len(issued) != 1 || issued[0].Key != "a" || issued[0].Bucket != bucket {
t.Errorf("a's membership issues %+v", issued)
}
}
@@ -2,6 +2,7 @@ package buscompose
import (
"fmt"
sdkbc "git.novox.be/novox/mesh-sdk/go/buscompose"
"slices"
"strings"
"time"
@@ -36,8 +37,8 @@ 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 says each machine's instance writes only the key named for its machine and the keys below it (novox/hq
// ADR 0260; design 46 approval mark §13), and is granted those and no other machine's.
PerMachine bool
// KeyTTL says the bucket allows a time to live per key (novox/hq ADR 0433 §1): its owner writes each key with its
// own, and the server writes a delete marker, kept KeyTTLMarkerTTL, when it ends.
@@ -166,8 +167,11 @@ func stateGrants(a stateAccess) []string {
if machineKey == "" {
continue // a machine with no usable name reaches none of it
}
read(bucket, machineKey)
out = append(out, "$KV."+bucket+"."+machineKey)
// Its machine's key and the keys below it, no other machine's (design 46 approval mark §13).
for _, k := range sdkbc.PerMachineKeys(machineKey) {
read(bucket, k)
out = append(out, "$KV."+bucket+"."+k)
}
continue
}
read(bucket, "")
File diff suppressed because one or more lines are too long