From 7498d689e2bc33424ba0a1af9522461c64ff9010 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:36:10 +0100 Subject: [PATCH] The node-engine seat whole: every verb's answer, its events' bodies, and ADR 0618's manifest-reads and manifest-reads-changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat's area changes in this milestone, so it moves whole into its contracts (ADR 0613 §3): the eleven verbs it serves get their answers as the engine writes them, asked, done and refused their body, and the seat serves the optional verb manifest-reads (ADR 0618 §3) and says manifest-reads-changed (§4). Each event is published per node, the node's name its last token (by-caller), and kept with the seat's events for seven days. compose-candidate's input is a boolean, as the engine and the checker read it. Fixtures for every event, compose-candidate's input and three answers. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- go/seats/nodeengine/contract.go | 554 ++++++++++++++++ go/seats/nodeengine/contract_test.go | 74 +++ go/seats/seats.go | 35 +- seats/node-engine/fixtures/events.json | 69 ++ seats/node-engine/fixtures/verbs.json | 76 +++ seats/node-engine/seat.json | 883 ++++++++++++++++++++++++- ts/src/index.ts | 1 + ts/src/node-engine.ts | 539 +++++++++++++++ ts/src/seats.ts | 867 +++++++++++++++++++++++- 9 files changed, 3058 insertions(+), 40 deletions(-) create mode 100644 go/seats/nodeengine/contract.go create mode 100644 go/seats/nodeengine/contract_test.go create mode 100644 seats/node-engine/fixtures/events.json create mode 100644 seats/node-engine/fixtures/verbs.json create mode 100644 ts/src/node-engine.ts diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go new file mode 100644 index 0000000..efc6d4d --- /dev/null +++ b/go/seats/nodeengine/contract.go @@ -0,0 +1,554 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package nodeengine is the contract of the seat "node-engine", defined whole (seats/node-engine/seat.json): 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. +package nodeengine + +// Seat is the seat's name. +const Seat = "node-engine" + +const ( + VerbHandOver = "hand-over" + VerbAccessAct = "access-act" + VerbJudgeDirectories = "judge-directories" + VerbSetuidSearch = "setuid-search" + VerbRootFree = "root-free" + VerbActs = "acts" + VerbPackages = "packages" + VerbComposition = "composition" + VerbCompose = "compose" + VerbComposeCandidate = "compose-candidate" + VerbPullWaits = "pull-waits" + VerbManifestReads = "manifest-reads" +) + +const ( + EventAsked = "asked" + EventDone = "done" + EventRefused = "refused" + EventManifestReadsChanged = "manifest-reads-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbPullWaits, VerbManifestReads} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventAsked, EventDone, EventRefused, EventManifestReadsChanged} +} + +// Family is where the seat's events are kept. +const ( + Family = "node-engine.events" + FamilyMaxAgeDays = 7 +) + +// SubjectAsked is where asked is published for one caller. +func SubjectAsked(caller string) string { return "mesh.seat.node-engine.event.asked." + caller } + +// SubjectDone is where done is published for one caller. +func SubjectDone(caller string) string { return "mesh.seat.node-engine.event.done." + caller } + +// SubjectRefused is where refused is published for one caller. +func SubjectRefused(caller string) string { return "mesh.seat.node-engine.event.refused." + caller } + +// SubjectManifestReadsChanged is where manifest-reads-changed is published for one caller. +func SubjectManifestReadsChanged(caller string) string { + return "mesh.seat.node-engine.event.manifest-reads-changed." + caller +} + +// Act is one act asked through the seat, as acts reads it (ADR 0563 §9). +type Act struct { + // the act: hand-over, access-act, judge-directories or setuid-search + Act string `json:"act"` + // the operator's ask of a hand-over or an access act + Ask string `json:"ask,omitempty"` + // when it was asked, RFC 3339 + At string `json:"at"` + // the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4) + Auto bool `json:"auto,omitempty"` + // who answered, as the operator's warrant names them + By string `json:"by,omitempty"` + // the caller as the runtime proved it + Caller string `json:"caller"` + // what the engine read on disk when it judged + Disk string `json:"disk,omitempty"` + // when it ended, RFC 3339 + Ended string `json:"ended,omitempty"` + // when the ask ends unanswered, RFC 3339 + Expires string `json:"expires,omitempty"` + // the act's id + ID string `json:"id"` + // the directory the act changes + Path string `json:"path,omitempty"` + // the warrant's proofs + Proofs []string `json:"proofs,omitempty"` + // what the engine said of it: its outcome or why it was refused + Said string `json:"said,omitempty"` + // the owner, group and mode it sets, in words + Sets string `json:"sets,omitempty"` + // where it stands + State string `json:"state"` + // the path of a hand-over, or the access of an access act + Subject string `json:"subject,omitempty"` + // the channel the warrant came on + Via string `json:"via,omitempty"` + // why it was asked, in words + Why string `json:"why,omitempty"` +} + +// ActSaid is 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). +type ActSaid struct { + // the act + Act string `json:"act"` + // the operator's ask + Ask string `json:"ask,omitempty"` + // the caller as the runtime proved it + Caller string `json:"caller"` + // the node whose engine says it + Node string `json:"node"` + // what the engine said of it + Said string `json:"said,omitempty"` + // where the act stands + State string `json:"state"` + // the path or the access + Subject string `json:"subject,omitempty"` +} + +// Acts is the acts kept. +type Acts struct { + // the acts, newest first + Acts []Act `json:"acts"` + // the node + Node string `json:"node"` +} + +// Asked is the answer of an act the operator is asked. +type Asked struct { + // the ask's id: acts shows where it stands + Asked string `json:"asked"` + // the node + Node string `json:"node"` + // what happens next, in words + Says string `json:"says"` +} + +// ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). +type ManifestReadsChanged struct { + // when it says it, RFC 3339 + At string `json:"at"` + // what its engine states it reads now + ManifestReads []string `json:"manifest-reads,omitempty"` + // the node + Node string `json:"node"` + // the layout's version + V int64 `json:"v"` + // started: the engine started or what it states changed; returned: its node returned after being away + Why string `json:"why"` +} + +// Module is one module's composition with the candidate. +type Module struct { + // the candidate changes it from the one composed without it + Changed bool `json:"changed,omitempty"` + // the composition's digest when composed + Digest string `json:"digest,omitempty"` + // the fingerprint it was composed at + Fp string `json:"fp,omitempty"` + // the module, node for the node's own composition + Module string `json:"module"` + // composed (digest set), refused by the candidate, or held without it too (words set) + State string `json:"state"` + // why, for a refused or held one + Words string `json:"words,omitempty"` +} + +// Revision is one revision of a composition key. +type Revision struct { + // its digest + Digest string `json:"digest,omitempty"` + // the composition's key + Key string `json:"key"` + // the latest revision + Latest bool `json:"latest,omitempty"` + // why it cannot be read or trusted + Refused string `json:"refused,omitempty"` + // how many resources it names + Resources int64 `json:"resources,omitempty"` + // its revision + Revision int64 `json:"revision,omitempty"` +} + +// RootVerdict is the engine's root-free verdict (ADR 0563 §6). +type RootVerdict struct { + // the machine is root-free + Free bool `json:"free"` + // when the engine judged it, RFC 3339 + Judged string `json:"judged"` + // the machine + Machine string `json:"machine"` + // why, in words + Why string `json:"why"` +} + +// Said is the answer of a read-only judgement. +type Said struct { + // the node + Node string `json:"node"` + // the engine's judgement or search, one line per directory or program + Said string `json:"said"` +} + +// HandOverInput is what hand-over takes. +type HandOverInput struct { + // the directory's absolute path, as the engine states it + Path string `json:"path"` + // why it is asked, in words the operator reads + Why string `json:"why,omitempty"` +} + +// HandOverAnswer is what hand-over answers. +type HandOverAnswer = Asked + +// AccessActInput is what access-act takes. +type AccessActInput struct { + // the access, library: + Access string `json:"access"` + // the act: take + Act string `json:"act"` + // why it is asked, in words the operator reads + Why string `json:"why,omitempty"` +} + +// AccessActAnswer is what access-act answers. +type AccessActAnswer = Asked + +// JudgeDirectoriesInput is what judge-directories takes. +type JudgeDirectoriesInput struct { +} + +// JudgeDirectoriesAnswer is what judge-directories answers. +type JudgeDirectoriesAnswer = Said + +// SetuidSearchInput is what setuid-search takes. +type SetuidSearchInput struct { + // why it is asked, in words + Why string `json:"why,omitempty"` +} + +// SetuidSearchAnswer is what setuid-search answers. +type SetuidSearchAnswer = Said + +// RootFreeInput is what root-free takes. +type RootFreeInput struct { +} + +// RootFreeAnswer is what root-free answers. +type RootFreeAnswer struct { + // this machine's verdict + Machines []RootVerdict `json:"machines"` +} + +// ActsInput is what acts takes. +type ActsInput struct { + // one act, by its id + ID string `json:"id,omitempty"` + // at most this many, newest + Limit string `json:"limit,omitempty"` +} + +// ActsAnswer is what acts answers. +type ActsAnswer = Acts + +// PackagesInput is what packages takes. +type PackagesInput struct { + // "true": write this machine's key again from what the engine composed + Write string `json:"write,omitempty"` +} + +// PackagesAnswerKeyPackagesItem is a type of the contract. +type PackagesAnswerKeyPackagesItem struct { + // declared absent + Absent bool `json:"absent"` + // the modules that declare it + Modules []string `json:"modules"` + // the package + Package string `json:"package"` +} + +// PackagesAnswerKey is the node's key as the bus holds it; absent when the engine wrote none. +type PackagesAnswerKey struct { + // the node + Node string `json:"node"` + // its packages sorted by name, absent after present + Packages []PackagesAnswerKeyPackagesItem `json:"packages"` +} + +// PackagesAnswer is what packages answers. +type PackagesAnswer struct { + // the node's key as the bus holds it; absent when the engine wrote none + Key *PackagesAnswerKey `json:"key,omitempty"` + // why the key could not be read + KeyError string `json:"key_error,omitempty"` + // the node + Node string `json:"node"` + // a note, such as that no key is written yet + Note string `json:"note,omitempty"` + // where the key is read: the engine's state packages + Source string `json:"source"` + // with write: the key was written again (false: it already held what the engine composed) + Wrote bool `json:"wrote,omitempty"` +} + +// CompositionInput is what composition takes. +type CompositionInput struct { + // "true": list the resources that differ from what runs + Diff string `json:"diff,omitempty"` + // a composition's digest (sha256:…), to read that revision rather than the latest + Digest string `json:"digest,omitempty"` + // the module, as placed on this machine; node for the node's own + Module string `json:"module"` +} + +// CompositionAnswerAttempt is the module's last apply that did not end running; absent when none. +type CompositionAnswerAttempt struct { + // the want's revision the hold took + AnsweredRevision int64 `json:"answered_revision,omitempty"` + // the digest tried + Composed string `json:"composed,omitempty"` + // the composition's revision it was tried at + CompositionRevision int64 `json:"composition_revision"` + // the fingerprint tried + Fp string `json:"fp"` + // held for a failed backup + Held bool `json:"held,omitempty"` + // it failed because the bus did not answer for a moment + Transient bool `json:"transient,omitempty"` + // the want's revision it was tried at + WantRevision int64 `json:"want_revision"` +} + +// CompositionAnswerHave is what the module's have key says now; absent when it says nothing. +type CompositionAnswerHave struct { + // digest + Composed string `json:"composed,omitempty"` + // detail + Detail string `json:"detail,omitempty"` + // fingerprint + Fp string `json:"fp,omitempty"` + // judgement + Judgement string `json:"judgement,omitempty"` + // running, failed, held or waiting-yes + Phase string `json:"phase"` + // the digest that waits for a yes + Waits string `json:"waits,omitempty"` +} + +// CompositionAnswerJudged is the move from what runs to the composition read. +type CompositionAnswerJudged struct { + // the judgement + Judgement string `json:"judgement"` + // the agent would wait for a person's yes before applying it + WaitsForYes bool `json:"waits_for_yes"` + // its words + Words string `json:"words"` +} + +// CompositionAnswerOwn is of the node's own composition, its own fields beside what runs names. +type CompositionAnswerOwn struct { + // the revision's adoption; absent for a converged node + Adoption map[string]any `json:"adoption,omitempty"` + // groups + Groups int64 `json:"groups,omitempty"` + // each module the node's composer held in its last pass, with why + Held map[string]string `json:"held,omitempty"` + // identities the revision names + Identities int64 `json:"identities,omitempty"` + // libraries + Libraries int64 `json:"libraries,omitempty"` + // the account the provisioner's client runs as, as the revision names it + ProvisionerAccount string `json:"provisioner_account"` + // as what runs names it + RunsProvisionerAccount string `json:"runs_provisioner_account,omitempty"` +} + +// CompositionAnswerRuns is what runs of the module; absent when nothing runs. +type CompositionAnswerRuns struct { + // digest + Composed string `json:"composed,omitempty"` + // fingerprint + Fp string `json:"fp,omitempty"` + // applied, have or kept + From string `json:"from"` + // phase + Phase string `json:"phase,omitempty"` + // how many resources run + Resources int64 `json:"resources"` +} + +// CompositionAnswerWant is the module's want as the agent holds it; absent when it holds none. +type CompositionAnswerWant struct { + // the operator's answer to a failed backup, again or skip + Backup string `json:"backup,omitempty"` + // the composition's digest it names + Composed string `json:"composed,omitempty"` + // its build's fingerprint + Fp string `json:"fp"` + // the module is removed + Removed bool `json:"removed,omitempty"` + // the revision of the want's key read last + Revision int64 `json:"revision"` + // the node composes the module itself (ADR 0547 §2) + Self bool `json:"self,omitempty"` + // its words + Words string `json:"words,omitempty"` + // the digest a person said yes to + Yes string `json:"yes,omitempty"` +} + +// CompositionAnswer is what composition answers. +type CompositionAnswer struct { + // the module's last apply that did not end running; absent when none + Attempt *CompositionAnswerAttempt `json:"attempt,omitempty"` + Composition Revision `json:"composition"` + // every resource that differs from what runs, when asked + Diff []map[string]any `json:"diff,omitempty"` + // what the module's have key says now; absent when it says nothing + Have *CompositionAnswerHave `json:"have,omitempty"` + // the move from what runs to the composition read + Judged CompositionAnswerJudged `json:"judged"` + // the module + Module string `json:"module"` + // the node + Node string `json:"node"` + // of the node's own composition, its own fields beside what runs names + Own *CompositionAnswerOwn `json:"own,omitempty"` + // the revisions of its key the bucket keeps, oldest first + Revisions []Revision `json:"revisions,omitempty"` + // what runs of the module; absent when nothing runs + Runs *CompositionAnswerRuns `json:"runs,omitempty"` + // the agent has looked at the module once since it started + Started bool `json:"started"` + // the module's want as the agent holds it; absent when it holds none + Want *CompositionAnswerWant `json:"want,omitempty"` +} + +// ComposeInput is what compose takes. +type ComposeInput struct { + // a module placed on this machine whose apply is also tried again (optional) + Module string `json:"module,omitempty"` +} + +// ComposeAnswerPass is how the pass went. +type ComposeAnswerPass struct { + // when the node composes again on its own, RFC 3339 + Again string `json:"again,omitempty"` + // when the pass ended, RFC 3339 + At string `json:"at"` + // the answer is the last pass, made moments ago + Earlier bool `json:"earlier,omitempty"` + // how many compositions the pass handed the agent that differ from before + Handed int64 `json:"handed"` + // each module refused alone, with why + Held map[string]string `json:"held,omitempty"` + // why the node composed none of its modules + Refused string `json:"refused,omitempty"` +} + +// ComposeAnswer is what compose answers. +type ComposeAnswer struct { + // how the pass went + Pass ComposeAnswerPass `json:"pass"` + // the module whose apply was tried again + Retried string `json:"retried,omitempty"` +} + +// ComposeCandidateInput is what compose-candidate takes. +type ComposeCandidateInput struct { + // the key of the indexer's candidate, . + Candidate string `json:"candidate"` + // also answer the composition input the candidate was composed from, for the sandbox path + Input bool `json:"input,omitempty"` +} + +// ComposeCandidateAnswer is what compose-candidate answers. +type ComposeCandidateAnswer struct { + // the candidate's key + Candidate string `json:"candidate"` + // when asked: the composition input it composed from + Input map[string]any `json:"input,omitempty"` + // every module placed on the node, sorted by name + Modules []Module `json:"modules"` + // the node + Node string `json:"node"` + Own Module `json:"own"` + // why the node could not compose with the candidate at all + Refused string `json:"refused,omitempty"` + // how long the engine took, in milliseconds + Took int64 `json:"took"` + // the layout's version + V int64 `json:"v"` + // the node's verdict (ADR 0593 §7, §9) + Verdict string `json:"verdict"` +} + +// PullWaitsInput is what pull-waits takes. +type PullWaitsInput struct { +} + +// PullWaitsAnswerWaitsItem is a type of the contract. +type PullWaitsAnswerWaitsItem struct { + // that seat's holder, whose settled the retry waits on + Holder string `json:"holder,omitempty"` + // the image whose pull failed + Image string `json:"image"` + // the module whose apply failed + Module string `json:"module"` + // when the failed pull began, RFC 3339 + PullBegan string `json:"pull-began,omitempty"` + // when it was last tried again, RFC 3339 + RetryLast string `json:"retry-last,omitempty"` + // what started that retry: settled, or the engine's start + RetryTrigger string `json:"retry-trigger,omitempty"` + // the seat the image came through + Seat string `json:"seat,omitempty"` + // the settled events of the holder heard since the engine started + SettledCount int64 `json:"settled-count,omitempty"` + // when the last was heard, RFC 3339 + SettledLast string `json:"settled-last,omitempty"` +} + +// PullWaitsAnswer is what pull-waits answers. +type PullWaitsAnswer struct { + // every failed pull waiting on a holder + Waits []PullWaitsAnswerWaitsItem `json:"waits"` +} + +// ManifestReadsInput is what manifest-reads takes. +type ManifestReadsInput struct { +} + +// ManifestReadsAnswer is what manifest-reads answers. +type ManifestReadsAnswer struct { + // 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 + ManifestReads []string `json:"manifest-reads,omitempty"` + // the node + Node string `json:"node"` + // when the engine last stated them, RFC 3339 + StatedAt string `json:"stated-at"` + // the layout's version + V int64 `json:"v"` +} + +// AskedEvent is the body of the event asked. +type AskedEvent = ActSaid + +// DoneEvent is the body of the event done. +type DoneEvent = ActSaid + +// RefusedEvent is the body of the event refused. +type RefusedEvent = ActSaid + +// ManifestReadsChangedEvent is the body of the event manifest-reads-changed. +type ManifestReadsChangedEvent = ManifestReadsChanged diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go new file mode 100644 index 0000000..b1d7720 --- /dev/null +++ b/go/seats/nodeengine/contract_test.go @@ -0,0 +1,74 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package nodeengine + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/node-engine/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "asked on the node's subject", subject: "mesh.seat.node-engine.event.asked.ace", given: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"state\":\"asked\",\"subject\":\"/srv/media\"}", wire: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"state\":\"asked\",\"subject\":\"/srv/media\"}", + decode: func(b []byte) (any, error) { var v AskedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.asked\\.[A-Za-z0-9_-]+$")}, + {name: "done on the node's subject", subject: "mesh.seat.node-engine.event.done.ace", given: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"handed over: owner media, group media, mode 2775\",\"state\":\"done\",\"subject\":\"/srv/media\"}", wire: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"handed over: owner media, group media, mode 2775\",\"state\":\"done\",\"subject\":\"/srv/media\"}", + decode: func(b []byte) (any, error) { var v DoneEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.done\\.[A-Za-z0-9_-]+$")}, + {name: "refused on the node's subject", subject: "mesh.seat.node-engine.event.refused.ace", given: "{\"act\":\"access-act\",\"ask\":\"access-act-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"the operator said no\",\"state\":\"refused\",\"subject\":\"library:music\"}", wire: "{\"act\":\"access-act\",\"ask\":\"access-act-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"the operator said no\",\"state\":\"refused\",\"subject\":\"library:music\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.refused\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads-changed when the engine started", subject: "mesh.seat.node-engine.event.manifest-reads-changed.ace", given: "{\"at\":\"2026-11-04T10:00:00Z\",\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"v\":1,\"why\":\"started\"}", wire: "{\"at\":\"2026-11-04T10:00:00Z\",\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"v\":1,\"why\":\"started\"}", + decode: func(b []byte) (any, error) { + var v ManifestReadsChangedEvent + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads-changed when the node returned, from an engine that states none", subject: "mesh.seat.node-engine.event.manifest-reads-changed.g14", given: "{\"at\":\"2026-11-04T10:05:00Z\",\"node\":\"g14\",\"v\":1,\"why\":\"returned\"}", wire: "{\"at\":\"2026-11-04T10:05:00Z\",\"node\":\"g14\",\"v\":1,\"why\":\"returned\"}", + decode: func(b []byte) (any, error) { + var v ManifestReadsChangedEvent + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "compose-candidate asks the candidate and, as a boolean, its composition input", subject: "", given: "{\"candidate\":\"mesh-host.0123abcd\",\"input\":true}", wire: "{\"candidate\":\"mesh-host.0123abcd\",\"input\":true}", + decode: func(b []byte) (any, error) { var v ComposeCandidateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "compose-candidate answers per module and the node's own", subject: "", given: "{\"candidate\":\"mesh-host.0123abcd\",\"modules\":[{\"changed\":true,\"digest\":\"sha256:aa\",\"fp\":\"fp-1\",\"module\":\"plex\",\"state\":\"composed\"}],\"node\":\"ace\",\"own\":{\"digest\":\"sha256:bb\",\"module\":\"node\",\"state\":\"composed\"},\"took\":1200,\"v\":1,\"verdict\":\"composed\"}", wire: "{\"candidate\":\"mesh-host.0123abcd\",\"modules\":[{\"changed\":true,\"digest\":\"sha256:aa\",\"fp\":\"fp-1\",\"module\":\"plex\",\"state\":\"composed\"}],\"node\":\"ace\",\"own\":{\"digest\":\"sha256:bb\",\"module\":\"node\",\"state\":\"composed\"},\"took\":1200,\"v\":1,\"verdict\":\"composed\"}", + decode: func(b []byte) (any, error) { var v ComposeCandidateAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "acts answers each act kept", subject: "", given: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", wire: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v ActsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/seats.go b/go/seats/seats.go index eebcaef..b389720 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -662,31 +662,44 @@ func List() []Seat { {Name: "restore", Description: "Put an item of the node's trash back where it was; refuses when something is there now. 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: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "an id from trash", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"id", "reason"}, "type": "object"}, Replaces: []string{"trash-restore"}}, }}, - {Name: NodeEngine, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused"}, HeldByTheEngine: true, + {Name: NodeEngine, Scope: "node", Grain: "whole", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused", "manifest-reads-changed"}, HeldByTheEngine: true, Serves: []Verb{ {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: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}, + Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}, + Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"machines": map[string]any{"description": "this machine's verdict", "items": map[string]any{"description": "the engine's root-free verdict (ADR 0563 §6)", "properties": map[string]any{"free": map[string]any{"description": "the machine is root-free", "type": "boolean"}, "judged": map[string]any{"description": "when the engine judged it, RFC 3339", "type": "string"}, "machine": map[string]any{"description": "the machine", "type": "string"}, "why": map[string]any{"description": "why, in words", "type": "string"}}, "required": []string{"machine", "free", "why", "judged"}, "type": "object"}, "type": "array"}}, "required": []string{"machines"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the acts kept", "properties": map[string]any{"acts": map[string]any{"description": "the acts, newest first", "items": map[string]any{"description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", "properties": map[string]any{"act": map[string]any{"description": "the act: hand-over, access-act, judge-directories or setuid-search", "type": "string"}, "ask": map[string]any{"description": "the operator's ask of a hand-over or an access act", "type": "string"}, "at": map[string]any{"description": "when it was asked, RFC 3339", "type": "string"}, "auto": map[string]any{"description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", "type": "boolean"}, "by": map[string]any{"description": "who answered, as the operator's warrant names them", "type": "string"}, "caller": map[string]any{"description": "the caller as the runtime proved it", "type": "string"}, "disk": map[string]any{"description": "what the engine read on disk when it judged", "type": "string"}, "ended": map[string]any{"description": "when it ended, RFC 3339", "type": "string"}, "expires": map[string]any{"description": "when the ask ends unanswered, RFC 3339", "type": "string"}, "id": map[string]any{"description": "the act's id", "type": "string"}, "path": map[string]any{"description": "the directory the act changes", "type": "string"}, "proofs": map[string]any{"description": "the warrant's proofs", "items": map[string]any{"type": "string"}, "type": "array"}, "said": map[string]any{"description": "what the engine said of it: its outcome or why it was refused", "type": "string"}, "sets": map[string]any{"description": "the owner, group and mode it sets, in words", "type": "string"}, "state": map[string]any{"description": "where it stands", "enum": []string{"asked", "done", "refused", "answered"}, "type": "string"}, "subject": map[string]any{"description": "the path of a hand-over, or the access of an access act", "type": "string"}, "via": map[string]any{"description": "the channel the warrant came on", "type": "string"}, "why": map[string]any{"description": "why it was asked, in words", "type": "string"}}, "required": []string{"id", "act", "caller", "at", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}}, "required": []string{"node", "acts"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the node's key as the bus holds it; absent when the engine wrote none", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "packages": map[string]any{"description": "its packages sorted by name, absent after present", "items": map[string]any{"properties": map[string]any{"absent": map[string]any{"description": "declared absent", "type": "boolean"}, "modules": map[string]any{"description": "the modules that declare it", "items": map[string]any{"type": "string"}, "type": "array"}, "package": map[string]any{"description": "the package", "type": "string"}}, "required": []string{"package", "modules", "absent"}, "type": "object"}, "type": "array"}}, "required": []string{"node", "packages"}, "type": "object"}, "key_error": map[string]any{"description": "why the key could not be read", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "note": map[string]any{"description": "a note, such as that no key is written yet", "type": "string"}, "source": map[string]any{"description": "where the key is read: the engine's state packages", "type": "string"}, "wrote": map[string]any{"description": "with write: the key was written again (false: it already held what the engine composed)", "type": "boolean"}}, "required": []string{"node", "source"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"attempt": map[string]any{"description": "the module's last apply that did not end running; absent when none", "properties": map[string]any{"answered_revision": map[string]any{"description": "the want's revision the hold took", "type": "integer"}, "composed": map[string]any{"description": "the digest tried", "type": "string"}, "composition_revision": map[string]any{"description": "the composition's revision it was tried at", "type": "integer"}, "fp": map[string]any{"description": "the fingerprint tried", "type": "string"}, "held": map[string]any{"description": "held for a failed backup", "type": "boolean"}, "transient": map[string]any{"description": "it failed because the bus did not answer for a moment", "type": "boolean"}, "want_revision": map[string]any{"description": "the want's revision it was tried at", "type": "integer"}}, "required": []string{"fp", "want_revision", "composition_revision"}, "type": "object"}, "composition": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "diff": map[string]any{"description": "every resource that differs from what runs, when asked", "items": map[string]any{"description": "one resource added, removed or changed, as the engine's judge says it", "type": "object"}, "type": "array"}, "have": map[string]any{"description": "what the module's have key says now; absent when it says nothing", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "detail": map[string]any{"description": "detail", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "judgement": map[string]any{"description": "judgement", "type": "string"}, "phase": map[string]any{"description": "running, failed, held or waiting-yes", "type": "string"}, "waits": map[string]any{"description": "the digest that waits for a yes", "type": "string"}}, "required": []string{"phase"}, "type": "object"}, "judged": map[string]any{"description": "the move from what runs to the composition read", "properties": map[string]any{"judgement": map[string]any{"description": "the judgement", "type": "string"}, "waits_for_yes": map[string]any{"description": "the agent would wait for a person's yes before applying it", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}}, "required": []string{"judgement", "words", "waits_for_yes"}, "type": "object"}, "module": map[string]any{"description": "the module", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "of the node's own composition, its own fields beside what runs names", "properties": map[string]any{"adoption": map[string]any{"description": "the revision's adoption; absent for a converged node", "type": "object"}, "groups": map[string]any{"description": "groups", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module the node's composer held in its last pass, with why", "type": "object"}, "identities": map[string]any{"description": "identities the revision names", "type": "integer"}, "libraries": map[string]any{"description": "libraries", "type": "integer"}, "provisioner_account": map[string]any{"description": "the account the provisioner's client runs as, as the revision names it", "type": "string"}, "runs_provisioner_account": map[string]any{"description": "as what runs names it", "type": "string"}}, "required": []string{"provisioner_account"}, "type": "object"}, "revisions": map[string]any{"description": "the revisions of its key the bucket keeps, oldest first", "items": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "type": "array"}, "runs": map[string]any{"description": "what runs of the module; absent when nothing runs", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "from": map[string]any{"description": "applied, have or kept", "type": "string"}, "phase": map[string]any{"description": "phase", "type": "string"}, "resources": map[string]any{"description": "how many resources run", "type": "integer"}}, "required": []string{"from", "resources"}, "type": "object"}, "started": map[string]any{"description": "the agent has looked at the module once since it started", "type": "boolean"}, "want": map[string]any{"description": "the module's want as the agent holds it; absent when it holds none", "properties": map[string]any{"backup": map[string]any{"description": "the operator's answer to a failed backup, again or skip", "type": "string"}, "composed": map[string]any{"description": "the composition's digest it names", "type": "string"}, "fp": map[string]any{"description": "its build's fingerprint", "type": "string"}, "removed": map[string]any{"description": "the module is removed", "type": "boolean"}, "revision": map[string]any{"description": "the revision of the want's key read last", "type": "integer"}, "self": map[string]any{"description": "the node composes the module itself (ADR 0547 §2)", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}, "yes": map[string]any{"description": "the digest a person said yes to", "type": "string"}}, "required": []string{"fp", "revision"}, "type": "object"}}, "required": []string{"node", "module", "started", "composition", "judged"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"pass": map[string]any{"description": "how the pass went", "properties": map[string]any{"again": map[string]any{"description": "when the node composes again on its own, RFC 3339", "type": "string"}, "at": map[string]any{"description": "when the pass ended, RFC 3339", "type": "string"}, "earlier": map[string]any{"description": "the answer is the last pass, made moments ago", "type": "boolean"}, "handed": map[string]any{"description": "how many compositions the pass handed the agent that differ from before", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module refused alone, with why", "type": "object"}, "refused": map[string]any{"description": "why the node composed none of its modules", "type": "string"}}, "required": []string{"at", "handed"}, "type": "object"}, "retried": map[string]any{"description": "the module whose apply was tried again", "type": "string"}}, "required": []string{"pass"}, "type": "object"}}, {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: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"candidate"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "also answer the composition input the candidate was composed from, for the sandbox path", "type": "boolean"}}, "required": []string{"candidate"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the candidate's key", "type": "string"}, "input": map[string]any{"description": "when asked: the composition input it composed from", "type": "object"}, "modules": map[string]any{"description": "every module placed on the node, sorted by name", "items": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "refused": map[string]any{"description": "why the node could not compose with the candidate at all", "type": "string"}, "took": map[string]any{"description": "how long the engine took, in milliseconds", "type": "integer"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}, "verdict": map[string]any{"description": "the node's verdict (ADR 0593 §7, §9)", "enum": []string{"composed", "refused", "unready", "not-judged"}, "type": "string"}}, "required": []string{"v", "node", "candidate", "verdict", "modules", "own", "took"}, "type": "object"}}, {Name: "pull-waits", Description: "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", Optional: true, Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"waits": map[string]any{"description": "every failed pull waiting on a holder", "items": map[string]any{"properties": map[string]any{"holder": map[string]any{"description": "that seat's holder, whose settled the retry waits on", "type": "string"}, "image": map[string]any{"description": "the image whose pull failed", "type": "string"}, "module": map[string]any{"description": "the module whose apply failed", "type": "string"}, "pull-began": map[string]any{"description": "when the failed pull began, RFC 3339", "type": "string"}, "retry-last": map[string]any{"description": "when it was last tried again, RFC 3339", "type": "string"}, "retry-trigger": map[string]any{"description": "what started that retry: settled, or the engine's start", "type": "string"}, "seat": map[string]any{"description": "the seat the image came through", "type": "string"}, "settled-count": map[string]any{"description": "the settled events of the holder heard since the engine started", "type": "integer"}, "settled-last": map[string]any{"description": "when the last was heard, RFC 3339", "type": "string"}}, "required": []string{"module", "image"}, "type": "object"}, "type": "array"}}, "required": []string{"waits"}, "type": "object"}}, + {Name: "manifest-reads", 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.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"description": "what this machine's engine reads of a manifest (ADR 0618 §3)", "properties": map[string]any{"manifest-reads": map[string]any{"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", "items": map[string]any{"minLength": float64(1), "type": "string"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "stated-at": map[string]any{"description": "when the engine last stated them, RFC 3339", "type": "string"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}}, "required": []string{"v", "node", "stated-at"}, "type": "object"}}, }}, {Name: NodeLoginManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", Serves: []Verb{ diff --git a/seats/node-engine/fixtures/events.json b/seats/node-engine/fixtures/events.json new file mode 100644 index 0000000..eb2c8d7 --- /dev/null +++ b/seats/node-engine/fixtures/events.json @@ -0,0 +1,69 @@ +[ + { + "name": "asked on the node's subject", + "of": "event:asked", + "subject": "mesh.seat.node-engine.event.asked.ace", + "given": { + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "ask": "hand-over-1", + "state": "asked", + "node": "ace" + } + }, + { + "name": "done on the node's subject", + "of": "event:done", + "subject": "mesh.seat.node-engine.event.done.ace", + "given": { + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "ask": "hand-over-1", + "state": "done", + "said": "handed over: owner media, group media, mode 2775", + "node": "ace" + } + }, + { + "name": "refused on the node's subject", + "of": "event:refused", + "subject": "mesh.seat.node-engine.event.refused.ace", + "given": { + "act": "access-act", + "subject": "library:music", + "caller": "mesh-tools", + "ask": "access-act-1", + "state": "refused", + "said": "the operator said no", + "node": "ace" + } + }, + { + "name": "manifest-reads-changed when the engine started", + "of": "event:manifest-reads-changed", + "subject": "mesh.seat.node-engine.event.manifest-reads-changed.ace", + "given": { + "v": 1, + "node": "ace", + "manifest-reads": [ + "form:seat-reference", + "word:series" + ], + "at": "2026-11-04T10:00:00Z", + "why": "started" + } + }, + { + "name": "manifest-reads-changed when the node returned, from an engine that states none", + "of": "event:manifest-reads-changed", + "subject": "mesh.seat.node-engine.event.manifest-reads-changed.g14", + "given": { + "v": 1, + "node": "g14", + "at": "2026-11-04T10:05:00Z", + "why": "returned" + } + } +] diff --git a/seats/node-engine/fixtures/verbs.json b/seats/node-engine/fixtures/verbs.json new file mode 100644 index 0000000..40fff31 --- /dev/null +++ b/seats/node-engine/fixtures/verbs.json @@ -0,0 +1,76 @@ +[ + { + "name": "manifest-reads answers what the engine reads and when it stated it", + "of": "verb:manifest-reads.answer", + "given": { + "v": 1, + "node": "ace", + "manifest-reads": [ + "form:seat-reference", + "word:series" + ], + "stated-at": "2026-11-04T10:00:00Z" + } + }, + { + "name": "manifest-reads of an engine built before it said them is the baseline", + "of": "verb:manifest-reads.answer", + "given": { + "v": 1, + "node": "g14", + "stated-at": "2026-11-04T10:05:00Z" + } + }, + { + "name": "compose-candidate asks the candidate and, as a boolean, its composition input", + "of": "verb:compose-candidate.input", + "given": { + "candidate": "mesh-host.0123abcd", + "input": true + } + }, + { + "name": "compose-candidate answers per module and the node's own", + "of": "verb:compose-candidate.answer", + "given": { + "v": 1, + "node": "ace", + "candidate": "mesh-host.0123abcd", + "verdict": "composed", + "modules": [ + { + "module": "plex", + "fp": "fp-1", + "state": "composed", + "digest": "sha256:aa", + "changed": true + } + ], + "own": { + "module": "node", + "state": "composed", + "digest": "sha256:bb" + }, + "took": 1200 + } + }, + { + "name": "acts answers each act kept", + "of": "verb:acts.answer", + "given": { + "node": "ace", + "acts": [ + { + "id": "hand-over-1", + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "at": "2026-11-04T09:00:00Z", + "state": "asked", + "ask": "hand-over-1", + "expires": "2026-11-05T09:00:00Z" + } + ] + } + } +] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 21b288c..229e220 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -2,8 +2,9 @@ "format": 1, "seat": "node-engine", "scope": "node", - "grain": "seat-list", + "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": [ { @@ -24,6 +25,9 @@ "path" ], "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" } }, { @@ -49,6 +53,9 @@ "access" ], "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" } }, { @@ -57,6 +64,9 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" } }, { @@ -70,6 +80,9 @@ } }, "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" } }, { @@ -78,6 +91,21 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "machines": { + "type": "array", + "items": { + "$ref": "#/defs/RootVerdict" + }, + "description": "this machine's verdict" + } + }, + "required": [ + "machines" + ] } }, { @@ -95,6 +123,9 @@ } }, "type": "object" + }, + "answer": { + "$ref": "#/defs/Acts" } }, { @@ -113,6 +144,78 @@ } }, "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" + ] } }, { @@ -138,6 +241,255 @@ "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" + ] } }, { @@ -152,6 +504,55 @@ } }, "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" + ] } }, { @@ -165,18 +566,72 @@ "type": "string" }, "input": { - "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", - "enum": [ - "true", - "false" - ], - "type": "string" + "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" + ] } }, { @@ -244,17 +699,423 @@ } } } + }, + { + "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" + "name": "asked", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } }, { - "name": "done" + "name": "done", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } }, { - "name": "refused" + "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/ts/src/index.ts b/ts/src/index.ts index 18984e2..d7d9552 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -10,3 +10,4 @@ export * as npmPackageRegistry from "./npm-package-registry.js"; export * as goPackageRegistry from "./go-package-registry.js"; export * as containerImageRegistry from "./container-image-registry.js"; export * as ociArtifactRegistry from "./oci-artifact-registry.js"; +export * as nodeEngine from "./node-engine.js"; diff --git a/ts/src/node-engine.ts b/ts/src/node-engine.ts new file mode 100644 index 0000000..8c0f71e --- /dev/null +++ b/ts/src/node-engine.ts @@ -0,0 +1,539 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "node-engine", defined whole (seats/node-engine/seat.json): 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. + +export const seat = "node-engine"; + +export const verbs = { + handOver: "hand-over", + accessAct: "access-act", + judgeDirectories: "judge-directories", + setuidSearch: "setuid-search", + rootFree: "root-free", + acts: "acts", + packages: "packages", + composition: "composition", + compose: "compose", + composeCandidate: "compose-candidate", + pullWaits: "pull-waits", + manifestReads: "manifest-reads", +} as const; + +export const events = { + asked: "asked", + done: "done", + refused: "refused", + manifestReadsChanged: "manifest-reads-changed", +} as const; + +export const family = { name: "node-engine.events", maxAgeDays: 7 } as const; + +export function askedSubject(caller: string): string { + return "mesh.seat.node-engine.event.asked." + caller; +} + +export function doneSubject(caller: string): string { + return "mesh.seat.node-engine.event.done." + caller; +} + +export function refusedSubject(caller: string): string { + return "mesh.seat.node-engine.event.refused." + caller; +} + +export function manifestReadsChangedSubject(caller: string): string { + return "mesh.seat.node-engine.event.manifest-reads-changed." + caller; +} + +/** one act asked through the seat, as acts reads it (ADR 0563 §9) */ +export interface Act { + /** the act: hand-over, access-act, judge-directories or setuid-search */ + "act": string; + /** the operator's ask of a hand-over or an access act */ + "ask"?: string; + /** when it was asked, RFC 3339 */ + "at": string; + /** the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4) */ + "auto"?: boolean; + /** who answered, as the operator's warrant names them */ + "by"?: string; + /** the caller as the runtime proved it */ + "caller": string; + /** what the engine read on disk when it judged */ + "disk"?: string; + /** when it ended, RFC 3339 */ + "ended"?: string; + /** when the ask ends unanswered, RFC 3339 */ + "expires"?: string; + /** the act's id */ + "id": string; + /** the directory the act changes */ + "path"?: string; + /** the warrant's proofs */ + "proofs"?: string[]; + /** what the engine said of it: its outcome or why it was refused */ + "said"?: string; + /** the owner, group and mode it sets, in words */ + "sets"?: string; + /** where it stands */ + "state": "asked" | "done" | "refused" | "answered"; + /** the path of a hand-over, or the access of an access act */ + "subject"?: string; + /** the channel the warrant came on */ + "via"?: string; + /** why it was asked, in words */ + "why"?: string; +} + +/** 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) */ +export interface ActSaid { + /** the act */ + "act": string; + /** the operator's ask */ + "ask"?: string; + /** the caller as the runtime proved it */ + "caller": string; + /** the node whose engine says it */ + "node": string; + /** what the engine said of it */ + "said"?: string; + /** where the act stands */ + "state": "asked" | "done" | "refused"; + /** the path or the access */ + "subject"?: string; +} + +/** the acts kept */ +export interface Acts { + /** the acts, newest first */ + "acts": Act[]; + /** the node */ + "node": string; +} + +/** the answer of an act the operator is asked */ +export interface Asked { + /** the ask's id: acts shows where it stands */ + "asked": string; + /** the node */ + "node": string; + /** what happens next, in words */ + "says": string; +} + +/** what a node-engine says when what it reads changed or its node returned (ADR 0618 §4) */ +export interface ManifestReadsChanged { + /** when it says it, RFC 3339 */ + "at": string; + /** what its engine states it reads now */ + "manifest-reads"?: string[]; + /** the node */ + "node": string; + /** the layout's version */ + "v": number; + /** started: the engine started or what it states changed; returned: its node returned after being away */ + "why": "started" | "returned"; +} + +/** one module's composition with the candidate */ +export interface Module { + /** the candidate changes it from the one composed without it */ + "changed"?: boolean; + /** the composition's digest when composed */ + "digest"?: string; + /** the fingerprint it was composed at */ + "fp"?: string; + /** the module, node for the node's own composition */ + "module": string; + /** composed (digest set), refused by the candidate, or held without it too (words set) */ + "state": "composed" | "refused" | "held"; + /** why, for a refused or held one */ + "words"?: string; +} + +/** one revision of a composition key */ +export interface Revision { + /** its digest */ + "digest"?: string; + /** the composition's key */ + "key": string; + /** the latest revision */ + "latest"?: boolean; + /** why it cannot be read or trusted */ + "refused"?: string; + /** how many resources it names */ + "resources"?: number; + /** its revision */ + "revision"?: number; +} + +/** the engine's root-free verdict (ADR 0563 §6) */ +export interface RootVerdict { + /** the machine is root-free */ + "free": boolean; + /** when the engine judged it, RFC 3339 */ + "judged": string; + /** the machine */ + "machine": string; + /** why, in words */ + "why": string; +} + +/** the answer of a read-only judgement */ +export interface Said { + /** the node */ + "node": string; + /** the engine's judgement or search, one line per directory or program */ + "said": string; +} + +/** is what hand-over takes. */ +export interface HandOverInput { + /** the directory's absolute path, as the engine states it */ + "path": string; + /** why it is asked, in words the operator reads */ + "why"?: string; +} + +/** is what hand-over answers. */ +export type HandOverAnswer = Asked; + +/** is what access-act takes. */ +export interface AccessActInput { + /** the access, library: */ + "access": string; + /** the act: take */ + "act": string; + /** why it is asked, in words the operator reads */ + "why"?: string; +} + +/** is what access-act answers. */ +export type AccessActAnswer = Asked; + +/** is what judge-directories takes. */ +export interface JudgeDirectoriesInput { +} + +/** is what judge-directories answers. */ +export type JudgeDirectoriesAnswer = Said; + +/** is what setuid-search takes. */ +export interface SetuidSearchInput { + /** why it is asked, in words */ + "why"?: string; +} + +/** is what setuid-search answers. */ +export type SetuidSearchAnswer = Said; + +/** is what root-free takes. */ +export interface RootFreeInput { +} + +/** is what root-free answers. */ +export interface RootFreeAnswer { + /** this machine's verdict */ + "machines": RootVerdict[]; +} + +/** is what acts takes. */ +export interface ActsInput { + /** one act, by its id */ + "id"?: string; + /** at most this many, newest */ + "limit"?: string; +} + +/** is what acts answers. */ +export type ActsAnswer = Acts; + +/** is what packages takes. */ +export interface PackagesInput { + /** "true": write this machine's key again from what the engine composed */ + "write"?: "true" | "false"; +} + +export interface PackagesAnswerKeyPackagesItem { + /** declared absent */ + "absent": boolean; + /** the modules that declare it */ + "modules": string[]; + /** the package */ + "package": string; +} + +/** the node's key as the bus holds it; absent when the engine wrote none */ +export interface PackagesAnswerKey { + /** the node */ + "node": string; + /** its packages sorted by name, absent after present */ + "packages": PackagesAnswerKeyPackagesItem[]; +} + +/** is what packages answers. */ +export interface PackagesAnswer { + /** the node's key as the bus holds it; absent when the engine wrote none */ + "key"?: PackagesAnswerKey; + /** why the key could not be read */ + "key_error"?: string; + /** the node */ + "node": string; + /** a note, such as that no key is written yet */ + "note"?: string; + /** where the key is read: the engine's state packages */ + "source": string; + /** with write: the key was written again (false: it already held what the engine composed) */ + "wrote"?: boolean; +} + +/** is what composition takes. */ +export interface CompositionInput { + /** "true": list the resources that differ from what runs */ + "diff"?: string; + /** a composition's digest (sha256:…), to read that revision rather than the latest */ + "digest"?: string; + /** the module, as placed on this machine; node for the node's own */ + "module": string; +} + +/** the module's last apply that did not end running; absent when none */ +export interface CompositionAnswerAttempt { + /** the want's revision the hold took */ + "answered_revision"?: number; + /** the digest tried */ + "composed"?: string; + /** the composition's revision it was tried at */ + "composition_revision": number; + /** the fingerprint tried */ + "fp": string; + /** held for a failed backup */ + "held"?: boolean; + /** it failed because the bus did not answer for a moment */ + "transient"?: boolean; + /** the want's revision it was tried at */ + "want_revision": number; +} + +/** what the module's have key says now; absent when it says nothing */ +export interface CompositionAnswerHave { + /** digest */ + "composed"?: string; + /** detail */ + "detail"?: string; + /** fingerprint */ + "fp"?: string; + /** judgement */ + "judgement"?: string; + /** running, failed, held or waiting-yes */ + "phase": string; + /** the digest that waits for a yes */ + "waits"?: string; +} + +/** the move from what runs to the composition read */ +export interface CompositionAnswerJudged { + /** the judgement */ + "judgement": string; + /** the agent would wait for a person's yes before applying it */ + "waits_for_yes": boolean; + /** its words */ + "words": string; +} + +/** of the node's own composition, its own fields beside what runs names */ +export interface CompositionAnswerOwn { + /** the revision's adoption; absent for a converged node */ + "adoption"?: Record; + /** groups */ + "groups"?: number; + /** each module the node's composer held in its last pass, with why */ + "held"?: Record; + /** identities the revision names */ + "identities"?: number; + /** libraries */ + "libraries"?: number; + /** the account the provisioner's client runs as, as the revision names it */ + "provisioner_account": string; + /** as what runs names it */ + "runs_provisioner_account"?: string; +} + +/** what runs of the module; absent when nothing runs */ +export interface CompositionAnswerRuns { + /** digest */ + "composed"?: string; + /** fingerprint */ + "fp"?: string; + /** applied, have or kept */ + "from": string; + /** phase */ + "phase"?: string; + /** how many resources run */ + "resources": number; +} + +/** the module's want as the agent holds it; absent when it holds none */ +export interface CompositionAnswerWant { + /** the operator's answer to a failed backup, again or skip */ + "backup"?: string; + /** the composition's digest it names */ + "composed"?: string; + /** its build's fingerprint */ + "fp": string; + /** the module is removed */ + "removed"?: boolean; + /** the revision of the want's key read last */ + "revision": number; + /** the node composes the module itself (ADR 0547 §2) */ + "self"?: boolean; + /** its words */ + "words"?: string; + /** the digest a person said yes to */ + "yes"?: string; +} + +/** is what composition answers. */ +export interface CompositionAnswer { + /** the module's last apply that did not end running; absent when none */ + "attempt"?: CompositionAnswerAttempt; + "composition": Revision; + /** every resource that differs from what runs, when asked */ + "diff"?: Record[]; + /** what the module's have key says now; absent when it says nothing */ + "have"?: CompositionAnswerHave; + /** the move from what runs to the composition read */ + "judged": CompositionAnswerJudged; + /** the module */ + "module": string; + /** the node */ + "node": string; + /** of the node's own composition, its own fields beside what runs names */ + "own"?: CompositionAnswerOwn; + /** the revisions of its key the bucket keeps, oldest first */ + "revisions"?: Revision[]; + /** what runs of the module; absent when nothing runs */ + "runs"?: CompositionAnswerRuns; + /** the agent has looked at the module once since it started */ + "started": boolean; + /** the module's want as the agent holds it; absent when it holds none */ + "want"?: CompositionAnswerWant; +} + +/** is what compose takes. */ +export interface ComposeInput { + /** a module placed on this machine whose apply is also tried again (optional) */ + "module"?: string; +} + +/** how the pass went */ +export interface ComposeAnswerPass { + /** when the node composes again on its own, RFC 3339 */ + "again"?: string; + /** when the pass ended, RFC 3339 */ + "at": string; + /** the answer is the last pass, made moments ago */ + "earlier"?: boolean; + /** how many compositions the pass handed the agent that differ from before */ + "handed": number; + /** each module refused alone, with why */ + "held"?: Record; + /** why the node composed none of its modules */ + "refused"?: string; +} + +/** is what compose answers. */ +export interface ComposeAnswer { + /** how the pass went */ + "pass": ComposeAnswerPass; + /** the module whose apply was tried again */ + "retried"?: string; +} + +/** is what compose-candidate takes. */ +export interface ComposeCandidateInput { + /** the key of the indexer's candidate, . */ + "candidate": string; + /** also answer the composition input the candidate was composed from, for the sandbox path */ + "input"?: boolean; +} + +/** is what compose-candidate answers. */ +export interface ComposeCandidateAnswer { + /** the candidate's key */ + "candidate": string; + /** when asked: the composition input it composed from */ + "input"?: Record; + /** every module placed on the node, sorted by name */ + "modules": Module[]; + /** the node */ + "node": string; + "own": Module; + /** why the node could not compose with the candidate at all */ + "refused"?: string; + /** how long the engine took, in milliseconds */ + "took": number; + /** the layout's version */ + "v": number; + /** the node's verdict (ADR 0593 §7, §9) */ + "verdict": "composed" | "refused" | "unready" | "not-judged"; +} + +/** is what pull-waits takes. */ +export interface PullWaitsInput { +} + +export interface PullWaitsAnswerWaitsItem { + /** that seat's holder, whose settled the retry waits on */ + "holder"?: string; + /** the image whose pull failed */ + "image": string; + /** the module whose apply failed */ + "module": string; + /** when the failed pull began, RFC 3339 */ + "pull-began"?: string; + /** when it was last tried again, RFC 3339 */ + "retry-last"?: string; + /** what started that retry: settled, or the engine's start */ + "retry-trigger"?: string; + /** the seat the image came through */ + "seat"?: string; + /** the settled events of the holder heard since the engine started */ + "settled-count"?: number; + /** when the last was heard, RFC 3339 */ + "settled-last"?: string; +} + +/** is what pull-waits answers. */ +export interface PullWaitsAnswer { + /** every failed pull waiting on a holder */ + "waits": PullWaitsAnswerWaitsItem[]; +} + +/** is what manifest-reads takes. */ +export interface ManifestReadsInput { +} + +/** is what manifest-reads answers. */ +export interface ManifestReadsAnswer { + /** 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 */ + "manifest-reads"?: string[]; + /** the node */ + "node": string; + /** when the engine last stated them, RFC 3339 */ + "stated-at": string; + /** the layout's version */ + "v": number; +} + +/** is the body of the event asked. */ +export type AskedEvent = ActSaid; + +/** is the body of the event done. */ +export type DoneEvent = ActSaid; + +/** is the body of the event refused. */ +export type RefusedEvent = ActSaid; + +/** is the body of the event manifest-reads-changed. */ +export type ManifestReadsChangedEvent = ManifestReadsChanged; + diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 5eb57ff..6ab54ab 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -6432,9 +6432,10 @@ export const list: readonly Seat[] = [ "emits": [ "asked", "done", - "refused" + "refused", + "manifest-reads-changed" ], - "grain": "seat-list", + "grain": "whole", "heldByTheEngine": true, "name": "node-engine", "scope": "node", @@ -6457,7 +6458,30 @@ export const list: readonly Seat[] = [ ], "type": "object" }, - "name": "hand-over" + "name": "hand-over", + "output": { + "description": "the answer of an act the operator is asked", + "properties": { + "asked": { + "description": "the ask's id: acts shows where it stands", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "says": { + "description": "what happens next, in words", + "type": "string" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "type": "object" + } }, { "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.", @@ -6482,7 +6506,30 @@ export const list: readonly Seat[] = [ ], "type": "object" }, - "name": "access-act" + "name": "access-act", + "output": { + "description": "the answer of an act the operator is asked", + "properties": { + "asked": { + "description": "the ask's id: acts shows where it stands", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "says": { + "description": "what happens next, in words", + "type": "string" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "type": "object" + } }, { "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.", @@ -6490,7 +6537,25 @@ export const list: readonly Seat[] = [ "properties": {}, "type": "object" }, - "name": "judge-directories" + "name": "judge-directories", + "output": { + "description": "the answer of a read-only judgement", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "said": { + "description": "the engine's judgement or search, one line per directory or program", + "type": "string" + } + }, + "required": [ + "node", + "said" + ], + "type": "object" + } }, { "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.", @@ -6503,7 +6568,25 @@ export const list: readonly Seat[] = [ }, "type": "object" }, - "name": "setuid-search" + "name": "setuid-search", + "output": { + "description": "the answer of a read-only judgement", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "said": { + "description": "the engine's judgement or search, one line per directory or program", + "type": "string" + } + }, + "required": [ + "node", + "said" + ], + "type": "object" + } }, { "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.", @@ -6511,7 +6594,47 @@ export const list: readonly Seat[] = [ "properties": {}, "type": "object" }, - "name": "root-free" + "name": "root-free", + "output": { + "properties": { + "machines": { + "description": "this machine's verdict", + "items": { + "description": "the engine's root-free verdict (ADR 0563 §6)", + "properties": { + "free": { + "description": "the machine is root-free", + "type": "boolean" + }, + "judged": { + "description": "when the engine judged it, RFC 3339", + "type": "string" + }, + "machine": { + "description": "the machine", + "type": "string" + }, + "why": { + "description": "why, in words", + "type": "string" + } + }, + "required": [ + "machine", + "free", + "why", + "judged" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "machines" + ], + "type": "object" + } }, { "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.", @@ -6528,7 +6651,119 @@ export const list: readonly Seat[] = [ }, "type": "object" }, - "name": "acts" + "name": "acts", + "output": { + "description": "the acts kept", + "properties": { + "acts": { + "description": "the acts, newest first", + "items": { + "description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", + "properties": { + "act": { + "description": "the act: hand-over, access-act, judge-directories or setuid-search", + "type": "string" + }, + "ask": { + "description": "the operator's ask of a hand-over or an access act", + "type": "string" + }, + "at": { + "description": "when it was asked, RFC 3339", + "type": "string" + }, + "auto": { + "description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", + "type": "boolean" + }, + "by": { + "description": "who answered, as the operator's warrant names them", + "type": "string" + }, + "caller": { + "description": "the caller as the runtime proved it", + "type": "string" + }, + "disk": { + "description": "what the engine read on disk when it judged", + "type": "string" + }, + "ended": { + "description": "when it ended, RFC 3339", + "type": "string" + }, + "expires": { + "description": "when the ask ends unanswered, RFC 3339", + "type": "string" + }, + "id": { + "description": "the act's id", + "type": "string" + }, + "path": { + "description": "the directory the act changes", + "type": "string" + }, + "proofs": { + "description": "the warrant's proofs", + "items": { + "type": "string" + }, + "type": "array" + }, + "said": { + "description": "what the engine said of it: its outcome or why it was refused", + "type": "string" + }, + "sets": { + "description": "the owner, group and mode it sets, in words", + "type": "string" + }, + "state": { + "description": "where it stands", + "enum": [ + "asked", + "done", + "refused", + "answered" + ], + "type": "string" + }, + "subject": { + "description": "the path of a hand-over, or the access of an access act", + "type": "string" + }, + "via": { + "description": "the channel the warrant came on", + "type": "string" + }, + "why": { + "description": "why it was asked, in words", + "type": "string" + } + }, + "required": [ + "id", + "act", + "caller", + "at", + "state" + ], + "type": "object" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + } + }, + "required": [ + "node", + "acts" + ], + "type": "object" + } }, { "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.", @@ -6546,7 +6781,79 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "packages", - "optional": true + "optional": true, + "output": { + "properties": { + "key": { + "description": "the node's key as the bus holds it; absent when the engine wrote none", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "packages": { + "description": "its packages sorted by name, absent after present", + "items": { + "properties": { + "absent": { + "description": "declared absent", + "type": "boolean" + }, + "modules": { + "description": "the modules that declare it", + "items": { + "type": "string" + }, + "type": "array" + }, + "package": { + "description": "the package", + "type": "string" + } + }, + "required": [ + "package", + "modules", + "absent" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "node", + "packages" + ], + "type": "object" + }, + "key_error": { + "description": "why the key could not be read", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "note": { + "description": "a note, such as that no key is written yet", + "type": "string" + }, + "source": { + "description": "where the key is read: the engine's state packages", + "type": "string" + }, + "wrote": { + "description": "with write: the key was written again (false: it already held what the engine composed)", + "type": "boolean" + } + }, + "required": [ + "node", + "source" + ], + "type": "object" + } }, { "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.", @@ -6571,7 +6878,316 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "composition", - "optional": true + "optional": true, + "output": { + "properties": { + "attempt": { + "description": "the module's last apply that did not end running; absent when none", + "properties": { + "answered_revision": { + "description": "the want's revision the hold took", + "type": "integer" + }, + "composed": { + "description": "the digest tried", + "type": "string" + }, + "composition_revision": { + "description": "the composition's revision it was tried at", + "type": "integer" + }, + "fp": { + "description": "the fingerprint tried", + "type": "string" + }, + "held": { + "description": "held for a failed backup", + "type": "boolean" + }, + "transient": { + "description": "it failed because the bus did not answer for a moment", + "type": "boolean" + }, + "want_revision": { + "description": "the want's revision it was tried at", + "type": "integer" + } + }, + "required": [ + "fp", + "want_revision", + "composition_revision" + ], + "type": "object" + }, + "composition": { + "description": "one revision of a composition key", + "properties": { + "digest": { + "description": "its digest", + "type": "string" + }, + "key": { + "description": "the composition's key", + "type": "string" + }, + "latest": { + "description": "the latest revision", + "type": "boolean" + }, + "refused": { + "description": "why it cannot be read or trusted", + "type": "string" + }, + "resources": { + "description": "how many resources it names", + "type": "integer" + }, + "revision": { + "description": "its revision", + "type": "integer" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "diff": { + "description": "every resource that differs from what runs, when asked", + "items": { + "description": "one resource added, removed or changed, as the engine's judge says it", + "type": "object" + }, + "type": "array" + }, + "have": { + "description": "what the module's have key says now; absent when it says nothing", + "properties": { + "composed": { + "description": "digest", + "type": "string" + }, + "detail": { + "description": "detail", + "type": "string" + }, + "fp": { + "description": "fingerprint", + "type": "string" + }, + "judgement": { + "description": "judgement", + "type": "string" + }, + "phase": { + "description": "running, failed, held or waiting-yes", + "type": "string" + }, + "waits": { + "description": "the digest that waits for a yes", + "type": "string" + } + }, + "required": [ + "phase" + ], + "type": "object" + }, + "judged": { + "description": "the move from what runs to the composition read", + "properties": { + "judgement": { + "description": "the judgement", + "type": "string" + }, + "waits_for_yes": { + "description": "the agent would wait for a person's yes before applying it", + "type": "boolean" + }, + "words": { + "description": "its words", + "type": "string" + } + }, + "required": [ + "judgement", + "words", + "waits_for_yes" + ], + "type": "object" + }, + "module": { + "description": "the module", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "own": { + "description": "of the node's own composition, its own fields beside what runs names", + "properties": { + "adoption": { + "description": "the revision's adoption; absent for a converged node", + "type": "object" + }, + "groups": { + "description": "groups", + "type": "integer" + }, + "held": { + "additionalProperties": { + "type": "string" + }, + "description": "each module the node's composer held in its last pass, with why", + "type": "object" + }, + "identities": { + "description": "identities the revision names", + "type": "integer" + }, + "libraries": { + "description": "libraries", + "type": "integer" + }, + "provisioner_account": { + "description": "the account the provisioner's client runs as, as the revision names it", + "type": "string" + }, + "runs_provisioner_account": { + "description": "as what runs names it", + "type": "string" + } + }, + "required": [ + "provisioner_account" + ], + "type": "object" + }, + "revisions": { + "description": "the revisions of its key the bucket keeps, oldest first", + "items": { + "description": "one revision of a composition key", + "properties": { + "digest": { + "description": "its digest", + "type": "string" + }, + "key": { + "description": "the composition's key", + "type": "string" + }, + "latest": { + "description": "the latest revision", + "type": "boolean" + }, + "refused": { + "description": "why it cannot be read or trusted", + "type": "string" + }, + "resources": { + "description": "how many resources it names", + "type": "integer" + }, + "revision": { + "description": "its revision", + "type": "integer" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "type": "array" + }, + "runs": { + "description": "what runs of the module; absent when nothing runs", + "properties": { + "composed": { + "description": "digest", + "type": "string" + }, + "fp": { + "description": "fingerprint", + "type": "string" + }, + "from": { + "description": "applied, have or kept", + "type": "string" + }, + "phase": { + "description": "phase", + "type": "string" + }, + "resources": { + "description": "how many resources run", + "type": "integer" + } + }, + "required": [ + "from", + "resources" + ], + "type": "object" + }, + "started": { + "description": "the agent has looked at the module once since it started", + "type": "boolean" + }, + "want": { + "description": "the module's want as the agent holds it; absent when it holds none", + "properties": { + "backup": { + "description": "the operator's answer to a failed backup, again or skip", + "type": "string" + }, + "composed": { + "description": "the composition's digest it names", + "type": "string" + }, + "fp": { + "description": "its build's fingerprint", + "type": "string" + }, + "removed": { + "description": "the module is removed", + "type": "boolean" + }, + "revision": { + "description": "the revision of the want's key read last", + "type": "integer" + }, + "self": { + "description": "the node composes the module itself (ADR 0547 §2)", + "type": "boolean" + }, + "words": { + "description": "its words", + "type": "string" + }, + "yes": { + "description": "the digest a person said yes to", + "type": "string" + } + }, + "required": [ + "fp", + "revision" + ], + "type": "object" + } + }, + "required": [ + "node", + "module", + "started", + "composition", + "judged" + ], + "type": "object" + } }, { "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.", @@ -6585,7 +7201,56 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "compose", - "optional": true + "optional": true, + "output": { + "properties": { + "pass": { + "description": "how the pass went", + "properties": { + "again": { + "description": "when the node composes again on its own, RFC 3339", + "type": "string" + }, + "at": { + "description": "when the pass ended, RFC 3339", + "type": "string" + }, + "earlier": { + "description": "the answer is the last pass, made moments ago", + "type": "boolean" + }, + "handed": { + "description": "how many compositions the pass handed the agent that differ from before", + "type": "integer" + }, + "held": { + "additionalProperties": { + "type": "string" + }, + "description": "each module refused alone, with why", + "type": "object" + }, + "refused": { + "description": "why the node composed none of its modules", + "type": "string" + } + }, + "required": [ + "at", + "handed" + ], + "type": "object" + }, + "retried": { + "description": "the module whose apply was tried again", + "type": "string" + } + }, + "required": [ + "pass" + ], + "type": "object" + } }, { "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.", @@ -6596,12 +7261,8 @@ export const list: readonly Seat[] = [ "type": "string" }, "input": { - "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", - "enum": [ - "true", - "false" - ], - "type": "string" + "description": "also answer the composition input the candidate was composed from, for the sandbox path", + "type": "boolean" } }, "required": [ @@ -6610,7 +7271,137 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "compose-candidate", - "optional": true + "optional": true, + "output": { + "properties": { + "candidate": { + "description": "the candidate's key", + "type": "string" + }, + "input": { + "description": "when asked: the composition input it composed from", + "type": "object" + }, + "modules": { + "description": "every module placed on the node, sorted by name", + "items": { + "description": "one module's composition with the candidate", + "properties": { + "changed": { + "description": "the candidate changes it from the one composed without it", + "type": "boolean" + }, + "digest": { + "description": "the composition's digest when composed", + "type": "string" + }, + "fp": { + "description": "the fingerprint it was composed at", + "type": "string" + }, + "module": { + "description": "the module, node for the node's own composition", + "type": "string" + }, + "state": { + "description": "composed (digest set), refused by the candidate, or held without it too (words set)", + "enum": [ + "composed", + "refused", + "held" + ], + "type": "string" + }, + "words": { + "description": "why, for a refused or held one", + "type": "string" + } + }, + "required": [ + "module", + "state" + ], + "type": "object" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + }, + "own": { + "description": "one module's composition with the candidate", + "properties": { + "changed": { + "description": "the candidate changes it from the one composed without it", + "type": "boolean" + }, + "digest": { + "description": "the composition's digest when composed", + "type": "string" + }, + "fp": { + "description": "the fingerprint it was composed at", + "type": "string" + }, + "module": { + "description": "the module, node for the node's own composition", + "type": "string" + }, + "state": { + "description": "composed (digest set), refused by the candidate, or held without it too (words set)", + "enum": [ + "composed", + "refused", + "held" + ], + "type": "string" + }, + "words": { + "description": "why, for a refused or held one", + "type": "string" + } + }, + "required": [ + "module", + "state" + ], + "type": "object" + }, + "refused": { + "description": "why the node could not compose with the candidate at all", + "type": "string" + }, + "took": { + "description": "how long the engine took, in milliseconds", + "type": "integer" + }, + "v": { + "description": "the layout's version", + "type": "integer" + }, + "verdict": { + "description": "the node's verdict (ADR 0593 §7, §9)", + "enum": [ + "composed", + "refused", + "unready", + "not-judged" + ], + "type": "string" + } + }, + "required": [ + "v", + "node", + "candidate", + "verdict", + "modules", + "own", + "took" + ], + "type": "object" + } }, { "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", @@ -6677,6 +7468,46 @@ export const list: readonly Seat[] = [ ], "type": "object" } + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "name": "manifest-reads", + "optional": true, + "output": { + "description": "what this machine's engine reads of a manifest (ADR 0618 §3)", + "properties": { + "manifest-reads": { + "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", + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + }, + "stated-at": { + "description": "when the engine last stated them, RFC 3339", + "type": "string" + }, + "v": { + "description": "the layout's version", + "type": "integer" + } + }, + "required": [ + "v", + "node", + "stated-at" + ], + "type": "object" + } } ] },