From db7cd273f0304f1f633e9f6804d6f44387f62983 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 11:55:26 +0100 Subject: [PATCH] The node-engine finds the tool runner by what its manifest says (tool-runner), never by its name, pinned at mesh-sdk a973bfa (issue 774, the operator 2026-11-03) --- go.mod | 2 +- go.sum | 2 + internal/compose/composed.go | 4 +- internal/compose/composed_test.go | 12 +-- internal/compose/frombus/answer.go | 9 +- internal/compose/frombus/held_rows_test.go | 17 ++-- internal/compose/resolution/declaration.go | 4 +- .../resolution/foundation_manifests_test.go | 6 +- internal/compose/resolution/proofs_test.go | 4 +- internal/compose/resolution/reads_test.go | 10 +-- internal/compose/resolution/runtime.go | 36 ++++---- .../compose/resolution/runtime_health_test.go | 10 +-- internal/compose/resolution/runtime_test.go | 84 +++++++++++-------- 13 files changed, 105 insertions(+), 95 deletions(-) diff --git a/go.mod b/go.mod index 5c8d904..ece9da5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103103643-4f141ff08892 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103105230-a973bfaf42b2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 0fd448a..a8a3c3c 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128 h1:aI1DhZAu git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103103643-4f141ff08892 h1:XWorhIHDVECRCYzGbkaexEpJ7v03B9LrZ8IVy7vkUKs= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103103643-4f141ff08892/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103105230-a973bfaf42b2 h1:CMNEBKlWI36rQEG5lhFRKoICquvYP4hBD7h/jCVFIcc= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103105230-a973bfaf42b2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/internal/compose/composed.go b/internal/compose/composed.go index fffd0b8..03be678 100644 --- a/internal/compose/composed.go +++ b/internal/compose/composed.go @@ -316,7 +316,6 @@ func RunsAnyOf(runs *delivery.Composed, module string) bool { // nothing of has no Loads: the runtime dropped it, and the composition reads its loads again from the module's build // (issue 1126, review: shanks had already applied the drop of systemd-resolved). Nil when running holds no runtime process. func ToolsRunning(running []delivery.Composed) map[string][]catalogue.Bundle { - runtimeID := catalogue.RuntimeModule + "." + resolution.RuntimeProcessID() type resource struct { ID string `json:"id"` Module string `json:"module"` @@ -339,7 +338,8 @@ func ToolsRunning(running []delivery.Composed) map[string][]catalogue.Bundle { owner, _ = declaration.ModuleGuess(r.ID) } switch { - case r.Type == "process" && r.ID == runtimeID && owner == catalogue.RuntimeModule: + case r.Type == "process" && owner != "" && r.ID == owner+"."+resolution.RuntimeProcessID(): + // The tool runner's process, by its id alone: `.runtime` (resolutionRuntimeProcess). env = r.Env case r.Type == "archive" && owner != "" && strings.HasPrefix(r.ID, owner+"."+resolution.BundleID("")): name := strings.TrimPrefix(r.ID, owner+"."+resolution.BundleID("")) diff --git a/internal/compose/composed_test.go b/internal/compose/composed_test.go index 09e36a7..59cb564 100644 --- a/internal/compose/composed_test.go +++ b/internal/compose/composed_test.go @@ -104,7 +104,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { // indexer's commit has no bundles (shanks's runtime lost the folded systemd-resolved, novox's the folded nats), and a // held module's new build's bundles are not delivered. dg := func(c byte) string { return "sha256:" + strings.Repeat(string(c), 64) } - runtime := catalogue.Manifest{Module: catalogue.RuntimeModule, Version: "1", + runtime := catalogue.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/node-tools/broker"}}, Bundles: []catalogue.Bundle{{Name: "runtime", Source: catalogue.ArtifactStoreScheme + "node-tools/runtime/blobs/" + dg('a'), Digest: dg('a'), Language: "go", Binary: "node-tools"}}} @@ -125,7 +125,7 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { if dropped { env = `"MESH_TOOL_MODULES":""` // the drop already applied (shanks, 2026-10-29) } - return delivery.Composed{Node: "anchor", Module: catalogue.RuntimeModule, Resources: []json.RawMessage{json.RawMessage( + return delivery.Composed{Node: "anchor", Module: "node-tools", Resources: []json.RawMessage{json.RawMessage( `{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{` + env + `}}`)}} } lampRuns := `{"id":"lamp.bundle-tools","type":"archive","source":"` + catalogue.ArtifactStoreScheme + `lamp/tools/blobs/` + dg('b') + @@ -155,13 +155,13 @@ func TestCompositionsAreTheDeclarationSplitByModule(t *testing.T) { }, map[string]string{"lamp": fpFor('1')}, false}, } { tools := aMesh(t) - tools.Catalogue[catalogue.RuntimeModule], tools.Catalogue["artifacts"], tools.Catalogue["lamp"] = runtime, store, lamp + tools.Catalogue["node-tools"], tools.Catalogue["artifacts"], tools.Catalogue["lamp"] = runtime, store, lamp anchor := nodeOf(t, &tools, "anchor") - anchor.Assigned = append(anchor.Assigned, "artifacts", catalogue.RuntimeModule, "lamp") - tools.BusMinted = []string{"anchor." + catalogue.RuntimeModule} + anchor.Assigned = append(anchor.Assigned, "artifacts", "node-tools", "lamp") + tools.BusMinted = []string{"anchor." + "node-tools"} anchor.SealingKey = keyOf(t, "anchor") anchor.RunsTools = ToolsRunning([]delivery.Composed{runtimeRuns(row.dropped), row.set(anchor)}) - row.fps[overlay.Name], row.fps["artifacts"], row.fps[catalogue.RuntimeModule] = fpFor('a'), fpFor('5'), fpFor('e') + row.fps[overlay.Name], row.fps["artifacts"], row.fps["node-tools"] = fpFor('a'), fpFor('5'), fpFor('e') out, _, _, err := CompositionsIn(tools, "anchor", row.fps, Foreseeing) if err != nil { t.Fatalf("%s: %v", row.name, err) diff --git a/internal/compose/frombus/answer.go b/internal/compose/frombus/answer.go index d2b0162..79be25e 100644 --- a/internal/compose/frombus/answer.go +++ b/internal/compose/frombus/answer.go @@ -404,11 +404,11 @@ func firstReason(err error, failed map[string]string) error { return fmt.Errorf("%s; its stand-in is then refused: %w", strings.Join(named, "; "), err) } -// blames says the composition with m's stub (again) makes m the culprit of err: it composes, or fails otherwise. The +// blames says the composition with m's stub (again) makes m the culprit of err, m the tool runner (runner) or not: it composes, or fails otherwise. The // runtime's stub is never a runtime (no build is taken in, so it carries no bundle) and always fails otherwise: it is // the culprit only where its stub composes (issue 774, 2026-10-30 19:30: node-tools held for any error naming it). -func blames(m string, again, err error) bool { - if m == catalogue.RuntimeModule { +func blames(runner bool, again, err error) bool { + if runner { // Or where its stub fails only for the modules of their own account it would run (issue 774): they are held // after it, each for its own words, and the node composes the rest. var none *resolution.NoRuntimeToRunError @@ -470,7 +470,8 @@ func culprit(in compose.Input, node string, fps map[string]string, mode compose. } changes := func(m, leave string) bool { _, _, _, again := compose.CompositionsIn(stubbed(in, node, m, leave), node, fps, mode) - return blames(m, again, err) + man, known := manifestOn(in, node, m) + return blames(known && catalogue.IsToolRunner(man), again, err) } for _, m := range order { if _, done := failed[m]; done { diff --git a/internal/compose/frombus/held_rows_test.go b/internal/compose/frombus/held_rows_test.go index bc8fae7..ef66cee 100644 --- a/internal/compose/frombus/held_rows_test.go +++ b/internal/compose/frombus/held_rows_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/compose" "github.com/novox/mesh-host/internal/compose/resolution" @@ -100,12 +99,12 @@ func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) { {"lamp", nil, true}, {"lamp", errors.New("other"), true}, {"lamp", errors.New("lamp refuses"), false}, - {catalogue.RuntimeModule, nil, true}, - {catalogue.RuntimeModule, errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false}, - {catalogue.RuntimeModule, fmt.Errorf("compose: %w", &resolution.NoRuntimeToRunError{Held: true}), true}, - {catalogue.RuntimeModule, &resolution.NoRuntimeToRunError{}, false}, + {"node-tools", nil, true}, + {"node-tools", errors.New("node-tools is assigned to n1 and its build produced 0 bundle(s)"), false}, + {"node-tools", fmt.Errorf("compose: %w", &resolution.NoRuntimeToRunError{Held: true}), true}, + {"node-tools", &resolution.NoRuntimeToRunError{}, false}, } { - if got := blames(row.module, row.again, err); got != row.blamed { + if got := blames(row.module == "node-tools", row.again, err); got != row.blamed { t.Errorf("%s with %v: blamed %v, want %v", row.module, row.again, got, row.blamed) } } @@ -117,13 +116,13 @@ func theRuntimeIsBlamedOnlyWhereItsStubComposes(t *testing.T) { // another build than the trunk's is composed from the build its want names. func aRunningModuleWaitsForItsBuild(t *testing.T) { dg := func(c string) string { return "sha256:" + strings.Repeat(c, 64) } - tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` + + tools := `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` + `"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}` lamp := `{"module":"lamp","version":"1","resources":[{"id":"service","type":"service","unit":"lamp.service","state":"running","boot":"enabled"}]}` store := `{"module":"artifacts","version":"1","provides":[{"name":"artifact-store","scope":"mesh"}],"serves":{"artifact-store":{"port":5100}},` + `"receives":{"artifact-store":"/var/lib/artifacts/received.json"}}` old, next := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64) - running := []delivery.Composed{{Node: "n1", Module: catalogue.RuntimeModule, FP: old, Resources: []json.RawMessage{ + running := []delivery.Composed{{Node: "n1", Module: "node-tools", FP: old, Resources: []json.RawMessage{ json.RawMessage(`{"id":"node-tools.runtime","module":"node-tools","type":"process","name":"node-tools","env":{"MESH_TOOL_MODULES":""}}`), json.RawMessage(`{"id":"node-tools.bundle-runtime","module":"node-tools","type":"archive","source":"artifact-store://node-tools/runtime/blobs/` + dg("1") + `","digest":"` + dg("1") + `","path":"` + resolution.BundlePath("node-tools", "runtime") + `"}`)}}} @@ -207,7 +206,7 @@ func aHeldModulesFirstReasonLeads(t *testing.T) { // secrets: the node composes every other module, and the held one keeps the words it was first held for. func aHeldModuleOfItsOwnAccountNeverRefusesTheNode(t *testing.T) { dg := "sha256:" + strings.Repeat("1", 64) - tools := `{"module":"node-tools","version":"1","own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` + + tools := `{"module":"node-tools","version":"1","tool-runner":true,"own-secrets":{"broker":"/var/lib/mesh/node-tools/broker"},` + `"build":{"artifacts":[{"name":"runtime","kind":"bundle","language":"go","system":"arch","from":"cmd/node-tools"}]}}` own := func(name string) string { return `{"module":"` + name + `","version":"1","runs-as":"` + name + `","secrets-owner":"` + name + `","tools":["status"],` + diff --git a/internal/compose/resolution/declaration.go b/internal/compose/resolution/declaration.go index 3a774e3..3807e06 100644 --- a/internal/compose/resolution/declaration.go +++ b/internal/compose/resolution/declaration.go @@ -1046,7 +1046,7 @@ func resolutionCompose(r mcat.Resolution, with mcat.Rendering, owner map[string] return nil, err } if process != nil { // none for a held runtime's stand-in: what runs of it is carried - owner[fmt.Sprint(process["id"])] = mcat.RuntimeModule + owner[fmt.Sprint(process["id"])] = fmt.Sprint(process["name"]) // the tool runner's module (resolutionRuntimeProcess) out = append(out, process) } // And a runtime of its own for each module of its own account, after it (novox/hq ADR 0259 §8). @@ -1308,7 +1308,7 @@ func resolutionOwnSecretOwner(r mcat.Resolution, m mcat.Manifest, name string) s if o := m.OwnSecrets[name].Owner; o != "" { return o } - if r.Account != "" && (m.Module == mcat.RuntimeModule || ToolsBusSecret(m, name)) { + if r.Account != "" && (mcat.IsToolRunner(m) || ToolsBusSecret(m, name)) { return r.Account } return m.SecretsOwner diff --git a/internal/compose/resolution/foundation_manifests_test.go b/internal/compose/resolution/foundation_manifests_test.go index 515f171..f495d15 100644 --- a/internal/compose/resolution/foundation_manifests_test.go +++ b/internal/compose/resolution/foundation_manifests_test.go @@ -113,7 +113,7 @@ func TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt(t *testing.T) { // The number this node was given for the forge — the one the machine it is about to run on // already publishes. out, err := Declaration(r, mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-broker"}}, Given: map[string]map[int]int{"gitea": {3000: 2999}}, }) if err != nil { @@ -129,7 +129,7 @@ func TestTheForgesOwnAddressFollowsThePortTheNodeGaveIt(t *testing.T) { t.Fatalf("the forge is not published on the port this node gave it: %v", server["ports"]) } // The forge's own code runs in the node's runtime (novox/hq ADR 0198), given its words there. - runtime := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + runtime := fileNamed(out, "node-tools"+"."+RuntimeProcessID()) if runtime == nil { t.Fatalf("the node's runtime is not in the declaration: %v", ids(out)) } @@ -168,7 +168,7 @@ func declaredGiteaSsh(t *testing.T, given map[int]int) map[string]any { givenPorts[k] = v } out, err := Declaration(r, mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-broker"}}, Ports: map[string]map[int]int{"gitea": givenPorts}, Given: map[string]map[int]int{"gitea": given}, }) diff --git a/internal/compose/resolution/proofs_test.go b/internal/compose/resolution/proofs_test.go index cd1396b..892f382 100644 --- a/internal/compose/resolution/proofs_test.go +++ b/internal/compose/resolution/proofs_test.go @@ -46,9 +46,9 @@ func twoMergeableFiles(app, sidecar []any) mcat.Manifest { // The indexer's judge's proofs, items 1 to 13 of its list. func TestTheJudgesProofsOfAComposition(t *testing.T) { runtimeWith := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "dash": {"token": "sealed-token"}}} + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "dash": {"token": "sealed-token"}}} processOf := func(out []map[string]any) map[string]any { - return fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + return fileNamed(out, "node-tools"+"."+RuntimeProcessID()) } t.Run("1 a bundle's words resolve per machine into the runtime's tool env", func(t *testing.T) { diff --git a/internal/compose/resolution/reads_test.go b/internal/compose/resolution/reads_test.go index 91a6ade..ced1bd1 100644 --- a/internal/compose/resolution/reads_test.go +++ b/internal/compose/resolution/reads_test.go @@ -13,13 +13,13 @@ import ( // place the operator-sealed secrets are kept — and no other module is told. func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "reads": {"broker": "own"}, "desk": {"broker": "own"}}} - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "reads": {"broker": "own"}, "desk": {"broker": "own"}}} + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -59,7 +59,7 @@ func TestTheHolderOfNodeReadsIsToldWhereItsNodeKeepsASecret(t *testing.T) { t.Fatalf("the holder is not told its node's secret places: %v (%v)", err, given) } want := []string{"/etc/app/db.secret", "/home/ops/.config/app", "/var/lib/desk", "/var/lib/mesh-reads", - "/var/lib/mesh-vault", "/var/lib/mesh/" + mcat.RuntimeModule} + "/var/lib/mesh-vault", "/var/lib/mesh/" + "node-tools"} if strings.Join(places, " ") != strings.Join(want, " ") { t.Errorf("secret places:\n got %v\nwant %v", places, want) } diff --git a/internal/compose/resolution/runtime.go b/internal/compose/resolution/runtime.go index 9eedf9c..ad85423 100644 --- a/internal/compose/resolution/runtime.go +++ b/internal/compose/resolution/runtime.go @@ -21,12 +21,8 @@ func BundlePath(module, bundle string) string { return mcat.BundleRoot + "/" + m // a node is sent exactly what it was sent before, bundles included, because a bundle nothing loads // is bytes nobody reads. func resolutionRuntimeHere(r mcat.Resolution) bool { - for _, m := range r.Modules { - if m.Module == mcat.RuntimeModule { - return true - } - } - return false + _, here := mcat.ToolRunnerOf(r.Modules) + return here } // bundleArchives is one archive per tools bundle of a module — a bundle the runtime LOADS something @@ -59,13 +55,13 @@ func RuntimeProcessID() string { return "runtime" } // interpreterFor is how a bundle in a language is run: the program the host's unit starts, with the // bundle's entrypoint after it. The one thing the composer takes from a language, and said here // rather than in a manifest because the runtime's process is the mesh's to compose (to-be 38 WP3). -func interpreterFor(language string) (string, error) { +func interpreterFor(module, language string) (string, error) { switch language { case "typescript": return "node", nil } return "", fmt.Errorf( - "%s is written in %q, and the mesh knows no interpreter to run a %q bundle with", mcat.RuntimeModule, language, language) + "%s is written in %q, and the mesh knows no interpreter to run a %q bundle with", module, language, language) } // runtimeProcess is the one process a machine runs the node's tool runtime as (novox/hq ADR 0175, @@ -80,7 +76,7 @@ func interpreterFor(language string) (string, error) { func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[string]any, error) { var runtime *mcat.Manifest for i := range r.Modules { - if r.Modules[i].Module == mcat.RuntimeModule { + if mcat.IsToolRunner(r.Modules[i]) { runtime = &r.Modules[i] } } @@ -93,7 +89,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin if len(runtime.Bundles) != 1 { return nil, fmt.Errorf( "%s is assigned to %s and its build produced %d bundle(s); the runtime is one bundle "+ - "the mesh runs, so the module declares exactly one (novox/hq to-be 38)", mcat.RuntimeModule, r.Node, len(runtime.Bundles)) + "the mesh runs, so the module declares exactly one (novox/hq to-be 38)", runtime.Module, r.Node, len(runtime.Bundles)) } bundle := runtime.Bundles[0] // What runs it (novox/hq ADR 0193): a runtime compiled to a binary runs itself, from its own @@ -105,9 +101,9 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin if len(bundle.Entrypoints) != 1 { return nil, fmt.Errorf( "%s's bundle %q names %d entrypoint(s); the runtime is run from one, so the module "+ - "declares exactly one (novox/hq to-be 38)", mcat.RuntimeModule, bundle.Name, len(bundle.Entrypoints)) + "declares exactly one (novox/hq to-be 38)", runtime.Module, bundle.Name, len(bundle.Entrypoints)) } - interpreter, err := interpreterFor(bundle.Language) + interpreter, err := interpreterFor(runtime.Module, bundle.Language) if err != nil { return nil, err } @@ -117,7 +113,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin if !declared { return nil, fmt.Errorf( "%s declares no own secret named broker, and the node's credential is delivered there: "+ - "a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", mcat.RuntimeModule, + "a module that speaks on the bus declares \"own-secrets\": {\"broker\": }", runtime.Module, ) } @@ -163,7 +159,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin } } sort.Strings(served) - restartOn = append(restartOn, mcat.RuntimeModule+"."+mcat.NeedID("broker")) + restartOn = append(restartOn, runtime.Module+"."+mcat.NeedID("broker")) sort.Strings(restartOn) env := map[string]string{mcat.RuntimeToolModules: strings.Join(served, ","), mcat.RuntimeBrokerFile: credential.Path} @@ -176,7 +172,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin env[mcat.RuntimeToolEnv] = string(body) } process := map[string]any{ - "id": mcat.RuntimeModule + "." + RuntimeProcessID(), "type": "process", "name": mcat.RuntimeModule, + "id": runtime.Module + "." + RuntimeProcessID(), "type": "process", "name": runtime.Module, "source": bundle.Source, "digest": bundle.Digest, "run": run, "env": env, @@ -193,7 +189,7 @@ func resolutionRuntimeProcess(r mcat.Resolution, with mcat.Rendering) (map[strin } // Routed through the artifact store as this network reaches it now, like everything the mesh // built; refused with the same words when there is no store to route through. - if err := artifactsInto(process, mcat.RuntimeModule, with); err != nil { + if err := artifactsInto(process, runtime.Module, with); err != nil { return nil, err } return process, nil @@ -229,14 +225,14 @@ func resolutionOwnRuntimes(r mcat.Resolution, with mcat.Rendering) ([]map[string } var runtime *mcat.Manifest for i := range r.Modules { - if r.Modules[i].Module == mcat.RuntimeModule { + if mcat.IsToolRunner(r.Modules[i]) { runtime = &r.Modules[i] } } if runtime == nil || len(runtime.Bundles) != 1 || runtime.Bundles[0].Binary == "" { return nil, &NoRuntimeToRunError{Held: runtime != nil && runtime.Stubbed, said: fmt.Sprintf("%s runs as its own "+ - "account in a runtime of its own, and %s is not here to run it from: assign %s to %s first (novox/hq ADR 0259)", - own[0].Module, mcat.RuntimeModule, mcat.RuntimeModule, r.Node)} + "account in a runtime of its own, and the tool runner is not here to run it from: assign the tool runner to %s first "+ + "(novox/hq ADR 0259)", own[0].Module, r.Node)} } program := runtime.Bundles[0] var out []map[string]any @@ -314,7 +310,7 @@ func resolutionOwnRuntimes(r mcat.Resolution, with mcat.Rendering) ([]map[string if yielding != nil { process["yields"] = yielding } - if err := artifactsInto(process, mcat.RuntimeModule, with); err != nil { + if err := artifactsInto(process, runtime.Module, with); err != nil { return nil, err } out = append(out, process) diff --git a/internal/compose/resolution/runtime_health_test.go b/internal/compose/resolution/runtime_health_test.go index 65043cf..b4d3377 100644 --- a/internal/compose/resolution/runtime_health_test.go +++ b/internal/compose/resolution/runtime_health_test.go @@ -9,12 +9,12 @@ import ( // The runtime-health is composed onto .own-runtime, in the words the node-engine takes; to an engine // that does not read health it is not sent, as for every resource. func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) { - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -26,7 +26,7 @@ func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) { "looks": 2.0} for _, reads := range []bool{true, false} { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", ReadsHealth: reads, - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed"}, "keeper": {"broker": "own"}}} + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed"}, "keeper": {"broker": "own"}}} out, err := Declaration(mcat.Resolution{Node: "anchor", Account: "ops", Modules: []mcat.Manifest{keeper, goRuntime}}, with) if err != nil { t.Fatal(err) @@ -46,7 +46,7 @@ func TestARuntimeHealthIsComposedOntoTheModulesOwnRuntime(t *testing.T) { h["timeout"] != "10s" || h["looks"] != 2 || h["grace"] != "1m0s" { t.Errorf("its own runtime's health: %v", own[mcat.HealthField]) } - if machine := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()); machine[mcat.HealthField] != nil { + if machine := fileNamed(out, "node-tools"+"."+RuntimeProcessID()); machine[mcat.HealthField] != nil { t.Errorf("the machine's runtime was given the module's health: %v", machine[mcat.HealthField]) } } diff --git a/internal/compose/resolution/runtime_test.go b/internal/compose/resolution/runtime_test.go index 0075bfe..ba0932f 100644 --- a/internal/compose/resolution/runtime_test.go +++ b/internal/compose/resolution/runtime_test.go @@ -31,12 +31,12 @@ func aToolsModule(t *testing.T, name string, entrypoints ...string) mcat.Manifes // loaded, and its broker secret to receive the node's credential in. func theRuntime(t *testing.T) mcat.Manifest { t.Helper() - m := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + m := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "typescript", Entrypoints: []string{"src/main.js"}}}}} resolved, err := m.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -45,7 +45,7 @@ func theRuntime(t *testing.T) mcat.Manifest { func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) { store := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}} + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}} nftables := aToolsModule(t, "nftables", "tools/index.js") zsh := aToolsModule(t, "zsh", "tools/index.js", "tools/more.js") @@ -70,7 +70,7 @@ func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) { t.Errorf("zsh's tools bundle was not delivered: %v", ids(out)) } // The runtime's own bundle is run, not loaded: its process delivers it, not an archive. - if fileNamed(out, mcat.RuntimeModule+"."+BundleID("runtime")) != nil { + if fileNamed(out, "node-tools"+"."+BundleID("runtime")) != nil { t.Error("the runtime's own bundle was delivered as an archive beside its process") } }) @@ -86,6 +86,18 @@ func TestEveryToolsBundleIsDeliveredWhereTheRuntimeRuns(t *testing.T) { t.Errorf("%s was delivered to a machine running no runtime to load it", id) } } + // The tool runner is what says it is one, never a name (issue 774): node-tools saying nothing is no runner. + silent := theRuntime(t) + silent.ToolRunner = false + out, err = Declaration(mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{nftables, zsh, silent}}, store) + if err != nil { + t.Fatal(err) + } + for _, id := range ids(out) { + if strings.Contains(id, BundleID("")) || strings.HasSuffix(id, "."+RuntimeProcessID()) { + t.Errorf("%s was composed for a module that does not say it is the tool runner", id) + } + } }) } @@ -101,7 +113,7 @@ func ids(out []map[string]any) []string { // where its credential is, and who the operator is — restarted when any of that changes. func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}} + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}} nftables := aToolsModule(t, "nftables", "tools/index.js") // A bundle carrying a daemon beside its tools says which files the runtime loads. showcase := mcat.Manifest{Module: "showcase", Version: "1", Tools: []string{"greet"}, @@ -118,12 +130,12 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { if err != nil { t.Fatal(err) } - process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + process := fileNamed(out, "node-tools"+"."+RuntimeProcessID()) if process == nil { t.Fatalf("no runtime process was composed: %v", ids(out)) } - if process["type"] != "process" || process["name"] != mcat.RuntimeModule || process["digest"] != bundleDigest || - process["source"] != "http://anchor.internal:5101/v2/"+mcat.RuntimeModule+"/runtime/blobs/"+bundleDigest { + if process["type"] != "process" || process["name"] != "node-tools" || process["digest"] != bundleDigest || + process["source"] != "http://anchor.internal:5101/v2/"+"node-tools"+"/runtime/blobs/"+bundleDigest { t.Errorf("the runtime's process is %v", process) } if fmt.Sprint(process["run"]) != "[node src/main.js]" { @@ -134,7 +146,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { "showcase="+mcat.BundleRoot+"/showcase/code/tools/index.js" { t.Errorf("the runtime is told to serve %q: every loaded file, by module, and nothing a bundle runs", env[mcat.RuntimeToolModules]) } - if env[mcat.RuntimeBrokerFile] != "/var/lib/mesh/"+mcat.RuntimeModule+"/broker" { + if env[mcat.RuntimeBrokerFile] != "/var/lib/mesh/"+"node-tools"+"/broker" { t.Errorf("the runtime reads its credential at %q, not where the module's own secret is placed", env[mcat.RuntimeBrokerFile]) } if env[mcat.RuntimeOperatorAccount] != "ops" || env[mcat.RuntimeOperatorHome] != "/home/ops" || process["user"] != "ops" { @@ -142,18 +154,18 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { } // The credential the process reads belongs to the account it runs as, or it could not read it // (to-be 38 WP3); other modules' secrets are left as their manifests say. - if credential := fileNamed(out, mcat.RuntimeModule+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != "ops" { + if credential := fileNamed(out, "node-tools"+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != "ops" { t.Errorf("the runtime's credential is not the account's to read: %v", credential) } restarts := fmt.Sprint(process["restart-on"]) - for _, want := range []string{"nftables." + BundleID("tools"), "showcase." + BundleID("code"), mcat.RuntimeModule + "." + mcat.NeedID("broker")} { + for _, want := range []string{"nftables." + BundleID("tools"), "showcase." + BundleID("code"), "node-tools" + "." + mcat.NeedID("broker")} { if !strings.Contains(restarts, want) { t.Errorf("the runtime is not restarted when %s changes: %s", want, restarts) } } // After every bundle and the credential, so both exist before it starts. names := ids(out) - if names[len(names)-1] != mcat.RuntimeModule+"."+RuntimeProcessID() { + if names[len(names)-1] != "node-tools"+"."+RuntimeProcessID() { t.Errorf("the runtime's process is not last: %v", names) } @@ -162,7 +174,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { if err != nil { t.Fatal(err) } - process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + process := fileNamed(out, "node-tools"+"."+RuntimeProcessID()) env := process["env"].(map[string]string) if _, set := env[mcat.RuntimeOperatorAccount]; set { t.Error("an operator account was named on a machine that has none") @@ -170,13 +182,13 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { if _, set := process["user"]; set { t.Error("a user was set on a machine with no account") } - if credential := fileNamed(out, mcat.RuntimeModule+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != nil { + if credential := fileNamed(out, "node-tools"+"."+mcat.NeedID("broker")); credential == nil || credential["owner"] != nil { t.Errorf("the runtime's credential was given an owner on a machine with no account: %v", credential) } }) t.Run("a runtime module built wrong is refused by name", func(t *testing.T) { - two := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/b"}}, + two := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/b"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "typescript", Entrypoints: []string{"a.js", "b.js"}}}}} resolved, err := two.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, @@ -195,7 +207,7 @@ func TestTheMachineRunsOneRuntimeLoadingEveryDeliveredBundle(t *testing.T) { // build from before launchers — so the move needs no flag day. func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}} + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}} launched := mcat.Manifest{Module: "dash", Version: "1", Tools: []string{"status"}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "tools", Kind: mcat.ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}} @@ -210,7 +222,7 @@ func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) { if err != nil { t.Fatal(err) } - env := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string) + env := fileNamed(out, "node-tools"+"."+RuntimeProcessID())["env"].(map[string]string) want := "dash=" + mcat.BundleRoot + "/dash/tools/tools/index.serve.mjs,nftables=" + mcat.BundleRoot + "/nftables/tools/tools/index.js" if env[mcat.RuntimeToolModules] != want { t.Errorf("the runtime is told %q, want %q", env[mcat.RuntimeToolModules], want) @@ -220,13 +232,13 @@ func TestTheRuntimeStartsTheLauncherWhereTheBuildWroteOne(t *testing.T) { // novox/hq ADR 0193: a runtime compiled to a binary runs itself from its own unpacked bundle. func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}}} - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}}} + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -234,7 +246,7 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { if err != nil { t.Fatal(err) } - process := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + process := fileNamed(out, "node-tools"+"."+RuntimeProcessID()) if fmt.Sprint(process["run"]) != "[./node-tools]" { t.Errorf("a Go runtime is run as %v, want its own binary", process["run"]) } @@ -249,13 +261,13 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) { // which runs as the operator's account and is given none of its words. func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}} - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}} + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -267,7 +279,7 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) { if err != nil { t.Fatal(err) } - machine := fileNamed(out, mcat.RuntimeModule+"."+RuntimeProcessID()) + machine := fileNamed(out, "node-tools"+"."+RuntimeProcessID()) if served := machine["env"].(map[string]string)[mcat.RuntimeToolModules]; strings.Contains(served, "telegram") || !strings.Contains(served, "nftables") { t.Errorf("the machine's runtime serves %q", served) } @@ -342,13 +354,13 @@ func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) { // as the account agents run as there — either would hand what it holds back to the accounts it is kept from. func TestAModuleOfItsOwnAccountIsRefusedTheOperatorsAndTheAgentsAccount(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}} - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}} + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) } @@ -368,16 +380,16 @@ func TestAModuleOfItsOwnAccountIsRefusedTheOperatorsAndTheAgentsAccount(t *testi // files it holds for its consumers, and what its words name — belongs to that account, never the operator's. func TestWhatAModuleOfItsOwnAccountReadsIsItsAccounts(t *testing.T) { with := mcat.Rendering{ArtifactStore: "anchor.internal:5101", - Needed: map[string]map[string]string{mcat.RuntimeModule: {"broker": "sealed-credential"}, "vault": {"broker": "own"}}, + Needed: map[string]map[string]string{"node-tools": {"broker": "sealed-credential"}, "vault": {"broker": "own"}}, Kept: &mcat.KeptExport{Export: 1, OperatorKey: "k"}, Grants: []mcat.Grant{{Provision: "secret", Consumer: "anchor", From: "telegram", Sealed: "sealed-to-anchor", Values: map[string]any{"name": "telegram"}}}} - goRuntime := mcat.Manifest{Module: mcat.RuntimeModule, Version: "1", - OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + mcat.RuntimeModule + "/broker"}}, + goRuntime := mcat.Manifest{Module: "node-tools", Version: "1", ToolRunner: true, + OwnSecrets: mcat.OwnSecrets{"broker": {Path: "/var/lib/mesh/" + "node-tools" + "/broker"}}, Build: &mcat.Build{Artifacts: []mcat.Artifact{{Name: "runtime", Kind: mcat.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}} goRuntime, err := goRuntime.Resolve([]mcat.Built{{Name: "runtime", Kind: mcat.ArtifactBundle, - Reference: mcat.ArtifactStoreScheme + mcat.RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) + Reference: mcat.ArtifactStoreScheme + "node-tools" + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}}) if err != nil { t.Fatal(err) }