Carry a module on the tool runner only when the runner has something of it to serve, so a part on its own credential has one writer (issue 1329)
This commit is contained in:
@@ -67,8 +67,8 @@ func TestTheGatesBucketsAreWritableByItsMachinesToolRunner(t *testing.T) {
|
|||||||
if !canRead(runner, read, "k") {
|
if !canRead(runner, read, "k") {
|
||||||
t.Errorf("novox's tool runner may not read %s, which the gate declares reading", read)
|
t.Errorf("novox's tool runner may not read %s, which the gate declares reading", read)
|
||||||
}
|
}
|
||||||
// (The runner may write the teller's answers: the teller declares no runs-as, so the runner carries it too. See the report of issue 1326.)
|
// 1329/AC3: the runner no longer carries the teller, so it may not write the teller's answers either.
|
||||||
if canWrite(runner, read, "k") && read != "mesh-delivery-teller_answers" {
|
if canWrite(runner, read, "k") {
|
||||||
t.Errorf("novox's tool runner may write %s, which the gate only reads", read)
|
t.Errorf("novox's tool runner may write %s, which the gate only reads", read)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import (
|
|||||||
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
|
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
|
||||||
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
|
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
|
||||||
//
|
//
|
||||||
// The three manifests are copied from mesh-delivery's main at 6a23627426c6 (modules/<module>/module.json, read
|
// The four manifests (the watchman's is the fourth, issue 1329) are copied from mesh-delivery's main at 9c15112d935a
|
||||||
// 2026-10-19), whole, so a change to one of them there that changes its grant is a change to read here.
|
// (modules/<module>/module.json, read 2026-10-20; the first three are unchanged since 6a23627426c6), whole, so a change
|
||||||
|
// to one of them there that changes its grant is a change to read here.
|
||||||
const bridgeManifest = `{
|
const bridgeManifest = `{
|
||||||
"module": "mesh-delivery-forge-bridge",
|
"module": "mesh-delivery-forge-bridge",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
@@ -160,6 +161,69 @@ const indexerManifest = `{
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const watchmanManifest = `{
|
||||||
|
"module": "mesh-delivery-watchman",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "watchman",
|
||||||
|
"reads": [
|
||||||
|
"mesh-controller.checks-running"
|
||||||
|
],
|
||||||
|
"state": [
|
||||||
|
{ "name": "presence", "history": 20 },
|
||||||
|
{ "name": "pulse", "key-ttl": true },
|
||||||
|
{ "name": "stuck", "history": 20 }
|
||||||
|
],
|
||||||
|
"own-secrets": {
|
||||||
|
"broker": "${dir:state}/broker"
|
||||||
|
},
|
||||||
|
"secrets-owner": "mesh-delivery-watchman",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"type": "user",
|
||||||
|
"name": "mesh-delivery-watchman",
|
||||||
|
"shell": "/usr/bin/nologin",
|
||||||
|
"home": "/var/lib/mesh-delivery-watchman"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"owner": "mesh-delivery-watchman",
|
||||||
|
"place": ".",
|
||||||
|
"unknown": "program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "watchman",
|
||||||
|
"type": "process",
|
||||||
|
"name": "mesh-delivery-watchman",
|
||||||
|
"artifact": "program",
|
||||||
|
"run": [
|
||||||
|
"./mesh-delivery-watchman"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MESH_BROKER_FILE": "${dir:state}/broker"
|
||||||
|
},
|
||||||
|
"user": "mesh-delivery-watchman",
|
||||||
|
"health": {
|
||||||
|
"kind": "unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "program",
|
||||||
|
"kind": "bundle",
|
||||||
|
"language": "go",
|
||||||
|
"system": "arch",
|
||||||
|
"from": "cmd/mesh-delivery-watchman",
|
||||||
|
"binary": "mesh-delivery-watchman"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
const tellerManifest = `{
|
const tellerManifest = `{
|
||||||
"module": "mesh-delivery-teller",
|
"module": "mesh-delivery-teller",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
@@ -488,3 +552,87 @@ func TestNoPartsCredentialReachesBeyondItsDeclaration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolServer is a module that serves a tool on its own broker credential: the runner carries it (ADR 0175).
|
||||||
|
func toolServer() Declared {
|
||||||
|
return Declared{Module: "some-tools", Serves: []string{"some_tool"},
|
||||||
|
State: []Bucket{{Module: "some-tools", Name: "kept", History: 1}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1329/AC1: composed from the manifests of mesh-delivery's main, the machine's tool runner may write no bucket of the
|
||||||
|
// forge bridge, the indexer, the teller or the watchman, publish none of their stream events nor the teller's
|
||||||
|
// operator-channel accept subjects, and pull none of their consumers; each part's own credential still may.
|
||||||
|
func TestTheToolRunnerCarriesNoneOfTheDeliverysParts(t *testing.T) {
|
||||||
|
streams := map[string]ModuleStream{"mesh-delivery-forge-bridge": {Module: "mesh-delivery-forge-bridge", Name: "FORGE"}}
|
||||||
|
all := composed(t, Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
|
||||||
|
{Module: RuntimeModule}, declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams),
|
||||||
|
declaredOf(t, tellerManifest, streams), declaredOf(t, watchmanManifest, streams),
|
||||||
|
}}})
|
||||||
|
runner := all["runner@novox"]
|
||||||
|
buckets := map[string][]string{
|
||||||
|
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
|
||||||
|
"mesh-delivery-teller": {"answers"}, "mesh-delivery-watchman": {"presence", "pulse", "stuck"},
|
||||||
|
}
|
||||||
|
for part, names := range buckets {
|
||||||
|
own, there := all[part+"@novox"]
|
||||||
|
if !there {
|
||||||
|
t.Errorf("%s has no credential of its own", part)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
if canWrite(runner, part+"_"+name, "some.key") {
|
||||||
|
t.Errorf("novox's tool runner may write %s_%s", part, name)
|
||||||
|
}
|
||||||
|
if !canWrite(own, part+"_"+name, "some.key") {
|
||||||
|
t.Errorf("%s may not write its own bucket %s", part, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, subject := range []string{
|
||||||
|
"mesh.stream.mesh-delivery-forge-bridge.event.forge.merged",
|
||||||
|
"mesh.seat.operator-channel.accept.ask.mesh-delivery-teller",
|
||||||
|
"mesh.seat.operator-channel.accept.cancel.mesh-delivery-teller",
|
||||||
|
"mesh.seat.operator-channel.accept.condition.mesh-delivery-teller",
|
||||||
|
"$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-indexer",
|
||||||
|
} {
|
||||||
|
if allowed(runner.Publish, subject) {
|
||||||
|
t.Errorf("novox's tool runner may publish %s", subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowed(all["mesh-delivery-forge-bridge@novox"].Publish, "mesh.stream.mesh-delivery-forge-bridge.event.forge.merged") ||
|
||||||
|
!allowed(all["mesh-delivery-teller@novox"].Publish, "mesh.seat.operator-channel.accept.ask.mesh-delivery-teller") ||
|
||||||
|
!allowed(all["mesh-delivery-indexer@novox"].Publish, "$JS.API.CONSUMER.MSG.NEXT.FORGE.novox_mesh-delivery-indexer") {
|
||||||
|
t.Error("a part's own credential lost what its manifest grants")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1329/AC2: a module with tools and a broker secret of its own stays carried by the tool runner, beside its own
|
||||||
|
// credential (ADR 0175); a module with neither tools, a seat, waits nor a check is not.
|
||||||
|
func TestTheToolRunnerStillCarriesAModuleWithToolsAndItsOwnCredential(t *testing.T) {
|
||||||
|
process := Declared{Module: "only-process", State: []Bucket{{Module: "only-process", Name: "mine", History: 1}}}
|
||||||
|
all := composed(t, Records{Nodes: []string{"novox"}, Assigned: map[string][]Declared{"novox": {
|
||||||
|
{Module: RuntimeModule}, toolServer(), process,
|
||||||
|
}}})
|
||||||
|
runner := all["runner@novox"]
|
||||||
|
if !canWrite(runner, "some-tools_kept", "k") {
|
||||||
|
t.Error("novox's tool runner does not carry a module with tools")
|
||||||
|
}
|
||||||
|
if !canWrite(all["some-tools@novox"], "some-tools_kept", "k") {
|
||||||
|
t.Error("the module with tools lost its own credential")
|
||||||
|
}
|
||||||
|
if canWrite(runner, "only-process_mine", "k") {
|
||||||
|
t.Error("novox's tool runner carries a module that has nothing to serve")
|
||||||
|
}
|
||||||
|
if !canWrite(all["only-process@novox"], "only-process_mine", "k") {
|
||||||
|
t.Error("the process module lost its own credential")
|
||||||
|
}
|
||||||
|
for name, d := range map[string]Declared{
|
||||||
|
"a held seat": {Module: "m", Holds: []Seat{{Name: "some-seat"}}},
|
||||||
|
"a wait": {Module: "m", Waits: []Wait{{}}},
|
||||||
|
"a check": {Module: "m", Checks: []string{"m.check"}},
|
||||||
|
} {
|
||||||
|
if !runnerServes(d) {
|
||||||
|
t.Errorf("the tool runner does not serve a module with %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func TestTheToolRunnerNeverHearsARestrictedStream(t *testing.T) {
|
|||||||
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
records := Records{Nodes: []string{"ace", "novox"}, Assigned: map[string][]Declared{
|
||||||
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
|
"novox": {{Module: "mesh-mail", Streams: []ModuleStream{mailStream}}, {Module: "news", Streams: []ModuleStream{newsStream}}},
|
||||||
"ace": {{Module: RuntimeModule},
|
"ace": {{Module: RuntimeModule},
|
||||||
{Module: "carried", Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
|
{Module: "carried", NoAccount: true, Hears: []Heard{mailHeard, newsHeard}, HearsAStream: true},
|
||||||
{Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
|
{Module: "mailer", RunsAs: "mailer", Hears: []Heard{mailHeard}, HearsAStream: true}},
|
||||||
}}
|
}}
|
||||||
users, err := Users(records)
|
users, err := Users(records)
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ func Users(r Records) ([]Principal, error) {
|
|||||||
if runtimeHere {
|
if runtimeHere {
|
||||||
var carried []Declared
|
var carried []Declared
|
||||||
for _, d := range r.Assigned[node] {
|
for _, d := range r.Assigned[node] {
|
||||||
if d.RunsAs == "" {
|
if d.RunsAs == "" && runnerServes(d) {
|
||||||
// Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
|
// Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
|
||||||
d.Hears = ComposedHears(d)
|
d.Hears = ComposedHears(d)
|
||||||
carried = append(carried, d)
|
carried = append(carried, d)
|
||||||
@@ -391,3 +391,14 @@ func boundBucketsOf(r Records) []string {
|
|||||||
sort.Strings(out)
|
sort.Strings(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// or a check its health asks. A module with none of these is a process of its own on its own credential, and
|
||||||
|
// carrying it would hand every session on the machine the runner's account with that module's state, seat
|
||||||
|
// traffic and streams. A module with no account of its own (NoAccount) is spoken for by the runner, so it
|
||||||
|
// stays carried, and so does the runtime module itself. Whether the module has its own broker secret is not
|
||||||
|
// the test: a module with tools and its own credential is still carried (ADR 0175).
|
||||||
|
func runnerServes(d Declared) bool {
|
||||||
|
return d.Module == RuntimeModule || d.NoAccount || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user