From da2f8cfc105f5f1f24b80869c9c32adb5ca02988 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:39:04 +0100 Subject: [PATCH] =?UTF-8?q?Judge:=20no=20seat=20list=20compiled=20in;=20it?= =?UTF-8?q?=20judges=20against=20the=20seat=20list=20its=20host=20reads=20?= =?UTF-8?q?(the=20request's=20seats,=20UseSeatList),=20the=20seat=20names?= =?UTF-8?q?=20from=20the=20contracts=20(D-29,=20ADR=200634=20=C2=A74,=20AD?= =?UTF-8?q?R=200636=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../cmd/mesh-judge/main.go | 2 +- modules/mesh-delivery-indexer/go.mod | 1 + .../judge/busstate_test.go | 2 +- modules/mesh-delivery-indexer/judge/judge.go | 18 +- .../mesh-delivery-indexer/judge/judge_test.go | 2 +- .../mesh-delivery-indexer/judge/main_test.go | 15 + modules/mesh-delivery-indexer/judge/rules.go | 1207 +---------------- .../mesh-delivery-indexer/judge/rules_test.go | 114 +- sdk/composer/composer.go | 4 + 9 files changed, 56 insertions(+), 1309 deletions(-) create mode 100644 modules/mesh-delivery-indexer/judge/main_test.go diff --git a/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go b/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go index 8cd30382..0ca1dd94 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-judge/main.go @@ -13,7 +13,7 @@ import ( "io" "os" - "git.novox.be/novox/mesh-sdk/go/composer" + "git.novox.be/novox/mesh-delivery/sdk/composer" "mesh-delivery-indexer/judge" ) diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 7abefe1e..a50d0f35 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -31,6 +31,7 @@ require ( ) require ( + git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639 diff --git a/modules/mesh-delivery-indexer/judge/busstate_test.go b/modules/mesh-delivery-indexer/judge/busstate_test.go index 725a2ccc..a38f7943 100644 --- a/modules/mesh-delivery-indexer/judge/busstate_test.go +++ b/modules/mesh-delivery-indexer/judge/busstate_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" ) // A manifest's bus state, asked for in `requires` by template, is judged once and each refusal names the module, the entry diff --git a/modules/mesh-delivery-indexer/judge/judge.go b/modules/mesh-delivery-indexer/judge/judge.go index 6a688ab0..f2d2a867 100644 --- a/modules/mesh-delivery-indexer/judge/judge.go +++ b/modules/mesh-delivery-indexer/judge/judge.go @@ -15,6 +15,7 @@ import ( "encoding/hex" json "encoding/json" "fmt" + "git.novox.be/novox/mesh-sdk/go/seatdef" "io" "maps" "os" @@ -25,8 +26,8 @@ import ( "strings" "sync" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" ) // Manifest judges one manifest alone: decoded strictly, and every rule a manifest is held to by itself. The list of @@ -209,6 +210,17 @@ const Unnamed = "(between manifests)" // are judged against (nil judges no name). The verdict's SDK names this judge (Identity). A manifest left as a digest, or // a case named twice, is a request it cannot run. func Request(r composer.Request) (composer.Verdict, error) { + if len(r.Seats) > 0 { + defs := make([]seatdef.Definition, 0, len(r.Seats)) + for _, raw := range r.Seats { + d, err := seatdef.Parse(raw) + if err != nil { + return composer.Verdict{}, fmt.Errorf("a seat definition of the request: %w", err) + } + defs = append(defs, d) + } + UseSeatList(defs) + } v := composer.Verdict{V: composer.Version, SDK: Identity()} seen := map[string]bool{} counts := trunkOldSections(r) @@ -242,6 +254,10 @@ func Request(r composer.Request) (composer.Verdict, error) { return v, nil } +// UseSeatList gives the judge the seat list it judges against: the definitions the seat list reader holds (mesh-sdk seatlist, ADR +// 0636 §1), or a request's Seats. No list is compiled into the judge (ADR 0634 §4); an empty one is ignored, as the catalogue's is. +func UseSeatList(defs []seatdef.Definition) { catalogue.UseSeats(catalogue.SeatsFromDefinitions(defs)) } + // TrunkCase is the case of a request holding the catalogue as the trunk holds it (the checker's JudgeTrunk). const TrunkCase = "trunk" diff --git a/modules/mesh-delivery-indexer/judge/judge_test.go b/modules/mesh-delivery-indexer/judge/judge_test.go index 92b9e9a9..3e743737 100644 --- a/modules/mesh-delivery-indexer/judge/judge_test.go +++ b/modules/mesh-delivery-indexer/judge/judge_test.go @@ -6,8 +6,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/overlay" ) diff --git a/modules/mesh-delivery-indexer/judge/main_test.go b/modules/mesh-delivery-indexer/judge/main_test.go new file mode 100644 index 00000000..8f10053a --- /dev/null +++ b/modules/mesh-delivery-indexer/judge/main_test.go @@ -0,0 +1,15 @@ +package judge + +import ( + "os" + "testing" + + "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" +) + +// TestMain gives the judge the seat list a host reads (ADR 0636 §1): the definitions of the mesh's seats as the contracts +// modules define them, in the form mesh-sdk keeps for its tests. No list is compiled into the judge (ADR 0634 §4). +func TestMain(m *testing.M) { + UseSeatList(seatlisttest.Definitions()) + os.Exit(m.Run()) +} diff --git a/modules/mesh-delivery-indexer/judge/rules.go b/modules/mesh-delivery-indexer/judge/rules.go index 93137c5f..bd97b48b 100644 --- a/modules/mesh-delivery-indexer/judge/rules.go +++ b/modules/mesh-delivery-indexer/judge/rules.go @@ -10,6 +10,7 @@ import ( json "encoding/json" errors "errors" fmt "fmt" + "git.novox.be/novox/mesh-contracts/go/seats/meshbroker" catalogue "git.novox.be/novox/mesh-sdk/go/catalogue" math "math" net "net" @@ -2580,7 +2581,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M // `*`, nor one of the module's own tools, nor a verb of a seat it holds (its seat's acting verbs reach the keeper // through its state, ADR 0626 §4). The bus state the foundation keeps on the old sections (ADR 0595 §10, ADR 0619) // is the keeper's state and the bus's own event stream, so a bus module of the foundation may declare it. - if _, account := m.OwnSecrets["broker"]; account && m.ClaimsSeat(catalogue.BrokerSeat) { + if _, account := m.OwnSecrets["broker"]; account && m.ClaimsSeat(meshbroker.Seat) { var said []string bundled := manifestShipsABundle(m) foundation := slices.Contains(Foundation, m.Module) @@ -2599,7 +2600,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M if len(said) > 0 { problems = append(problems, fmt.Sprintf( "%s holds %s and declares a bus account, which is granted the bus's snapshot API and nothing "+ - "else (novox/hq ADR 0235); its %s would be granted nothing", m.Module, catalogue.BrokerSeat, strings.Join(said, ", "))) + "else (novox/hq ADR 0235); its %s would be granted nothing", m.Module, meshbroker.Seat, strings.Join(said, ", "))) } problems = append(problems, keeperBusProblems(m)...) } else { @@ -2607,7 +2608,7 @@ func parseManifest(raw []byte, predefined catalogue.PredefinedList) (catalogue.M if fmt.Sprint(r["type"]) == "process" && busKeeps(r) { problems = append(problems, fmt.Sprintf("%s's process %v's bus keeps the bus's entries; only the keeper "+ "of the module holding %s with its own bus account may (novox/hq ADR 0626 §1, §3)", m.Module, r["id"], - catalogue.BrokerSeat)) + meshbroker.Seat)) } } } @@ -4083,7 +4084,7 @@ func keeperBusProblems(m catalogue.Manifest) []string { } if len(keepers) > 1 { problems = append(problems, fmt.Sprintf("%s holds %s and its processes %s declare bus; only its keeper has a "+ - "process account of its own (novox/hq ADR 0626 §1, §3)", m.Module, catalogue.BrokerSeat, strings.Join(keepers, ", "))) + "process account of its own (novox/hq ADR 0626 §1, §3)", m.Module, meshbroker.Seat, strings.Join(keepers, ", "))) } return problems } @@ -4114,7 +4115,7 @@ func toolsBusProblems(m catalogue.Manifest) []string { if b.Keeps { problems = append(problems, fmt.Sprintf("%s's tools bundle %s's bus keeps the bus's entries; only the keeper, the "+ "process of the module holding %s with its own bus account, may (novox/hq ADR 0626 §1, §3)", m.Module, a.Name, - catalogue.BrokerSeat)) + meshbroker.Seat)) } if len(b.Queues) > 0 || b.Asks { problems = append(problems, fmt.Sprintf("%s's tools bundle %s's bus consumes a queue or reads asks; a tools "+ @@ -4695,8 +4696,8 @@ func isSystemSeatName(name string) bool { } func seatNames() string { - names := make([]string, 0, len(seats)) - for _, s := range seats { + names := make([]string, 0) + for _, s := range catalogue.Seats() { names = append(names, s.Name) } sort.Strings(names) @@ -6909,8 +6910,6 @@ func placeholderWhere(r catalogue.Receivable, where string) (map[string]string, return out, nil } -var seats = defaultSeats - // pathSeats is which seats name each path: the mesh's own, and then those modules declare, in the order // given. A module's seat naming a path a seat already names loses it, and the refusal says so (novox/hq // ADR 0287 — as for a seat's name, the second declaration loses). @@ -7450,356 +7449,6 @@ func namesOfAccessIDs(accesses map[string]string) []string { var renderFuncs = template.FuncMap{"quote": quote} -var defaultSeats = append([]catalogue.Seat{ - // The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable, - // served by whichever module holds the seat with tools of these names. - {Name: "mesh-store", Scope: catalogue.ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079", - Serves: []catalogue.Verb{ - {Name: "databases", Description: "Every database the store holds, with its on-disk size.", - Input: schema(map[string]string{}, nil), Replaces: []string{"psql -l"}}, - {Name: "query", Description: "One read-only statement against one database the store holds.", - Input: schema(map[string]string{"database": "the database to query", "sql": "the read-only statement"}, - []string{"database", "sql"}), Replaces: []string{"psql"}}, - }}, - // **Delivers the mesh's own bus, not `amqp`.** Those were the same word until - // ADR 0127 separated them: `amqp` is a backing service a module may require, and this seat is - // the mesh's own transport. ADR 0128 then made that connection something a module requires - // rather than receives ambiently — 23 of the catalogue's modules never speak, and an ambient - // connection would mint a credential for each. - {Name: catalogue.BrokerSeat, Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "novox/hq ADR 0079"}, - // The vault: the controller seals every minted credential with what it provides, which is the - // test for a seat of the mesh's own (novox/hq ADR 0161) — a second provider of `secret` is a - // second claimant, refused by name, rather than a candidate for a pin. - // - // Its verb `get` hands a secret on request and reply, sealed to the key the asker sends and only to an - // asker the vault allows (novox/hq ADR 0349 §11, issue 674). `accept`, `secret-ask`, `issued` and `rotate` - // take a value in and make one anew (ADR 0349 clauses 8 to 10, issue 675): the first three in two steps from - // the controller alone, which carries the value sealed to a key the vault made for the call and cannot open - // it. All optional until the vault that runs serves them (design 33 §7). - {Name: "mesh-vault", Scope: catalogue.ScopeMesh, Delivers: "secret", Decision: "novox/hq ADR 0161", - Serves: []catalogue.Verb{ - {Name: "get", Optional: true, Description: "One secret, sealed to the public key the asker sends (the " + - "sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the " + - "bus proved it. Never the value in the clear, and never to a private key.", - Input: schema(map[string]string{"name": "the secret's name", - "public_key": "the asker's X25519 public key as SPKI PEM — never a private key"}, - []string{"name", "public_key"})}, - {Name: "accept", Optional: true, Description: "Take a value given at the controller's terminal, from the " + - "controller alone: name the secret and get a call and a key made for it, then send the call and the " + - "value sealed to that key. The vault alone opens it, and never remakes an accepted credential.", - Input: schema(vaultIntake(map[string]string{"node": "the machine, for a module's own secret", - "module": "the module, for a module's own secret", "name": "the secret's name in the module", - "provider": "the provider's machine, for a provision's credential", "provision": "the provision", - "by": "who gave it at the terminal", - "origin": "issued for a bus account the controller minted for a core or moved module and hands " + - "at once (ADR 0374, issue 787); left out for a value given at the terminal"}), nil)}, - {Name: "secret-ask", Optional: true, Description: "Ask the operator for a module's own secret at a desk, " + - "from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR " + - "0563 §8): the ask is recorded in the vault's store and bounded before the prompt " + - "opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", - Input: schema(vaultIntake(map[string]string{"node": "the machine the secret is for", - "module": "the module", "name": "the secret's name in the module", - "desk": "the machine whose prompt asks", "asked_by": "who asked, as the bus named its caller", - "outcome": "instead of sealed: how the prompt ended"}), nil)}, - {Name: "issued", Optional: true, Description: "Take the credential a provider's software issued for a " + - "provision, from the controller alone, which asks the provider's tool with a key the vault made for " + - "the call; kept as the provision's one credential, or, when the tool answers a value per holder named, " + - "each as that holder's own; the same value again writes nothing.", - Input: withHolders(schema(vaultIntake(map[string]string{"provision": "the provision", - "provider": "the provider's machine", "module": "the provider's module"}), nil))}, - {Name: "rotate", Optional: true, Description: "Make a secret anew. Refused for a value a provider's " + - "software issued and for an accepted credential. Never answers the value.", - Input: schema(map[string]string{"node": "the machine, for a module's own secret", "module": "the module", - "name": "the secret's name in the module", "provider": "the provider's machine", - "provision": "the provision", "by": "who asked, when the controller carries the act"}, nil)}, - // The move of the controller's secrets (design 57's V3, issue 676): each machine's node-engine hands in what - // was sealed to its machine, which the controller cannot open; the controller copies its record of asks. - {Name: "move-in", Optional: true, Description: "Take one value of the controller's store in, as it is, " + - "from the node-engine of a machine it was sealed to and no other caller: name the row, its origin " + - "and generation and get a call and a key made for it, then send the call, the value sealed to that " + - "key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read " + - "back sealed to that key.", - Input: schema(vaultIntake(map[string]string{"node": "a module's own secret: its machine", - "module": "its module", "name": "its name in the module", - "provider": "a pair credential: the provider's machine", "provision": "the provision", - "consumer": "the consumer's machine", "consumer_module": "the consuming module", - "local": "the credential's local name, when it has one", - "origin": "made, accepted or issued, as the controller's store says", - "generation": "the controller's generation for the value", "made_at": "when it was made (RFC 3339)", - "read_back_to": "the second step: a raw X25519 public key the read-back is sealed to"}), nil)}, - // A machine's node-engine states its own sealing key (novox/hq ADR 0372, issue 786). - {Name: "node-key", Optional: true, Description: "State this machine's sealing key to the vault, in its " + - "own name, from the node-engine of the machine alone: at its start and whenever the key changes. A " + - "first or changed key is recorded and the machine's core secrets are said changed; the same key " + - "again changes nothing.", - Input: schema(map[string]string{"key": "the machine's sealing key, a raw X25519 public key in base64"}, nil)}, - // A machine's node-engine composes its own modules and asks their secrets sealed to its key (novox/hq ADR 0547 - // §2, ADR 0550 §5, issue 1126); granted as buscompose's Principal.AsksItsOwnSecrets says. - {Name: "own", Optional: true, Description: "Answer the node-engine of one machine, about its own machine " + - "alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair " + - "credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A " + - "credential the vault does not hold is named missing; never a value in the clear.", - Input: map[string]any{"type": "object", "properties": map[string]any{"credentials": map[string]any{ - "type": "array", "items": map[string]any{"type": "object"}, - "description": "the pair credentials the machine's composition reads, each {role: consumer|provider, " + - "provision, consumer, consumer_module, provider, local}"}}}}, - {Name: "move-asks", Optional: true, Description: "Copy rows of the controller's record of asks at a desk " + - "into the vault's, as they are and once, from the controller alone. No value is in any of them.", - Input: map[string]any{"type": "object", "properties": map[string]any{"asks": map[string]any{ - "type": "array", "items": map[string]any{"type": "object"}, - "description": "the controller's rows: id, node, module, name, desk, asked_by, opened_at, ended_at, outcome"}}}}, - {Name: "issue-account", Optional: true, Description: "Mint one bus account the grants part wants, now (novox/hq " + - "ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the " + - "controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", - Input: schema(map[string]string{"user": "the bus user, e.g. ace.mesh-build-go"}, []string{"user"})}, - {Name: "sign", Optional: true, Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): " + - "kind node-certificate signs the certificate of the calling machine's internal name for the serving key " + - "its record states, built from bus state alone and asked by that machine's node-engine only; kind " + - "enrolment signs an enrolment reply under its own context, for a caller the adoption record names. " + - "Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", - Input: schema(map[string]string{"kind": "node-certificate or enrolment", - "node": "the machine, for a node certificate: the caller's own", - "reply": "the enrolment reply's bytes in base64, for an enrolment"}, []string{"kind"})}, - }}, - // The owner of every config value (novox/hq ADR 0348 §9, design 57 K3): its holder alone writes the buckets - // mesh-config_values, _resolved, _predefined and _proposals, and serves the one verb `config` (§1, §10). - // Optional until the mesh-config that runs serves it (design 33 §7). - {Name: "mesh-config", Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0348, ADR 0565", - Serves: []catalogue.Verb{ - {Name: "config", Optional: true, Description: "Set, clear, list or read the history of config values: " + - "mesh config or node config for a predefined key, or a module's own value on one node, each set " + - "with why, validated, and naming each module on each node whose value moves.", - Input: configInput()}, - }}, - // The provisioner's server (novox/hq ADR 0349 clause 3, design 57 P4): its holder keeps the registry of which - // domain answers which kind of placeholder, and the buckets mesh-provisioner_needs, _applied and _collect, - // which it and each machine's provisioner client (for its own machine's keys) alone write. Its one verb reads - // them. Optional until the mesh-provisioner that runs serves it (design 33 §7). - {Name: "mesh-provisioner", Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0349", - Serves: []catalogue.Verb{ - {Name: "provisioning", Optional: true, Description: "Read what the provisioner keeps: the registry " + - "(which domain answers which kind of placeholder), the needs each module on each node recorded, what " + - "is applied where, or the items of every collection, narrowed by node and module. Reads only.", - Input: schema(map[string]string{"what": "registry (when not given), needs, applied or collect", - "node": "a node", "module": "a module: for collect, a contributor", "collection": "for collect: a collection"}, nil)}, - }}, - // Named for its scope since 2026-09-30 (novox/hq ADR 0156); `the-artifact-store` resolves to it as - // an alias on a mesh that predates the rename. It serves artifacts of every kind a build makes — - // images and archives, by digest. It delivers nothing since the provision artifact-store retired - // (novox/hq ADR 0632 §2): the store's address is the image seat holder's listen. - {Name: "mesh-artifact-store", Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0075"}, - // The catalog (novox/hq ADR 0604): the SDK's entry, its four verbs optional while the module that claimed it before stands. - {Name: "mesh-catalog", Scope: catalogue.ScopeMesh, Serves: catalogVerbs(), Decision: "novox/hq ADR 0604"}, - // The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every - // machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act. - // What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it. - {Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"}, - // Placement (novox/hq ADR 0567): which module runs on which node, state on the bus held by the assigner, changed only by - // a proposal the nodes answer and the operator approves. Its states and events are the holder module's. - {Name: catalogue.PlacementSeat, Scope: catalogue.ScopeMesh, Serves: placementVerbs(), Decision: "novox/hq ADR 0567"}, - // The delivery's checker (novox/hq ADR 0547 §10, issue 774): the try is its verb, not the controller's. - {Name: catalogue.CheckerSeat, Scope: catalogue.ScopeMesh, Serves: checkerVerbs(), Decision: "novox/hq ADR 0547"}, - // The mesh's health, out of the controller (novox/hq ADR 0564 clause 1, health_seats.go): the condition keeper, the - // watchman's watchdogs, the self-check and the healers. - {Name: catalogue.ConditionsSeat, Scope: catalogue.ScopeMesh, Decision: healthDecision, - Emits: []string{"condition-raised", "condition-changed", "condition-cleared"}, Serves: conditionsVerbs()}, - {Name: catalogue.WatchdogsSeat, Scope: catalogue.ScopeMesh, Decision: healthDecision, Serves: watchdogsVerbs()}, - {Name: catalogue.SelfCheckSeat, Scope: catalogue.ScopeMesh, Decision: healthDecision, Emits: []string{"checked"}, Serves: selfCheckVerbs()}, - {Name: catalogue.HealersSeat, Scope: catalogue.ScopeMesh, Decision: healthDecision, Emits: []string{"healer-acted"}, Serves: healersVerbs()}, - // The control records, out of the controller (novox/hq ADR 0565 clause 1, control_records_seats.go): a person's - // policy and the bus step at the assigner, and the hand-act log. - {Name: catalogue.WantsSeat, Scope: catalogue.ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()}, - {Name: catalogue.HandActsSeat, Scope: catalogue.ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()}, - // The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go). - {Name: catalogue.ArtifactSweepSeat, Scope: catalogue.ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"}, - Serves: artifactSweepVerbs()}, - // Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a - // delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight. - // They keep their names until that migration is done deliberately, apart from the node-* pass. - {Name: "npm-package-registry", Scope: catalogue.ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, - {Name: "git", Scope: catalogue.ScopeMesh, Delivers: "git", Decision: "novox/hq ADR 0111"}, - // **A build node's work, one seat per build language** (novox/hq ADR 0525 §2, §10; ADR 0536 §4; issue 1887). The - // build module of each language claims its seat on every node it runs on; the builder and the checker put their - // asks on the seat's accepted kinds, which the broker derives as one work queue with a shared worker per kind, and - // each holder pulls delivery first, then check, then recheck, at most its node's build capacity at once. Its verbs - // say what the node runs and pause or resume it there: every holder pulls from the same worker, so a node's pause is - // its holder taking nothing, kept on the module's own pause key (issue 1887's design limit 2). They replace - // node-build-agent, retired with build-agent (ADR 0524 §5). - {Name: "node-build-go", Scope: catalogue.ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Go"), - Yields: true, Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553"}, - {Name: "node-build-node", Scope: catalogue.ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Node"), - Yields: true, Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553"}, - {Name: "node-build-image", Scope: catalogue.ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("image"), - Yields: true, Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553"}, - // **The mesh's resolvers** (novox/hq ADR 0194, 0196, 0223): every node's internal domain, and - // every node and container asks them and nothing else. Replicated since ADR 0223: held on more - // than one machine, each answering the same names from the same roster, and every machine's - // resolver file lists every holder — its own first — and no public resolver, so whichever answers - // first gives the one answer. Delivers what a machine's resolver configuration requires, so that - // requirement resolves to a holder wherever they are placed. - {Name: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Delivers: "wildcard-resolution", Replicated: true, - Decision: "novox/hq ADR 0194, ADR 0223"}, - // **A machine's names are one module's** (novox/hq ADR 0199, ADR 0223): its holder writes - // /etc/hostname and the machine's own lines in /etc/hosts, and keeps every other line of the hosts - // file as the operator's, changed through these three verbs on that machine alone. The controller - // holds none of it. Named node-hosts-file until ADR 0223; the former name resolves to it as an - // alias on a mesh that knew it. - {Name: "node-hostname", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0199, ADR 0223", - // The machine's names are the role's files, whatever program fills it (novox/hq ADR 0287). - Paths: []catalogue.SeatPath{{Path: "/etc/hosts"}, {Path: "/etc/hostname"}}, - Serves: []catalogue.Verb{ - {Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: " + - "the operator's, or the block of the module or tool that writes it.", - Input: schema(map[string]string{}, nil), Replaces: []string{"cat /etc/hosts", "getent hosts"}}, - {Name: "add", Description: "Add one address and its names to the operator's lines of this machine's " + - "/etc/hosts — a name for this machine's own programs, not the mesh's.", - Input: schema(map[string]string{"address": "the IPv4 or IPv6 address", - "names": "the names for it, separated by spaces"}, []string{"address", "names"}), - Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}, - {Name: "remove", Description: "Remove one name, or every line of one address, from the operator's " + - "lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", - Input: schema(map[string]string{"name": "a host name, or an address to remove every line of"}, - []string{"name"}), Replaces: []string{"sed -i /etc/hosts"}}, - }}, - // The intrusion prevention's verbs (novox/hq ADR 0179): what a person asks a machine's ban list - // whatever keeps it — who is banned and why, ban one address, let one go. Every holder serves all - // four; the jails themselves are composed from the modules the machine runs (to-be 31). - {Name: "node-intrusion-prevention", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0121", - Serves: []catalogue.Verb{ - {Name: "status", Description: "Every jail on this machine with how many it is watching and " + - "holding now, and the totals since the jail started; one jail's detail when named.", - Input: schema(map[string]string{"jail": "one jail (optional)"}, nil), Replaces: []string{"fail2ban-client status"}}, - {Name: "banned", Description: "Every address banned on this machine right now, with the jail " + - "that holds it and when the ban ends.", - Input: schema(map[string]string{"jail": "one jail (optional)"}, nil), Replaces: []string{"fail2ban-client banned"}}, - {Name: "ban", Description: "Ban one address in one jail now, for the jail's ban time — an " + - "operator's act on the live ban list, which the mesh never writes itself.", - Input: schema(map[string]string{"ip": "the address", "jail": "the jail to hold it"}, []string{"ip", "jail"}), - Replaces: []string{"fail2ban-client set banip"}}, - {Name: "unban", Description: "Let one address go, from one jail or from every jail when none is named.", - Input: schema(map[string]string{"ip": "the address", "jail": "one jail (optional)"}, []string{"ip"}), - Replaces: []string{"fail2ban-client unban", "fail2ban-client set unbanip"}}, - }}, - // The packet filter's verbs (novox/hq ADR 0170): what a person asks a machine's filter whatever - // filter answers — the rules as enforced, reload the mesh's own, remove one thing the mesh did - // not write. Every holder serves all three; what differs by filter is the holder's own tools. - {Name: "node-packet-filter", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0121", - Serves: []catalogue.Verb{ - {Name: "rules", Description: "The packet filter as this machine enforces it now: the nftables " + - "ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or " + - "chain when asked.", - Input: schema(map[string]string{"table": "one nftables table, as `family name` (optional)", - "chain": "one chain of that table (optional)"}, nil), - Replaces: []string{"nft list ruleset", "iptables -L", "iptables-save"}}, - {Name: "reload", Description: "Load the mesh's own filter again from the file the mesh writes, " + - "and answer with the mesh's table as loaded.", - Input: schema(map[string]string{}, nil), Replaces: []string{"nft -f"}}, - {Name: "remove", Description: "Remove one rule set the mesh did not write, named exactly as the " + - "host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain " + - "DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an " + - "active found firewall's chains. An operator's act, by name, never a flush.", - Input: schema(map[string]string{"where": "the rule set, as `node show` lists it"}, []string{"where"}), - Replaces: []string{"nft delete", "iptables -X"}}, - }}, - // The machine's service manager (novox/hq ADR 0177). The host applies every declared unit, - // system or user scope; the holder answers questions and operator acts about them, each verb - // taking the unit and an optional scope. The holder runs nothing of its own: its verbs are - // served by the node tools runtime (ADR 0175). - {Name: catalogue.ServiceManagerSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0177", - Serves: serviceManagerVerbs()}, - // The machine's package manager (novox/hq ADR 0207). A module declaring a `package` depends on - // it being held on its node, as one declaring a `service` depends on node-service-manager: the - // mesh's word for "something on this machine answers for installing", where a capability only - // says the software is there. No verbs yet — the seat says who answers, and what may be asked - // of it is decided when someone needs to ask. - {Name: catalogue.PackageManagerSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0207"}, - // The machine's container runtime (novox/hq ADR 0166, seeded now by ADR 0207): a module - // declaring a `container` depends on it being held on its node. Its verbs, and the host creating - // containers through its holder, wait for ADR 0166's acceptance — seeded without them so the - // dependency has a seat to name and the runtime's module has one to claim. - {Name: catalogue.ContainerRuntimeSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0166, ADR 0207"}, - // The operator account's environment (novox/hq ADR 0203): one module per machine writes it, and - // every module contributes to it. No verbs — the seat says who places the environment's files, - // and their path is its protocol: a shell sources ~/.config/mesh/environment.sh without knowing - // which module wrote it. - {Name: catalogue.EnvironmentSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0203"}, - // The provisioner client on every machine (novox/hq ADR 0349 clause 3, design 57 P5): a service of its own on a - // bus account of its own (mesh-controller internal/broker NodeProvisionerSeat, design 57 P3), which fills the - // machine's templates. It serves no verb an agent calls. - {Name: "node-provisioner", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0349"}, - // The login shell (novox/hq ADR 0204, replacing the module-declared `login-shell` of ADR 0176): - // the mesh's, so a second shell module claims the seat rather than declaring a second one, and - // the seat exists whether or not zsh's definition is registered. `execute` is the contract a caller - // may call where the machine serves it (optional: ADR 0268); the holder places every module's shell - // code in its slots. - {Name: catalogue.LoginShellSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0204", - Serves: loginShellVerbs()}, - // A machine's power (novox/hq ADR 0211): its holder owns logind's power handling, places the - // code modules contribute for the power moments, and publishes the machine's power states as - // its events. Every machine has one — every machine boots and shuts down. No verbs yet. - {Name: catalogue.PowerSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0211"}, - // The machine's hotkeys (novox/hq ADR 0212): the daemon that sees the keys the window manager - // does not — a laptop's vendor keys — run by one module per machine, which owns its - // configuration. Every other module with keys contributes trigger lines to it. - {Name: catalogue.HotkeysSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0212", - Receives: []catalogue.Receivable{{Kind: "trigger", Comment: "#"}}}, - // The machine's message bus (novox/hq ADR 0215): its holder owns the D-Bus implementation and its - // system service, never restarts it live, and publishes curated events about it, never its traffic. - // It receives nothing yet: packages ship their own policies. - {Name: catalogue.MessageBusSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0215"}, - // The machine's backups (novox/hq ADR 0214, to-be 43): its holder keeps nightly restore points of - // the data every module on the machine declares, on the machine, against mistakes rather than - // disasters. A module contributes `backup` lines — what to run to take a consistent copy, and - // which of its directories to keep. - {Name: catalogue.BackupSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0214", - Serves: backupVerbs(), - // `backup` is what to run and which paths to keep; `data` every item a module declares, with its - // class, for the holder to measure (novox/hq ADR 0233). Both derived from modules' data sections. - Receives: []catalogue.Receivable{{Kind: catalogue.BackupKindBackup, Comment: "#", Dirs: true}, {Kind: catalogue.BackupKindData, Comment: "#", Dirs: true}}}, - // Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client - // model change, not a rename, so it stays until that is built. - {Name: "the-private-network", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0110"}, - // The program that manages the machine's own network. It delivers nothing: its holder keeps the - // manager and the mesh from contradicting each other — the private network's interface left - // alone — and writes the machine's resolver file itself, because the manager is what would - // otherwise rewrite it (novox/hq ADR 0223, which retired node-resolver-config into this seat). - // It never declares a link, an address or a wireless network, because the link is the only - // channel a fix could arrive on. A seat - // rather than a condition in the resolver's module, so a machine running two managers is - // refused at assignment instead of found by the resolver being rewritten (novox/hq ADR 0117). - // - // Its verbs say what the machine resolves through and over which links (novox/hq ADR 0241): the - // resolver file as it is and who wrote it, and every link with its addresses, routes and the resolvers - // the manager knows for it — the same for every holder, so a question about a machine's names is asked - // the same way whatever manages its network. Optional until both holders serve them. - // - // It owns the resolver file, stepping back for node-resolver where that is held (novox/hq ADR 0287, - // ADR 0247). - {Name: "node-uplink", Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs(), - Paths: []catalogue.SeatPath{{Path: catalogue.ResolverFile, StepsBackFor: catalogue.ResolverSeat}}}, - // The machine's own resolver (novox/hq ADR 0247): held only where something requires `split-dns` — a - // VPN client whose domains must go to its own servers while every other name goes to the mesh's. - // Where it is held, its holder writes the resolver file and the uplink's holder steps back from it - // (node_resolver.go). It knows nothing of any VPN: its verbs route domains to servers over a link. - {Name: catalogue.ResolverSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0247", Serves: resolverVerbs(), - Paths: []catalogue.SeatPath{{Path: catalogue.ResolverFile}}}, - // A machine's shares and the shares it mounts (novox/hq ADR 0263): the holder of node-nfs-server - // exports a machine's folders to the private network and provides each as `nfs-share`; the holder of - // node-mounts writes a mount and an automount unit per share on a machine that asks (shares.go). - {Name: catalogue.NFSServerSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0263", Serves: nfsServerVerbs()}, - {Name: catalogue.MountsSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0263", Serves: mountsVerbs()}, - // A node's files and accounts, read (novox/hq ADR 0310, reads.go): by an account of the holder's own that holds - // no secret, on every node, so the reads ADR 0268 §5 takes over from the login shell's execute have a tool. - {Name: catalogue.ReadsSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0310", Serves: readsVerbs()}, - // A node's files, read and managed (novox/hq ADR 0394, files.go): beside node-reads until mesh-files is assigned - // on every node, then node-reads goes (ADR 0394 §9). - {Name: catalogue.FilesSeat, Scope: catalogue.ScopeNode, Decision: "novox/hq ADR 0394", Serves: filesVerbs()}, - // The node's engine's own verbs and asks (novox/hq ADR 0563 §1): held by the engine on every node, by no module. - engineSeat(), -}, - // The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's. - graphicalSessionSeats()...) - // declaredPaths is every machine path a module declares it owns: a resource's literal path, and a file // the mesh renders for it outside the operator's home. An access is not here — reading is not owning. // @@ -8008,850 +7657,10 @@ func configInput() map[string]any { return in } -// catalogVerbs are the catalog seat's four verbs, as the SDK's seat serves them (catalogue.CatalogSeat). -func catalogVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "modules", Optional: true, Description: "Every module the trunk holds, each with its repository, its " + - "directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the " + - "catalog of the mesh, read from its holder's state. Reads only.", - Input: schema(map[string]string{}, nil)}, - {Name: "module", Optional: true, Description: "One module whole: its repository, directory, commit and " + - "fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", - Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})}, - {Name: "provides", Optional: true, Description: "Which modules provide a provision (a module provides its own " + - "name too) and which require it, from the module graph's provision edges. Reads only.", - Input: schema(map[string]string{"provision": "the provision, such as postgres-database"}, []string{"provision"})}, - {Name: "dependents", Optional: true, Description: "The modules whose build edges reach a module, directly or " + - "through others (a base, a mesh package it publishes, the build module that builds them): what a change of it " + - "builds again, from the module graph. Reads only.", - Input: schema(map[string]string{"module": "the module's name"}, []string{"module"})}, - } -} - -// deliveryVerbs are the delivery seat's tools: what reads the merges and checks, and the acts of a person and healer H2 -// on a merge (ADR 0539 clause 17). -func deliveryVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line " + - "each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed " + - "by state, repository or group.", - Input: schema(map[string]string{"state": "one state, e.g. delivering or held", - "repository": "owner/repository", "group": "a group's id (its branch name)", - "all": "\"true\": the final ones of the last thirty days too"}, nil, "all")}, - deliveryRemovedInR6("times"), - {Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each " + - "machine receives, what is not an ordinary send), every transition with when and why, the machine " + - "steps of its walk, its group and its order.", - Input: schema(map[string]string{"id": "a delivery's id, or a group's"}, []string{"id"})}, - {Name: "groups", Description: "Every delivery group: its members in order, why each pair is ordered " + - "(declared, built by, version skew, engine before controller, by name), its composed check and its " + - "state, derived from its members.", - Input: schema(map[string]string{"all": "\"true\": the groups that ended too"}, nil, "all")}, - deliveryRemovedInR6("what-if"), - {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each " + - "of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, " + - "mesh/delivery, …) with its state, " + - "description and when it was set; the merge check's full verdict as the controller said it — each " + - "layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's " + - "protection would let it merge, every required status being success.", - Input: schema(map[string]string{"repository": "owner/repository", - "number": "a pull request's number: its head is read", - "commit": "a commit's sha, or the start of one, instead of a pull request"}, []string{"repository"}), - // Added after the seat's first holder shipped: optional until mesh-delivery serves it everywhere. - Optional: true}, - {Name: "table", Description: "The state table every delivery runs by: each transition with its guard, " + - "each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", - Input: schema(map[string]string{}, nil)}, - {Name: "stalled", Description: "Every delivery held past its state's bound, with the transition the table " + - "lets healer H2 take for it, or none.", - Input: schema(map[string]string{}, nil)}, - {Name: "recheck", Description: "Check a rejected or ready delivery again: it goes back to proposed and the " + - "controller is asked for its check. With why.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"}, - []string{"id", "why"})}, - deliveryRemovedInR6("release"), - deliveryRemovedInR6("stop"), - {Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held " + - "past its bound. Refused for any other.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "the condition it answers"}, - []string{"id", "why"})}, - deliveryRemovedInR6("retire-history"), - {Name: "settle", Description: "Ask the operator to settle deliveries held because their merge reached the " + - "trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. " + - "The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines " + - "is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another " + - "reason, naming why.", - Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"ids", "why"}, - "properties": map[string]any{ - "ids": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "minItems": 1, - "description": "the held deliveries' ids (owner/repository@commit)"}, - "why": map[string]any{"type": "string", "description": "why they may be settled, shown to the operator"}, - }}, - // Added after the seat's release 6 holder shipped: optional until the gate serves it everywhere. - Optional: true}, - } -} - -// placementVerbs are the seat's verbs: what reads all it keeps and does by hand all it does by itself (ADR 0567 §10). -// Optional while the holder that serves them is not yet on every node (design 33 §7): the seat is new in the same -// delivery as its holder. -func placementVerbs() []catalogue.Verb { - changes := map[string]any{"type": "array", "minItems": 1, "description": "the changes, each an object with `op`: " + - "place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), " + - "unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide " + - "(node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, " + - "layer, mesh-wide-values, values)", - "items": map[string]any{"type": "object", "required": []string{"op", "node"}}} - return []catalogue.Verb{ - {Name: catalogue.PlacementVerbPlacement, Optional: true, Description: "Read placement: one node's key, the nodes one " + - "module is placed on, or every key, each with its revision, and how full the state is against its cap. With " + - "history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the " + - "proposal's changes from its placement-changed event.", - Input: schema(map[string]string{"node": "one node", "module": "one module: the nodes it is placed on", - "history": "\"true\": the changes in order, from the stream and the state's revisions", - // ADR 0571 clause 4: who holds which seat on which node, from the state seat-holders. - "seats": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's"}, - nil, "history", "seats")}, - {Name: catalogue.PlacementVerbProposals, Optional: true, Description: "Every open proposal of placement: its changes, " + - "the revision of each key it was laid over, its candidates, every node's answer, the differences computed and " + - "answered, its verdict and its ask. Narrowed to one by id.", - Input: schema(map[string]string{"id": "one proposal's id"}, nil)}, - {Name: catalogue.PlacementVerbPropose, Optional: true, Description: "Propose a change of placement, with why: laid over " + - "the current keys and refused at once, naming every finding, when the result cannot be placed (two modules " + - "on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two " + - "hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose " + - "it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the " + - "caller, as the runtime proves it.", - Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"why", "changes"}, - "properties": map[string]any{ - "why": map[string]any{"type": "string", "description": "why, shown to the operator with the proposal"}, - "changes": changes, - }}}, - {Name: catalogue.PlacementVerbWithdraw, Optional: true, Description: "Withdraw an open proposal: its key and its " + - "candidates are deleted and placement-withdrawn carries it whole. With why.", - Input: schema(map[string]string{"id": "the proposal's id", "why": "why, kept in the event"}, []string{"id", "why"})}, - {Name: catalogue.PlacementVerbAgain, Optional: true, Description: "Judge an open proposal now with the answers that " + - "stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " + - "Nothing more: the recovery of a proposal whose judgement or ask was lost.", - Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})}, - } -} - -// checkerVerbs are the checker seat's verbs: ask a try, read one, end one by hand. Optional until the checker serves them -// everywhere. -func checkerVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "try", Optional: true, Description: "Try a pushed head before its pull request is opened: the checker " + - "judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration " + - "levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read " + - "reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most " + - "4 wait or run at once, mesh-wide.", - Input: schema(map[string]string{"repository": "/", - "head": "the full commit to try, 40 or 64 hex digits", - "trunk": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", - "tests": "a go test -run pattern: only those tests run, and nothing is kept", - "level": "unit or integration: only this level; both when absent"}, - []string{"repository", "head"}), - Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, - {Name: "try-read", Optional: true, Description: "Where a try stands, as stored: queued, running, or its end " + - "(pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit " + - "it was merged onto, the time it took and what failed first. Read-only.", - Input: schema(map[string]string{"id": "the try's id, try-"}, []string{"id"})}, - {Name: "try-end", Optional: true, Description: "End a try that waits or runs, as error, by hand: say why (kept in " + - "what it says). Its place among the tries is free at once and the next try of its head asks anew with a new " + - "id. A try that ended already is refused.", - Input: schema(map[string]string{"id": "the try's id, try-", "why": "why it is ended, in words"}, - []string{"id", "why"})}, - } -} - const healthDecision = "novox/hq ADR 0564" -// conditionsVerbs are the condition keeper's verbs (ADR 0564 clauses 3, 9, 12). -func conditionsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, then oldest, " + - "raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when " + - "the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition " + - "lately.", - Input: schema(map[string]string{ - "key": "a condition's key: that one whole; with history, only its transitions", - "scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", - "severity": "only urgent, or only warning", - "machine": "only those about this machine", - "history": "\"true\": every raising, change, silence and clearing lately, oldest first", - "days": "with history: how many days back (default 7, at most 90)", - }, nil, "history")}, - {Name: "silence", Description: "Stop one condition's messages for a while, a hand act with why: it stays open and in " + - "status, and the silence is kept as its transition in history.", - Input: schema(map[string]string{ - "key": "the condition's key", - "for": "how long: 30m, 4h, 2d; at most 7d", - "why": "why, required and recorded", - "cause": "the cause in a word (the condition's kind when absent)", - }, []string{"key", "for", "why"})}, - {Name: "resync", Description: "Read every observer's findings again and raise and clear what differs from the open " + - "conditions: the recovery of a keeper that missed a change.", - Input: schema(map[string]string{}, nil)}, - } -} - -// watchdogsVerbs are the watchman's verbs (ADR 0564 clauses 4, 5, 12). -func watchdogsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "signals", Description: "Every watchdog of the signals table the watchman keeps, each a key that expires unless " + - "its signal renews it: its row, what renews it, its bound, the age of its newest signal, and the finding its " + - "expiry wrote.", - Input: schema(map[string]string{"row": "one row, e.g. S1"}, nil)}, - {Name: "stuck", Description: "Every want, build or step the watchman says stopped moving past its bound, with since " + - "when and the words of its finding.", - Input: schema(map[string]string{"machine": "only those on this machine"}, nil)}, - {Name: "presence", Description: "Every machine and tool runner the watchman sees connected, with its last word, and " + - "those it says silent; a machine that said it sleeps is shown as sleeping, not silent.", - Input: schema(map[string]string{"machine": "one machine"}, nil)}, - {Name: "dead-letters", Description: "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose " + - "consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one " + - "whole. With deliver or drop, a hand act with why.", - Input: schema(map[string]string{ - "id": "a dead letter's id, as the list gives it: that one whole, with its message", - "consumer": "a consumer's name, or .: only what that one gave up on", - "limit": "how many to list, newest first (default 50); only when listing", - "deliver": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", - "drop": "a dead letter's id: drop it for good (needs why)", - "why": "with deliver or drop: why, required and recorded", - "cause": "with deliver or drop: the cause in a word (dead-letter when absent)", - }, nil)}, - {Name: "resync", Description: "Rebuild presence from the bus's connections, and the pulses and findings from the keys " + - "the watchman watches, and take every dead-letter notice waiting: the recovery of a watchman that missed a change.", - Input: schema(map[string]string{}, nil)}, - } -} - -// selfCheckVerbs are the self-check's verbs (ADR 0564 clause 6). -func selfCheckVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "doctor", Description: "The self-check: the last run's verdict at once, each probe passed, failed, deferred " + - "(its facts not on the bus yet, named) or could not run, and how long ago. With run, a run now; with probe, one " + - "probe's verdict whole. Runs every five minutes on its own; each failure is a finding.", - Input: schema(map[string]string{ - "run": "\"true\": run every probe now and answer the verdict", - "probe": "one probe's id (D6): its verdict alone, with every finding", - }, nil, "run")}, - {Name: "probes", Description: "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", - Input: schema(map[string]string{}, nil)}, - } -} - -// healersVerbs are the healers' verbs (ADR 0564 clauses 7, 8). -func healersVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "healers", Description: "The healers: each registered response to one kind of condition, its repair, its " + - "budget and what happens when it is spent; every act taken lately with its outcome, and the mesh-wide brake.", - Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)}, - {Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.", - Input: schema(map[string]string{ - "key": "the condition's key", - "why": "why now, kept with the act", - }, []string{"key", "why"})}, - {Name: "drill", Description: "Record a drill: something broken on purpose to see the mesh raise and clear it, with " + - "what it tests. A drill is never a repair: no healer is wanted for it however often it is run.", - Input: schema(map[string]string{ - "what": "what was done on purpose, in a line", - "why": "what the drill tests", - "condition": "the key of the condition the drill is meant to raise, if any (optional)", - }, []string{"what", "why"})}, - } -} - const controlRecordsDecision = "novox/hq ADR 0565" -// wantsVerbs are the assigner's verbs on mesh-wants (ADR 0565 clauses 7, 8, 9, 15). -func wantsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "upgrade", Description: "Whether each module's new builds wait for a person: its policy (roll, together or record) " + - "and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never " + - "rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; " + - "with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.", - Input: schema(map[string]string{ - "module": "one module", - "policy": "with module: roll, together, record, or default to take a person's choice back", - "why": "with policy: why, required and kept", - "compare": "\"true\": each node's own composition, by its digest, and the assigner's findings", - }, nil, "compare")}, - {Name: "bus", Description: "What a bus upgrade would do: the fingerprint of the bus module each node runs against the " + - "indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: " + - "the step itself is the put on the operator's warrant.", - Input: schema(map[string]string{}, nil)}, - } -} - -// handActsVerbs are mesh-hand-acts' verbs (ADR 0565 clause 10), as the controller's took them. -func handActsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "hand-act", Description: "Record one act done by hand: what, why, its cause in a word or a condition's kind " + - "(the word a second act for the same reason uses) and the condition it addresses. Who did it is the caller the " + - "runtime proves, never said by the caller. Said as act-recorded.", - Input: schema(map[string]string{ - "what": "what was done, in a line", - "why": "why it had to be done by hand", - "cause": "the cause in a word, or a condition's kind", - "condition": "the key of the condition it addressed, if any (optional)", - "verb": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", - }, []string{"what", "why"})}, - {Name: "hand-acts", Description: "What was done by hand lately, over this log and the controller's: who, why and the " + - "cause of each, and the causes recorded twice within a fortnight (a healer is wanted).", - Input: schema(map[string]string{"days": "how many days back (default 14)", "cause": "only this cause", - "who": "only acts whose who contains this", "warrants": "\"true\": every warrant recorded"}, nil, "warrants")}, - {Name: "warranted", Description: "Record what a module did on the operator's warrant: who chose, through which channel, " + - "with which proofs and which answer read from the router's own record of that module's ask, never from the " + - "caller; once per ask. Refused for an ask still open, ended without a choice, or not that module's.", - Input: schema(map[string]string{"asker": "the module that asked", "ask": "its ask's id"}, []string{"asker", "ask"})}, - } -} - -// artifactSweepVerbs are the seat's verbs, with the controller's arguments so a caller changes only the seat it asks. -func artifactSweepVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "artifacts", Description: "Every artifact the mesh made that lives in its artifact store, each with its " + - "repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five " + - "newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or " + - "unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.", - Input: schema(map[string]string{ - "repository": "only this repository's references, as / (the counts stay whole)", - "collected": "\"true\": list the collected references too (counted always)", - }, nil, "collected")}, - {Name: "held", Description: "Whether the artifact store holds each reference given, asked with one HEAD each: " + - "held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. " + - "Reads only.", - Input: listed(schema(map[string]string{ - "references": "the artifacts' references, artifact-store:////blobs/sha256:: a list, " + - "or one text separated by commas", - }, []string{"references"}), "references")}, - {Name: "collect", Description: "The artifact store's sweep, now: what is eligible is let go of. Without confirm a " + - "dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, " + - "then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index " + - "names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 " + - "seconds; what is left is said. Never touches a digest the mesh did not make.", - Input: schema(map[string]string{ - "confirm": "\"true\": let go of what is eligible (needs why); without it nothing is done", - "why": "with confirm: why, required and recorded", - "most": "let go of at most this many (500 by default, at most 5000)", - }, nil, "confirm")}, - {Name: "mirrors", Description: "The bases the mesh copied into its artifact store, each kept (a kept build stood on " + - "it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on " + - "it: never removed), by what the builder's log says each build stood on. Records nothing by hand.", - Input: schema(map[string]string{}, nil)}, - {Name: "images", Description: "Every container image one machine's want and have name, each with the resources " + - "naming it: what the machine keeps when it prunes its images. Reads only.", - Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})}, - } -} - -// buildAskKinds are a build seat's accepted kinds, in the order a holder pulls them (ADR 0536 §4; mesh-sdk -// delivery.AskKinds). -func buildAskKinds() []string { return []string{"delivery", "check", "recheck"} } - -// buildSeatVerbs are what a build module answers on its own node for its seat (ADR 0525 §10). -func buildSeatVerbs(language string) []catalogue.Verb { - return []catalogue.Verb{ - {Name: "current", Description: "The " + language + " asks this node runs now (each ask's id, kind, module and when it " + - "started), its build capacity, and whether it is paused, taking no new ask.", - Input: schema(map[string]string{}, nil)}, - {Name: "pause", Description: "Take no new " + language + " ask on this node until resumed; an ask running here " + - "ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the " + - "lasting way to keep a node from building.", - Input: schema(map[string]string{}, nil)}, - {Name: "resume", Description: "Take " + language + " asks again on this node.", - Input: schema(map[string]string{}, nil)}, - } -} - -// serviceManagerVerbs is the contract every holder of node-service-manager serves (novox/hq ADR -// 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an -// optional scope — "system" when absent, "user" for the operator account's own manager — so a -// caller asks for a user unit the way it asks for a system one; `failed` alone reads both managers -// when none is named. `failed`, `reset-failed`, `wanted-by` and `unlink-dangling` are optional -// (Verb.Optional) until every holder serves them (ADR 0246). -func serviceManagerVerbs() []catalogue.Verb { - scoped := func(more map[string]string, required []string) map[string]any { - props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"} - for k, v := range more { - props[k] = v - } - return schema(props, required) - } - unit := map[string]string{"unit": "the unit's name, as the service manager knows it"} - return []catalogue.Verb{ - {Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", - Input: scoped(map[string]string{"pattern": "a glob the unit's name must match (optional)"}, nil), Replaces: []string{"systemctl list-units"}}, - {Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl status", "systemctl is-active"}}, - {Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl start"}}, - {Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl stop"}}, - {Name: "restart", Description: "Restart one unit.", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl restart"}}, - {Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl enable"}}, - {Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl disable"}}, - // **A window, not only a tail** (the operator's direction 2026-10-07): an incident is read for the - // minutes it happened in, and with no window on the verb a person reached for a shell. Every - // argument is the holder's to validate — passed to journalctl as one word of its own, never through - // a shell — and what the unit printed of a secret is redacted before it is answered. - {Name: "journal", Description: "The last lines of one unit's journal (at most 2000), in a time window and " + - "narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as " + - "[redacted: ].", - Input: scoped(map[string]string{ - "unit": unit["unit"], - "lines": "how many lines from the end of what matches (default 100, at most 2000)", - "since": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", - "until": "the window's end, in the same forms (optional; now when absent)", - "match": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", - "priority": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", - }, []string{"unit"}), - Replaces: []string{"journalctl"}}, - // What has failed, on the seat rather than as one holder's own tool: whatever holds the role answers - // it, so a caller asks every machine the same way. **Optional while its holders catch up**: the - // systemd module running today serves it as its own systemd_failed, and a required verb would - // refuse it before the version serving `failed` could be delivered. - {Name: "failed", Optional: true, Description: "Every failed unit on this machine, in the system manager and in the operator " + - "account's; a manager that does not answer is reported with its error, never as nothing failed.", - Input: schema(map[string]string{"scope": "\"system\" or \"user\": only that manager (both when absent)"}, nil), - Replaces: []string{"systemctl --failed"}}, - // **Clearing a failed record, and finding what starts a unit** (novox/hq issue 332): a unit whose - // file is gone stays failed in its manager until the record is reset, and what keeps asking for it - // is a dependency or an enable link no verb could show. Required since every holder serves them - // (ADR 0246 step 3). - {Name: "reset-failed", Description: "Clear one unit's failed record in its manager — " + - "its failed state and its start-limit count — and answer its state after. It removes no file and " + - "starts nothing.", - Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl reset-failed"}}, - {Name: "wanted-by", Description: "What wants, requires or triggers one unit, read-only: the " + - "manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and " + - "every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), " + - "each with where it points and whether it dangles — the account's own directory included in user scope.", - Input: scoped(unit, []string{"unit"}), - Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy", - "ls ~/.config/systemd/user/*.wants"}}, - // **A dangling enable link removed on the person's word** (novox/hq issue 332): `systemctl disable` - // leaves a link whose unit file is gone, so the manager keeps asking for the unit. Required since - // every holder serves it (ADR 0246 step 3). - {Name: "unlink-dangling", Description: "Remove the dangling enable links named for one " + - "unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own " + - "configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on " + - "the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is " + - "removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the " + - "runtime placed, is left and said.", - Input: scoped(map[string]string{"unit": unit["unit"], - "why": "the person's reason for removing the links, kept in the record"}, []string{"unit", "why"}), - Replaces: []string{"rm ~/.config/systemd/user/*.wants/", "unlink /etc/systemd/system/*.wants/"}}, - } -} - -// loginShellVerbs is the contract of node-login-shell (novox/hq ADR 0176, ADR 0204): one command, run -// the way the operator's own terminal would run it, bounded below the runtime's thirty-second call limit -// so a hung command answers rather than times the caller out. -// -// **`execute` is optional** (novox/hq ADR 0268). It runs any command as the operator account, which can -// become root without a person, so a machine may withhold it: the control-node does, through the -// holder's own `execute` setting, until a call to it needs a person's approval (hq research 039). The mark -// is ADR 0246's: a holder that does not serve the verb on a machine still holds the seat there, and its -// silence on the bus is not judged — a holder withholding it serves nothing on the seat. -func loginShellVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "execute", Optional: true, Description: "Run one command on this machine as the operator account, in a " + - "non-interactive login shell in its home; answers with what it printed and how it exited.", - Input: schema(map[string]string{ - "command": "the command line, as you would type it", - "timeout_seconds": "give up after this long, at most 25 (default 20)", - }, []string{"command"})}, - } -} - -// backupVerbs is the node-backup seat's protocol (novox/hq to-be 43): what is kept, take one now, -// and restore beside the live data — never over it. -func backupVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "backed-up", Description: "What this machine backs up: each module, what it declared, " + - "its last good night, how many restore points are kept and the repository's size.", - Input: schema(map[string]string{"module": "one module (optional)"}, nil), Replaces: []string{"restic snapshots"}}, - {Name: "now", Description: "Take a backup now, of one module or of every module on this " + - "machine — before a migration, a retirement or anything else that could go wrong.", - Input: schema(map[string]string{"module": "one module (optional)"}, nil), Replaces: []string{"restic backup"}}, - {Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live " + - "data, never over it: each directory as .restored-. Swapping it in is a " + - "person's act. Lists the restore points when none is named.", - Input: schema(map[string]string{ - "module": "the module", - "snapshot": "the restore point (from `backed-up`; the newest when omitted)", - "path": "one of the module's directories (all of them when omitted)", - }, []string{"module"}), Replaces: []string{"restic restore"}}, - } -} - -// uplinkVerbs is the contract every holder of node-uplink serves (novox/hq ADR 0241): read-only answers -// about what the machine resolves through and over which links, whatever program manages its network. -func uplinkVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "resolvers", Description: "The machine's resolver file as it is now: the resolvers, search " + - "domains and options it lists, whether it is the file the mesh declares, and when it is not, who " + - "wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link " + - "in its place, a known writer running.", - Input: schema(nil, nil), Optional: true}, - {Name: "links", Description: "Every network link on the machine: its state, its addresses, whether " + - "the default route leaves through it, and the resolvers and search domains the network manager " + - "knows for it — a VPN's tunnel included.", - Input: schema(nil, nil), Optional: true}, - } -} - -// resolverVerbs is the contract every holder of node-resolver serves (novox/hq ADR 0247): what is routed -// where, route a set of domains, and take a route away. The same verbs are served on the machine itself to -// the modules there, so what a VPN pushed never crosses the bus to be routed; on the mesh they are the -// operator's way to read and correct the same. -func resolverVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, " + - "which answer every name not routed elsewhere, and each link given servers of its own with the " + - "domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who " + - "wrote it as far as the file says, whether a module took it, and when the resolver's own file was " + - "put back.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}}, - {Name: "route", Description: "Send these domains, and every name under them, to these servers over " + - "this link — and only them: the link is never the machine's default route for names, and the " + - "mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away " + - "takes its route with it.", - Input: schema(map[string]string{ - "link": "the network link the servers are reached over, by name (a VPN's tunnel interface)", - "domains": "the domains to route there, separated by spaces or commas", - "servers": "the servers' addresses, separated by spaces or commas", - }, []string{"link", "domains", "servers"}), - Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}}, - {Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's " + - "resolvers. Nothing changes when the link has none.", - Input: schema(map[string]string{"link": "the network link, by name"}, []string{"link"}), - Replaces: []string{"resolvectl revert"}}, - } -} - -// nfsServerVerbs is the contract every holder of node-nfs-server serves (novox/hq ADR 0263). -func nfsServerVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "exports", Description: "Every share this machine exports: its name, its path, read-write or " + - "read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that " + - "node's private address and access (only the nodes the server grants it to and that ask for it), " + - "what is granted and not asked for or asked for and not granted, and whether the kernel holds it " + - "now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in " + - "/etc/exports.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"exportfs -v", "cat /etc/exports", "zfs get sharenfs"}}, - {Name: "clients", Description: "Which machines have mounted which share now, as the NFS server " + - "knows its clients.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"ss -tn sport = :2049", "cat /proc/fs/nfsd/clients/*/info"}}, - {Name: "test", Description: "Whether this machine exports one share for the mesh now, and whether its " + - "NFS service is up; with an address, also whether the share is exported to that address: a node's " + - "own private address, when the server grants it the share and the node asks for it. What a machine " + - "mounting the share asks before it mounts.", - Input: schema(map[string]string{ - "share": "the share, by its name", - "address": "a node's private address, to ask whether the share is exported to it (optional)", - }, []string{"share"}), - Replaces: []string{"showmount -e"}}, - {Name: "reload", Description: "Have the kernel read this machine's export files again now (exportfs " + - "-ra) and answer the shares as it then holds them. The module's own process writes its export " + - "file; this is for after a change made outside it. Changes no shared path.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"exportfs -ra"}}, - {Name: "adopt", Description: "Take over an export a person made by hand: clear a dataset's sharenfs " + - "property for a path the mesh's own export now serves — only when that export is live. Without " + - "confirm, says what it would do and changes nothing.", - Input: schema(map[string]string{ - "path": "the shared path whose hand-made export is taken over", - "confirm": "\"true\": change it (needs why); anything else is a dry run", - "why": "why, for the record", - }, []string{"path"}, "confirm"), - Replaces: []string{"zfs set sharenfs=off"}}, - } -} - -// mountsVerbs is the contract every holder of node-mounts serves (novox/hq ADR 0263). -func mountsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "list", Description: "Every mount point on this machine: its fstab line, the mesh's mount and " + - "automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, " + - "and which settings asked for it. A password in a mount's options is never shown.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"cat /etc/fstab", "findmnt", "systemctl list-units --type=mount"}}, - {Name: "test", Description: "Whether one share's or occasional source's server answers from this " + - "machine now, without touching its mount point.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"showmount -e", "ping"}}, - {Name: "mount", Description: "Mount one share or occasional source now, rather than at its first " + - "access.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"mount"}}, - {Name: "unmount", Description: "Release one share or occasional source now; its automount stays " + - "armed, so the next access mounts it again.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"umount"}}, - {Name: "adopt", Description: "Take over a mount a person wrote by hand: comment out the /etc/fstab " + - "line for a mount point the mesh's own units now serve, keeping a copy of the file — only when " + - "the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", - Input: schema(map[string]string{ - "mountpoint": "the mount point whose fstab line is taken over", - "confirm": "\"true\": change it (needs why); anything else is a dry run", - "why": "why, for the record", - }, []string{"mountpoint"}, "confirm"), - Replaces: []string{"sed -i /etc/fstab"}}, - } -} - -// readsVerbs is the contract every holder of node-reads serves (novox/hq ADR 0310 §2). The bounds are the -// build's to set and each verb's description to state (ADR 0310, Consequences); the holder states them in -// every answer it cuts. -func readsVerbs() []catalogue.Verb { - return []catalogue.Verb{ - {Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group " + - "(name and number), mode, whether it is a link and to what, and whether it is a mount point and of which " + - "file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which " + - "holds no secret: a component it may not reach says so. At most 200 paths in one call.", - Input: schema(map[string]string{ - "paths": "one absolute path, or several separated by line breaks", - "follow": "\"true\": also say where the path leads once every link is followed", - }, []string{"paths"}, "follow"), - Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}}, - {Name: "readlink", Description: "What one link points to, as written, and where the path leads once every " + - "link is followed, and whether that exists.", - Input: schema(map[string]string{"path": "an absolute path"}, []string{"path"}), - Replaces: []string{"readlink", "readlink -f", "realpath"}}, - {Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a " + - "link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept " + - "is refused by name.", - Input: schema(map[string]string{"path": "an absolute path to a directory"}, []string{"path"}), - Replaces: []string{"ls", "ls -la"}}, - {Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer " + - "(64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is " + - "kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its " + - "number said. /proc and /sys are read as far as the holder's account may.", - Input: schema(map[string]string{ - "path": "an absolute path to a file", - "from": "the first line to answer, counted from 1 (optional)", - "lines": "how many lines to answer (optional)", - "bytes": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", - }, []string{"path"}), - Replaces: []string{"cat", "head", "tail", "less"}}, - {Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: " + - "every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), " + - "8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and " + - "binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a " + - "secret is withheld. The answer says which bound it met.", - Input: schema(map[string]string{ - "text": "the fixed string to find", - "path": "an absolute path to a file or a directory", - "depth": "how many directories deep, up to 8 (optional)", - "matches": "the most matches to answer, up to 500 (optional)", - "case": "\"false\": match whatever the case (optional)", - }, []string{"text", "path"}, "case"), - Replaces: []string{"grep", "grep -r", "grep -F"}}, - {Name: "account", Description: "One account or group by name or number, as this node's account files say: " + - "an account's number, group, home, shell and every group it is in; a group's number and members. With " + - "neither, every account and group. Never a password field.", - Input: schema(map[string]string{ - "name": "an account's name or number (optional)", - "group": "a group's name or number (optional)", - }, nil), - Replaces: []string{"getent passwd", "getent group", "id", "groups"}}, - } -} - -// filesVerbs is the contract every holder of node-files serves (novox/hq ADR 0394 §1): the reads of node-reads, -// with their descriptions restated for the operator's account, and the writes. -func filesVerbs() []catalogue.Verb { - var verbs []catalogue.Verb - for _, v := range readsVerbs() { - v.Description = strings.Replace(v.Description, - "Read as the holder's own account, which holds no secret: a component it may not reach says so.", - "", 1) - v.Description = strings.TrimSpace(v.Description) + " Read " + filesAsAccount + "." - verbs = append(verbs, v) - } - write := func(name, description string, in map[string]string, required []string, optional []string, replaces ...string) catalogue.Verb { - in["reason"] = "why the file is changed, for the audit trail" - in["override"] = overrideWord - return catalogue.Verb{Name: name, Description: description + " Refuses a path a module owns " + - "unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: schema(in, append(required, "reason"), append(optional, "override")...), Replaces: replaces} - } - return append(verbs, - write("write", "Write one text file whole, replacing what it holds; the old content goes to the node's trash.", - map[string]string{"path": "an absolute path", "content": "the text to write", - "mode": "the file's mode, such as 0644 (optional)"}, - []string{"path", "content"}, []string{"mode"}, "tee", ">"), - write("copy", "Copy a file or directory to a path on this node. A target on another node (:/path) is "+ - "refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one "+ - "kept in the node's trash for 7 days.", - map[string]string{"from": "an absolute path", "to": "an absolute path on this node"}, - []string{"from", "to"}, nil, "cp", "cp -a", "ssh cp"), - write("move", "Move or rename a file or directory. A target on another node is refused: copying between nodes "+ - "comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days.", - map[string]string{"from": "an absolute path", "to": "an absolute path"}, - []string{"from", "to"}, nil, "mv"), - write("delete", "Delete a file or directory into the node's trash, where restore can bring it back.", - map[string]string{"path": "an absolute path", "recursive": "\"true\": delete a directory with what it holds"}, - []string{"path"}, []string{"recursive"}, "rm", "rm -r"), - write("mkdir", "Make a directory.", - map[string]string{"path": "an absolute path", "parents": "\"true\": make the missing parents too"}, - []string{"path"}, []string{"parents"}, "mkdir", "mkdir -p"), - write("chmod", "Change the mode of a file or directory.", - map[string]string{"path": "an absolute path", "mode": "the mode, such as 0644 or u+x", - "recursive": "\"true\": also everything under a directory"}, - []string{"path", "mode"}, []string{"recursive"}, "chmod"), - write("chown", "Change the owner and group of a file or directory.", - map[string]string{"path": "an absolute path", "owner": "an account's name or number", - "group": "a group's name or number (optional)", "recursive": "\"true\": also everything under a directory"}, - []string{"path", "owner"}, []string{"group", "recursive"}, "chown", "chgrp"), - catalogue.Verb{Name: "upload", Description: "Send one file of this node into a bucket of the object store, over a short signed link " + - "and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. " + - "At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, " + - "before anything is sent. Read " + filesAsAccount + ".", - Input: schema(map[string]string{"path": "an absolute path to a file", "bucket": "the bucket the file goes into", - "key": "the object's key (optional; the file's SHA-256 when not given)"}, []string{"path", "bucket"})}, - // download is a plain Verb, not through write(): write() types its optional arguments as true/false - // switches, which a hash and a reason are not (issue 1719). - catalogue.Verb{Name: "download", Description: "Write one object of a bucket to a file of this node, over a short signed link and " + - "never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to " + - "the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns " + - "unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: schema(map[string]string{"bucket": "the bucket", "key": "the object's key", "path": "an absolute path", - "sha256": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", - "reason": "why the file is changed, for the audit trail", - "override": overrideWord}, []string{"bucket", "key", "path", "reason"})}, - catalogue.Verb{Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, " + - "original path and when it went.", - Input: schema(map[string]string{}, nil), Replaces: []string{"ls ~/.local/share/Trash"}}, - write("restore", "Put an item of the node's trash back where it was; refuses when something is there now.", - map[string]string{"id": "an id from trash"}, []string{"id"}, nil, "trash-restore"), - ) -} - -// engineSeat is the seat's row in the closed set. -func engineSeat() catalogue.Seat { - return catalogue.Seat{Name: catalogue.EngineSeat, Scope: catalogue.ScopeNode, Serves: catalogue.EngineVerbs(), Emits: append([]string(nil), catalogue.EngineEvents...), - HeldByTheEngine: true, Decision: "novox/hq ADR 0563"} -} - -// graphicalSessionSeats are the eleven, in the order ADR 0208's table reads, each with the verbs -// research 026/05 starts it with. Three have none yet: the bar (which receives blocks instead, -// ADR 0255), the compositor and the secret -// service are roles a second holder competes for, and nothing has needed to ask them anything. -func graphicalSessionSeats() []catalogue.Seat { - const decided = "novox/hq ADR 0208" - return []catalogue.Seat{ - {Name: catalogue.LoginManagerSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "sessions", Description: "The sessions the login manager offers on this machine, and which " + - "one the operator account starts by default.", - Input: schema(map[string]string{}, nil)}, - }}, - {Name: catalogue.DisplayServerSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "displays", Description: "The monitors connected now, each with its identity, its modes and " + - "where it is placed; and the layout profile in force, if one matches.", - Input: schema(map[string]string{}, nil)}, - // Profiles are keyed by the monitors' identities and are the operator's data (ADR 0208 §6). - {Name: "layout", Description: "The monitor layout profiles, keyed by the connected monitors' " + - "identities: list them, save the current arrangement under a name, or apply one.", - Input: withEnum(schema(map[string]string{ - "action": "list, save or apply", - "name": "the profile to save or apply (save and apply only)", - }, []string{"action"}), "action", "list", "save", "apply")}, - }}, - // It receives window-manager configuration lines from every other module (novox/hq ADR 0212): - // bindings, start-up commands, rules — placed by the session's holder, never written into - // its directory by the contributor. - {Name: catalogue.DisplaySessionSeat, Scope: catalogue.ScopeNode, Decision: decided, - Receives: []catalogue.Receivable{{Kind: "config", Comment: "#"}}, Serves: []catalogue.Verb{ - {Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.", - Input: schema(map[string]string{}, nil)}, - {Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " + - "it is visible or focused.", - Input: schema(map[string]string{}, nil)}, - {Name: "windows", Description: "The session's windows: each one's title, class, workspace and " + - "whether it has focus; narrowed to one workspace when named.", - Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)}, - }}, - {Name: catalogue.TerminalEmulatorSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "open", Description: "Open a terminal window in the operator's session, running a command " + - "or the login shell, in a directory or the account's home.", - Input: schema(map[string]string{ - "command": "what to run in it (optional; the login shell when absent)", - "directory": "where it starts (optional; the account's home when absent)", - }, nil)}, - }}, - {Name: catalogue.LauncherSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "menu", Description: "Put a menu of choices in front of the operator and answer with the " + - "one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", - Input: map[string]any{"type": "object", "required": []string{"choices"}, - "properties": map[string]any{ - "choices": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, - "description": "the lines to choose between, in order"}, - "prompt": map[string]any{"type": "string", "description": "what the menu asks (optional)"}, - }}}, - // A value the operator types and nobody sees (novox/hq ADR 0259 §10): a hidden prompt whose answer - // is sealed to the asker's key, so it is never plaintext on the bus or in any call's record. - // **Optional while its holders catch up** (ADR 0246): rofi serves it once this is live. - {Name: "secret", Optional: true, Description: "Ask the operator for a value in a prompt that " + - "does not show what is typed, and answer it sealed to the key the asker gives — never in " + - "the clear — or cancelled when the prompt was dismissed or not answered in time.", - // By name, never by words (the review of 2026-10-09, M4): the holder writes the prompt from the - // module, the secret and the machine, and says the controller asks — the bus lets nobody else - // ask it (broker.ControllerOnly) — so no caller puts words of its own before the operator. - Input: schema(map[string]string{ - "module": "the module whose own secret is asked for", - "secret": "the own secret's name", - "node": "the machine the module runs on", - "seal_to": "the asker's public sealing key: the answer is sealed to it", - "timeout_seconds": "give up after this long (optional)", - }, []string{"module", "secret", "node", "seal_to"})}, - }}, - {Name: catalogue.NotifierSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "send", Description: "Show the operator a notification.", - Input: withEnum(schema(map[string]string{ - "title": "the notification's summary", - "body": "its text (optional)", - "urgency": "low, normal (the default) or critical", - }, []string{"title"}), "urgency", "low", "normal", "critical")}, - {Name: "history", Description: "The notifications shown lately, newest first.", - Input: schema(map[string]string{"limit": "how many (optional, default 20)"}, nil)}, - }}, - {Name: catalogue.LockScreenSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "lock", Description: "Lock the operator's session now.", - Input: schema(map[string]string{}, nil)}, - }}, - {Name: catalogue.ClipboardSeat, Scope: catalogue.ScopeNode, Decision: decided, Serves: []catalogue.Verb{ - {Name: "history", Description: "What the clipboard held lately, newest first.", - Input: schema(map[string]string{"limit": "how many (optional, default 20)"}, nil)}, - {Name: "copy", Description: "Put text on the operator's clipboard.", - Input: schema(map[string]string{"text": "the text"}, []string{"text"})}, - }}, - // It receives blocks as data (novox/hq ADR 0255): what a block shows, said for any bar, which - // the holder renders in its own grammar. A module that knows something about the machine — the - // power module its battery — gives the block; the bar knows no machine's hardware. - {Name: catalogue.BarSeat, Scope: catalogue.ScopeNode, Decision: decided, - Receives: []catalogue.Receivable{{Kind: catalogue.BarKindBlock, Comment: "#", Offered: true, Shape: barBlockShape()}}}, - {Name: catalogue.CompositorSeat, Scope: catalogue.ScopeNode, Decision: decided}, - {Name: catalogue.SecretServiceSeat, Scope: catalogue.ScopeNode, Decision: decided}, - } -} - -// deliveryRemovedInR6 is the row of a verb release 6 removed from the delivery seat. -func deliveryRemovedInR6(name string) catalogue.Verb { - return catalogue.Verb{Name: name, Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: " + - catalogue.RemovedDeliveryVerbs[name], Input: schema(nil, nil), Optional: true} -} - // listed makes the named properties of a schema lists of text: a caller gives them as a JSON array (or, as // any argument, one text separated by commas). func listed(in map[string]any, names ...string) map[string]any { diff --git a/modules/mesh-delivery-indexer/judge/rules_test.go b/modules/mesh-delivery-indexer/judge/rules_test.go index 9c82189a..c85d6866 100644 --- a/modules/mesh-delivery-indexer/judge/rules_test.go +++ b/modules/mesh-delivery-indexer/judge/rules_test.go @@ -16,6 +16,7 @@ import ( reflect "reflect" regexp "regexp" slices "slices" + "sort" strconv "strconv" strings "strings" sync "sync" @@ -3938,7 +3939,8 @@ func TestNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) { yielding = append(yielding, s.Name) } } - if strings.Join(yielding, ",") != "node-build-go,node-build-node,node-build-image" { + sort.Strings(yielding) + if strings.Join(yielding, ",") != "node-build-go,node-build-image,node-build-node" { t.Errorf("the seats that yield are %v", yielding) } } @@ -3972,7 +3974,8 @@ func TestNoModuleMayDeclareTheLoginShell(t *testing.T) { if err != nil { t.Fatal(err) } - if err := catalogue.CanHold(m, catalogue.Seat{Name: catalogue.LoginShellSeat, Scope: catalogue.ScopeNode, Serves: loginShellVerbs()}); err != nil { + loginShell, _ := catalogue.SeatNamed(catalogue.LoginShellSeat) + if err := catalogue.CanHold(m, loginShell); err != nil { t.Fatalf("a shell module claiming the seat cannot hold it: %v", err) } } @@ -4261,7 +4264,7 @@ func TestAStoreHolderThatBuildsIsNotRefusedByItsManifest(t *testing.T) { s, _ := catalogue.SeatNamed(name) holder.Claims = append(holder.Claims, catalogue.Claim{Name: name, Scope: catalogue.ScopeMesh, Serves: catalogue.VerbNames(s.Serves)}) } - for _, s := range append([]catalogue.Seat{}, seats...) { + for _, s := range append([]catalogue.Seat{}, catalogue.Seats()...) { if s.Delivers == "artifact-store" { t.Errorf("the judge's %s still delivers the retired artifact-store", s.Name) } @@ -4401,7 +4404,7 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) { if err != nil { t.Fatalf("a bus module with an account and tools was refused: %v", err) } - if !m.ClaimsSeat(catalogue.BrokerSeat) { + if !m.ClaimsSeat("mesh-broker") { t.Fatal("it does not read as holding the broker seat") } } @@ -4546,7 +4549,7 @@ func TestTheCataloguesBarPlacesBlocksAndItsShapeCoversEveryShow(t *testing.T) { // The compiled set keeps the rules: node seats only, absolute and literal, one seat per path without // stepping back, and a stepping back that cannot cycle. func TestTheCompiledSeatPathsKeepTheRules(t *testing.T) { - if p := SeatPathProblems(catalogue.DefaultSeats()); len(p) != 0 { + if p := SeatPathProblems(catalogue.Seats()); len(p) != 0 { t.Fatalf("the compiled set breaks ADR 0287: %v", p) } for _, c := range []struct { @@ -4942,107 +4945,6 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } - // Forty-seven with mesh-placement (novox/hq ADR 0567), serving its five verbs: adopt and hand-back went with the - // controller (novox/hq ADR 0590, issue 1126). - if s, ok := catalogue.SeatNamed(catalogue.PlacementSeat); !ok || s.Scope != catalogue.ScopeMesh || !reflect.DeepEqual(catalogue.VerbNames(s.Serves), - []string{"placement", "proposals", "propose", "withdraw", "again"}) { - t.Errorf("the placement seat is %v, %v", ok, s) - } - // The judge's copy of the catalog seat is the SDK's entry exactly (ADR 0604 §1): four optional verbs, so the claim of the - // module that held the seat before is not outgrown while both stand. - if sdk, ok := catalogue.SeatNamed(catalogue.CatalogSeat); !ok || len(sdk.Serves) != 4 { - t.Errorf("the SDK's catalog seat is %v, %+v", ok, sdk) - } else { - for _, s := range defaultSeats { - if s.Name == catalogue.CatalogSeat && !reflect.DeepEqual(s, sdk) { - t.Errorf("the judge's copy of the catalog seat is %+v, want the SDK's %+v", s, sdk) - } - } - } - // The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245). - var checkerVerbs []string - for _, s := range catalogue.Seats() { - if s.Name == catalogue.CheckerSeat { - for _, v := range s.Serves { - checkerVerbs = append(checkerVerbs, v.Name) - if v.Name == "try" && strings.Join(v.Replaces, "|") != "sh merge-check.sh|docker run postgres" { - t.Errorf("try replaces %v", v.Replaces) - } - } - } - } - if strings.Join(checkerVerbs, ",") != "try,try-read,try-end" { - t.Errorf("the checker seat serves %v", checkerVerbs) - } - // The four health seats (novox/hq ADR 0564 clause 1): at mesh scope, each held once, each naming ADR 0564, with the - // verbs and events the record names; the events keep the controller's names (clause 3). - for _, c := range []struct{ seat, verbs, emits string }{ - {catalogue.ConditionsSeat, "conditions,silence,resync", "condition-raised,condition-changed,condition-cleared"}, - {catalogue.WatchdogsSeat, "signals,stuck,presence,dead-letters,resync", ""}, - {catalogue.SelfCheckSeat, "doctor,probes", "checked"}, - {catalogue.HealersSeat, "healers,heal,drill", "healer-acted"}, - // And mesh-config's config; its switch went with the controller (ADR 0590 clause 3); and its open renames - // (design 32 *A config value renamed*, ADR 0620). - {"mesh-config", "config,renames", ""}, - // And ADR 0565 clause 1's two. - {catalogue.WantsSeat, "upgrade,bus", "policy-changed"}, - {catalogue.HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"}, - // ADR 0571 clause 2: the artifact store's sweep. - {catalogue.ArtifactSweepSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"}, - } { - s, ok := catalogue.SeatNamed(c.seat) - if !ok || s.Scope != catalogue.ScopeMesh || s.Replicated || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 05") || - strings.Join(catalogue.VerbNames(s.Serves), ",") != c.verbs || strings.Join(s.Emits, ",") != c.emits { - t.Errorf("the health seat %s is %v, %+v", c.seat, ok, s) - } - } - // The node seat node-engine is the node's engine's (novox/hq ADR 0563 §1): at node scope, delivering nothing, its verbs - // (compose-candidate since ADR 0593 §7, manifest-reads since ADR 0618 §3) and four events (manifest-reads-changed since - // ADR 0618 §4), held by no assignment, and a module's claim of it refused. - if engine, ok := catalogue.SeatNamed(catalogue.EngineSeat); !ok || engine.Scope != catalogue.ScopeNode || engine.Delivers != "" || !engine.HeldByTheEngine { - t.Errorf("node-engine is %+v, not a node seat delivering nothing that the engine holds", engine) - } else { - var verbs []string - for _, v := range engine.Serves { - verbs = append(verbs, v.Name) - } - // acts reads one act by its id too (ADR 0563 §9: the seat's tools are its command line). - if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil { - t.Errorf("acts takes no id: %v", engine.Serves[5].Input) - } - if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition,compose,compose-candidate,manifest-reads" || - strings.Join(engine.Emits, ",") != "asked,done,refused,manifest-reads-changed" { - t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits) - } - } - if _, err := ParseManifest([]byte(`{"module":"thing","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)); err == nil || - !strings.Contains(err.Error(), "node-engine") { - t.Errorf("a module's claim of node-engine was not refused: %v", err) - } - // Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq - // ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. - // Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired - // into node-uplink (novox/hq ADR 0223); thirty-seven - // since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with - // node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215); - // thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199, now - // node-hostname); thirty-four - // with node-hotkeys (ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the - // graphical session's eleven (ADR 0208); twenty-one with node-package-manager and - // node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203, - // ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired - // mesh-build-machine row goes, when no registered manifest claims it. - // Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1); - // fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts - // (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2); - // fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1); fifty-six - // without the controller's seat and mesh-grants, which served its lease write alone (novox/hq ADR 0590, issue 1126); - // fifty-nine with the registry seats go-package-registry, container-image-registry and oci-artifact-registry beside - // npm-package-registry (novox/hq ADR 0613, design 51), the seat list now mesh-contracts' (ADR 0625). - if len(catalogue.Seats()) != 59 { - t.Errorf("the mesh defines %d seats rather than 59; the set is closed, so a change here is "+ - "a decision (novox/hq ADR 0110): %s", len(catalogue.Seats()), seatNames()) - } } // §4: xinitrc and xresources slots, placed by the display server's holder alone: the judge takes a session diff --git a/sdk/composer/composer.go b/sdk/composer/composer.go index 73afb4be..1b534365 100644 --- a/sdk/composer/composer.go +++ b/sdk/composer/composer.go @@ -63,6 +63,10 @@ type Request struct { // 0578 §5, ADR 0606 §4); the composer does not read them. Nil is no list at hand, and the judge judges no name; an // empty list is a list, and every name is unknown to it. Packages []string `json:"packages,omitzero"` + // Seats are the seat definitions the judge judges against, as the asker's seat list reader holds them (the definitions of the + // seat mesh-catalog, resolved), raw so that this package names no seat (ADR 0635 §4, ADR 0636 §1). The judge reads them; the + // composer does not. None is no list at hand: the judge then judges against the list its process was given. + Seats []json.RawMessage `json:"seats,omitempty"` } // Case is one catalogue composed: every module's manifest by name, and per node the manifests kept at the build it runs.