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:
@@ -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:
|
||||
|
||||
Vendored
+4
-1
@@ -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"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user