Compose no placed user from the node-engine's manifest, so the tool runner never becomes a second writer of the engine's state (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-sdk@42988a734b3d
mesh/delivery-group group fix/runner-carries-no-engine-state merged: every member is merged
mesh/check checking 6e7820395952 on 42988a734b3d | novox/mesh-sdk#136 in group fix/runner-carries-no-engine-state

Second meeting of the engine-state-in-runner kind (mesh-host_acts this
morning, mesh-host_health now); thorough: the runner never carries the
engine module. One predicate, Composed, keeps the engine out of the
placed loop (no user, no process, nothing carried); CarriedByRunner is
the one test of what the runner carries. The engine's grants stay the
node account's, its state created through Records.Engine.
This commit is contained in:
2026-10-30 16:30:27 +01:00
parent d0625eea3d
commit 6e78203959
2 changed files with 30 additions and 1 deletions
+12
View File
@@ -22,6 +22,10 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
Streams: []ModuleStream{{Module: "mesh-delivery-builder", Name: "BUILDASKS", Events: []string{"ask"}}}},
{Module: GrantsModule, State: []Bucket{{Module: GrantsModule, Name: "assertions"}}, Reads: []string{VaultBusAccounts}},
{Module: RuntimeModule},
// The node-engine's manifest as a placement names it (issue 774, met twice on 2026-10-30): no account of its
// own, so the runner would carry it, and its state is the node's to write alone.
{Module: EngineModule, NoAccount: true, Consumes: []string{"restic.backed-up"},
State: []Bucket{{Module: EngineModule, Name: "health"}, {Module: EngineModule, Name: EngineActsState}}},
}},
// The node-engine's own manifest, placed on every node though no node is assigned it (novox/hq ADR 0563 §2, §3).
Engine: &Declared{Module: EngineModule, State: []Bucket{{Module: EngineModule, Name: EngineActsState}}},
@@ -32,6 +36,9 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
}
perms := map[string]Permissions{}
for _, u := range users {
if u.Module == EngineModule {
t.Errorf("the node-engine's manifest composed a user of its own: %s", u.Username())
}
if u.Kind == KindNode || u.Kind == KindNodeTools {
p, err := PermissionsFor(u)
if err != nil {
@@ -75,6 +82,11 @@ func TestTheGrantsPartCreatesOnlyDeclaredStreams(t *testing.T) {
{"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.ace.ask.hand-over-1", true},
{"node.ace", "$JS.API.DIRECT.GET.KV_mesh-host_acts.$KV.mesh-host_acts.novox.ask.hand-over-1", false},
{"novox.node-tools", "$KV.mesh-host_acts.novox.act.hand-over-1", false},
// The runner never carries the engine (issue 774): its health report is the node's own key alone.
{"novox.node-tools", "$KV.mesh-host_health.novox", false},
{"novox.node-tools", "$KV.mesh-host_acts.novox.ask.x", false},
{"node.novox", "$KV.mesh-host_health.novox", true},
{"node.novox", "$KV.mesh-host_health.ace", false},
// The sealed user list (ADR 0547 clause 7): the grants part writes its own bucket and no other.
{GrantsModule, "$JS.API.STREAM.CREATE.OBJ_mesh-grants-user-list", true},
{GrantsModule, "$O.mesh-grants-user-list.C.abc", true},
+18 -1
View File
@@ -200,6 +200,11 @@ func Users(r Records) ([]Principal, error) {
}
}
for _, d := range r.Assigned[node] {
// **The node-engine's manifest is no placed module's** (novox/hq ADR 0563 §2, §3; issue 774): where a placement
// names it, it adds no user, no process and nothing the runner carries; its grants are the node's account's.
if !Composed(d) {
continue
}
// A state with named readers is read by them alone (ADR 0547 clause 9): a read named for another module is
// granted nothing, here and in the runtime carrying the module.
d = withReadLogs(withNamedReaders(d), logs)
@@ -253,7 +258,7 @@ func Users(r Records) ([]Principal, error) {
if runtimeHere {
var carried []Declared
for _, d := range r.Assigned[node] {
if d.RunsAs == "" && runnerServes(d) {
if CarriedByRunner(d) {
d = withReadLogs(withNamedReaders(d), logs)
// Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
d.Hears = ComposedHears(d)
@@ -450,6 +455,18 @@ func boundBucketsOf(r Records) []string {
return out
}
// Composed says a placed module is composed as one: its own user, its processes and what the runner carries of it.
// The node-engine's manifest (EngineModule) is not, though a placement may name it: each node's engine speaks as the
// node's account, which alone writes the engine's state under its own key (engineGrants, novox/hq ADR 0563 §2, §3), and
// Records.Engine creates that state. Granted to another account it is a second writer, and the writers table refuses
// the whole list (issue 774: met for mesh-host_acts and then mesh-host_health, 2026-10-30).
func Composed(d Declared) bool { return d.Module != EngineModule }
// CarriedByRunner says the machine's tool runner carries this placed module: a module composed at all (Composed) that
// runs as no account of its own and has something for the runner to serve (runnerServes). The one test of what the
// runner's grants are composed from.
func CarriedByRunner(d Declared) bool { return Composed(d) && d.RunsAs == "" && runnerServes(d) }
// runnerServes says the machine's tool runner has something of this module to serve (novox/hq ADR 0436 §1,
// ADR 0437 §4, issue 1329): tools or loads it answers, a seat it holds, event families a session waits on,
// a bundle the runtime loads (Loads), or a check its health asks. A module with none of these is a process of its own on its own credential, and