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/merge-gate head 6c2b79acf106 merged into trunk d68d7ef3d9af — fail: builds mesh-delivery-view → shanks; no bus step; a manifest the change touches fai…
mesh/repo-check head 6c2b79acf106 merged into trunk d68d7ef3d9af — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-21 13:24:10 +02:00
parent e6ce4be4b4
commit 6c2b79acf1
7 changed files with 73 additions and 71 deletions
+13 -24
View File
@@ -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)
}
}
-15
View File
@@ -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"
]
}
]
}
@@ -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.<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.",
@@ -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.<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{
@@ -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)
}
}
@@ -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)
}
@@ -1,8 +1,8 @@
package index
package view
// The indexer's read tools (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; 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.<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"
@@ -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)
+20 -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.**
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)
}
}
+15
View File
@@ -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",