Grants: the node-engine's records keep its consumes, so each node account hears the backup holder's word and settled

Dropped since the hear was removed (ADR 0625 §4); the golden user list and the grants case now hold the node user's subscriptions. The indexer's open-tries comment says it refuses an answer without the key.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 23:50:05 +01:00
parent 723213c3f7
commit 49f5596781
5 changed files with 44 additions and 2 deletions
@@ -302,7 +302,7 @@ func openTriesVia(call toolCaller) func(ctx context.Context) ([]checkermsg.Open,
return nil, err
}
// No try open is an answer: the checker's list is empty, or null in an older checker (issue 1921). An answer without the
// key is none.
// key is refused below, never read as none.
var o map[string]json.RawMessage
if err := json.Unmarshal(raw, &o); err != nil {
return nil, fmt.Errorf("the answer of %s is not {tries}: %v", openTriesTool, err)
+11
View File
@@ -1388,6 +1388,17 @@ func TestGrants(t *testing.T) {
if slices.ContainsFunc(runtime.Publish, func(s string) bool { return strings.HasPrefix(s, "$KV.mesh-host_") }) {
t.Errorf("novox.node-tools is granted a write of the node-engine's state: %v", runtime.Publish)
}
// The node's account hears what the node-engine's manifest consumes (ADR 0625 §4: the manifest alone gives it, ADR
// 0539 clause 16): dropping `consumes` from the engine's records left every node deaf to the backup holder's word.
var node grants.User
for _, u := range shadowOf(t, n) {
if u.User == "node.novox" {
node = u
}
}
if !slices.Contains(node.Subscribe, "mesh.mod.restic.event.backed-up") {
t.Errorf("node.novox does not hear restic's backed-up, which the node-engine's manifest consumes: %v", node.Subscribe)
}
for _, s := range live.Users[0].Subscribe {
if strings.Contains(s, ".call.") && !slices.Contains(runtime.Subscribe, s) {
t.Errorf("under the seeded assignments novox.node-tools is not granted %s, which the bus grants it today", s)
@@ -73,6 +73,14 @@ func UserListFixtures() []UserListFixture {
},
RootFree: map[string]bool{"ace": true},
}},
// The node-engine's manifest as it ships places on every node: its account hears what the manifest consumes (the
// backup holder's word, the status view's settled, the seat-definition events), and nothing else names them.
{Name: "a node-engine that consumes", Records: Records{
Nodes: []string{"ace"},
Assigned: map[string][]Declared{"ace": {{Module: "web"}}},
Engine: &Declared{Module: EngineModule, Consumes: []string{"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed", "mesh-delivery-view.settled", "restic.backed-up"}},
}},
// ADR 0618 §4: an event of the node-engine's seat names the engine's machine, so its watcher hears it from every
// machine; a node seat held by a module says its other events flat, and is heard flat.
{Name: "a node seat's events, by machine and flat", Records: Records{
File diff suppressed because one or more lines are too long
@@ -111,7 +111,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
// The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this
// part; it adds no principal, since each node's engine speaks as the node's account.
if m, ok := declared[buscompose.EngineModule]; ok {
out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
out.Engine = &buscompose.Declared{Module: m.Module, Consumes: m.Consumes, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)}
}
return out, gaps
}