From 4194e93a6742a634e398278e737535507ac07f33 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 12:17:50 +0100 Subject: [PATCH] Area 2 (join 11): every seat verb and build-seat queue a delivery module calls is in its invokes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grants part grants a seat verb only to a module that names it in invokes (GR8), so a call missing from a manifest is refused live. Listed every module's seat-verb calls in code (constant clients, seatlist.HolderOf, operatorask, buildrun's drops and puts) against its invokes: the teller asks who holds operator-channel (operatorask.Bucket) and the checker drops asks of the image build seat. Every asker of a build seat names the queues it asks as seat:.accept. (A-231): the runner check and delivery, the assigner check, the builder delivery, the checker check and recheck; the indexer had its entry. The validator judges that form through mesh-sdk's catalogue.QueueInvoke against the kinds the seat accepts. mesh-grants: a consumed seat view whose seat has no holder on record grants nothing instead of refusing the whole module (TestGrants); TestCallersAndWhyRefused and TestSentStoresRead take §610's class defaults. Builds: design 47 G §544, §601, §610, §616, A-231, A-179 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-delivery-assigner/manifest_test.go | 1 + modules/mesh-delivery-assigner/module.json | 3 ++- modules/mesh-delivery-builder/manifest_test.go | 4 +++- modules/mesh-delivery-builder/module.json | 5 ++++- modules/mesh-delivery-checker/manifest_test.go | 7 ++++++- modules/mesh-delivery-checker/module.json | 9 ++++++++- modules/mesh-delivery-indexer/judge/rules.go | 5 +++++ modules/mesh-delivery-indexer/judge/rules_test.go | 4 ++++ modules/mesh-delivery-pipeline/manifest_test.go | 5 ++++- modules/mesh-delivery-pipeline/module.json | 8 +++++++- modules/mesh-delivery-teller/manifest_test.go | 4 +++- modules/mesh-delivery-teller/module.json | 3 ++- modules/mesh-grants/internal/buscompose/nats.go | 15 +++++++++++++++ .../mesh-grants/internal/grants/callers_test.go | 14 ++++++++------ .../internal/grants/sentobjects_test.go | 2 +- 15 files changed, 73 insertions(+), 16 deletions(-) diff --git a/modules/mesh-delivery-assigner/manifest_test.go b/modules/mesh-delivery-assigner/manifest_test.go index f190cfcf..6c4b465b 100644 --- a/modules/mesh-delivery-assigner/manifest_test.go +++ b/modules/mesh-delivery-assigner/manifest_test.go @@ -144,6 +144,7 @@ func TestManifest(t *testing.T) { "seat:mesh-delivery.source", // §507: the branches the gate keeps a source revision of, before the first registrations "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone "seat:node-backup.backed-up", "seat:node-backup.now", + "seat:node-build-go.accept.check", // A-231: the composition run's ask on the Go build seat's queue check (accept.check.) "seat:node-build-go.output", // A-254: a composition run's answer kept as the build seat's object "seat:node-engine.compose-candidate", // §354, A-226: each machine's composition input "seat:node-engine.manifest-reads", // ADR 0618 §3 diff --git a/modules/mesh-delivery-assigner/module.json b/modules/mesh-delivery-assigner/module.json index 0f38ffb2..08a3e60b 100644 --- a/modules/mesh-delivery-assigner/module.json +++ b/modules/mesh-delivery-assigner/module.json @@ -84,7 +84,8 @@ "seat:node-build-go.output", "seat:node-engine.compose-candidate", "seat:node-engine.manifest-reads", - "seat:operator-channel.asks" + "seat:operator-channel.asks", + "seat:node-build-go.accept.check" ], "streams": [ { diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index e09f37f8..01fdc05f 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -146,7 +146,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 if want := []string{"seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", "seat:mesh-delivery.source", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", "seat:git.tags", "seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove", - "seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { + "seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions", + // A-231: the build asks on each build seat's queue delivery (accept.delivery.). + "seat:node-build-go.accept.delivery", "seat:node-build-node.accept.delivery", "seat:node-build-image.accept.delivery"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // The builder hears the build seats' toolchain-reported and the node-engine seat's running (design 47 G §492, §546) and reads no diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index e27cc4b4..1b724b7f 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -106,7 +106,10 @@ "seat:mesh-catalog.registrations", "seat:mesh-catalog.seat-definitions", "seat:mesh-catalog.revision", - "seat:mesh-delivery.source" + "seat:mesh-delivery.source", + "seat:node-build-go.accept.delivery", + "seat:node-build-node.accept.delivery", + "seat:node-build-image.accept.delivery" ], "state": [ { "name": "builds", "history": 5 }, diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index ca6dcf3f..d57e1e72 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -132,7 +132,12 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 if want := []string{"mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined", "mesh-delivery-indexer.indexer_read_seat_definitions", - "seat:git.file", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + "seat:git.file", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", "seat:node-engine.compose-candidate", + // The image build seat's drop too: a check of a module built by it is asked of it, and dropped of it (buildrun.SeatDrops). + "seat:node-build-image.drop", + // A-231: the check asks on each build seat's queues check and recheck. + "seat:node-build-go.accept.check", "seat:node-build-go.accept.recheck", "seat:node-build-node.accept.check", + "seat:node-build-node.accept.recheck", "seat:node-build-image.accept.check", "seat:node-build-image.accept.recheck"}; !reflect.DeepEqual(sorted(m.Invokes), sorted(want)) { t.Errorf("invokes = %v, want the seat git's file, the seats' drop and compose-candidate", m.Invokes) } if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index d004849d..8efc3476 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -95,7 +95,14 @@ "seat:mesh-module-assigner.assignments", "seat:node-build-go.drop", "seat:node-build-node.drop", - "seat:node-engine.compose-candidate" + "seat:node-engine.compose-candidate", + "seat:node-build-image.drop", + "seat:node-build-go.accept.check", + "seat:node-build-go.accept.recheck", + "seat:node-build-node.accept.check", + "seat:node-build-node.accept.recheck", + "seat:node-build-image.accept.check", + "seat:node-build-image.accept.recheck" ], "own-secrets": { diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 6a389aca..eeb762f9 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -4580,6 +4580,11 @@ func seatInvokeProblems(shelf catalogue.Shelf, declared map[string]catalogue.Sea problems = append(problems, fmt.Sprintf("%s invokes %s, a verb of the seat %q, "+catalogue.UndeclaredSeat, module, invoke, seat)) continue } + // An asker's queue of a build seat, `seat:.accept.` (design 47 G §544, §601, A-231), read by mesh-sdk's + // one reader: judged against the kinds the seat's definition accepts. + if _, queue, isQueue := catalogue.QueueInvoke(invoke); isQueue { + verb = queue + } if !slices.ContainsFunc(verbs, func(v catalogue.Verb) bool { return v.Name == verb }) && !acceptsKind(seat, verb, declared) { problems = append(problems, fmt.Sprintf("%s invokes %s, and the seat %s serves no verb %q", module, invoke, seat, verb)) continue diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 05d8f78a..a590f2b2 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -6538,6 +6538,10 @@ func TestASeatNamedReadIsJudgedAgainstTheSeatsStates(t *testing.T) { {"seat:mesh-provisioner.check.step", ""}, {"seat:mesh-provisioner.delivery.step", `serves no verb "delivery.step"`}, {"seat:mesh-provisioner.check.other", `serves no verb "check.other"`}, + // A-231: an asker names the queue it asks as `seat:.accept.`, a kind the seat accepts. + {"seat:mesh-provisioner.accept.step", ""}, + {"seat:mesh-provisioner.accept.other", `serves no verb "other"`}, + {"seat:mesh-provisioner.accept.check.step", `serves no verb "accept.check.step"`}, } { shelf["caller"] = catalogue.Manifest{Module: "caller", Invokes: []string{row.invoke}} // The manifest's own invokes rule (at parse) takes the dotted kind as well; the seat's rule then judges it. diff --git a/modules/mesh-delivery-pipeline/manifest_test.go b/modules/mesh-delivery-pipeline/manifest_test.go index ea6925a7..76a9cc5f 100644 --- a/modules/mesh-delivery-pipeline/manifest_test.go +++ b/modules/mesh-delivery-pipeline/manifest_test.go @@ -100,7 +100,10 @@ func TestManifest(t *testing.T) { "seat:mesh-builds.step", "seat:mesh-wants.step", "seat:node-build-go.check.step", "seat:node-build-node.check.step", "seat:node-build-image.check.step", // RN22 (§86, §415, A-207): a recovery's steps on the build seats' stream delivery. - "seat:node-build-go.delivery.step", "seat:node-build-node.delivery.step", "seat:node-build-image.delivery.step"})}, + "seat:node-build-go.delivery.step", "seat:node-build-node.delivery.step", "seat:node-build-image.delivery.step", + // A-231: the queues of the build seats it asks, check for the steps and delivery for a recovery's. + "seat:node-build-go.accept.check", "seat:node-build-go.accept.delivery", "seat:node-build-node.accept.check", + "seat:node-build-node.accept.delivery", "seat:node-build-image.accept.check", "seat:node-build-image.accept.delivery"})}, {"uses operator-channel", equalSets(m.Uses, []string{"operator-channel"})}, // RN25 (§539): the runner's tools carry the prefix runner_. {"first-view tools", equalSets(m.Tools, []string{"runner_first_views", "runner_first_view_again", "runner_health", "runner_waits", "runner_release", "runner_unlogged", "runner_unlogged_append"})}, diff --git a/modules/mesh-delivery-pipeline/module.json b/modules/mesh-delivery-pipeline/module.json index 665b4fd9..98214688 100644 --- a/modules/mesh-delivery-pipeline/module.json +++ b/modules/mesh-delivery-pipeline/module.json @@ -73,7 +73,13 @@ "seat:node-build-image.delivery.step", "seat:node-build-go.drop", "seat:node-build-node.drop", - "seat:node-build-image.drop" + "seat:node-build-image.drop", + "seat:node-build-go.accept.check", + "seat:node-build-go.accept.delivery", + "seat:node-build-node.accept.check", + "seat:node-build-node.accept.delivery", + "seat:node-build-image.accept.check", + "seat:node-build-image.accept.delivery" ], "uses": [ "operator-channel" diff --git a/modules/mesh-delivery-teller/manifest_test.go b/modules/mesh-delivery-teller/manifest_test.go index c043cadf..65a3c6e3 100644 --- a/modules/mesh-delivery-teller/manifest_test.go +++ b/modules/mesh-delivery-teller/manifest_test.go @@ -104,7 +104,9 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1 } // Its own states answers and teller-marks are said as its views family views.answers, read by teller_read_answers (A-175, A-178). // The join: its first view asked again by hand is a hand act, recorded through the seat (views.StatusTools). - if want := []string{"mesh-delivery-assigner.assigner_read_asks", "mesh-delivery-view.view_read_answers", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", "seat:operator-channel.notify"}; !reflect.DeepEqual(m.Invokes, want) { + if want := []string{"mesh-delivery-assigner.assigner_read_asks", "mesh-delivery-view.view_read_answers", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", "seat:operator-channel.notify", + // A-126: who holds the seat operator-channel, whose state asks holds the router's record of its asks (operatorask.Bucket). + "seat:mesh-module-assigner.assignments"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // design 47 G §335 (TL1): the widening of a pipeline end is told through the seat operator-channel and kept in widens-said. diff --git a/modules/mesh-delivery-teller/module.json b/modules/mesh-delivery-teller/module.json index 79b3fc66..424f3a17 100644 --- a/modules/mesh-delivery-teller/module.json +++ b/modules/mesh-delivery-teller/module.json @@ -16,7 +16,8 @@ "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.registrations", "seat:mesh-broker.show", - "seat:operator-channel.notify" + "seat:operator-channel.notify", + "seat:mesh-module-assigner.assignments" ], "consumes": [ "mesh-conditions.findings-asked", diff --git a/modules/mesh-grants/internal/buscompose/nats.go b/modules/mesh-grants/internal/buscompose/nats.go index 320910a3..fb77edff 100644 --- a/modules/mesh-grants/internal/buscompose/nats.go +++ b/modules/mesh-grants/internal/buscompose/nats.go @@ -874,6 +874,9 @@ func permissionsFor(p Principal) (Permissions, error) { // And the events the node-engine's manifest consumes (the backup holder's word that it backed the module up, ADR 0539 // clause 16; the status view's `settled`, design 51): by the emitter's own subject, as any consumer's. for _, c := range p.Consumes { + if unresolvedSeatView(c) { + continue + } subject, err := consumedSubject(c) if err != nil { return Permissions{}, err @@ -1014,6 +1017,9 @@ func permissionsFor(p Principal) (Permissions, error) { // 2. What it consumes, by the emitter's own subject — an event is addressed to its // emitter, because the emitter's identity is the meaning (ADR 0118). for _, c := range p.Consumes { + if unresolvedSeatView(c) { + continue + } subject, err := consumedSubject(c) if err != nil { return Permissions{}, err @@ -1490,6 +1496,15 @@ func consumedSubject(pattern string) (string, error) { return "mesh.mod." + emitter + ".event." + strings.Join(out, "."), nil } +// unresolvedSeatView is a `consumes` entry still naming a seat's view as `seat:.views.`: the grants part +// resolves it to the seat's holder on record, and one whose seat has no holder on record stays as written and grants +// nothing (design 47 G §616), rather than refusing the whole module as a malformed event would. +func unresolvedSeatView(c string) bool { + rest, isSeat := strings.CutPrefix(c, "seat:") + _, view, named := strings.Cut(rest, ".views.") + return isSeat && named && view != "" +} + // catalogueTheRest is the mesh's wildcard for "the rest of a name", duplicated from the catalogue // package for the one direction of dependency the build queue's name is duplicated for. const catalogueTheRest = "**" diff --git a/modules/mesh-grants/internal/grants/callers_test.go b/modules/mesh-grants/internal/grants/callers_test.go index 2c73338b..c6977e93 100644 --- a/modules/mesh-grants/internal/grants/callers_test.go +++ b/modules/mesh-grants/internal/grants/callers_test.go @@ -23,16 +23,18 @@ func (c *canCall) Call(key string, body any) (json.RawMessage, error) { return json.Marshal(map[string]any{"allowed": c.allowed}) } -// grants_callers and grants_why_refused read the definitions' callers (design 47 G §83): a verb that names none takes its -// class's default, and a refused account is told the call subject it lacks and the callers the definition admits. +// grants_callers and grants_why_refused read the definitions' callers (design 47 G §83): every verb takes its class's +// default (§610: no verb names its callers; an act's are the operator, sessions and invokers, a read's invokers, persons +// and sessions), and a refused account is told the call subject it lacks and the callers the definition admits. func TestCallersAndWhyRefused(t *testing.T) { defs := map[string]seatdef.Definition{"mesh-wants": {Seat: "mesh-wants", Scope: "mesh", Verbs: []seatdef.Verb{ {Name: "upgrade", Class: seatdef.ClassAct}, - {Name: "entry", Callers: &seatdef.Callers{List: []string{"seat:mesh-pipeline-runner", "operator"}}}, + {Name: "entry", Class: seatdef.ClassRead}, }}} lines := CallerLines(defs, "mesh-wants", "") - if len(lines) != 2 || lines[0].Verb != "entry" || !lines[0].Named || lines[1].Named || - strings.Join(lines[1].Callers, ",") != "operator,sessions" { + if len(lines) != 2 || lines[0].Verb != "entry" || lines[0].Named || lines[1].Named || + strings.Join(lines[0].Callers, ",") != "invokers,persons,sessions" || + strings.Join(lines[1].Callers, ",") != "operator,sessions,invokers" { t.Fatalf("lines: %+v", lines) } can := &canCall{} @@ -43,7 +45,7 @@ func TestCallersAndWhyRefused(t *testing.T) { } out := got.(map[string]any) if out["lacks"] != "mesh.seat.mesh-wants.call.upgrade.one~shop" || can.asked["subject"] != out["lacks"] || - !strings.Contains(out["says"].(string), "operator, sessions") { + !strings.Contains(out["says"].(string), "operator, sessions, invokers") { t.Errorf("a refused caller: %+v", out) } can.allowed = true diff --git a/modules/mesh-grants/internal/grants/sentobjects_test.go b/modules/mesh-grants/internal/grants/sentobjects_test.go index efce0b8b..3f02cf7f 100644 --- a/modules/mesh-grants/internal/grants/sentobjects_test.go +++ b/modules/mesh-grants/internal/grants/sentobjects_test.go @@ -28,7 +28,7 @@ func TestSentStoresRead(t *testing.T) { Invokes: []string{"seat:orders-seat.place"}}, []string{"shop_sent-objects"}}, {"invokes a verb that names it", catalogue.Manifest{Module: "plain", Invokes: []string{"seat:orders-seat.place"}}, []string{"shop_sent-objects"}}, {"invokes a read, its callers include invokers", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.list"}}, []string{"shop_sent-objects"}}, - {"invokes an act no one names it for", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.wipe"}}, nil}, + {"invokes an act, its callers include invokers (§610)", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.wipe"}}, []string{"shop_sent-objects"}}, {"invokes a verb that names others", catalogue.Manifest{Module: "bank", Invokes: []string{"seat:orders-seat.place"}}, nil}, {"declares neither", catalogue.Manifest{Module: "bank"}, nil}, {"its own store is its binding's", holder, nil},