Merge pull request 'Read the indexer's modules keys as stored from the view's tools, so the new-form fingerprint, path and toolchains can be checked live (issue 1562)' (#13) from fix/1562-indexer-reads into main

This commit was merged in pull request #13.
This commit is contained in:
2026-10-21 12:04:07 +00:00
5 changed files with 269 additions and 8 deletions
@@ -0,0 +1,54 @@
// 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 (
"encoding/json"
"fmt"
"os"
stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-delivery-view/internal/view"
)
// Tools are the read tools over a modules state.
func Tools(r view.Reader) []stdio.Tool {
return []stdio.Tool{
{Name: view.ModuleReadTool,
Description: "What the indexer stored for one module under modules.<module>, 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: view.ModulesReadTool,
Description: "Every module the indexer holds, each as stored under modules.<module>. 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("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
}
return e.Value, nil
}}}
if err := stdio.Serve("", Tools(r)); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-view-tools] %v\n", err)
os.Exit(1)
}
}
@@ -0,0 +1,96 @@
package main
import (
"encoding/json"
"strings"
"testing"
"mesh-delivery-view/internal/view"
)
type fake map[string]string
func (f fake) reader() view.Reader {
return view.Reader{State: view.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 view.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, view.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, 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)
}
}
// 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, 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, view.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, view.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, view.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)
}
}
@@ -0,0 +1,82 @@
package view
// The read tools of the indexer's modules (novox/hq issue 1562): what the indexer stored under modules.<m>, 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"
"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 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)
}
// Reader answers the read tools from a modules state.
type Reader struct{ State StateRead }
// Module is the stored value of modules.<module>, 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
}
+22 -8
View File
@@ -19,12 +19,13 @@ import (
// takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.** // takes this manifest (1134/AC7); these tests hold the rules so a unit run catches a refusal early.**
type artifact struct { type artifact struct {
Name string `json:"name"` Name string `json:"name"`
Kind string `json:"kind"` Kind string `json:"kind"`
Language string `json:"language"` Language string `json:"language"`
System string `json:"system"` System string `json:"system"`
From string `json:"from"` From string `json:"from"`
Binary string `json:"binary"` Binary string `json:"binary"`
Loads []string `json:"loads"`
Context *struct { Context *struct {
Repository string `json:"repository"` Repository string `json:"repository"`
} `json:"context"` } `json:"context"`
@@ -41,6 +42,7 @@ type manifest struct {
Version string `json:"version"` Version string `json:"version"`
Slug string `json:"slug"` Slug string `json:"slug"`
Reads []string `json:"reads"` Reads []string `json:"reads"`
Tools []string `json:"tools"`
OwnSecrets map[string]string `json:"own-secrets"` OwnSecrets map[string]string `json:"own-secrets"`
SecretsOwner string `json:"secrets-owner"` SecretsOwner string `json:"secrets-owner"`
Resources []map[string]any `json:"resources"` Resources []map[string]any `json:"resources"`
@@ -91,11 +93,23 @@ func TestManifestIsTakenAndReadsOnly(t *testing.T) { // 1134/AC7
if m.SecretsOwner != "mesh-delivery-view" { if m.SecretsOwner != "mesh-delivery-view" {
t.Errorf("secrets-owner = %q", m.SecretsOwner) 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 {
// 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)
}
}
} }
func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7 func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
m, _ := load(t) m, _ := load(t)
if len(m.Build.Artifacts) != 3 { if len(m.Build.Artifacts) != 4 {
t.Fatalf("artifacts = %+v", m.Build.Artifacts) t.Fatalf("artifacts = %+v", m.Build.Artifacts)
} }
for _, a := range m.Build.Artifacts { for _, a := range m.Build.Artifacts {
@@ -119,7 +133,7 @@ func TestManifestIsWholeInItsRepository(t *testing.T) { // 1134/AC7
for _, a := range m.Build.Artifacts { for _, a := range m.Build.Artifacts {
names[a.Name] = a.Binary 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) t.Errorf("artifacts = %v", names)
} }
} }
+15
View File
@@ -3,6 +3,10 @@
"version": "1", "version": "1",
"moved": true, "moved": true,
"slug": "view", "slug": "view",
"tools": [
"indexer_module",
"indexer_modules"
],
"reads": [ "reads": [
"mesh-controller.assignments", "mesh-controller.assignments",
"mesh-controller.builds", "mesh-controller.builds",
@@ -97,6 +101,17 @@
"from": "cmd/mesh-delivery-cli", "from": "cmd/mesh-delivery-cli",
"binary": "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-tools"
]
},
{ {
"name": "files", "name": "files",
"kind": "archive", "kind": "archive",