diff --git a/internal/bootstrap/bus_adoption.go b/internal/bootstrap/bus_adoption.go index 2e7ca7a..ee8d8dc 100644 --- a/internal/bootstrap/bus_adoption.go +++ b/internal/bootstrap/bus_adoption.go @@ -69,8 +69,9 @@ func adoptTheBus(ctx context.Context, o Options, control controlPlane, bus *decl return nil } -// meshBuiltImage is an image the mesh built, as it records it: `artifact-store:///@sha256:…`. -var meshBuiltImage = regexp.MustCompile(`artifact-store://([a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64})`) +// meshBuiltImage is an image the mesh built, as it records it in either form it reads (ADR 0605 §9, issue 1966 ENG-4): +// `artifact-store:///@sha256:…`, or the registry seat's `container-image-registry//@sha256:…`. +var meshBuiltImage = regexp.MustCompile(`(?:artifact-store://|container-image-registry/)([a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*@sha256:[0-9a-f]{64})`) // theModulesBusImage is the nats module's server image, as the build just made it, pulled from this // mesh's registry: the image that carries the snapshot program. diff --git a/internal/bootstrap/bus_adoption_test.go b/internal/bootstrap/bus_adoption_test.go index 1c16bf1..71a12c1 100644 --- a/internal/bootstrap/bus_adoption_test.go +++ b/internal/bootstrap/bus_adoption_test.go @@ -139,4 +139,12 @@ func TestTheBundlesBusRunsTheReleaseTheModulesImageIsBuiltOn(t *testing.T) { if got, want := containerNamed(rewritten.Declaration, "mesh-broker-nats").Image, repository+"@"+digest; got != want { t.Errorf("the bundle raises the bus on %s; the nats module's server is %s (%s)", got, want, from) } + + // The image the mesh built is read in either form it is recorded in (ENG-4). + sum := strings.Repeat("c", 64) + for _, said := range []string{"artifact-store://nats/server@sha256:" + sum, "container-image-registry/nats/server@sha256:" + sum} { + if m := meshBuiltImage.FindStringSubmatch("build 1: " + said); m == nil || m[1] != "nats/server@sha256:"+sum { + t.Errorf("%s is read as %v", said, m) + } + } } diff --git a/internal/compose/rendering.go b/internal/compose/rendering.go index eb0e47d..770db5e 100644 --- a/internal/compose/rendering.go +++ b/internal/compose/rendering.go @@ -167,14 +167,21 @@ func (c *composer) places(nodeName string, resolved Resolved) (Placed, error) { // Where this machine reaches each mesh seat's holder, for ${seat::reach} (novox/hq ADR 0222). if store != "" { - r.SeatReach = map[string]string{"mesh-artifact-store": store} - // And its holder, from the holdings on record, written beside each image routed through it (design 51, ENG-10). - for _, h := range c.in.Holdings { - if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == "mesh-artifact-store" && h.Scope == catalogue.ScopeMesh && h.Module != "" { - r.SeatHolder = map[string]string{"mesh-artifact-store": h.Module} - break + // Both names the store is reached through answer the one address (ENG-3), each with its holder from the holdings on + // record, written beside every image routed through it (design 51, ENG-10). + r.SeatReach, r.SeatHolder = map[string]string{}, map[string]string{} + for _, seat := range []string{"mesh-artifact-store", "container-image-registry"} { + r.SeatReach[seat] = store + for _, h := range c.in.Holdings { + if hs, ok := catalogue.SeatNamed(h.Claim); ok && hs.Name == seat && h.Scope == catalogue.ScopeMesh && h.Module != "" { + r.SeatHolder[seat] = h.Module + break + } } } + if len(r.SeatHolder) == 0 { + r.SeatHolder = nil + } } r.Settings = settings r.Given = given diff --git a/internal/compose/resolution/artifacts.go b/internal/compose/resolution/artifacts.go index ed10c5d..be3ce85 100644 --- a/internal/compose/resolution/artifacts.go +++ b/internal/compose/resolution/artifacts.go @@ -41,7 +41,7 @@ func artifactsInto(resource map[string]any, module string, with mcat.Rendering) module, resource["id"], written, mcat.ArtifactStoreProvision) } resource[key] = mcat.Routed(written, with.ArtifactStore) - imageSeatInto(resource, key, with) + imageSeatInto(resource, key, written, with) continue } if with.ArtifactStore == "" { @@ -60,7 +60,7 @@ func artifactsInto(resource map[string]any, module string, with mcat.Rendering) } if with.Built[repository] { resource[key] = mcat.Routed(recorded, with.ArtifactStore) - imageSeatInto(resource, key, with) + imageSeatInto(resource, key, written, with) } } return nil @@ -74,10 +74,18 @@ const artifactSeat = "mesh-artifact-store" // *distribution's first delivery*, issue 1966 ENG-10), so the node-engine finds by the declaration whom a failed pull waits // on, never by a module's name. Neither is part of what a container is: a declaration composed again for a new holder // replaces no container. Nothing is written where the holder is not on record, or for a resource that is no container. -func imageSeatInto(resource map[string]any, key string, with mcat.Rendering) { - if key != "image" || resource["type"] != "container" || with.SeatHolder[artifactSeat] == "" { +func imageSeatInto(resource map[string]any, key, written string, with mcat.Rendering) { + if key != "image" || resource["type"] != "container" { return } - resource["image-seat"] = artifactSeat - resource["image-holder"] = with.SeatHolder[artifactSeat] + // The seat a reference of the registry form names; the older forms came through the artifact store's. + seat := artifactSeat + if named, _, ok := mcat.InRegistrySeat(written); ok { + seat = named + } + if with.SeatHolder[seat] == "" { + return + } + resource["image-seat"] = seat + resource["image-holder"] = with.SeatHolder[seat] } diff --git a/internal/compose/resolution/artifacts_test.go b/internal/compose/resolution/artifacts_test.go index 9cdd471..a43e083 100644 --- a/internal/compose/resolution/artifacts_test.go +++ b/internal/compose/resolution/artifacts_test.go @@ -64,6 +64,22 @@ func TestAnImageTheMeshBuiltIsRoutedThroughTheStoreWhenDeclared(t *testing.T) { t.Errorf("%s names a seat: %v", id, got) } } + // A reference in the form a registry seat answers is routed the same way, names the seat it came through (ENG-4), and its + // bundle is fetched as an archive from the store. + seatForm := mcat.Resolution{Node: "anchor", Modules: []mcat.Manifest{{Module: "gitea", Version: "1", Resources: []map[string]any{ + {"id": "server", "type": "container", "name": "mesh-gitea", "image": "container-image-registry/gitea/server@" + digest}, + {"id": "config", "type": "archive", "path": "/etc/gitea", "source": "oci-artifact-registry/gitea/config@" + digest}}}}} + held.SeatHolder["container-image-registry"] = "distribution" + seated, err := Declaration(seatForm, held) + if err != nil { + t.Fatal(err) + } + if got := fileNamed(seated, "gitea.server"); got["image"] != "anchor.internal:5101/gitea/server@"+digest || got["image-seat"] != "container-image-registry" || got["image-holder"] != "distribution" { + t.Errorf("a seat-form image is %v, naming %v %v", got["image"], got["image-seat"], got["image-holder"]) + } + if got := fileNamed(seated, "gitea.config")["source"]; got != "http://anchor.internal:5101/v2/gitea/config/blobs/"+digest { + t.Errorf("a seat-form archive is fetched from %v", got) + } held.SeatHolder = map[string]string{"mesh-artifact-store": "other-holder"} if held2, err := Declaration(r, held); err != nil || fileNamed(held2, "gitea.server")["image-holder"] != "other-holder" { t.Errorf("a new holder is not written: %v", err) diff --git a/internal/compose/resolution/control_records_seats.go b/internal/compose/resolution/control_records_seats.go deleted file mode 100644 index d3b53f1..0000000 --- a/internal/compose/resolution/control_records_seats.go +++ /dev/null @@ -1,52 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// controlRecordsDecision is the record that made the two seats. -const controlRecordsDecision = "novox/hq ADR 0565" - -// wantsVerbs are the assigner's verbs on mesh-wants (ADR 0565 clauses 7, 8, 9, 15). -func wantsVerbs() []mcat.Verb { - return []mcat.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() []mcat.Verb { - return []mcat.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"})}, - } -} diff --git a/internal/compose/resolution/engine_seat.go b/internal/compose/resolution/engine_seat.go deleted file mode 100644 index 58408be..0000000 --- a/internal/compose/resolution/engine_seat.go +++ /dev/null @@ -1,11 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// engineSeat is the seat's row in the closed set. -func engineSeat() mcat.Seat { - return mcat.Seat{Name: mcat.EngineSeat, Scope: mcat.ScopeNode, Serves: mcat.EngineVerbs(), Emits: append([]string(nil), mcat.EngineEvents...), - HeldByTheEngine: true, Decision: "novox/hq ADR 0563"} -} diff --git a/internal/compose/resolution/files.go b/internal/compose/resolution/files.go index 3b20ba9..0b7d706 100644 --- a/internal/compose/resolution/files.go +++ b/internal/compose/resolution/files.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "path" - "strings" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -14,73 +13,6 @@ const filesAsAccount = "as the node's operator account, through sudo -n where it const overrideWord = "a reason, to change a path a module owns anyway (optional)" -// 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() []mcat.Verb { - var verbs []mcat.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) mcat.Verb { - in["reason"] = "why the file is changed, for the audit trail" - in["override"] = overrideWord - return mcat.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"), mcat.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"})}, mcat. - // 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). - 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"})}, mcat.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"), - ) -} - // ownedPaths is every path a module owns on this node, as the holder is told (novox/hq ADR 0394 §5): the // paths every seat declares (ADR 0287) with the module holding the seat here, and the paths and files each // module's manifest places (declaredPaths). A seat's path wins where both name one. diff --git a/internal/compose/resolution/graphical_session.go b/internal/compose/resolution/graphical_session.go deleted file mode 100644 index 059d9fd..0000000 --- a/internal/compose/resolution/graphical_session.go +++ /dev/null @@ -1,228 +0,0 @@ -package resolution - -import ( - "fmt" - "strings" - - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// 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() []mcat.Seat { - const decided = "novox/hq ADR 0208" - return []mcat.Seat{ - {Name: mcat.LoginManagerSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.DisplayServerSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.DisplaySessionSeat, Scope: mcat.ScopeNode, Decision: decided, - Receives: []mcat.Receivable{{Kind: "config", Comment: "#"}}, Serves: []mcat.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: mcat.TerminalEmulatorSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.LauncherSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.NotifierSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.LockScreenSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.Verb{ - {Name: "lock", Description: "Lock the operator's session now.", - Input: schema(map[string]string{}, nil)}, - }}, - {Name: mcat.ClipboardSeat, Scope: mcat.ScopeNode, Decision: decided, Serves: []mcat.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: mcat.BarSeat, Scope: mcat.ScopeNode, Decision: decided, - Receives: []mcat.Receivable{{Kind: mcat.BarKindBlock, Comment: "#", Offered: true, Shape: barBlockShape()}}}, - {Name: mcat.CompositorSeat, Scope: mcat.ScopeNode, Decision: decided}, - {Name: mcat.SecretServiceSeat, Scope: mcat.ScopeNode, Decision: decided}, - } -} - -// withEnum narrows one string property of a schema to the values it may take, so a caller is told -// the choices by the schema rather than by a refusal. -func withEnum(s map[string]any, property string, values ...string) map[string]any { - props := s["properties"].(map[string]any) - p := props[property].(map[string]any) - p["enum"] = values - return s -} - -// barShows is what a contributed block may show, and the options each takes — the bar-neutral -// vocabulary every holder of node-bar renders. A new entry is a decision, and every holder's template -// renders it (the shape's examples) before it is accepted. -var barShows = map[string]map[string]barOption{ - // The machine's battery: its charge, and how long it lasts or takes to fill. - "battery": {"device": {kind: "text"}}, - // One line a command prints, run every interval seconds: what the bar cannot know itself. - "command": {"command": {kind: "text", required: true}, "interval": {kind: "seconds", required: true}}, - // A value the contributor keeps on the bus (novox/hq ADR 0255): its state `.` and the - // key, this machine's name when absent. The value is an object whose `text` is what is shown. The - // holder is granted to read it and shows it however its bar shows a changing value; no file and - // no command passes between the two modules. - "state": {"state": {kind: "text", required: true}, "key": {kind: "text"}}, -} - -// barBlockReads is the state and key a block shows, if any; an empty key is the machine's own. -func barBlockReads(data map[string]any) []mcat.StateRead { - if data["shows"] != "state" { - return nil - } - options, _ := data["options"].(map[string]any) - s, ok := options["state"].(string) - if !ok || s == "" { - return nil - } - key, _ := options["key"].(string) - return []mcat.StateRead{{Read: s, Key: key}} -} - -type barOption struct { - kind string // text, or seconds (a whole number from 1 to 86400) - required bool -} - -// barBlockShape is the shape of a block (novox/hq ADR 0255): which bar, where on it, in what order, -// what it shows, and that kind's options. -// -// - bar: top or bottom; -// - place: resources (beside the processor, memory and disks) or status (beside the sound, before the -// clock) — the two places every holder keeps for contributed blocks; -// - order: 0–99 within the place, 50 when absent; -// - shows: battery or command; options: what that kind takes. -func barBlockShape() *mcat.Shape { - return &mcat.Shape{ - Text: true, - Places: []mcat.PlaceField{{Field: "bar", Values: []string{"bottom", "top"}}, - {Field: "place", Values: []string{"resources", "status"}}}, - Order: "order", - Check: barBlockProblems, - Reads: barBlockReads, - Examples: []map[string]any{ - {"bar": "bottom", "place": "status", "shows": "battery"}, - {"bar": "bottom", "place": "status", "shows": "battery", "options": map[string]any{"device": "BAT0"}}, - {"bar": "bottom", "place": "status", "shows": "command", - "options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}}, - {"bar": "bottom", "place": "status", "shows": "state", "options": map[string]any{"state": "power.draw"}}, - {"bar": "bottom", "place": "status", "shows": "state", - "options": map[string]any{"state": "power.draw", "key": "a-machine"}}, - }, - } -} - -func barBlockProblems(data map[string]any) []string { - var problems []string - for field := range data { - if !oneOf([]string{"bar", "place", "order", "shows", "options"}, field) { - problems = append(problems, fmt.Sprintf("a block has no field %q; it has bar, place, order, shows and options", field)) - } - } - shows, _ := data["shows"].(string) - takes, known := barShows[shows] - if !known { - return append(problems, fmt.Sprintf("a block shows %v; it shows one of %s", data["shows"], - strings.Join(sortedKeys(barShows), ", "))) - } - options := map[string]any{} - if raw, has := data["options"]; has { - o, ok := raw.(map[string]any) - if !ok { - return append(problems, "a block's options are an object") - } - options = o - } - for name, v := range options { - opt, ok := takes[name] - if !ok { - problems = append(problems, fmt.Sprintf("a %s block has no option %q; it takes %s", shows, name, - orNothing(sortedKeys(takes)))) - continue - } - switch opt.kind { - case "text": - if t, isText := v.(string); !isText || strings.TrimSpace(t) == "" { - problems = append(problems, fmt.Sprintf("a %s block's %s is text", shows, name)) - } - case "seconds": - if _, whole := wholeIn(v, 1, 86400); !whole { - problems = append(problems, fmt.Sprintf("a %s block's %s is a whole number of seconds from 1 to 86400", shows, name)) - } - } - } - for _, name := range sortedKeys(takes) { - if _, has := options[name]; takes[name].required && !has { - problems = append(problems, fmt.Sprintf("a %s block needs the option %s", shows, name)) - } - } - return problems -} diff --git a/internal/compose/resolution/health_seats.go b/internal/compose/resolution/health_seats.go deleted file mode 100644 index 78fa348..0000000 --- a/internal/compose/resolution/health_seats.go +++ /dev/null @@ -1,104 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// healthDecision is the record that made the four seats. -const healthDecision = "novox/hq ADR 0564" - -// conditionsVerbs are the condition keeper's verbs (ADR 0564 clauses 3, 9, 12). -func conditionsVerbs() []mcat.Verb { - return []mcat.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() []mcat.Verb { - return []mcat.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() []mcat.Verb { - return []mcat.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() []mcat.Verb { - return []mcat.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"})}, - } -} diff --git a/internal/compose/resolution/holdings_test.go b/internal/compose/resolution/holdings_test.go index b0396fe..103d133 100644 --- a/internal/compose/resolution/holdings_test.go +++ b/internal/compose/resolution/holdings_test.go @@ -16,12 +16,12 @@ func busSeatDelivering(t *testing.T, delivers string) { func oldBroker() mcat.Manifest { return mcat.Manifest{Module: "old-broker", Provides: []mcat.Offer{{Name: "mesh-bus", Scope: mcat.ScopeMesh}}, - Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh}}} + Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh, Serves: brokerVerbs()}}} } func newBroker() mcat.Manifest { return mcat.Manifest{Module: "new-broker", Provides: []mcat.Offer{{Name: "mesh-bus", Scope: mcat.ScopeMesh}}, - Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh}}} + Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh, Serves: brokerVerbs()}}} } // Nothing on record: exactly the old rule. One claimant holds; two are refused. @@ -108,3 +108,16 @@ func TestARefusalWithNothingOnRecordNamesTheHandover(t *testing.T) { t.Fatalf("the refusal does not name the handover that records the holder: %s", problems[0]) } } + +// brokerVerbs are the verbs the seat mesh-broker promises its holder, as the contracts define them: a holder names every one +// (ADR 0132), so a fixture holder does (issue 1966 ENG-3). +func brokerVerbs() []string { + seat, _ := mcat.SeatNamed("mesh-broker") + var out []string + for _, v := range seat.Serves { + if !v.Optional { + out = append(out, v.Name) + } + } + return out +} diff --git a/internal/compose/resolution/reads.go b/internal/compose/resolution/reads.go index ce313bd..66be41c 100644 --- a/internal/compose/resolution/reads.go +++ b/internal/compose/resolution/reads.go @@ -10,64 +10,6 @@ import ( mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// 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() []mcat.Verb { - return []mcat.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"}}, - } -} - // secretPlaces is every place on this node a secret is kept, as the placed manifests say (novox/hq ADR 0310 // §5): the directory of every module's own secret, every credential file a module is given and every grant // directory a provider is handed its consumers' credentials in, the place a module keeps the operator-sealed diff --git a/internal/compose/resolution/seat_into.go b/internal/compose/resolution/seat_into.go index 4a08686..c78ec5c 100644 --- a/internal/compose/resolution/seat_into.go +++ b/internal/compose/resolution/seat_into.go @@ -33,7 +33,10 @@ var ofSeat = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):([0-9]+)\}`) var ofSeatReach = regexp.MustCompile(`\$\{seat:([a-z0-9][a-z0-9-]*):reach\}`) // reachedSeats is every seat ${seat:…:reach} answers for. -var reachedSeats = map[string]bool{"mesh-artifact-store": true} +// +// Both names of the store are answered with one address (ADR 0605, issue 1966 ENG-3): the seat it was held through and the +// seat the registries' holder serves the same port as. +var reachedSeats = map[string]bool{"mesh-artifact-store": true, "container-image-registry": true} // seatInto replaces a resource's ${seat:…} placeholders with where this machine put each seat's // holder — in a file's content, and in a value of a container's or a process's environment. The diff --git a/internal/compose/resolution/seat_into_test.go b/internal/compose/resolution/seat_into_test.go index 77f6cb6..ddb7246 100644 --- a/internal/compose/resolution/seat_into_test.go +++ b/internal/compose/resolution/seat_into_test.go @@ -110,6 +110,17 @@ func TestASeatsReachIsWhereThisMachineReachesItsHolder(t *testing.T) { if got := process["env"].(map[string]any)["REGISTRY"]; got != "anchor.internal:5100" { t.Fatalf("an environment value was filled with %q", got) } + + // And the seat the registries' holder serves the same port through answers the same address (ENG-3). + both := map[string]any{"type": "process", "name": "p", "env": map[string]any{ + "OLD": "${seat:mesh-artifact-store:reach}", "NEW": "${seat:container-image-registry:reach}"}} + with.SeatReach["container-image-registry"] = "anchor.internal:5100" + if err := seatInto(both, "x", with); err != nil { + t.Fatal(err) + } + if env := both["env"].(map[string]any); env["OLD"] != "anchor.internal:5100" || env["NEW"] != "anchor.internal:5100" { + t.Fatalf("the two names of the store answer %v", env) + } } // With no holder reachable, the runtime is not told to trust "": the member is dropped, and the diff --git a/internal/compose/resolution/seats.go b/internal/compose/resolution/seats.go index 5e1beb8..1420499 100644 --- a/internal/compose/resolution/seats.go +++ b/internal/compose/resolution/seats.go @@ -4,408 +4,10 @@ import ( mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the -// fallback when it has none (novox/hq ADR 0122). It is the one place the closed set 0110 defines is -// written; the store's table is seeded from it and thereafter is the live, editable copy. -// -// In the order a person reads it: the mesh's own, then a node's. -var defaultSeats = append([]mcat.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: mcat.ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079", - Serves: []mcat.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: mcat.BrokerSeat, Scope: mcat.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: mcat.ScopeMesh, Delivers: "secret", Decision: "novox/hq ADR 0161", - Serves: []mcat.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 a machine's internal name, asked by that machine's " + - "node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for " + - "a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a " + - "redeem answer under its own context, for mesh-enrolment alone. 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, or the node mesh-enrolment enrols", - "serving_key": "from mesh-enrolment: the serving key's public half the enrolling node presented", - "reply": "the redeem answer's bytes in base64, for an enrolment"}, []string{"kind"})}, - // Enrolment (novox/hq ADR 0561 §5, ADR 0587 §3, §5; issue 774): mesh-enrolment is the only caller the vault's - // manifest names for these two. - {Name: "enrolment-token", Optional: true, Description: "Make a one-time token for a node mesh-enrolment " + - "issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash " + - "written as the bus user enrol.. Answers only what the joining machine must trust (the bus's " + - "address, the bus certificate's fingerprint, the vault's public signing key), never the token; with " + - "sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash " + - "dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token " + - "carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", - Input: withObject(schema(map[string]string{"node": "the node the token is for", - "expires": "when the token expires (RFC 3339), as the enrolment's key says", - "sealed": "\"true\": answer the token sealed to the operator key", - "drop": "\"true\": drop the node's token and its hash"}, []string{"node"}, "sealed", "drop"), - "tunnel", "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's "+ - "public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", - map[string]string{"hub_key": "the hub's tunnel public key", "hub_endpoint": "the hub's endpoint, host:port", - "address": "the joining node's tunnel address", - "key": "the joining machine's own tunnel public key, as given at issue", - "range": "the private network's range, a prefix"})}, - {Name: "node-account", Optional: true, Description: "Mint a joining node's bus account node., sealed " + - "to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the " + - "account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the " + - "node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", - Input: schema(map[string]string{"node": "the joining node", - "sealing_key": "the sealing key's public half the node presented, which the account is sealed to", - "keys_digest": "the digest of every public key the node presented (delivery.KeysDigest)", - "retire": "\"true\": retire the account minted for this node"}, []string{"node"}, "retire")}, - // A node's account rotates over two logins (novox/hq ADR 0583 §2-§5; issue 774), one node at a time, the bus - // node last; the state node-logins says where each node stands. - {Name: "rotate-node", Optional: true, Description: "Begin a node's account rotation to its other login: " + - "answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by " + - "its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says " + - "rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One " + - "node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", - Input: schema(map[string]string{"node": "the node whose account rotates", "why": "why, in a sentence", - "act": "the id of the hand act recorded for it in mesh-hand-acts"}, []string{"node", "why", "act"})}, - {Name: "node-account-confirm", Optional: true, Description: "Confirm a node's switch to its new login: " + - "asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. " + - "The old login's hash is then removed, and rotation-done said once the bus node's have shows the list " + - "without it.", - Input: schema(map[string]string{}, nil)}, - {Name: "abandon", Optional: true, Description: "Abandon a node's rotation before its old login is retired: " + - "the new login and its value dropped, the old one kept; a node that switched switches back first.", - Input: schema(map[string]string{"node": "the node whose rotation is abandoned"}, []string{"node"})}, - }}, - // 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: mcat.ScopeMesh, Decision: "novox/hq ADR 0348, ADR 0565", - Serves: []mcat.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: mcat.ScopeMesh, Decision: "novox/hq ADR 0349", - Serves: []mcat.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 — which is why the provision is the artifact store and not an - // image registry. - {Name: "mesh-artifact-store", Scope: mcat.ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"}, - // The catalog (novox/hq ADR 0604): the SDK's entry with its four verbs, so the copies of the table agree. - sdkSeat(mcat.CatalogSeat), - // 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: mcat.DeliverySeat, Scope: mcat.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: mcat.PlacementSeat, Scope: mcat.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: mcat.CheckerSeat, Scope: mcat.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: mcat.ConditionsSeat, Scope: mcat.ScopeMesh, Decision: healthDecision, - Emits: []string{"condition-raised", "condition-changed", "condition-cleared"}, Serves: conditionsVerbs()}, - {Name: mcat.WatchdogsSeat, Scope: mcat.ScopeMesh, Decision: healthDecision, Serves: watchdogsVerbs()}, - {Name: mcat.SelfCheckSeat, Scope: mcat.ScopeMesh, Decision: healthDecision, Emits: []string{"checked"}, Serves: selfCheckVerbs()}, - {Name: mcat.HealersSeat, Scope: mcat.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: mcat.WantsSeat, Scope: mcat.ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()}, - {Name: mcat.HandActsSeat, Scope: mcat.ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()}, - // Joining a machine (novox/hq ADR 0587 §1, enrolment_seat.go): held once by the module mesh-enrolment. - {Name: mcat.EnrolmentSeat, Scope: mcat.ScopeMesh, Decision: enrolmentDecision, Emits: mcat.EnrolmentEvents, Serves: enrolmentVerbs()}, - // The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go). - {Name: mcat.ArtifactSweepSeat, Scope: mcat.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: mcat.ScopeMesh, Delivers: "npm-package-registry", Decision: "novox/hq ADR 0109"}, - {Name: "git", Scope: mcat.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: mcat.ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Go"), - Yields: true, Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553"}, - {Name: "node-build-node", Scope: mcat.ScopeNode, Accepts: buildAskKinds(), PulledByKind: true, Serves: buildSeatVerbs("Node"), - Yields: true, Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553"}, - {Name: "node-build-image", Scope: mcat.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: mcat.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: mcat.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: []mcat.SeatPath{{Path: "/etc/hosts"}, {Path: "/etc/hostname"}}, - Serves: []mcat.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: mcat.ScopeNode, Decision: "novox/hq ADR 0121", - Serves: []mcat.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: mcat.ScopeNode, Decision: "novox/hq ADR 0121", - Serves: []mcat.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: mcat.ServiceManagerSeat, Scope: mcat.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: mcat.PackageManagerSeat, Scope: mcat.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: mcat.ContainerRuntimeSeat, Scope: mcat.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: mcat.EnvironmentSeat, Scope: mcat.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: mcat.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: mcat.LoginShellSeat, Scope: mcat.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: mcat.PowerSeat, Scope: mcat.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: mcat.HotkeysSeat, Scope: mcat.ScopeNode, Decision: "novox/hq ADR 0212", - Receives: []mcat.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: mcat.MessageBusSeat, Scope: mcat.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: mcat.BackupSeat, Scope: mcat.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: []mcat.Receivable{{Kind: mcat.BackupKindBackup, Comment: "#", Dirs: true}, {Kind: mcat.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: mcat.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: mcat.ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs(), - Paths: []mcat.SeatPath{{Path: mcat.ResolverFile, StepsBackFor: mcat.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: mcat.ResolverSeat, Scope: mcat.ScopeNode, Decision: "novox/hq ADR 0247", Serves: resolverVerbs(), - Paths: []mcat.SeatPath{{Path: mcat.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: mcat.NFSServerSeat, Scope: mcat.ScopeNode, Decision: "novox/hq ADR 0263", Serves: nfsServerVerbs()}, - {Name: mcat.MountsSeat, Scope: mcat.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: mcat.ReadsSeat, Scope: mcat.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: mcat.FilesSeat, Scope: mcat.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()...) +// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the fallback when it has none +// (novox/hq ADR 0122). It is mesh-contracts' generated list, as mesh-sdk compiles it, taken whole: the node-engine keeps no copy of +// a seat's verbs or events (design 26, ADR 0608 §5; issue 1966 ENG-3). +var defaultSeats = mcat.DefaultSeats() // seats is the working set the lookups read. It starts as the compiled defaults and is replaced by // what the control plane loaded from its store (novox/hq ADR 0122), so a change to the set is a @@ -467,195 +69,3 @@ func HolderAmong(provision string, providers []mcat.Provider, held []mcat.Held) } return mcat.Provider{}, false } - -// 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() []mcat.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 []mcat.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/"}}, - } -} - -// 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() []mcat.Verb { - return []mcat.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}, - } -} - -// 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() []mcat.Verb { - return []mcat.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"}}, - } -} - -// 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() []mcat.Verb { - return []mcat.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"})}, - } -} - -// 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) []mcat.Verb { - return []mcat.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)}, - } -} - -// vaultIntake is a two-step vault verb's arguments: what its first step names, then the call and what was sealed. -// withHolders adds to `issued` the provision's holders from that provider (issue 749): a provider's tool may answer a -// value for each, which the vault keeps as that holder's own and hands to it alone. -func withHolders(input map[string]any) map[string]any { - props, _ := input["properties"].(map[string]any) - props["holders"] = map[string]any{"type": "array", - "description": "the provision's holders from that provider, each {node, module}: its tool may answer a value " + - "for each, kept as that holder's own and handed to it alone", - "items": map[string]any{"type": "object", "properties": map[string]any{ - "node": map[string]any{"type": "string", "description": "the holder's machine"}, - "module": map[string]any{"type": "string", "description": "the consuming module"}}}} - return input -} - -func vaultIntake(first map[string]string) map[string]string { - out := map[string]string{"call": "the second step: the call the first step answered", - "sealed": "the second step: the value sealed to the call's key — never the value"} - for k, v := range first { - out[k] = v - } - return out -} - -// sdkSeat is the SDK's entry for a seat, taken whole, so this copy of the table cannot disagree with it. -func sdkSeat(name string) mcat.Seat { - seat, ok := mcat.SeatNamed(name) - if !ok { - panic("the SDK's seat table holds no seat " + name) - } - return seat -} diff --git a/internal/compose/seats_test.go b/internal/compose/seats_test.go index a07e0c2..d39cbd9 100644 --- a/internal/compose/seats_test.go +++ b/internal/compose/seats_test.go @@ -10,7 +10,20 @@ import ( func aBrokerClaiming(module string) catalogue.Manifest { return catalogue.Manifest{Module: module, Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}}, - Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Serves: brokerVerbs()}}} +} + +// brokerVerbs are the verbs the seat mesh-broker promises its holder, as the contracts define them: a holder names every one +// (ADR 0132), so a fixture holder does (issue 1966 ENG-3). +func brokerVerbs() []string { + seat, _ := catalogue.SeatNamed("mesh-broker") + var out []string + for _, v := range seat.Serves { + if !v.Optional { + out = append(out, v.Name) + } + } + return out } // withSeat is mesh-controller's withBusSeat: a single mesh seat, mesh-broker, for the test.