AM-D1: the validator refuses a mark nothing enforces

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
This commit is contained in:
2026-11-12 05:46:16 +01:00
parent 7ce80ce590
commit 558a93c25b
4 changed files with 143 additions and 2 deletions
@@ -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).
//
@@ -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)
@@ -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:
@@ -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"
],