From e6ce4be4b4fa5cd68c4c6c411acad4439cd8d444 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 13:06:05 +0200 Subject: [PATCH 1/3] Read the indexer's modules keys with two read-only tools, so the new-form fingerprint and the reason for none can be checked live (issue 1562) --- .../cmd/mesh-delivery-indexer-tools/main.go | 53 ++++++++++ .../mesh-delivery-indexer-tools/main_test.go | 96 +++++++++++++++++++ .../internal/index/read.go | 82 ++++++++++++++++ .../mesh-delivery-indexer/manifest_test.go | 37 ++++--- modules/mesh-delivery-indexer/module.json | 15 +++ 5 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go create mode 100644 modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go create mode 100644 modules/mesh-delivery-indexer/internal/index/read.go diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go new file mode 100644 index 0000000..2282273 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -0,0 +1,53 @@ +// Command mesh-delivery-indexer-tools serves the indexer's read tools (novox/hq issue 1562): the runtime launches it for a +// call, it reads the module's own modules state and writes nothing. The indexer itself is the other program. +package main + +import ( + "encoding/json" + "fmt" + "os" + + stdio "git.novox.be/novox/mesh-sdk/go" + "mesh-delivery-indexer/internal/index" +) + +// Tools are the read tools over a modules state. +func Tools(r index.Reader) []stdio.Tool { + return []stdio.Tool{ + {Name: index.ModuleReadTool, + Description: "What the indexer stored for one module under modules., as stored: its fingerprint (fp, none), the " + + "new-form fingerprint (fp2) or why it has none (none2), the inputs it was computed from (inputs2), the module's " + + "directory (path) and the toolchains it was computed with. Read-only.", + Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module's name"}}}, + Run: func(args map[string]any) (any, error) { + name, _ := args["module"].(string) + return r.Module(name) + }}, + {Name: index.ModulesReadTool, + Description: "Every module the indexer holds, each as stored under modules.. With none2 true, only the modules " + + "that have no new-form fingerprint, each with the reason (none2). Read-only.", + Input: map[string]any{"type": "object", "properties": map[string]any{ + "none2": map[string]any{"type": "boolean", "description": "only the modules without a new-form fingerprint"}}}, + Run: func(args map[string]any) (any, error) { + only, _ := args["none2"].(bool) + mods, err := r.Modules(only) + return map[string]any{"modules": mods}, err + }}, + } +} + +func main() { + state := stdio.State("modules") + r := index.Reader{State: index.StateRead{Keys: state.Keys, Get: func(key string) (json.RawMessage, error) { + e, err := state.Get(key) + if err != nil || e == nil { + return nil, err + } + return e.Value, nil + }}} + if err := stdio.Serve("", Tools(r)); err != nil { + fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err) + os.Exit(1) + } +} diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go new file mode 100644 index 0000000..4e4b2c5 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "strings" + "testing" + + "mesh-delivery-indexer/internal/index" +) + +type fake map[string]string + +func (f fake) reader() index.Reader { + return index.Reader{State: index.StateRead{ + Keys: func() ([]string, error) { + var k []string + for key := range f { + k = append(k, key) + } + return k, nil + }, + Get: func(key string) (json.RawMessage, error) { + if v, ok := f[key]; ok { + return json.RawMessage(v), nil + } + return nil, nil + }, + }} +} + +func run(t *testing.T, r index.Reader, name string, args map[string]any) (any, error) { + t.Helper() + for _, tl := range Tools(r) { + if tl.Name == name { + return tl.Run(args) + } + } + t.Fatalf("no tool %s", name) + return nil, nil +} + +const ( + withFP2 = `{"v":3,"fp":"a1","fp2":"b2","manifest":"m1","inputs2":{"tree":"t1","bases":["x"]},"repository":"novox/r","commit":"c1","path":".","toolchains":{"go":{"version":"go1.27.1","recipe":"2"}}}` + withNone = `{"v":3,"none":"no successful trunk build recorded its inputs","none2":"builder's fingerprint not yet known","repository":"novox/r","commit":"c2","path":"modules/b"}` +) + +// 1562/AC1: indexer_module answers the key's value as stored: FP, FP2, None2, Inputs2, Path and Toolchains are all there. +func TestModuleToolAnswersTheStoredValue(t *testing.T) { + r := fake{"a": withFP2, "b": withNone}.reader() + for key, want := range map[string]string{"a": withFP2, "b": withNone} { + out, err := run(t, r, index.ModuleReadTool, map[string]any{"module": key}) + if err != nil { + t.Fatal(err) + } + if got := string(out.(json.RawMessage)); got != want { + t.Errorf("%s = %s, want %s", key, got, want) + } + } + out, _ := run(t, r, index.ModuleReadTool, map[string]any{"module": "a"}) + var m map[string]any + if err := json.Unmarshal(out.(json.RawMessage), &m); err != nil || m["fp"] != "a1" || m["fp2"] != "b2" || m["path"] != "." || m["inputs2"] == nil || m["toolchains"] == nil { + t.Errorf("stored fields are not all there: %v %v", m, err) + } +} + +// A module with no key, and a name that is no key, are said so. +func TestModuleToolRefusesWhatIsNotThere(t *testing.T) { + r := fake{"a": withFP2}.reader() + if _, err := run(t, r, index.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") { + t.Errorf("a missing module: %v", err) + } + if _, err := run(t, r, index.ModuleReadTool, map[string]any{"module": ""}); err == nil { + t.Errorf("an empty name was answered") + } +} + +// 1562/AC1: indexer_modules answers every key as stored, and with none2 only those that say why they have no FP2. +func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) { + r := fake{"a": withFP2, "b": withNone}.reader() + out, err := run(t, r, index.ModulesReadTool, map[string]any{}) + if err != nil { + t.Fatal(err) + } + all := out.(map[string]any)["modules"].(map[string]json.RawMessage) + if len(all) != 2 || string(all["a"]) != withFP2 || string(all["b"]) != withNone { + t.Errorf("modules = %v", all) + } + out, err = run(t, r, index.ModulesReadTool, map[string]any{"none2": true}) + if err != nil { + t.Fatal(err) + } + only := out.(map[string]any)["modules"].(map[string]json.RawMessage) + if len(only) != 1 || string(only["b"]) != withNone { + t.Errorf("none2 modules = %v", only) + } +} diff --git a/modules/mesh-delivery-indexer/internal/index/read.go b/modules/mesh-delivery-indexer/internal/index/read.go new file mode 100644 index 0000000..61558ab --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/read.go @@ -0,0 +1,82 @@ +package index + +// The indexer's read tools (novox/hq issue 1562): what the indexer stored under modules., answered as stored. The new-form +// fingerprint (FP2, None2, Inputs2), the reason it has none, and the path and toolchains it was computed with are only +// there; a module reading its own state needs no grant. The reads write nothing. + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The read tools, named as the module is: indexer_ and the verb. +const ( + ModuleReadTool = "indexer_module" + ModulesReadTool = "indexer_modules" +) + +// StateRead reads the module's own modules state: its keys and a key's stored value (nil for a key with none). +type StateRead struct { + Keys func() ([]string, error) + Get func(key string) (json.RawMessage, error) +} + +// Reader answers the read tools from a modules state. +type Reader struct{ State StateRead } + +// Module is the stored value of modules., exactly as the indexer wrote it. +func (r Reader) Module(module string) (json.RawMessage, error) { + module = strings.TrimSpace(module) + key, err := delivery.ModulesKey(module) + if err != nil { + return nil, err + } + if r.State.Get == nil { + return nil, fmt.Errorf("the modules state cannot be read here") + } + raw, err := r.State.Get(key) + if err != nil { + return nil, err + } + if len(raw) == 0 { + return nil, fmt.Errorf("no module %s in the modules state", module) + } + return raw, nil +} + +// Modules is every key of the modules state with its stored value; with none2 only the modules that have no new-form +// fingerprint and say why (a None2 reason), which is what the fingerprint's check reads live. +func (r Reader) Modules(none2 bool) (map[string]json.RawMessage, error) { + if r.State.Keys == nil || r.State.Get == nil { + return nil, fmt.Errorf("the modules state cannot be read here") + } + keys, err := r.State.Keys() + if err != nil { + return nil, err + } + sort.Strings(keys) + out := map[string]json.RawMessage{} + for _, k := range keys { + raw, err := r.State.Get(k) + if err != nil { + return nil, err + } + if len(raw) == 0 { + continue + } + if none2 { + var m struct { + None2 string `json:"none2"` + } + if json.Unmarshal(raw, &m) != nil || m.None2 == "" { + continue + } + } + out[k] = raw + } + return out, nil +} diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index 90d96c3..fa4c335 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -23,12 +23,13 @@ type state struct { } type artifact struct { - Name string `json:"name"` - Kind string `json:"kind"` - Language string `json:"language"` - System string `json:"system"` - From string `json:"from"` - Binary string `json:"binary"` + Name string `json:"name"` + Kind string `json:"kind"` + Language string `json:"language"` + System string `json:"system"` + From string `json:"from"` + Binary string `json:"binary"` + Loads []string `json:"loads"` } type manifest struct { @@ -41,6 +42,7 @@ type manifest struct { OwnSecrets map[string]string `json:"own-secrets"` SecretsOwner string `json:"secrets-owner"` Resources []map[string]any `json:"resources"` + Tools []string `json:"tools"` Build struct { Artifacts []artifact `json:"artifacts"` } `json:"build"` @@ -93,8 +95,11 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build if m.SecretsOwner != "mesh-delivery-indexer" { t.Errorf("secrets-owner = %q", m.SecretsOwner) } - // No invokes, no emits, no streams, no bus, no seat, no tool (ADR 0437 §4). - for _, k := range []string{"invokes", "streams", "logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "tools", "seat", "seats", "bus"} { + // No invokes, no emits, no streams, no bus, no seat (ADR 0437 §4); the only tools are the two read-only ones of issue 1562. + if want := []string{"indexer_module", "indexer_modules"}; !reflect.DeepEqual(sorted(m.Tools), want) { + t.Errorf("tools = %v, want %v", m.Tools, want) + } + for _, k := range []string{"invokes", "streams", "logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} { if _, ok := top[k]; ok { t.Errorf("the manifest declares %q: the indexer has none", k) } @@ -109,7 +114,7 @@ func TestManifestProcess(t *testing.T) { // 1238/AC1 procs = append(procs, r) } if r["type"] == "tool" || r["type"] == "seat" { - t.Errorf("resource %v: the indexer serves no tool and holds no seat", r) + t.Errorf("resource %v: the indexer holds no seat and its tools are a bundle, not a tool resource", r) } } if len(procs) != 1 { @@ -126,12 +131,18 @@ func TestManifestProcess(t *testing.T) { // 1238/AC1 if _, bus := p["bus"]; bus { t.Errorf("the process declares a bus: it runs on its own broker credential") } - if len(m.Build.Artifacts) != 1 { + if len(m.Build.Artifacts) != 2 { t.Fatalf("artifacts = %+v", m.Build.Artifacts) } - a := m.Build.Artifacts[0] - if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-indexer" || !strings.HasSuffix(a.From, a.Binary) { - t.Errorf("artifact = %+v", a) + // The program, and the tools bundle the runtime launches for a read call (issue 1562); no process runs the tools. + for i, binary := range []string{"mesh-delivery-indexer", "mesh-delivery-indexer-tools"} { + a := m.Build.Artifacts[i] + if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != binary || !strings.HasSuffix(a.From, a.Binary) { + t.Errorf("artifact = %+v", a) + } + } + if a := m.Build.Artifacts[1]; a.Name != "tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-indexer"}) { + t.Errorf("tools artifact = %+v", a) } } diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index fcfe983..11de832 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -12,6 +12,10 @@ { "name": "manifests", "history": 1 }, { "name": "modules", "history": 20 } ], + "tools": [ + "indexer_module", + "indexer_modules" + ], "reads": [ "mesh-build-go.toolchains", "mesh-build-node.toolchains", @@ -64,6 +68,17 @@ "system": "arch", "from": "cmd/mesh-delivery-indexer", "binary": "mesh-delivery-indexer" + }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-delivery-indexer-tools", + "binary": "mesh-delivery-indexer-tools", + "loads": [ + "mesh-delivery-indexer" + ] } ] } From 6c2b79acf1066a15c4eeaecd22ded5d8179956e7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 13:24:10 +0200 Subject: [PATCH 2/3] Serve the indexer's read tools from the view, not the indexer (ADR 0437 4): indexer_module and indexer_modules read mesh-delivery-indexer.modules through the view's reads. Side effect: carrying the view's tools gives the tool runner read access to every bucket the view reads. (issue 1562) --- .../mesh-delivery-indexer/manifest_test.go | 37 +++++++------------ modules/mesh-delivery-indexer/module.json | 15 -------- .../cmd/mesh-delivery-view-tools}/main.go | 19 +++++----- .../mesh-delivery-view-tools}/main_test.go | 20 +++++----- .../internal/view}/read.go | 10 ++--- modules/mesh-delivery-view/manifest_test.go | 28 ++++++++++---- modules/mesh-delivery-view/module.json | 15 ++++++++ 7 files changed, 73 insertions(+), 71 deletions(-) rename modules/{mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools => mesh-delivery-view/cmd/mesh-delivery-view-tools}/main.go (69%) rename modules/{mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools => mesh-delivery-view/cmd/mesh-delivery-view-tools}/main_test.go (77%) rename modules/{mesh-delivery-indexer/internal/index => mesh-delivery-view/internal/view}/read.go (78%) diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index fa4c335..90d96c3 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -23,13 +23,12 @@ type state struct { } type artifact struct { - Name string `json:"name"` - Kind string `json:"kind"` - Language string `json:"language"` - System string `json:"system"` - From string `json:"from"` - Binary string `json:"binary"` - Loads []string `json:"loads"` + Name string `json:"name"` + Kind string `json:"kind"` + Language string `json:"language"` + System string `json:"system"` + From string `json:"from"` + Binary string `json:"binary"` } type manifest struct { @@ -42,7 +41,6 @@ type manifest struct { OwnSecrets map[string]string `json:"own-secrets"` SecretsOwner string `json:"secrets-owner"` Resources []map[string]any `json:"resources"` - Tools []string `json:"tools"` Build struct { Artifacts []artifact `json:"artifacts"` } `json:"build"` @@ -95,11 +93,8 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build if m.SecretsOwner != "mesh-delivery-indexer" { t.Errorf("secrets-owner = %q", m.SecretsOwner) } - // No invokes, no emits, no streams, no bus, no seat (ADR 0437 §4); the only tools are the two read-only ones of issue 1562. - if want := []string{"indexer_module", "indexer_modules"}; !reflect.DeepEqual(sorted(m.Tools), want) { - t.Errorf("tools = %v, want %v", m.Tools, want) - } - for _, k := range []string{"invokes", "streams", "logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "seat", "seats", "bus"} { + // No invokes, no emits, no streams, no bus, no seat, no tool (ADR 0437 §4). + for _, k := range []string{"invokes", "streams", "logs", "emits", "claims", "provides", "requires", "uses", "defines-seats", "tools", "seat", "seats", "bus"} { if _, ok := top[k]; ok { t.Errorf("the manifest declares %q: the indexer has none", k) } @@ -114,7 +109,7 @@ func TestManifestProcess(t *testing.T) { // 1238/AC1 procs = append(procs, r) } if r["type"] == "tool" || r["type"] == "seat" { - t.Errorf("resource %v: the indexer holds no seat and its tools are a bundle, not a tool resource", r) + t.Errorf("resource %v: the indexer serves no tool and holds no seat", r) } } if len(procs) != 1 { @@ -131,18 +126,12 @@ func TestManifestProcess(t *testing.T) { // 1238/AC1 if _, bus := p["bus"]; bus { t.Errorf("the process declares a bus: it runs on its own broker credential") } - if len(m.Build.Artifacts) != 2 { + if len(m.Build.Artifacts) != 1 { t.Fatalf("artifacts = %+v", m.Build.Artifacts) } - // The program, and the tools bundle the runtime launches for a read call (issue 1562); no process runs the tools. - for i, binary := range []string{"mesh-delivery-indexer", "mesh-delivery-indexer-tools"} { - a := m.Build.Artifacts[i] - if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != binary || !strings.HasSuffix(a.From, a.Binary) { - t.Errorf("artifact = %+v", a) - } - } - if a := m.Build.Artifacts[1]; a.Name != "tools" || !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-indexer"}) { - t.Errorf("tools artifact = %+v", a) + a := m.Build.Artifacts[0] + if st, err := os.Stat(a.From); err != nil || !st.IsDir() || a.Binary != "mesh-delivery-indexer" || !strings.HasSuffix(a.From, a.Binary) { + t.Errorf("artifact = %+v", a) } } diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 11de832..fcfe983 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -12,10 +12,6 @@ { "name": "manifests", "history": 1 }, { "name": "modules", "history": 20 } ], - "tools": [ - "indexer_module", - "indexer_modules" - ], "reads": [ "mesh-build-go.toolchains", "mesh-build-node.toolchains", @@ -68,17 +64,6 @@ "system": "arch", "from": "cmd/mesh-delivery-indexer", "binary": "mesh-delivery-indexer" - }, - { - "name": "tools", - "kind": "bundle", - "language": "go", - "system": "arch", - "from": "cmd/mesh-delivery-indexer-tools", - "binary": "mesh-delivery-indexer-tools", - "loads": [ - "mesh-delivery-indexer" - ] } ] } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go similarity index 69% rename from modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go rename to modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 2282273..ecef8be 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -1,5 +1,6 @@ -// Command mesh-delivery-indexer-tools serves the indexer's read tools (novox/hq issue 1562): the runtime launches it for a -// call, it reads the module's own modules state and writes nothing. The indexer itself is the other program. +// Command mesh-delivery-view-tools serves the read tools of the indexer's modules (novox/hq issue 1562): the runtime launches it for a +// call, it reads the indexer's modules bucket through the view's reads (mesh-delivery-indexer.modules) and writes nothing. +// The view is the other program: the indexer serves no tool (ADR 0437 §4). package main import ( @@ -8,13 +9,13 @@ import ( "os" stdio "git.novox.be/novox/mesh-sdk/go" - "mesh-delivery-indexer/internal/index" + "mesh-delivery-view/internal/view" ) // Tools are the read tools over a modules state. -func Tools(r index.Reader) []stdio.Tool { +func Tools(r view.Reader) []stdio.Tool { return []stdio.Tool{ - {Name: index.ModuleReadTool, + {Name: view.ModuleReadTool, Description: "What the indexer stored for one module under modules., as stored: its fingerprint (fp, none), the " + "new-form fingerprint (fp2) or why it has none (none2), the inputs it was computed from (inputs2), the module's " + "directory (path) and the toolchains it was computed with. Read-only.", @@ -24,7 +25,7 @@ func Tools(r index.Reader) []stdio.Tool { name, _ := args["module"].(string) return r.Module(name) }}, - {Name: index.ModulesReadTool, + {Name: view.ModulesReadTool, Description: "Every module the indexer holds, each as stored under modules.. With none2 true, only the modules " + "that have no new-form fingerprint, each with the reason (none2). Read-only.", Input: map[string]any{"type": "object", "properties": map[string]any{ @@ -38,8 +39,8 @@ func Tools(r index.Reader) []stdio.Tool { } func main() { - state := stdio.State("modules") - r := index.Reader{State: index.StateRead{Keys: state.Keys, Get: func(key string) (json.RawMessage, error) { + state := stdio.State("mesh-delivery-indexer.modules") + r := view.Reader{State: view.StateRead{Keys: state.Keys, Get: func(key string) (json.RawMessage, error) { e, err := state.Get(key) if err != nil || e == nil { return nil, err @@ -47,7 +48,7 @@ func main() { return e.Value, nil }}} if err := stdio.Serve("", Tools(r)); err != nil { - fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err) + fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err) os.Exit(1) } } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go similarity index 77% rename from modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go rename to modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go index 4e4b2c5..bfa3124 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main_test.go @@ -5,13 +5,13 @@ import ( "strings" "testing" - "mesh-delivery-indexer/internal/index" + "mesh-delivery-view/internal/view" ) type fake map[string]string -func (f fake) reader() index.Reader { - return index.Reader{State: index.StateRead{ +func (f fake) reader() view.Reader { + return view.Reader{State: view.StateRead{ Keys: func() ([]string, error) { var k []string for key := range f { @@ -28,7 +28,7 @@ func (f fake) reader() index.Reader { }} } -func run(t *testing.T, r index.Reader, name string, args map[string]any) (any, error) { +func run(t *testing.T, r view.Reader, name string, args map[string]any) (any, error) { t.Helper() for _, tl := range Tools(r) { if tl.Name == name { @@ -48,7 +48,7 @@ const ( func TestModuleToolAnswersTheStoredValue(t *testing.T) { r := fake{"a": withFP2, "b": withNone}.reader() for key, want := range map[string]string{"a": withFP2, "b": withNone} { - out, err := run(t, r, index.ModuleReadTool, map[string]any{"module": key}) + out, err := run(t, r, view.ModuleReadTool, map[string]any{"module": key}) if err != nil { t.Fatal(err) } @@ -56,7 +56,7 @@ func TestModuleToolAnswersTheStoredValue(t *testing.T) { t.Errorf("%s = %s, want %s", key, got, want) } } - out, _ := run(t, r, index.ModuleReadTool, map[string]any{"module": "a"}) + out, _ := run(t, r, view.ModuleReadTool, map[string]any{"module": "a"}) var m map[string]any if err := json.Unmarshal(out.(json.RawMessage), &m); err != nil || m["fp"] != "a1" || m["fp2"] != "b2" || m["path"] != "." || m["inputs2"] == nil || m["toolchains"] == nil { t.Errorf("stored fields are not all there: %v %v", m, err) @@ -66,10 +66,10 @@ func TestModuleToolAnswersTheStoredValue(t *testing.T) { // A module with no key, and a name that is no key, are said so. func TestModuleToolRefusesWhatIsNotThere(t *testing.T) { r := fake{"a": withFP2}.reader() - if _, err := run(t, r, index.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") { + if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": "gone"}); err == nil || !strings.Contains(err.Error(), "no module gone") { t.Errorf("a missing module: %v", err) } - if _, err := run(t, r, index.ModuleReadTool, map[string]any{"module": ""}); err == nil { + if _, err := run(t, r, view.ModuleReadTool, map[string]any{"module": ""}); err == nil { t.Errorf("an empty name was answered") } } @@ -77,7 +77,7 @@ func TestModuleToolRefusesWhatIsNotThere(t *testing.T) { // 1562/AC1: indexer_modules answers every key as stored, and with none2 only those that say why they have no FP2. func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) { r := fake{"a": withFP2, "b": withNone}.reader() - out, err := run(t, r, index.ModulesReadTool, map[string]any{}) + out, err := run(t, r, view.ModulesReadTool, map[string]any{}) if err != nil { t.Fatal(err) } @@ -85,7 +85,7 @@ func TestModulesToolAnswersEveryKeyAsStored(t *testing.T) { if len(all) != 2 || string(all["a"]) != withFP2 || string(all["b"]) != withNone { t.Errorf("modules = %v", all) } - out, err = run(t, r, index.ModulesReadTool, map[string]any{"none2": true}) + out, err = run(t, r, view.ModulesReadTool, map[string]any{"none2": true}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/internal/index/read.go b/modules/mesh-delivery-view/internal/view/read.go similarity index 78% rename from modules/mesh-delivery-indexer/internal/index/read.go rename to modules/mesh-delivery-view/internal/view/read.go index 61558ab..05e547e 100644 --- a/modules/mesh-delivery-indexer/internal/index/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -1,8 +1,8 @@ -package index +package view -// The indexer's read tools (novox/hq issue 1562): what the indexer stored under modules., answered as stored. The new-form -// fingerprint (FP2, None2, Inputs2), the reason it has none, and the path and toolchains it was computed with are only -// there; a module reading its own state needs no grant. The reads write nothing. +// The read tools of the indexer's modules (novox/hq issue 1562): what the indexer stored under modules., answered as +// stored. The new-form fingerprint (FP2, None2, Inputs2), the reason it has none, and the path and toolchains it was computed +// with are only there. The view reads the bucket through its reads; the reads write nothing. import ( "encoding/json" @@ -19,7 +19,7 @@ const ( ModulesReadTool = "indexer_modules" ) -// StateRead reads the module's own modules state: its keys and a key's stored value (nil for a key with none). +// StateRead reads the indexer's modules state, as the view's reads grant it: its keys and a key's stored value (nil for a key with none). type StateRead struct { Keys func() ([]string, error) Get func(key string) (json.RawMessage, error) diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index f3bc40e..0be6426 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -19,12 +19,13 @@ import ( // takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.** type artifact struct { - Name string `json:"name"` - Kind string `json:"kind"` - Language string `json:"language"` - System string `json:"system"` - From string `json:"from"` - Binary string `json:"binary"` + Name string `json:"name"` + Kind string `json:"kind"` + Language string `json:"language"` + System string `json:"system"` + From string `json:"from"` + Binary string `json:"binary"` + Loads []string `json:"loads"` Context *struct { Repository string `json:"repository"` } `json:"context"` @@ -41,6 +42,7 @@ type manifest struct { Version string `json:"version"` Slug string `json:"slug"` Reads []string `json:"reads"` + Tools []string `json:"tools"` OwnSecrets map[string]string `json:"own-secrets"` SecretsOwner string `json:"secrets-owner"` Resources []map[string]any `json:"resources"` @@ -91,11 +93,21 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 if m.SecretsOwner != "mesh-delivery-view" { t.Errorf("secrets-owner = %q", m.SecretsOwner) } + // The two read tools of the indexer's modules (issue 1562), carried by a bundle the runtime launches; the indexer + // itself serves no tool (ADR 0437 §4). Carrying them gives the tool runner read access to every bucket the view reads. + if want := []string{"indexer_module", "indexer_modules"}; !reflect.DeepEqual(m.Tools, want) { + t.Errorf("tools = %v, want %v", m.Tools, want) + } + for _, a := range m.Build.Artifacts { + if a.Name == "tools" && !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-view"}) { + t.Errorf("the tools artifact loads %v, want the view", a.Loads) + } + } } func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7 m, _ := load(t) - if len(m.Build.Artifacts) != 3 { + if len(m.Build.Artifacts) != 4 { t.Fatalf("artifacts = %+v", m.Build.Artifacts) } for _, a := range m.Build.Artifacts { @@ -119,7 +131,7 @@ func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7 for _, a := range m.Build.Artifacts { names[a.Name] = a.Binary } - if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" { + if names["program"] != "mesh-delivery-view" || names["cli"] != "mesh-delivery-cli" || names["tools"] != "mesh-delivery-view-tools" { t.Errorf("artifacts = %v", names) } } diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index eb33058..a879da8 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -3,6 +3,10 @@ "version": "1", "moved": true, "slug": "view", + "tools": [ + "indexer_module", + "indexer_modules" + ], "reads": [ "mesh-controller.assignments", "mesh-controller.builds", @@ -97,6 +101,17 @@ "from": "cmd/mesh-delivery-cli", "binary": "mesh-delivery-cli" }, + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-delivery-view-tools", + "binary": "mesh-delivery-view-tools", + "loads": [ + "mesh-delivery-view" + ] + }, { "name": "files", "kind": "archive", From 1239199b35dd96c9d8016ecdc9bcdc6e6b8ac6ed Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 13:43:26 +0200 Subject: [PATCH 3/3] Name the view's tools binary as what its bundle loads, as the controller's module check requires of a bundle compiled to a binary (issue 1562) --- modules/mesh-delivery-view/manifest_test.go | 6 ++++-- modules/mesh-delivery-view/module.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/mesh-delivery-view/manifest_test.go b/modules/mesh-delivery-view/manifest_test.go index 0be6426..aa54e36 100644 --- a/modules/mesh-delivery-view/manifest_test.go +++ b/modules/mesh-delivery-view/manifest_test.go @@ -99,8 +99,10 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7 t.Errorf("tools = %v, want %v", m.Tools, want) } for _, a := range m.Build.Artifacts { - if a.Name == "tools" && !reflect.DeepEqual(a.Loads, []string{"mesh-delivery-view"}) { - t.Errorf("the tools artifact loads %v, want the view", a.Loads) + // A bundle compiled to a binary is one executable: what the runtime loads is that binary's name, or the controller's + // module check refuses the manifest (mesh-sdk catalogue build.go, ADR 0193). + if a.Name == "tools" && !reflect.DeepEqual(a.Loads, []string{a.Binary}) { + t.Errorf("the tools artifact loads %v, want its binary %q", a.Loads, a.Binary) } } } diff --git a/modules/mesh-delivery-view/module.json b/modules/mesh-delivery-view/module.json index a879da8..dc14e37 100644 --- a/modules/mesh-delivery-view/module.json +++ b/modules/mesh-delivery-view/module.json @@ -109,7 +109,7 @@ "from": "cmd/mesh-delivery-view-tools", "binary": "mesh-delivery-view-tools", "loads": [ - "mesh-delivery-view" + "mesh-delivery-view-tools" ] }, {