Merge pull request 'Carry a module on the tool runner only when the runner serves something of it, so a part on its own credential has one writer (issues 1329, 1338)' (#404) from fix/1329-runner-carries-only-what-it-serves into main
This commit was merged in pull request #404.
This commit is contained in:
@@ -174,7 +174,9 @@ func TestAPlanGoesOnWithoutAMachineAwayWhenItsTierIsSent(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
first, stopFirst := context.WithCancel(ctx)
|
first, stopFirst := context.WithCancel(ctx)
|
||||||
defer stopFirst()
|
defer stopFirst()
|
||||||
startRolloutEvents(first, open, js)
|
waitFirst := startRolloutEvents(first, open, js)
|
||||||
|
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
|
||||||
|
t.Cleanup(func() { stopFirst(); waitFirst() })
|
||||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ func askToLook() {
|
|||||||
|
|
||||||
// startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a
|
// startPlanLooks starts the look at the open plans, and asks for the first: the controller took the lease, and a
|
||||||
// plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker.
|
// plan left by a replaced controller resumes from there (ADR 0406 §6). It replaces the rollout's 30 second ticker.
|
||||||
func startPlanLooks(ctx context.Context, open *stores) {
|
// It answers a channel that is closed once the look has returned after the context ended, for a test that restores what
|
||||||
|
// the look reads (issue 1338).
|
||||||
|
func startPlanLooks(ctx context.Context, open *stores) <-chan struct{} {
|
||||||
l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) })
|
l := newLooker(func(ctx context.Context) bool { return lookAtPlans(ctx, open) })
|
||||||
l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) }
|
l.first = func(ctx context.Context) { rearmWalkEnds(ctx, open) }
|
||||||
lookerMu.Lock()
|
lookerMu.Lock()
|
||||||
@@ -152,8 +154,13 @@ func startPlanLooks(ctx context.Context, open *stores) {
|
|||||||
}
|
}
|
||||||
lookerMu.Unlock()
|
lookerMu.Unlock()
|
||||||
}()
|
}()
|
||||||
go l.run(ctx)
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
l.run(ctx)
|
||||||
|
}()
|
||||||
l.ask()
|
l.ask()
|
||||||
|
return done
|
||||||
}
|
}
|
||||||
|
|
||||||
// applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's
|
// applySilentAfter is how long a machine may say nothing on a send before the plan names it silent: the mesh's
|
||||||
@@ -163,8 +170,10 @@ var applySilentAfter = inventory.ApplySilentAfter
|
|||||||
// startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974):
|
// startRolloutEvents starts what a rollout advances on, for as long as the context lives (novox/hq ADR 0406 §5, issue 974):
|
||||||
// the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own
|
// the wait for each machine's report as a key on the bus that expires (where the bus will not take it a timer of its own
|
||||||
// for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left
|
// for that wait looks instead), a provisioner client's words, and the look at the plans, asked for first so a plan left
|
||||||
// by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one.
|
// by a replaced controller resumes. The serving controller's start and the tests of a rollout call this one. It answers
|
||||||
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) {
|
// a function that waits until the look at the plans has returned, which only a test that ends the context needs (issue
|
||||||
|
// 1338): the serving controller ignores it.
|
||||||
|
func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream) (wait func()) {
|
||||||
if err := startRolloutSilent(ctx, js); err != nil {
|
if err := startRolloutSilent(ctx, js); err != nil {
|
||||||
fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err)
|
fmt.Printf("plans: the wait for the machines' reports could not be put on the bus (%v); each is looked at by a timer of its own\n", err)
|
||||||
}
|
}
|
||||||
@@ -174,7 +183,8 @@ func startRolloutEvents(ctx context.Context, open *stores, js *broker.JetStream)
|
|||||||
if err := startPlanMovedLooks(ctx, js); err != nil {
|
if err := startPlanMovedLooks(ctx, js); err != nil {
|
||||||
fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err)
|
fmt.Printf("plans: the plans moved by other processes could not be followed (%v)\n", err)
|
||||||
}
|
}
|
||||||
startPlanLooks(ctx, open)
|
done := startPlanLooks(ctx, open)
|
||||||
|
return func() { <-done }
|
||||||
}
|
}
|
||||||
|
|
||||||
// servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a
|
// servingPlanSaved is what the serving controller does for each plan it keeps: said as plan-moved, looked at (a build, a
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
|
|||||||
// The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything.
|
// The first controller. Its plan has tier 0 built and tier 1 not asked: nothing but the save asks anything.
|
||||||
first, stopFirst := context.WithCancel(ctx)
|
first, stopFirst := context.WithCancel(ctx)
|
||||||
defer stopFirst()
|
defer stopFirst()
|
||||||
startRolloutEvents(first, open, js)
|
waitFirst := startRolloutEvents(first, open, js)
|
||||||
built := time.Now().UTC()
|
built := time.Now().UTC()
|
||||||
p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built,
|
p := inventory.Plan{ID: "plan-974", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c2", Created: built,
|
||||||
State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}},
|
State: inventory.PlanBuilding, Tiers: [][]string{{"a"}, {"b"}},
|
||||||
@@ -199,12 +199,15 @@ func TestARolloutAdvancesOnEventsAndAReplacedControllerResumesIt(t *testing.T) {
|
|||||||
|
|
||||||
// The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody.
|
// The controller is replaced: its looks and its watcher end. anchor reports meanwhile, to nobody.
|
||||||
stopFirst()
|
stopFirst()
|
||||||
|
waitFirst()
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
reports("anchor")
|
reports("anchor")
|
||||||
|
|
||||||
second, stopSecond := context.WithCancel(ctx)
|
second, stopSecond := context.WithCancel(ctx)
|
||||||
defer stopSecond()
|
defer stopSecond()
|
||||||
startRolloutEvents(second, open, js)
|
waitSecond := startRolloutEvents(second, open, js)
|
||||||
|
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
|
||||||
|
t.Cleanup(func() { stopSecond(); waitSecond() })
|
||||||
// Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire.
|
// Its first look finds anchor reported, so its key goes; laptop's stays, and is the new controller's to hear expire.
|
||||||
eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool {
|
eventually(t, "the new controller's first look deletes the key of the machine that reported", func() bool {
|
||||||
h := heldKeys()
|
h := heldKeys()
|
||||||
@@ -321,7 +324,7 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
|
|||||||
t.Cleanup(func() { inventory.PlanSaved = wasSaved })
|
t.Cleanup(func() { inventory.PlanSaved = wasSaved })
|
||||||
started, stop := context.WithCancel(ctx)
|
started, stop := context.WithCancel(ctx)
|
||||||
defer stop()
|
defer stop()
|
||||||
startRolloutEvents(started, open, js)
|
wait := startRolloutEvents(started, open, js)
|
||||||
time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing
|
time.Sleep(500 * time.Millisecond) // the first look: the walk waits, and is asked nothing
|
||||||
|
|
||||||
// The verb as its child process runs it: the delivery command, whose saves are not the serving controller's.
|
// The verb as its child process runs it: the delivery command, whose saves are not the serving controller's.
|
||||||
@@ -333,6 +336,9 @@ func TestAWalkLetGoByTheDeliverVerbIsAskedWithNoOtherEvent(t *testing.T) {
|
|||||||
return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"})
|
return verbAnswer{}, deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"})
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { runTheVerb = wasRun })
|
t.Cleanup(func() { runTheVerb = wasRun })
|
||||||
|
// Registered after every variable cleanup above, so it runs before them (last in, first out): the look at the plans
|
||||||
|
// has returned when checkEvents, inventory.PlanSaved and runTheVerb are restored (issue 1338).
|
||||||
|
t.Cleanup(func() { stop(); wait() })
|
||||||
handlers, _, err := seatToolHandlers()
|
handlers, _, err := seatToolHandlers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -171,7 +171,9 @@ func TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToT
|
|||||||
}}
|
}}
|
||||||
first, stopFirst := context.WithCancel(ctx)
|
first, stopFirst := context.WithCancel(ctx)
|
||||||
defer stopFirst()
|
defer stopFirst()
|
||||||
startRolloutEvents(first, open, js)
|
waitFirst := startRolloutEvents(first, open, js)
|
||||||
|
// Ends with the test before the cleanups restore what the look reads, which run after it (issue 1338).
|
||||||
|
t.Cleanup(func() { stopFirst(); waitFirst() })
|
||||||
if err := inv.SavePlan(ctx, &p); err != nil {
|
if err := inv.SavePlan(ctx, &p); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,142 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeProxyExcerpt is the part of mesh-control's modules/route-proxy/module.json at 70cb217601f9 (read 2026-10-20)
|
||||||
|
// that decides its grants: its state, what it reads, its own broker secret, and the bundle `provider` that the machine's
|
||||||
|
// runtime loads (`loads`) though the module declares no tools.
|
||||||
|
const routeProxyExcerpt = `{
|
||||||
|
"module": "route-proxy",
|
||||||
|
"state": ["bound"],
|
||||||
|
"reads": ["mesh-controller.assignments"],
|
||||||
|
"own-secrets": { "broker": "${dir:mesh-state}/broker" },
|
||||||
|
"build": { "artifacts": [
|
||||||
|
{ "name": "provider", "kind": "bundle", "language": "go", "from": "cmd/route-provider", "binary": "route-provider", "loads": ["route-provider"] }
|
||||||
|
] }
|
||||||
|
}`
|
||||||
|
|
||||||
|
// 1329 (review): a module with no tools whose bundle the runtime loads stays carried, because its helper writes its state
|
||||||
|
// and reads on the runner's account; the delivery's five parts are still dropped.
|
||||||
|
func TestTheToolRunnerStillCarriesAModuleWhoseBundleItLoads(t *testing.T) {
|
||||||
|
var m struct {
|
||||||
|
Module string `json:"module"`
|
||||||
|
State []string `json:"state"`
|
||||||
|
Reads []string `json:"reads"`
|
||||||
|
Build struct {
|
||||||
|
Artifacts []struct {
|
||||||
|
Loads []string `json:"loads"`
|
||||||
|
} `json:"artifacts"`
|
||||||
|
} `json:"build"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(routeProxyExcerpt), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
proxy := Declared{Module: m.Module, Reads: m.Reads}
|
||||||
|
for _, s := range m.State {
|
||||||
|
proxy.State = append(proxy.State, Bucket{Module: m.Module, Name: s, History: 1})
|
||||||
|
}
|
||||||
|
for _, a := range m.Build.Artifacts {
|
||||||
|
proxy.Loads = proxy.Loads || len(a.Loads) > 0
|
||||||
|
}
|
||||||
|
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}, proxy, declaredOf(t, bridgeManifest, streams), declaredOf(t, indexerManifest, streams),
|
||||||
|
declaredOf(t, tellerManifest, streams), declaredOf(t, watchmanManifest, streams),
|
||||||
|
}}})
|
||||||
|
runner := all["runner@novox"]
|
||||||
|
if !canWrite(runner, "route-proxy_bound", "k") {
|
||||||
|
t.Error("novox's tool runner does not carry route-proxy, whose bundle it loads")
|
||||||
|
}
|
||||||
|
if !canWrite(all["route-proxy@novox"], "route-proxy_bound", "k") {
|
||||||
|
t.Error("route-proxy lost its own credential")
|
||||||
|
}
|
||||||
|
for _, b := range []string{"mesh-delivery-teller_answers", "mesh-delivery-forge-bridge_heads", "mesh-delivery-indexer_modules", "mesh-delivery-watchman_stuck"} {
|
||||||
|
if canWrite(runner, b, "k") {
|
||||||
|
t.Errorf("novox's tool runner may write %s", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ type Declared struct {
|
|||||||
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
|
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
|
||||||
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
|
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
|
||||||
Checks []string
|
Checks []string
|
||||||
|
// Loads says the module has a compiled bundle whose entrypoints the machine's tool runtime loads and runs
|
||||||
|
// (novox/hq ADR 0175; issue 1329): a helper that reaches the bus on the runner's account, so the runner
|
||||||
|
// must carry the module although it declares no tools.
|
||||||
|
Loads bool
|
||||||
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
|
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
|
||||||
// carried by the machine's runtime, and reaches the bus on its own account.
|
// carried by the machine's runtime, and reaches the bus on its own account.
|
||||||
RunsAs string
|
RunsAs string
|
||||||
@@ -206,7 +210,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 +395,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,
|
||||||
|
// 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
|
||||||
|
// 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 || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
|||||||
// name (novox/hq ADR 0305).
|
// name (novox/hq ADR 0305).
|
||||||
Issued: issuedTools(m),
|
Issued: issuedTools(m),
|
||||||
}
|
}
|
||||||
|
// Whether the node's runtime loads a bundle of it: a helper that runs on the runner's account (issue 1329).
|
||||||
|
for _, b := range m.Bundles {
|
||||||
|
if len(b.Loads) > 0 {
|
||||||
|
d.Loads = true
|
||||||
|
}
|
||||||
|
}
|
||||||
// Whether it can be given an account at all: delivered as its own secret named broker, so one
|
// Whether it can be given an account at all: delivered as its own secret named broker, so one
|
||||||
// that declares none has nowhere to read it (novox/hq issue 195).
|
// that declares none has nowhere to read it (novox/hq issue 195).
|
||||||
if _, reads := m.OwnSecrets["broker"]; !reads {
|
if _, reads := m.OwnSecrets["broker"]; !reads {
|
||||||
|
|||||||
Reference in New Issue
Block a user