From 558a93c25b56ff7aca249661ce136bd6eae3d4ab Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 05:46:16 +0100 Subject: [PATCH] AM-D1: the validator refuses a mark nothing enforces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A manifest marking a tool its tools do not list, a verb its claims do not serve or an event it does not carry, one name both a tool and an event, a marked tool or verb (or a held seat verb its seat marks) without operator-channel in uses, or a marked tool or verb while every bundle it builds is run by its own process, is refused by both paths, naming the entry and the rule. The login shell's captured manifest is copied at AM-K1, which gives it operator-channel. Builds: design 46 7-approval-mark §4, §7, §21; design 34 every-event §32; lead answers A-177, A-183 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-delivery-indexer/judge/rules.go | 98 +++++++++++++++++++ .../mesh-delivery-indexer/judge/rules_test.go | 40 +++++++- .../testdata/beside/CAPTURED | 2 + .../modules/zsh/module.json.captured | 5 +- 4 files changed, 143 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index affc57b8..6a389aca 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -2938,6 +2938,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M } } problems = append(problems, invokeProblems(m)...) + problems = append(problems, needsApprovalProblems(m)...) problems = append(problems, replacesProblems(m)...) problems = append(problems, UnitSettingProblems(m)...) problems = append(problems, OwnerSettingProblems(m)...) @@ -3389,6 +3390,103 @@ func replacesProblems(m catalogue.Manifest) []string { return problems } +// needsApprovalProblems refuses a mark the mesh cannot honour (design 46 *The approval mark* §4; design 34 every-event +// §32; ADR 0286 §4): a mark on a tool its tools do not list (where it lists them; a bundle's own tools are checked by +// the tool runner at load), on a verb its claims do not serve, or on an event neither its emits nor its streams' events +// carry; one name both one of its tools and one of its events; a tool or verb marked, or a held seat verb its seat +// marks, without operator-channel in uses (§7: a module marking only events needs none); and a tool or verb marked +// while every bundle it builds is run by one of its process resources, so its own program answers and no tool runner +// gates them. Each refusal names the entry and the rule. +func needsApprovalProblems(m catalogue.Manifest) []string { + var problems []string + tools, events, served := map[string]bool{}, map[string]bool{}, map[string]map[string]bool{} + for _, t := range m.Tools { + tools[t] = true + } + for _, e := range m.Emits { + events[e] = true + } + for _, st := range m.Streams { + for _, e := range st.Events { + events[e] = true + } + } + for _, c := range m.Claims { + served[c.Name] = map[string]bool{} + for _, v := range c.ServesFor(m) { + served[c.Name][v] = true + } + } + var acts []string // the tools and verbs it marks, and the held seat verbs its seats mark + for _, e := range m.NeedsApproval { + seat, verb, dotted := strings.Cut(e, ".") + switch { + case tools[e] && events[e]: + problems = append(problems, fmt.Sprintf("%s marks %q in needs-approval, which names both one of its tools "+ + "and one of its events: an entry names one thing (design 46 approval mark §4)", m.Module, e)) + case events[e]: + case tools[e], dotted && served[seat][verb]: + acts = append(acts, e) + case dotted && served[seat] != nil: + problems = append(problems, fmt.Sprintf("%s marks the verb %q in needs-approval, which its claim of %s does "+ + "not serve (ADR 0286 §4)", m.Module, e, seat)) + case dotted: + problems = append(problems, fmt.Sprintf("%s marks %q in needs-approval, which is neither a verb its claims "+ + "serve nor an event its emits or its streams' events carry (ADR 0286 §4; design 34 every-event §32)", m.Module, e)) + case len(m.Tools) > 0: + problems = append(problems, fmt.Sprintf("%s marks the tool %q in needs-approval, which its tools do not list "+ + "(ADR 0286 §4)", m.Module, e)) + default: // a tool its bundle registers: the tool runner checks it at load (§4) + acts = append(acts, e) + } + } + marked := len(acts) > 0 + for _, c := range m.Claims { + seat, ok := catalogue.SeatNamed(c.Name) + if !ok { + continue + } + for _, v := range seat.Serves { + if v.NeedsApproval && served[c.Name][v.Name] { + acts = append(acts, c.Name+"."+v.Name) + } + } + } + if len(acts) > 0 && !slices.Contains(m.Uses, "operator-channel") { + problems = append(problems, fmt.Sprintf("%s marks or holds a marked tool or verb (%s) without operator-channel in "+ + "uses: its tool runner asks the operator through that seat (design 46 approval mark §4, §7)", + m.Module, strings.Join(acts, ", "))) + } + if marked && everyBundleRunByAProcess(m) { + problems = append(problems, fmt.Sprintf("%s marks a tool or verb while every bundle it builds is run by one of "+ + "its process resources: its own program answers them and no tool runner gates them, so the mark would guard "+ + "nothing (design 46 approval mark §4)", m.Module)) + } + return problems +} + +// everyBundleRunByAProcess says every artifact of kind bundle the module builds is the artifact of one of its process +// resources (none built is every one run so: no tool runner loads any). +func everyBundleRunByAProcess(m catalogue.Manifest) bool { + run := map[string]bool{} + for _, r := range m.Resources { + if r["type"] == "process" { + if a, ok := r["artifact"].(string); ok { + run[a] = true + } + } + } + if m.Build == nil { + return true + } + for _, a := range m.Build.Artifacts { + if a.Kind == "bundle" && !run[a.Name] { + return false + } + } + return true +} + // whileStoppedProblems judges one container's maintenance window against its own definition // (novox/hq ADR 0189). // diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 15b0f1c6..214c8aea 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -3173,6 +3173,43 @@ func caseAManifestCarryingWaitsIsAFieldNotKnown(t *testing.T) { } } +// AM-D1 (design 46 approval mark §4, §21; design 34 every-event §32): a mark nothing enforces is refused, naming the +// entry and the rule; the login shell's and the forge's manifests of §21 are accepted, and a module marking only events +// needs no operator-channel. +func caseAMarkNothingEnforcesIsRefused(t *testing.T) { + const bundle = `"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["tools/index.js"]}]}` + const oc = `"uses":["operator-channel"]` + shell := `{"module":"zsh","claims":[{"name":"node-login-shell","scope":"node","serves":["execute"]}],"tools":["zsh_config"],` + bundle + for _, row := range []struct{ name, manifest, want string }{ + {"a tool its tools do not list", `{"module":"shop","tools":["sell"],"needs-approval":["refund"],` + oc + `,` + bundle + `}`, "its tools do not list"}, + {"a verb its claims do not serve", `{"module":"shop","claims":[{"name":"shop-till","serves":["price"]}],"tools":["price"],"needs-approval":["shop-till.refund"],` + oc + `,` + bundle + `}`, "does not serve"}, + {"a marked tool without operator-channel", `{"module":"shop","tools":["sell"],"needs-approval":["sell"],` + bundle + `}`, "without operator-channel"}, + {"a held seat verb its seat marks without operator-channel", shell + `}`, "without operator-channel"}, + {"every bundle run by its own process", `{"module":"shop","tools":["sell"],"needs-approval":["sell"],` + oc + `,` + bundle + + `,"resources":[{"id":"serve","type":"process","artifact":"tools","user":"shop"}]}`, "every bundle it builds is run"}, + {"one name a tool and an event", `{"module":"shop","tools":["sold"],"emits":["sold"],"needs-approval":["sold"],` + oc + `,` + bundle + `}`, "both one of its tools"}, + {"an event it does not carry", `{"module":"shop","emits":["sold"],"needs-approval":["sold.again"]}`, "nor an event its emits"}, + {"the login shell", shell + `,` + oc + `}`, ""}, + {"the forge", `{"module":"forge","needs-approval":["gitea_branch_protection_loosen"],` + oc + `,"build":{"artifacts":[` + + `{"name":"code","kind":"bundle","language":"typescript","entrypoints":["tools/index.js"],"loads":["tools/index.js"]},` + + `{"name":"repositories","kind":"bundle","language":"typescript","entrypoints":["repositories/index.js"]}]},` + + `"resources":[{"id":"repositories","type":"process","artifact":"repositories","user":"forge"}]}`, ""}, + {"only events, no operator-channel", `{"module":"shop","emits":["sold"],"streams":[{"name":"ledger","events":["booked"],"max-age-days":1,"max-mib":1}],"needs-approval":["sold","booked"]}`, ""}, + } { + var problems []string + WithSeats([]catalogue.Seat{{Name: "node-login-shell", Serves: []catalogue.Verb{{Name: "execute", NeedsApproval: true}}}}, func() { + _, problems = Manifest(versioned(t, []byte(row.manifest)), nil) + }) + got := strings.Join(problems, "\n") + switch { + case row.want == "" && got != "": + t.Errorf("%s is refused: %s", row.name, got) + case row.want != "" && !strings.Contains(got, row.want): + t.Errorf("%s: problems %q, want one saying %q", row.name, got, row.want) + } + } +} + // 1178/AC2: a work-queue stream is read by exactly one process of its module and by no other module's consumes: one // process naming it is accepted; none, two, or another module's consumes is refused, naming it. func caseAWorkQueueStreamIsReadByExactlyOneProcessOfItsModule(t *testing.T) { @@ -3974,7 +4011,7 @@ func caseNoModuleMayDeclareTheLoginShell(t *testing.T) { t.Fatalf("declaring login-shell was not refused by name: %q", got) } // And a shell module claiming the mesh's seat, serving execute, is what the seat is for. - m, err := ParseManifest([]byte(`{"module":"zsh","tools":["execute"],` + + m, err := ParseManifest([]byte(`{"module":"zsh","tools":["execute"],"uses":["operator-channel"],` + `"claims":[{"name":"node-login-shell","scope":"node"}]}`)) if err != nil { t.Fatal(err) @@ -6382,6 +6419,7 @@ func TestRulesCatalogue(t *testing.T) { // TestRulesManifests runs the tests of one kind as subtests, each by its former name. func TestRulesManifests(t *testing.T) { t.Run("AMalformedEnvironmentIsRefusedAtParse", caseAMalformedEnvironmentIsRefusedAtParse) + t.Run("AMarkNothingEnforcesIsRefused", caseAMarkNothingEnforcesIsRefused) t.Run("AMalformedFamilyIsRefused", caseAMalformedFamilyIsRefused) t.Run("AMalformedPromptIsRefused", caseAMalformedPromptIsRefused) t.Run("AManifestIsJudgedAgainstTheListOfItsTree", caseAManifestIsJudgedAgainstTheListOfItsTree) diff --git a/modules/mesh-delivery-indexer/testdata/beside/CAPTURED b/modules/mesh-delivery-indexer/testdata/beside/CAPTURED index 6db0cff8..f4430ea3 100644 --- a/modules/mesh-delivery-indexer/testdata/beside/CAPTURED +++ b/modules/mesh-delivery-indexer/testdata/beside/CAPTURED @@ -13,6 +13,8 @@ was mesh-controller's copy, which still held two modules that had left the catal mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json mesh-host 233bb23bbcb1a829ce2a6164a31a5c5cc4f3d950 internal/apply/placement_guard.go +mesh-catalog 2d5358b8 (milestone 47/G's AM-K1) modules/zsh/module.json alone: the login shell uses operator-channel for + its seat's marked execute (design 46 approval mark §21), which the validator requires (§4) To move them, from mesh-delivery's modules/mesh-delivery-indexer directory, with the two repositories checked out beside mesh-delivery: diff --git a/modules/mesh-delivery-indexer/testdata/beside/mesh-catalog/modules/zsh/module.json.captured b/modules/mesh-delivery-indexer/testdata/beside/mesh-catalog/modules/zsh/module.json.captured index da125af2..028f2213 100644 --- a/modules/mesh-delivery-indexer/testdata/beside/mesh-catalog/modules/zsh/module.json.captured +++ b/modules/mesh-delivery-indexer/testdata/beside/mesh-catalog/modules/zsh/module.json.captured @@ -1,6 +1,6 @@ { "module": "zsh", - "version": "1", + "version": "0.1", "moved": true, "capabilities": [ "package-manager" @@ -14,6 +14,9 @@ ] } ], + "uses": [ + "operator-channel" + ], "tools": [ "zsh_config" ],