diff --git a/go/seatlist/bootstrap.go b/go/seatlist/bootstrap.go new file mode 100644 index 0000000..7559f8e --- /dev/null +++ b/go/seatlist/bootstrap.go @@ -0,0 +1,42 @@ +package seatlist + +import ( + "embed" + "fmt" + "sort" + + "git.novox.be/novox/mesh-sdk/go/seatdef" +) + +// The foundation's bootstrapped definitions (novox/hq ADR 0608 §2, ADR 0635 §3): the seats the foundation's parts claim +// and invoke before the indexer has run, kept as the definitions' own files (`bootstrap/.json`, the format's own +// text, whole or at the seat list's grain) and embedded as data. A reader holds them until the owner answers, and says so +// (Status). **They are copies:** a seat's definition is the one in its contracts module, and a test there holds each of +// these files equal to it; where a seat moves or changes, its file here is replaced by the same change. Which seats are +// the foundation's, and how a fresh mesh gets them without a copy, is research 075's. +// +//go:embed bootstrap/*.json +var bootstrapFiles embed.FS + +// Bootstrapped are the foundation's definitions, by seat name. It panics on a file that does not parse, which the +// package's test refuses to let ship. +func Bootstrapped() []seatdef.Definition { + entries, err := bootstrapFiles.ReadDir("bootstrap") + if err != nil { + panic(err) + } + var out []seatdef.Definition + for _, e := range entries { + raw, err := bootstrapFiles.ReadFile("bootstrap/" + e.Name()) + if err != nil { + panic(err) + } + d, err := seatdef.Parse(raw) + if err != nil { + panic(fmt.Sprintf("seatlist: bootstrap/%s does not parse: %v", e.Name(), err)) + } + out = append(out, d) + } + sort.Slice(out, func(i, j int) bool { return out[i].Seat < out[j].Seat }) + return out +} diff --git a/go/seatlist/bootstrap/mesh-broker.json b/go/seatlist/bootstrap/mesh-broker.json new file mode 100644 index 0000000..f0ebf4a --- /dev/null +++ b/go/seatlist/bootstrap/mesh-broker.json @@ -0,0 +1,569 @@ +{ + "format": 1, + "seat": "mesh-broker", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0079", + "delivers": "mesh-bus", + "description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.", + "verbs": [ + { + "name": "list", + "description": "Every entry the bus module created, its template, its settings and its fill against its cap.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "answer": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/defs/Held" + }, + "description": "every entry" + } + }, + "required": [ + "entries" + ] + }, + "optional": true + }, + { + "name": "show", + "description": "One entry, its template, settings and fill.", + "callers": "any caller whose role grants it", + "input": { + "$ref": "#/defs/Named" + }, + "answer": { + "$ref": "#/defs/Held" + }, + "optional": true + }, + { + "name": "check", + "description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "answer": { + "type": "object", + "properties": { + "differences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry" + }, + "asked": { + "type": "object", + "description": "asked" + }, + "held": { + "type": "object", + "description": "held" + }, + "grows": { + "type": "boolean", + "description": "the difference only grows: apply may act on it" + } + }, + "required": [ + "module", + "local-name" + ] + }, + "description": "every difference" + } + }, + "required": [ + "differences" + ] + }, + "optional": true + }, + { + "name": "orphans", + "description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": {}, + "description": "no arguments" + }, + "answer": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/defs/Held" + }, + "description": "every orphan" + } + }, + "required": [ + "entries" + ] + }, + "optional": true + }, + { + "name": "create", + "description": "Create one entry now.", + "callers": "the bus module's own process only", + "input": { + "$ref": "#/defs/Entry" + }, + "answer": { + "$ref": "#/defs/Held" + }, + "optional": true + }, + { + "name": "apply", + "description": "Apply a difference that only grows: a larger cap, a longer age.", + "callers": "the bus module's own process only", + "input": { + "$ref": "#/defs/Named" + }, + "answer": { + "$ref": "#/defs/Held" + }, + "optional": true + }, + { + "name": "recreate", + "description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", + "callers": "the bus module's process, on the operator's warrant", + "input": { + "$ref": "#/defs/Warranted" + }, + "answer": { + "$ref": "#/defs/Held" + }, + "optional": true + }, + { + "name": "drop", + "description": "Remove one orphan, after a stream snapshot.", + "callers": "the bus module's process, on the operator's warrant", + "input": { + "$ref": "#/defs/Warranted" + }, + "answer": { + "type": "object", + "properties": { + "dropped": { + "type": "boolean", + "description": "it is gone" + }, + "snapshot": { + "type": "string", + "description": "the snapshot taken first" + } + }, + "required": [ + "dropped" + ] + }, + "optional": true + } + ], + "events": [ + { + "name": "created", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "refused", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "drifted", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "drift-cleared", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "full", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "full-cleared", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "orphaned", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "recreated", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "dropped", + "body": { + "$ref": "#/defs/Said" + } + } + ], + "family": { + "name": "mesh-broker.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "bus-state", + "form": { + "$ref": "#/defs/Binding" + } + } + ], + "defs": { + "Entry": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + } + }, + "required": [ + "module", + "local-name", + "storage-type" + ], + "description": "one piece of bus state as a manifest asks it" + }, + "Fill": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "messages, keys or objects held" + }, + "bytes": { + "type": "integer", + "description": "bytes held" + }, + "cap": { + "type": "integer", + "description": "its cap in bytes" + } + }, + "required": [ + "count", + "bytes", + "cap" + ] + }, + "Held": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + }, + "bus-name": { + "type": "string", + "description": "its name on the bus, _" + }, + "settings": { + "type": "object", + "description": "the settings the bus holds" + }, + "fill": { + "$ref": "#/defs/Fill" + }, + "state": { + "type": "string", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)" + }, + "why": { + "type": "string", + "description": "why it is in its state, in plain words; empty when held as asked" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "description": "one entry as the bus holds it" + }, + "Said": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + }, + "bus-name": { + "type": "string", + "description": "its name on the bus" + }, + "asked": { + "type": "object", + "description": "the settings asked" + }, + "held": { + "type": "object", + "description": "the settings held" + }, + "why": { + "type": "string", + "description": "why: a refusal's cause, a drift's difference, a drop's or recreate's reason" + }, + "fill": { + "$ref": "#/defs/Fill" + }, + "snapshot": { + "type": "string", + "description": "the stream snapshot taken before a recreate or drop" + }, + "binding": { + "$ref": "#/defs/Binding" + }, + "revision": { + "type": "integer", + "description": "the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears" + } + }, + "required": [ + "module", + "local-name", + "bus-name" + ], + "description": "the body of every event of the seat" + }, + "Named": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry's local name" + } + }, + "required": [ + "module", + "local-name" + ] + }, + "Warranted": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry's local name" + }, + "warrant": { + "type": "string", + "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)" + }, + "why": { + "type": "string", + "description": "why, kept in the record" + } + }, + "required": [ + "module", + "local-name", + "warrant", + "why" + ] + }, + "Binding": { + "type": "object", + "properties": { + "bus-name": { + "type": "string", + "description": "the entry's name on the bus" + }, + "storage-type": { + "type": "string", + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "description": "its template" + }, + "subjects": { + "type": "array", + "items": { + "type": "string" + }, + "description": "its subjects, for a stream" + } + }, + "required": [ + "bus-name", + "storage-type" + ], + "description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)" + } + } +} diff --git a/go/seatlist/bootstrap/mesh-catalog.json b/go/seatlist/bootstrap/mesh-catalog.json new file mode 100644 index 0000000..40b6618 --- /dev/null +++ b/go/seatlist/bootstrap/mesh-catalog.json @@ -0,0 +1,69 @@ +{ + "format": 1, + "seat": "mesh-catalog", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0604", + "verbs": [ + { + "name": "modules", + "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "module", + "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + }, + { + "name": "provides", + "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", + "optional": true, + "input": { + "properties": { + "provision": { + "description": "the provision, such as postgres-database", + "type": "string" + } + }, + "required": [ + "provision" + ], + "type": "object" + } + }, + { + "name": "dependents", + "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + } + ] +} diff --git a/go/seatlist/bootstrap/mesh-config.json b/go/seatlist/bootstrap/mesh-config.json new file mode 100644 index 0000000..d095825 --- /dev/null +++ b/go/seatlist/bootstrap/mesh-config.json @@ -0,0 +1,222 @@ +{ + "format": 1, + "seat": "mesh-config", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0348, ADR 0565", + "verbs": [ + { + "name": "config", + "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.", + "optional": true, + "input": { + "properties": { + "action": { + "description": "list (when not given), set, clear or history", + "type": "string" + }, + "by": { + "description": "who gave the value at the controller's terminal", + "type": "string" + }, + "key": { + "description": "the config key", + "type": "string" + }, + "limit": { + "description": "for history: at most this many, newest", + "type": "number" + }, + "module": { + "description": "a module: its own value, with node", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "value": { + "description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list" + }, + "why": { + "description": "for set and clear: why", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the values asked, or the one set or cleared, or its history" + } + }, + { + "name": "renames", + "optional": true, + "description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "callers": "any caller whose role grants it; the checker asks it", + "answer": { + "type": "object", + "properties": { + "renames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "from": { + "type": "string", + "description": "the old name" + }, + "to": { + "type": "string", + "description": "the new name" + }, + "at": { + "type": "string", + "description": "when it moved, RFC 3339" + }, + "open": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the nodes still holding the old name open" + } + }, + "required": [ + "module", + "from", + "to" + ] + }, + "description": "every open rename" + } + }, + "required": [ + "renames" + ] + } + } + ], + "description": "The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "config-changed", + "subject": "mesh.mod..event.config-changed" + }, + { + "name": "renamed", + "subject": "mesh.stream..event.renamed", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "from": { + "type": "string", + "description": "the old name" + }, + "to": { + "type": "string", + "description": "the new name" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "module", + "from", + "to", + "at" + ] + } + }, + { + "name": "retiring", + "subject": "mesh.stream..event.retiring", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "key": { + "type": "string", + "description": "the value's or secret's name" + }, + "kind": { + "type": "string", + "enum": [ + "value", + "secret" + ] + }, + "since": { + "type": "string", + "description": "since when, RFC 3339" + }, + "ask": { + "type": "string", + "description": "the ask's id" + } + }, + "required": [ + "module", + "key", + "kind", + "since" + ] + } + }, + { + "name": "retired", + "subject": "mesh.stream..event.retired", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "name": { + "type": "string", + "description": "the name" + }, + "node": { + "type": "string", + "description": "the node" + }, + "ask": { + "type": "string", + "description": "the ask's id" + } + }, + "required": [ + "module", + "name" + ] + } + } + ] +} diff --git a/go/seatlist/bootstrap/mesh-delivery-checker.json b/go/seatlist/bootstrap/mesh-delivery-checker.json new file mode 100644 index 0000000..7b56ab5 --- /dev/null +++ b/go/seatlist/bootstrap/mesh-delivery-checker.json @@ -0,0 +1,245 @@ +{ + "format": 1, + "seat": "mesh-delivery-checker", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0547", + "verbs": [ + { + "name": "try", + "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", + "optional": true, + "replaces": [ + "sh merge-check.sh", + "docker run postgres" + ], + "input": { + "properties": { + "head": { + "description": "the full commit to try, 40 or 64 hex digits", + "type": "string" + }, + "level": { + "description": "unit or integration: only this level; both when absent", + "type": "string" + }, + "repository": { + "description": "/", + "type": "string" + }, + "tests": { + "description": "a go test -run pattern: only those tests run, and nothing is kept", + "type": "string" + }, + "trunk": { + "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", + "type": "string" + } + }, + "required": [ + "repository", + "head" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try's id" + }, + "state": { + "type": "string", + "description": "its state" + } + }, + "required": [ + "id" + ] + } + }, + { + "name": "try-read", + "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the try: its state, its steps and its verdict when it has one" + } + }, + { + "name": "try-end", + "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + }, + "why": { + "description": "why it is ended, in words", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try" + }, + "ended": { + "type": "boolean", + "description": "it is ended" + } + }, + "required": [ + "id", + "ended" + ] + } + } + ], + "description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "verdict", + "subject": "mesh.stream..event.verdict", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request's number, for a pull request's check" + }, + "head": { + "type": "string", + "description": "the commit checked" + }, + "verdict": { + "type": "string", + "description": "pass or fail" + }, + "out-of-step": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the generated files out of step with their seat definitions, each path (design 26)" + }, + "series": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module whose artifact's series changes" + }, + "artifact": { + "type": "string", + "description": "its artifact, or the package's ecosystem name as the module's packages name it" + }, + "from": { + "type": "string", + "description": "the series on trunk" + }, + "to": { + "type": "string", + "description": "the series the checked head gives it" + } + }, + "required": [ + "module", + "artifact", + "from", + "to" + ] + }, + "description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none" + } + }, + "required": [ + "repository", + "head", + "verdict" + ] + } + }, + { + "name": "tag-wanted", + "subject": "mesh.stream..event.tag-wanted", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request" + }, + "head": { + "type": "string", + "description": "its head" + }, + "branch": { + "type": "string", + "description": "the pull request's branch: its tag is removed when the branch ends" + }, + "module": { + "type": "string", + "description": "the group member" + }, + "package": { + "type": "string", + "description": "the package it declares" + }, + "tag": { + "type": "string", + "description": "the tag wanted" + } + }, + "required": [ + "repository", + "head", + "branch", + "module", + "package" + ] + } + }, + { + "name": "preview.written", + "subject": "mesh.stream..event.preview.written", + "family": "checks" + } + ] +} diff --git a/go/seatlist/bootstrap/mesh-hand-acts.json b/go/seatlist/bootstrap/mesh-hand-acts.json new file mode 100644 index 0000000..a789dbb --- /dev/null +++ b/go/seatlist/bootstrap/mesh-hand-acts.json @@ -0,0 +1,97 @@ +{ + "format": 1, + "seat": "mesh-hand-acts", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0565", + "verbs": [ + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word, or a condition's kind", + "type": "string" + }, + "condition": { + "description": "the key of the condition it addressed, if any (optional)", + "type": "string" + }, + "verb": { + "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", + "type": "string" + }, + "what": { + "description": "what was done, in a line", + "type": "string" + }, + "why": { + "description": "why it had to be done by hand", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + } + }, + { + "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": { + "properties": { + "cause": { + "description": "only this cause", + "type": "string" + }, + "days": { + "description": "how many days back (default 14)", + "type": "string" + }, + "warrants": { + "description": "\"true\": every warrant recorded", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "who": { + "description": "only acts whose who contains this", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "ask": { + "description": "its ask's id", + "type": "string" + }, + "asker": { + "description": "the module that asked", + "type": "string" + } + }, + "required": [ + "asker", + "ask" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "act-recorded" + } + ] +} diff --git a/go/seatlist/bootstrap/mesh-watchdogs.json b/go/seatlist/bootstrap/mesh-watchdogs.json new file mode 100644 index 0000000..3ad9f3c --- /dev/null +++ b/go/seatlist/bootstrap/mesh-watchdogs.json @@ -0,0 +1,93 @@ +{ + "format": 1, + "seat": "mesh-watchdogs", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0564", + "verbs": [ + { + "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": { + "properties": { + "row": { + "description": "one row, e.g. S1", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "only those on this machine", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "one machine", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "cause": { + "description": "with deliver or drop: the cause in a word (dead-letter when absent)", + "type": "string" + }, + "consumer": { + "description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on", + "type": "string" + }, + "deliver": { + "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", + "type": "string" + }, + "drop": { + "description": "a dead letter's id: drop it for good (needs why)", + "type": "string" + }, + "id": { + "description": "a dead letter's id, as the list gives it: that one whole, with its message", + "type": "string" + }, + "limit": { + "description": "how many to list, newest first (default 50); only when listing", + "type": "string" + }, + "why": { + "description": "with deliver or drop: why, required and recorded", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": {}, + "type": "object" + } + } + ] +} diff --git a/go/seatlist/bootstrap/node-backup.json b/go/seatlist/bootstrap/node-backup.json new file mode 100644 index 0000000..465f68f --- /dev/null +++ b/go/seatlist/bootstrap/node-backup.json @@ -0,0 +1,80 @@ +{ + "format": 1, + "seat": "node-backup", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0214", + "verbs": [ + { + "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.", + "replaces": [ + "restic snapshots" + ], + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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.", + "replaces": [ + "restic backup" + ], + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "restore", + "description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as \u003cpath\u003e.restored-\u003cdate\u003e. Swapping it in is a person's act. Lists the restore points when none is named.", + "replaces": [ + "restic restore" + ], + "input": { + "properties": { + "module": { + "description": "the module", + "type": "string" + }, + "path": { + "description": "one of the module's directories (all of them when omitted)", + "type": "string" + }, + "snapshot": { + "description": "the restore point (from `backed-up`; the newest when omitted)", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + } + ], + "receives": [ + { + "kind": "backup", + "comment": "#", + "dirs": true + }, + { + "kind": "data", + "comment": "#", + "dirs": true + } + ] +} diff --git a/go/seatlist/bootstrap/node-engine.json b/go/seatlist/bootstrap/node-engine.json new file mode 100644 index 0000000..dfcaeb8 --- /dev/null +++ b/go/seatlist/bootstrap/node-engine.json @@ -0,0 +1,1055 @@ +{ + "format": 1, + "seat": "node-engine", + "scope": "node", + "grain": "whole", + "decision": "novox/hq ADR 0563", + "description": "The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token.", + "held-by-the-engine": true, + "verbs": [ + { + "name": "hand-over", + "description": "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", + "input": { + "properties": { + "path": { + "description": "the directory's absolute path, as the engine states it", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" + } + }, + { + "name": "access-act", + "description": "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", + "input": { + "properties": { + "access": { + "description": "the access, library:", + "type": "string" + }, + "act": { + "description": "the act: take", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "act", + "access" + ], + "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" + } + }, + { + "name": "judge-directories", + "description": "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" + } + }, + { + "name": "setuid-search", + "description": "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", + "input": { + "properties": { + "why": { + "description": "why it is asked, in words", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" + } + }, + { + "name": "root-free", + "description": "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "machines": { + "type": "array", + "items": { + "$ref": "#/defs/RootVerdict" + }, + "description": "this machine's verdict" + } + }, + "required": [ + "machines" + ] + } + }, + { + "name": "acts", + "description": "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): each hand-over, access act and setuid search, open, done or refused, with who asked, the ask and the operator's warrant. Read-only.", + "input": { + "properties": { + "id": { + "description": "one act, by its id", + "type": "string" + }, + "limit": { + "description": "at most this many, newest", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "$ref": "#/defs/Acts" + } + }, + { + "name": "packages", + "description": "The packages this machine's compositions declare, present or absent, each with the modules that declare it, as the engine keeps them in its key of `packages`; until the controller hands its lease away, beside the controller's mirror for this machine and their difference. With write, the key written again from what the engine composed.", + "optional": true, + "input": { + "properties": { + "write": { + "description": "\"true\": write this machine's key again from what the engine composed", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "source": { + "type": "string", + "description": "where the key is read: the engine's state packages" + }, + "key": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string", + "description": "the package" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the modules that declare it" + }, + "absent": { + "type": "boolean", + "description": "declared absent" + } + }, + "required": [ + "package", + "modules", + "absent" + ] + }, + "description": "its packages sorted by name, absent after present" + } + }, + "required": [ + "node", + "packages" + ], + "description": "the node's key as the bus holds it; absent when the engine wrote none" + }, + "key_error": { + "type": "string", + "description": "why the key could not be read" + }, + "wrote": { + "type": "boolean", + "description": "with write: the key was written again (false: it already held what the engine composed)" + }, + "note": { + "type": "string", + "description": "a note, such as that no key is written yet" + } + }, + "required": [ + "node", + "source" + ] + } + }, + { + "name": "composition", + "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", + "optional": true, + "input": { + "properties": { + "diff": { + "description": "\"true\": list the resources that differ from what runs", + "type": "string" + }, + "digest": { + "description": "a composition's digest (sha256:…), to read that revision rather than the latest", + "type": "string" + }, + "module": { + "description": "the module, as placed on this machine; node for the node's own", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "module": { + "type": "string", + "description": "the module" + }, + "want": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "its build's fingerprint" + }, + "composed": { + "type": "string", + "description": "the composition's digest it names" + }, + "yes": { + "type": "string", + "description": "the digest a person said yes to" + }, + "removed": { + "type": "boolean", + "description": "the module is removed" + }, + "self": { + "type": "boolean", + "description": "the node composes the module itself (ADR 0547 §2)" + }, + "backup": { + "type": "string", + "description": "the operator's answer to a failed backup, again or skip" + }, + "words": { + "type": "string", + "description": "its words" + }, + "revision": { + "type": "integer", + "description": "the revision of the want's key read last" + } + }, + "required": [ + "fp", + "revision" + ], + "description": "the module's want as the agent holds it; absent when it holds none" + }, + "attempt": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "the fingerprint tried" + }, + "composed": { + "type": "string", + "description": "the digest tried" + }, + "want_revision": { + "type": "integer", + "description": "the want's revision it was tried at" + }, + "composition_revision": { + "type": "integer", + "description": "the composition's revision it was tried at" + }, + "held": { + "type": "boolean", + "description": "held for a failed backup" + }, + "answered_revision": { + "type": "integer", + "description": "the want's revision the hold took" + }, + "transient": { + "type": "boolean", + "description": "it failed because the bus did not answer for a moment" + } + }, + "required": [ + "fp", + "want_revision", + "composition_revision" + ], + "description": "the module's last apply that did not end running; absent when none" + }, + "started": { + "type": "boolean", + "description": "the agent has looked at the module once since it started" + }, + "have": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "fingerprint" + }, + "composed": { + "type": "string", + "description": "digest" + }, + "phase": { + "type": "string", + "description": "running, failed, held or waiting-yes" + }, + "detail": { + "type": "string", + "description": "detail" + }, + "judgement": { + "type": "string", + "description": "judgement" + }, + "waits": { + "type": "string", + "description": "the digest that waits for a yes" + } + }, + "required": [ + "phase" + ], + "description": "what the module's have key says now; absent when it says nothing" + }, + "composition": { + "$ref": "#/defs/Revision" + }, + "revisions": { + "type": "array", + "items": { + "$ref": "#/defs/Revision" + }, + "description": "the revisions of its key the bucket keeps, oldest first" + }, + "runs": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "fingerprint" + }, + "composed": { + "type": "string", + "description": "digest" + }, + "phase": { + "type": "string", + "description": "phase" + }, + "from": { + "type": "string", + "description": "applied, have or kept" + }, + "resources": { + "type": "integer", + "description": "how many resources run" + } + }, + "required": [ + "from", + "resources" + ], + "description": "what runs of the module; absent when nothing runs" + }, + "judged": { + "type": "object", + "properties": { + "judgement": { + "type": "string", + "description": "the judgement" + }, + "words": { + "type": "string", + "description": "its words" + }, + "waits_for_yes": { + "type": "boolean", + "description": "the agent would wait for a person's yes before applying it" + } + }, + "required": [ + "judgement", + "words", + "waits_for_yes" + ], + "description": "the move from what runs to the composition read" + }, + "diff": { + "type": "array", + "items": { + "type": "object", + "description": "one resource added, removed or changed, as the engine's judge says it" + }, + "description": "every resource that differs from what runs, when asked" + }, + "own": { + "type": "object", + "properties": { + "provisioner_account": { + "type": "string", + "description": "the account the provisioner's client runs as, as the revision names it" + }, + "runs_provisioner_account": { + "type": "string", + "description": "as what runs names it" + }, + "adoption": { + "type": "object", + "description": "the revision's adoption; absent for a converged node" + }, + "identities": { + "type": "integer", + "description": "identities the revision names" + }, + "groups": { + "type": "integer", + "description": "groups" + }, + "libraries": { + "type": "integer", + "description": "libraries" + }, + "held": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "each module the node's composer held in its last pass, with why" + } + }, + "required": [ + "provisioner_account" + ], + "description": "of the node's own composition, its own fields beside what runs names" + } + }, + "required": [ + "node", + "module", + "started", + "composition", + "judged" + ] + } + }, + { + "name": "compose", + "description": "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "a module placed on this machine whose apply is also tried again (optional)", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "retried": { + "type": "string", + "description": "the module whose apply was tried again" + }, + "pass": { + "type": "object", + "properties": { + "at": { + "type": "string", + "description": "when the pass ended, RFC 3339" + }, + "refused": { + "type": "string", + "description": "why the node composed none of its modules" + }, + "held": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "each module refused alone, with why" + }, + "handed": { + "type": "integer", + "description": "how many compositions the pass handed the agent that differ from before" + }, + "again": { + "type": "string", + "description": "when the node composes again on its own, RFC 3339" + }, + "earlier": { + "type": "boolean", + "description": "the answer is the last pass, made moments ago" + } + }, + "required": [ + "at", + "handed" + ], + "description": "how the pass went" + } + }, + "required": [ + "pass" + ] + } + }, + { + "name": "compose-candidate", + "description": "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", + "optional": true, + "input": { + "properties": { + "candidate": { + "description": "the key of the indexer's candidate, .", + "type": "string" + }, + "input": { + "description": "also answer the composition input the candidate was composed from, for the sandbox path", + "type": "boolean" + } + }, + "required": [ + "candidate" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "candidate": { + "type": "string", + "description": "the candidate's key" + }, + "verdict": { + "type": "string", + "enum": [ + "composed", + "refused", + "unready", + "not-judged" + ], + "description": "the node's verdict (ADR 0593 §7, §9)" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/defs/Module" + }, + "description": "every module placed on the node, sorted by name" + }, + "own": { + "$ref": "#/defs/Module" + }, + "refused": { + "type": "string", + "description": "why the node could not compose with the candidate at all" + }, + "took": { + "type": "integer", + "description": "how long the engine took, in milliseconds" + }, + "input": { + "type": "object", + "description": "when asked: the composition input it composed from" + } + }, + "required": [ + "v", + "node", + "candidate", + "verdict", + "modules", + "own", + "took" + ] + } + }, + { + "name": "manifest-reads", + "optional": true, + "description": "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "manifest-reads": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline" + }, + "stated-at": { + "type": "string", + "description": "when the engine last stated them, RFC 3339" + } + }, + "required": [ + "v", + "node", + "stated-at" + ], + "description": "what this machine's engine reads of a manifest (ADR 0618 §3)" + } + } + ], + "events": [ + { + "name": "asked", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } + }, + { + "name": "done", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } + }, + { + "name": "refused", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } + }, + { + "name": "manifest-reads-changed", + "by-caller": true, + "body": { + "$ref": "#/defs/ManifestReadsChanged" + } + } + ], + "family": { + "name": "node-engine.events", + "max-age-days": 7 + }, + "defs": { + "Act": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the act's id" + }, + "act": { + "type": "string", + "description": "the act: hand-over, access-act, judge-directories or setuid-search" + }, + "subject": { + "type": "string", + "description": "the path of a hand-over, or the access of an access act" + }, + "caller": { + "type": "string", + "description": "the caller as the runtime proved it" + }, + "why": { + "type": "string", + "description": "why it was asked, in words" + }, + "at": { + "type": "string", + "description": "when it was asked, RFC 3339" + }, + "state": { + "type": "string", + "enum": [ + "asked", + "done", + "refused", + "answered" + ], + "description": "where it stands" + }, + "said": { + "type": "string", + "description": "what the engine said of it: its outcome or why it was refused" + }, + "ended": { + "type": "string", + "description": "when it ended, RFC 3339" + }, + "ask": { + "type": "string", + "description": "the operator's ask of a hand-over or an access act" + }, + "sets": { + "type": "string", + "description": "the owner, group and mode it sets, in words" + }, + "disk": { + "type": "string", + "description": "what the engine read on disk when it judged" + }, + "path": { + "type": "string", + "description": "the directory the act changes" + }, + "expires": { + "type": "string", + "description": "when the ask ends unanswered, RFC 3339" + }, + "by": { + "type": "string", + "description": "who answered, as the operator's warrant names them" + }, + "via": { + "type": "string", + "description": "the channel the warrant came on" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the warrant's proofs" + }, + "auto": { + "type": "boolean", + "description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)" + } + }, + "required": [ + "id", + "act", + "caller", + "at", + "state" + ], + "description": "one act asked through the seat, as acts reads it (ADR 0563 §9)" + }, + "ActSaid": { + "type": "object", + "properties": { + "act": { + "type": "string", + "description": "the act" + }, + "subject": { + "type": "string", + "description": "the path or the access" + }, + "caller": { + "type": "string", + "description": "the caller as the runtime proved it" + }, + "ask": { + "type": "string", + "description": "the operator's ask" + }, + "state": { + "type": "string", + "enum": [ + "asked", + "done", + "refused" + ], + "description": "where the act stands" + }, + "said": { + "type": "string", + "description": "what the engine said of it" + }, + "node": { + "type": "string", + "description": "the node whose engine says it" + } + }, + "required": [ + "act", + "caller", + "state", + "node" + ], + "description": "what the seat says of an act: the act, the path or access, the caller as the runtime proved it and the ask (ADR 0563 §1)" + }, + "Acts": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "acts": { + "type": "array", + "items": { + "$ref": "#/defs/Act" + }, + "description": "the acts, newest first" + } + }, + "required": [ + "node", + "acts" + ], + "description": "the acts kept" + }, + "Asked": { + "type": "object", + "properties": { + "asked": { + "type": "string", + "description": "the ask's id: acts shows where it stands" + }, + "node": { + "type": "string", + "description": "the node" + }, + "says": { + "type": "string", + "description": "what happens next, in words" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "description": "the answer of an act the operator is asked" + }, + "ManifestReadsChanged": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "manifest-reads": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "what its engine states it reads now" + }, + "at": { + "type": "string", + "description": "when it says it, RFC 3339" + }, + "why": { + "type": "string", + "enum": [ + "started", + "returned" + ], + "description": "started: the engine started or what it states changed; returned: its node returned after being away" + } + }, + "required": [ + "v", + "node", + "at", + "why" + ], + "description": "what a node-engine says when what it reads changed or its node returned (ADR 0618 §4)" + }, + "Module": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module, node for the node's own composition" + }, + "fp": { + "type": "string", + "description": "the fingerprint it was composed at" + }, + "state": { + "type": "string", + "enum": [ + "composed", + "refused", + "held" + ], + "description": "composed (digest set), refused by the candidate, or held without it too (words set)" + }, + "digest": { + "type": "string", + "description": "the composition's digest when composed" + }, + "changed": { + "type": "boolean", + "description": "the candidate changes it from the one composed without it" + }, + "words": { + "type": "string", + "description": "why, for a refused or held one" + } + }, + "required": [ + "module", + "state" + ], + "description": "one module's composition with the candidate" + }, + "Revision": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "the composition's key" + }, + "revision": { + "type": "integer", + "description": "its revision" + }, + "digest": { + "type": "string", + "description": "its digest" + }, + "resources": { + "type": "integer", + "description": "how many resources it names" + }, + "latest": { + "type": "boolean", + "description": "the latest revision" + }, + "refused": { + "type": "string", + "description": "why it cannot be read or trusted" + } + }, + "required": [ + "key" + ], + "description": "one revision of a composition key" + }, + "RootVerdict": { + "type": "object", + "properties": { + "machine": { + "type": "string", + "description": "the machine" + }, + "free": { + "type": "boolean", + "description": "the machine is root-free" + }, + "why": { + "type": "string", + "description": "why, in words" + }, + "judged": { + "type": "string", + "description": "when the engine judged it, RFC 3339" + } + }, + "required": [ + "machine", + "free", + "why", + "judged" + ], + "description": "the engine's root-free verdict (ADR 0563 §6)" + }, + "Said": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "said": { + "type": "string", + "description": "the engine's judgement or search, one line per directory or program" + } + }, + "required": [ + "node", + "said" + ], + "description": "the answer of a read-only judgement" + } + } +} diff --git a/go/seatlist/seatlist.go b/go/seatlist/seatlist.go index 68b8c50..ea5d413 100644 --- a/go/seatlist/seatlist.go +++ b/go/seatlist/seatlist.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "sync" @@ -92,6 +93,52 @@ type Reader struct { mu sync.Mutex defs map[string]seatdef.Definition // by seat boot map[string]bool // seats held from the bootstrap, not yet from the owner + // what the last ask of the owner came to (Status) + spoken bool + revision uint64 + lastErr string +} + +// Status is what the reader holds and what it waits for (ADR 0635 §3), for the host's health and its tool: Heard says +// the owner has answered, Waiting are the seats still held from the bootstrap alone (the owner has not named them), +// LastError is why the last ask failed, and Revision is the owner's state revision of the last listing. +type Status struct { + Heard bool `json:"heard"` + Held int `json:"held"` + Waiting []string `json:"waiting,omitempty"` + LastError string `json:"last-error,omitempty"` + Revision uint64 `json:"revision,omitempty"` +} + +// Status is the reader's state of mind. +func (r *Reader) Status() Status { + r.mu.Lock() + defer r.mu.Unlock() + st := Status{Heard: r.spoken, Held: len(r.defs), LastError: r.lastErr, Revision: r.revision} + for seat := range r.boot { + st.Waiting = append(st.Waiting, seat) + } + sort.Strings(st.Waiting) + return st +} + +// Health is Status as the one line a module's health says: empty when the owner has been heard and nothing waits. +func (st Status) Health() string { + switch { + case !st.Heard: + return fmt.Sprintf("the seat list is held from the foundation's bootstrap (%d seats): the definitions' owner has not answered%s", + st.Held, withError(st.LastError)) + case len(st.Waiting) > 0: + return fmt.Sprintf("%d seats are held from the bootstrap alone, the owner names none of them: %s", len(st.Waiting), strings.Join(st.Waiting, ", ")) + } + return "" +} + +func withError(e string) string { + if e == "" { + return "" + } + return " (" + e + ")" } // Start returns a reader holding the bootstrap, asks the owner for every definition and follows its event. A first @@ -148,11 +195,11 @@ func (r *Reader) seatsLocked() []catalogue.Seat { func (r *Reader) Reload() error { raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{}) if err != nil { - return fmt.Errorf("seatlist: asking %s.%s: %w", r.cfg.Owner, Tool, err) + return r.failed(fmt.Errorf("seatlist: asking %s.%s: %w", r.cfg.Owner, Tool, err)) } var l Listing if err := json.Unmarshal(raw, &l); err != nil { - return fmt.Errorf("seatlist: %s.%s answered what is not a listing: %w", r.cfg.Owner, Tool, err) + return r.failed(fmt.Errorf("seatlist: %s.%s answered what is not a listing: %w", r.cfg.Owner, Tool, err)) } parsed := map[string]seatdef.Definition{} for _, e := range l.Definitions { @@ -161,11 +208,12 @@ func (r *Reader) Reload() error { } d, err := seatdef.Parse(e.Definition) if err != nil { - return fmt.Errorf("seatlist: the definition of %s does not parse: %w", e.Seat, err) + return r.failed(fmt.Errorf("seatlist: the definition of %s does not parse: %w", e.Seat, err)) } parsed[d.Seat] = d } r.mu.Lock() + r.spoken, r.revision, r.lastErr = true, l.Revision, "" for seat := range r.defs { if !r.boot[seat] { delete(r.defs, seat) @@ -192,7 +240,7 @@ func (r *Reader) heard(_ string, body json.RawMessage) error { } raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{"seat": c.Seat}) if err != nil { - return err + return r.failed(err) } var a Answer if err := json.Unmarshal(raw, &a); err != nil { @@ -220,6 +268,13 @@ func (r *Reader) heard(_ string, body json.RawMessage) error { return nil } +func (r *Reader) failed(err error) error { + r.mu.Lock() + r.lastErr = err.Error() + r.mu.Unlock() + return err +} + func (r *Reader) changed() { if r.cfg.OnChange != nil { r.cfg.OnChange(r.Seats()) diff --git a/go/seatlist/seatlist_test.go b/go/seatlist/seatlist_test.go index 18750f8..e22d060 100644 --- a/go/seatlist/seatlist_test.go +++ b/go/seatlist/seatlist_test.go @@ -127,3 +127,31 @@ func TestStartRefusesWhatCannotRead(t *testing.T) { } } } + +// ADR 0635 §3: before the owner is heard the reader holds the foundation's definitions and says it waits. +func TestTheBootstrapParsesAndIsHeldUntilTheOwnerAnswers(t *testing.T) { + boot := Bootstrapped() + if len(boot) == 0 { + t.Fatal("no bootstrapped definition") + } + owner := &fakeOwner{down: true} + r, err := Start(Config{Owner: "mesh-owner", Bus: owner, Bootstrap: boot}) + if err != nil { + t.Fatal(err) + } + if got := len(r.Seats()); got != len(boot) { + t.Fatalf("the reader holds %d seats, the bootstrap has %d", got, len(boot)) + } + st := r.Status() + if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" { + t.Fatalf("status %+v: want the owner not heard, every bootstrapped seat waiting and the error said", st) + } + owner.down = false + owner.table = map[string]string{"node-b": def("node-b", "node", "put")} + if err := r.Reload(); err != nil { + t.Fatal(err) + } + if st := r.Status(); !st.Heard || len(st.Waiting) != len(boot) || st.LastError != "" || st.Revision != 7 { + t.Fatalf("status after the owner answered %+v: want heard, the bootstrap's seats the owner does not hold still waiting, no error, revision 7", st) + } +}