mesh-sdk compiles no seat list and imports no contracts module: catalogue takes its seats from the host, whole seats' names leave it (ADR 0635 §1, §4; issue 1966)

catalogue's working set starts empty and a host gives it the seats it read (UseSeats over SeatsFromDefinitions);
the node-engine's verbs and events take the seat's name from the caller. Tests take their seats from seatlisttest.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-05 12:54:23 +01:00
parent 7ea960fe20
commit 1aca4991d4
106 changed files with 8392 additions and 446 deletions
+1 -3
View File
@@ -1,7 +1,5 @@
package buscompose
import "git.novox.be/novox/mesh-sdk/go/catalogue"
// Declared is one module on one node, as composing its authority needs it.
type Declared struct {
Module string
@@ -104,4 +102,4 @@ type ProcessPrincipal struct {
}
// VaultSeat is the vault's seat: its holder alone asks the desks and the issuing providers in its own name.
const VaultSeat = catalogue.VaultSeat
const VaultSeat = "mesh-vault"
+1 -3
View File
@@ -1,7 +1,5 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// ArtifactSweepSeat is the seat artifact-store-tools holds. Not the seat mesh-artifact-store, the store itself held by
// distribution, and not the provision artifact-store it delivers.
const ArtifactSweepSeat = contractseats.MeshArtifactSweep
const ArtifactSweepSeat = "mesh-artifact-sweep"
+15 -22
View File
@@ -2,8 +2,6 @@ package catalogue
import (
"strings"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
)
// ArtifactStoreScheme marks a reference to something in the mesh's artifact store, kept without
@@ -47,7 +45,7 @@ func isRegistryHost(component string) bool {
// through container-image-registry or oci-artifact-registry, whose blob it names the way the older form did.
func InArtifactStore(reference string) (path string, kept bool) {
if seat, rest, ok := InRegistrySeat(reference); ok {
if seat == OciArtifactRegistrySeat {
if seat == ociArtifactRegistrySeat {
name, digest, _ := strings.Cut(rest, "@")
return name + "/blobs/" + digest, true
}
@@ -56,28 +54,23 @@ func InArtifactStore(reference string) (path string, kept bool) {
return strings.CutPrefix(reference, ArtifactStoreScheme)
}
// The registry seats' names, which this file's reference forms read and write. **They leave with these forms:** a whole
// seat's name is its contract's (ADR 0634 §4, ADR 0635 §4), and the reference of a registry seat is the registry
// clients' in mesh-delivery-contracts (prepare D-1); until those clients hold it, this package keeps the one copy and
// the package-list test names it as the one exception (S-8).
const (
npmPackageRegistrySeat = "npm-package-registry"
goPackageRegistrySeat = "go-package-registry"
containerImageRegistrySeat = "container-image-registry"
ociArtifactRegistrySeat = "oci-artifact-registry"
)
// RegistrySeats are the seats a reference of the registry form names first, the two that front the artifact store
// (novox/hq ADR 0605 §1, §9).
var RegistrySeats = []string{ContainerImageRegistrySeat, OciArtifactRegistrySeat}
// The four registry seats (novox/hq ADR 0605 §1), named here so the rest of mesh-sdk takes them from the seat set
// through catalogue alone (ADR 0625 §3).
const (
NpmPackageRegistrySeat = contractseats.NpmPackageRegistry
GoPackageRegistrySeat = contractseats.GoPackageRegistry
ContainerImageRegistrySeat = contractseats.ContainerImageRegistry
OciArtifactRegistrySeat = contractseats.OciArtifactRegistry
)
var RegistrySeats = []string{containerImageRegistrySeat, ociArtifactRegistrySeat}
// PublishSeats are the seats an artifact is published through (ADR 0605 §1).
var PublishSeats = []string{NpmPackageRegistrySeat, GoPackageRegistrySeat, ContainerImageRegistrySeat, OciArtifactRegistrySeat}
// The seats of the vault, the provisioner, the hand acts' record and the conditions' keeper, as the seat set names them,
// for the rest of mesh-sdk (ADR 0625 §3).
const (
VaultSeat = contractseats.MeshVault
ProvisionerSeat = contractseats.MeshProvisioner
)
var PublishSeats = []string{npmPackageRegistrySeat, goPackageRegistrySeat, containerImageRegistrySeat, ociArtifactRegistrySeat}
// SeatReference is the reference a build names an image, bundle, archive or upstream copy by once it published it
// through a registry seat (novox/hq ADR 0605 §9): `<seat>/<module>/<artifact>@sha256:<digest>`, as InRegistrySeat reads
@@ -97,7 +90,7 @@ func InRegistrySeat(reference string) (seat, path string, ok bool) {
if !found || !strings.Contains(rest, "@sha256:") {
return "", "", false
}
if _, _, upstream := InUpstream(rest); upstream && first == ContainerImageRegistrySeat {
if _, _, upstream := InUpstream(rest); upstream && first == containerImageRegistrySeat {
return first, rest, true
}
if strings.Count(rest, "/") != 1 {
-3
View File
@@ -1,7 +1,5 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
import "encoding/json"
// CatalogSeat is the seat of the catalog: what the mesh holds of every module (novox/hq ADR 0604 §1). Its states are
@@ -10,7 +8,6 @@ import "encoding/json"
// no verb. It names no event. What must be built again is no verb: a module whose fingerprint differs from its newest
// build, which the delivery view derives. The verbs' answers are CatalogModules, CatalogOne, CatalogProvides and
// CatalogDependents, whose field names are those of the catalog module's tools they replace.
const CatalogSeat = contractseats.MeshCatalog
// CatalogEntry is one module of the catalog: its repository, its directory there ("." for the root), the merge commit it
// was last changed at, and its new-form fingerprint or why it has none.
-3
View File
@@ -1,9 +1,6 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// CheckerSeat is the seat mesh-delivery-checker holds.
//
// Deprecated: a whole seat's name is mesh-contracts' (seats.MeshDeliveryChecker); kept for its importers until their
// next change takes it from there (testdata/seat-names.txt).
const CheckerSeat = contractseats.MeshDeliveryChecker
+1 -6
View File
@@ -1,12 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// The two seats.
const (
WantsSeat = contractseats.MeshWants
HandActsSeat = contractseats.MeshHandActs
)
const ()
// controlRecordsDecision is the record that made the two seats.
const controlRecordsDecision = "novox/hq ADR 0565"
-3
View File
@@ -1,9 +1,6 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// DeliverySeat is the seat mesh-delivery holds.
const DeliverySeat = contractseats.MeshDelivery
// RemovedDeliveryVerbs are the delivery seat's verbs release 6 removed (novox/hq ADR 0539 clause 17): with no plan the
// gate keeps the merge's states and derives nothing after the merge. Each stays in deliveryVerbs as an optional row
@@ -4,7 +4,7 @@ import "testing"
// The node-engine's compose-candidate verb (ADR 0593 §7) is optional, asks the candidate, and takes input as a boolean, as the engine and the checker read it.
func TestTheEngineServesComposeCandidate(t *testing.T) {
for _, v := range EngineVerbs() {
for _, v := range EngineVerbs(EngineSeat) {
if v.Name != "compose-candidate" {
continue
}
+8 -25
View File
@@ -1,32 +1,15 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// EngineSeat is the node seat every node's engine holds, from the seat set. The seat is defined whole in mesh-contracts
// (its contract, go/seats/nodeengine, and its client, go/clients/nodeengineclient): its verbs' and events' names and
// message types are taken from there (ADR 0627 §4).
const EngineSeat = contractseats.NodeEngine
// EngineVerbs are the node-engine seat's verbs, in the order ADR 0563 §1 names them: its definition's, read from the
// generated seat list (design 26), each with its description and its input's schema as discovery shows them.
func EngineVerbs() []Verb {
s, _ := seatIn(defaultSeats, EngineSeat)
// EngineVerbs are the node-engine seat's verbs as the seat list the host gave has them (ADR 0563 §1; design 26), each
// with its description and its input's schema as discovery shows them. The seat's name is its contract's
// (mesh-contracts' nodeengine), never this package's (ADR 0634 §4, ADR 0635 §4): the caller names it.
func EngineVerbs(seat string) []Verb {
s, _ := SeatNamed(seat)
return append([]Verb(nil), s.Serves...)
}
// EngineEvents are what the seat says: of an act, each naming the act, the path or access, the caller as the runtime
// proved it and the ask (ADR 0563 §1), and that what its engine reads of a manifest changed (ADR 0618 §4).
var EngineEvents = func() []string {
s, _ := seatIn(defaultSeats, EngineSeat)
// EngineEvents are what the node-engine seat says, as the seat list the host gave has them.
func EngineEvents(seat string) []string {
s, _ := SeatNamed(seat)
return append([]string(nil), s.Emits...)
}()
// seatIn is the seat of that name in a list.
func seatIn(list []Seat, name string) (Seat, bool) {
for _, s := range list {
if s.Name == name {
return s, true
}
}
return Seat{}, false
}
-3
View File
@@ -1,9 +1,6 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// EnrolmentSeat is the seat mesh-enrolment.
const EnrolmentSeat = contractseats.MeshEnrolment
// The seat's six verbs (ADR 0587 §1, §4-§8).
const (
+3 -5
View File
@@ -1,14 +1,12 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// EnvironmentSeat and LoginShellSeat are the seats whose holders may place what the modules
// contributed: the account's environment, and the login shell's code.
const (
EnvironmentSeat = contractseats.NodeEnvironment
LoginShellSeat = contractseats.NodeLoginShell
EnvironmentSeat = "node-environment"
LoginShellSeat = "node-login-shell"
// PowerSeat is the seat whose holder places code for the power moments (novox/hq ADR 0211).
PowerSeat = contractseats.NodePower
PowerSeat = "node-power"
)
// Where an environment entry on PATH goes: before the account's existing PATH, or after it.
+1 -3
View File
@@ -1,9 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// FilesSeat is the role that reads and manages a node's files and accounts (novox/hq ADR 0394).
const FilesSeat = contractseats.NodeFiles
const FilesSeat = "node-files"
// FilesOwnedPaths is the word the holder of node-files is given beside the secret places: a JSON object from
// each absolute path a module owns on its node to "module (tool to use)" (novox/hq ADR 0394 §5).
+11 -13
View File
@@ -3,8 +3,6 @@ package catalogue
import (
"fmt"
"strings"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
)
// The graphical session's seats (novox/hq ADR 0208): one module per piece of software, each piece's
@@ -12,17 +10,17 @@ import (
// a role rather than each inventing one — and a machine running two of one role is refused at
// assignment instead of found by two bars on one screen.
const (
LoginManagerSeat = contractseats.NodeLoginManager
DisplayServerSeat = contractseats.NodeDisplayServer
DisplaySessionSeat = contractseats.NodeDisplaySession
TerminalEmulatorSeat = contractseats.NodeTerminalEmulator
LauncherSeat = contractseats.NodeLauncher
NotifierSeat = contractseats.NodeNotifier
LockScreenSeat = contractseats.NodeLockScreen
ClipboardSeat = contractseats.NodeClipboard
BarSeat = contractseats.NodeBar
CompositorSeat = contractseats.NodeCompositor
SecretServiceSeat = contractseats.NodeSecretService
LoginManagerSeat = "node-login-manager"
DisplayServerSeat = "node-display-server"
DisplaySessionSeat = "node-display-session"
TerminalEmulatorSeat = "node-terminal-emulator"
LauncherSeat = "node-launcher"
NotifierSeat = "node-notifier"
LockScreenSeat = "node-lock-screen"
ClipboardSeat = "node-clipboard"
BarSeat = "node-bar"
CompositorSeat = "node-compositor"
SecretServiceSeat = "node-secret-service"
)
// BarKindBlock is the bar's one received kind: a block (novox/hq ADR 0255).
+1 -8
View File
@@ -1,14 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// The four health seats.
const (
ConditionsSeat = contractseats.MeshConditions
WatchdogsSeat = contractseats.MeshWatchdogs
SelfCheckSeat = contractseats.MeshSelfCheck
HealersSeat = contractseats.MeshHealers
)
const ()
// healthDecision is the record that made the four seats.
const healthDecision = "novox/hq ADR 0564"
+6 -13
View File
@@ -15,7 +15,7 @@ func busSeatDelivering(t *testing.T, delivers string) {
t.Helper()
was := Seats()
t.Cleanup(func() { UseSeats(was) })
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: delivers, Decision: "test"}})
UseSeats([]Seat{{Name: "mesh-broker", Scope: ScopeMesh, Delivers: delivers, Decision: "test", Serves: verbsNamed(brokerVerbs())}})
}
// brokerVerbs are the bus seat's verbs as a literal: mesh-sdk imports no seat's contract module, a test neither
@@ -80,17 +80,10 @@ func TestCanHoldJudgesClaimScopeAndWhatTheSeatDelivers(t *testing.T) {
}
}
// The store's seat rows have no protocol columns yet; loading them must not drop the protocol the
// bus is derived from, or no role's work queue is ever raised (found live, 2026-09-28).
func TestAStoreRowWithoutAProtocolKeepsTheCompiledOne(t *testing.T) {
was := Seats()
t.Cleanup(func() { UseSeats(was) })
UseSeats([]Seat{{Name: "node-build-go", Scope: ScopeNode, Decision: "row"}})
got, ok := SeatNamed("node-build-go")
if !ok || len(got.Accepts) == 0 || !got.PulledByKind || !got.Yields {
t.Fatalf("the Go build seat lost what it accepts when loaded from the store: %+v", got)
}
if got.Decision != "row" {
t.Fatalf("the store's own columns were not kept: %+v", got)
func verbsNamed(names []string) []Verb {
var out []Verb
for _, n := range names {
out = append(out, Verb{Name: n})
}
return out
}
-7
View File
@@ -31,11 +31,4 @@ func TestTheLoginShellsExecuteIsOptionalSoAMachineMayWithholdIt(t *testing.T) {
}
}
t.Run("compiled", check)
t.Run("read back from the store", func(t *testing.T) {
before := seats
t.Cleanup(func() { seats = before })
UseSeats([]Seat{{Name: LoginShellSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0204",
Serves: []Verb{{Name: "execute"}}}})
check(t)
})
}
+4 -4
View File
@@ -57,7 +57,7 @@ var artifactReferenceForms = []string{ReferenceFormSeat}
const ReferenceFormSeatReach = "seat-reach"
// claimsAtMesh says whether a manifest claims seat at the mesh's scope (a claim with no scope is the seat's own). The
// store holder's own manifest, a claim of ContainerImageRegistrySeat, needs form:seat-reach this way (ADR 0632 §2): read
// store holder's own manifest, a claim of containerImageRegistrySeat, needs form:seat-reach this way (ADR 0632 §2): read
// on every node, it leaves an engine before the reading without the old way to find the store, so it is held from every
// node whose engine does not state it.
func claimsAtMesh(top map[string]any, seat string) bool {
@@ -102,7 +102,7 @@ func ManifestReads() []string {
set[ReadForm+f] = true
}
set[ReadForm+ReferenceFormSeatReach] = true
for _, s := range DefaultSeats() {
for _, s := range Seats() {
set[ReadSeat+s.Name] = true
}
return sortedSet(set)
@@ -164,7 +164,7 @@ func ManifestNeeds(raw []byte) ([]string, error) {
}
used := map[string]bool{}
textNeeds(raw, used)
if claimsAtMesh(top, ContainerImageRegistrySeat) {
if claimsAtMesh(top, containerImageRegistrySeat) {
used[ReadForm+ReferenceFormSeatReach] = true
}
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
@@ -216,7 +216,7 @@ func ManifestNeeds(raw []byte) ([]string, error) {
// and a bucket's share with a seat.
func seatNamesUsed(top map[string]any) []string {
known := map[string]bool{}
for _, s := range DefaultSeats() {
for _, s := range Seats() {
known[s.Name] = true
}
set := map[string]bool{}
+1 -3
View File
@@ -1,9 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// ResolverSeat is the machine's own resolver (novox/hq ADR 0247).
const ResolverSeat = contractseats.NodeResolver
const ResolverSeat = "node-resolver"
// ResolverFile is the machine's resolver file: the uplink holder's, or the node-resolver holder's where
// one is held.
-23
View File
@@ -139,26 +139,3 @@ func TestResetFailedWantedByAndUnlinkDanglingAreRequiredVerbsOfTheServiceManager
t.Fatalf("read back from the row, a holder serving all twelve is refused: %v", err)
}
}
// The optional mark is not stored, so a seat set read back from the store's rows takes it from the
// compiled seat: otherwise `failed`, seeded into the row, would come back required.
func TestAnOptionalVerbStaysOptionalInASetReadFromTheStore(t *testing.T) {
defer UseSeats(DefaultSeats())
var rows []Seat
for _, s := range DefaultSeats() {
row := s
row.Serves = nil
for _, v := range s.Serves {
row.Serves = append(row.Serves, Verb{Name: v.Name, Description: v.Description, Input: v.Input})
}
rows = append(rows, row)
}
UseSeats(rows)
seat, _ := SeatNamed(ServiceManagerSeat)
holder := Manifest{Module: "systemd", Version: "1", Claims: []Claim{{Name: ServiceManagerSeat, Scope: ScopeNode,
Serves: []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal",
"reset-failed", "wanted-by", "unlink-dangling"}}}}
if err := CanHold(holder, seat); err != nil {
t.Fatalf("a set read from rows refuses a holder without the optional failed: %v", err)
}
}
-3
View File
@@ -1,9 +1,6 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// PlacementSeat is the seat the assigner holds for placement.
const PlacementSeat = contractseats.MeshPlacement
// The seat's five verbs (ADR 0567 §10).
const (
+1 -3
View File
@@ -1,9 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// ReadsSeat is the role that reads a node's files and accounts (novox/hq ADR 0310).
const ReadsSeat = contractseats.NodeReads
const ReadsSeat = "node-reads"
// ReadsSecretPlaces is the word the holder of node-reads is given: the places on its node where a secret is
// kept, as a JSON list of absolute paths, each refused by name before any read (novox/hq ADR 0310 §5).
-26
View File
@@ -62,29 +62,3 @@ func TestAVerbDefinitionCarriesWhatItReplaces(t *testing.T) {
t.Fatalf("replaces not read: %v", got)
}
}
// A seat row the store seeded before `replaces` existed carries none; the working set takes the compiled
// one, so the `tools` answer says it on a mesh whose rows exist.
func TestASeededRowTakesWhatItsVerbsReplaceFromTheCompiledSeat(t *testing.T) {
was := Seats()
t.Cleanup(func() { UseSeats(was) })
var row Seat
for _, s := range DefaultSeats() {
if s.Name == ServiceManagerSeat {
row = s
}
}
stored := make([]Verb, len(row.Serves))
for i, v := range row.Serves {
v.Replaces = nil
stored[i] = v
}
row.Serves = stored
UseSeats([]Seat{row})
got, _ := SeatNamed(ServiceManagerSeat)
for _, v := range got.Serves {
if v.Name == "journal" && (len(v.Replaces) == 0 || v.Replaces[0] != "journalctl") {
t.Fatalf("the seeded journal verb says it replaces %v", v.Replaces)
}
}
}
+3 -5
View File
@@ -1,15 +1,13 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5).
const HotkeysSeat = contractseats.NodeHotkeys
const HotkeysSeat = "node-hotkeys"
// MessageBusSeat is the machine's D-Bus (novox/hq ADR 0215).
const MessageBusSeat = contractseats.NodeMessageBus
const MessageBusSeat = "node-message-bus"
// BackupSeat is the machine's backups (novox/hq ADR 0214, to-be 43).
const BackupSeat = contractseats.NodeBackup
const BackupSeat = "node-backup"
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
type SeatContribution struct {
+3 -5
View File
@@ -4,16 +4,14 @@ import (
"fmt"
"sort"
"strings"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
)
// The three seats that apply resources (novox/hq ADR 0207 §1). Named once, because the derivation,
// the seed and the messages all turn on these strings.
const (
ServiceManagerSeat = contractseats.NodeServiceManager
PackageManagerSeat = contractseats.NodePackageManager
ContainerRuntimeSeat = contractseats.NodeContainerRuntime
ServiceManagerSeat = "node-service-manager"
PackageManagerSeat = "node-package-manager"
ContainerRuntimeSeat = "node-container-runtime"
)
// appliedThrough is which seat applies a resource of each type. **Only these three**: ADR 0207
+18 -1
View File
@@ -14,6 +14,12 @@ func SeatFromDefinition(d seatdef.Definition) Seat {
s := Seat{Name: d.Seat, Scope: d.Scope, Replicated: d.Replicated, Delivers: d.Delivers,
Accepts: seatdef.MessageNames(d.Accepts), Emits: seatdef.MessageNames(d.Events), PulledByKind: d.PulledByKind,
Yields: d.Yields, HeldByTheEngine: d.HeldByTheEngine, Decision: d.Decision}
if len(s.Accepts) == 0 {
s.Accepts = nil
}
if len(s.Emits) == 0 {
s.Emits = nil
}
for _, v := range d.Verbs {
s.Serves = append(s.Serves, Verb{Name: v.Name, Description: v.Description,
Input: seatdef.ResolveRefs(v.Input, d.Defs), Output: seatdef.ResolveRefs(v.Answer, d.Defs),
@@ -22,7 +28,7 @@ func SeatFromDefinition(d seatdef.Definition) Seat {
for _, r := range d.Receives {
rv := Receivable{Kind: r.Kind, Comment: r.Comment, Dirs: r.Dirs, Offered: r.Offered}
if r.Shape != nil {
sh := &Shape{Order: r.Shape.Order, Examples: r.Shape.Examples, Text: r.Shape.Text}
sh := &Shape{Order: r.Shape.Order, Examples: examples(r.Shape.Examples), Text: r.Shape.Text}
for _, f := range r.Shape.Places {
sh.Places = append(sh.Places, PlaceField{Field: f.Field, Values: f.Values})
}
@@ -55,3 +61,14 @@ func SeatsFromDefinitions(defs []seatdef.Definition) []Seat {
})
return out
}
func examples(in []map[string]any) []map[string]any {
if in == nil {
return nil
}
out := make([]map[string]any, len(in))
for i, e := range in {
out[i], _ = seatdef.StringLists(e).(map[string]any)
}
return out
}
-11
View File
@@ -31,17 +31,6 @@ func TestTheSeatsThatNamePathsAreTheStartingSet(t *testing.T) {
}
}
// What the store's rows carry is never the paths: a row read back keeps the compiled ones.
func TestTheSeatPathsSurviveTheStoresRows(t *testing.T) {
before := seats
t.Cleanup(func() { seats = before })
UseSeats([]Seat{{Name: "node-hostname", Scope: ScopeNode}})
s, _ := SeatNamed("node-hostname")
if len(s.Paths) != 2 {
t.Fatalf("node-hostname read back from its row names %v", s.Paths)
}
}
func hostnameSeat(t *testing.T) Seat {
t.Helper()
s, ok := SeatNamed("node-hostname")
+10 -127
View File
@@ -3,8 +3,6 @@ package catalogue
import (
"fmt"
"strings"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
)
// Seat is one role the mesh defines.
@@ -85,130 +83,22 @@ type Receivable struct {
Offered bool
}
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
// fallback when it has none (novox/hq ADR 0122). **It is generated, never written here** (novox/hq ADR 0608 §5,
// ADR 0613 §2, design 26 *The seat set has one source*): every seat's definition lives in mesh-contracts, and its
// generated seat list is what this runtime reads, in the order a person reads it: the mesh's own, then a node's. What
// a definition cannot say, being code, is attached here: a contribution kind's checks (withShapeCode).
var defaultSeats = fromContracts(contractseats.List())
// seats is the working set the lookups read, empty until a host gives it the seat list (UseSeats): mesh-sdk compiles no
// seat list and imports no contracts module (novox/hq ADR 0635 §1). A host reads the list from the bus
// (mesh-sdk go/seatlist, which holds the foundation's bootstrapped definitions until the indexer is heard) and gives
// its seats here at start and at each change.
var seats []Seat
// fromContracts is the generated seat list in this runtime's types, each data-shaped kind given its checks.
func fromContracts(list []contractseats.Seat) []Seat {
out := make([]Seat, 0, len(list))
for _, c := range list {
s := Seat{Name: c.Name, Scope: c.Scope, Replicated: c.Replicated, Delivers: c.Delivers, Accepts: c.Accepts,
Emits: c.Emits, PulledByKind: c.PulledByKind, Yields: c.Yields, HeldByTheEngine: c.HeldByTheEngine,
Decision: c.Decision}
for _, v := range c.Serves {
s.Serves = append(s.Serves, Verb{Name: v.Name, Description: v.Description, Input: v.Input, Output: v.Output,
Optional: v.Optional, Replaces: v.Replaces})
}
for _, r := range c.Receives {
rv := Receivable{Kind: r.Kind, Comment: r.Comment, Dirs: r.Dirs, Offered: r.Offered}
if r.Shape != nil {
sh := &Shape{Order: r.Shape.Order, Examples: r.Shape.Examples, Text: r.Shape.Text}
for _, f := range r.Shape.Places {
sh.Places = append(sh.Places, PlaceField{Field: f.Field, Values: f.Values})
}
withShapeCode(c.Name, r.Kind, sh)
rv.Shape = sh
}
s.Receives = append(s.Receives, rv)
}
for _, p := range c.Paths {
s.Paths = append(s.Paths, SeatPath{Path: p.Path, StepsBackFor: p.StepsBackFor})
}
out = append(out, s)
}
return out
}
// seats is the working set the lookups read. It starts as the compiled defaults and is replaced by
// what the control plane loaded from its store (novox/hq ADR 0122), so a change to the set is a
// change to data, not to this code.
var seats = defaultSeats
// DefaultSeats is the set the mesh ships with, for seeding the store's seat table.
func DefaultSeats() []Seat { return append([]Seat(nil), defaultSeats...) }
// UseSeats replaces the working set with the one the control plane read from its store.
// UseSeats replaces the working set with the seat list a host read (seatlist.Reader.Seats, via SeatsFromDefinitions).
//
// **Empty is ignored on purpose.** A store that has not been seeded yet — or one that could not be
// read — must leave the compiled defaults in force rather than emptying the set: an empty set would
// refuse every claim and could stop the control plane composing at all, which is a far worse failure
// than running on the set the binary shipped with. So the store can only ever *replace* the set with
// a non-empty one, never erase it.
// **Empty is ignored on purpose.** A list that was not read yet — or one that could not be — must leave the set in force
// rather than emptying it: an empty set would refuse every claim and could stop a composition altogether, which is a
// far worse failure than running on the set the host last had.
func UseSeats(s []Seat) {
if len(s) == 0 {
return
}
// **The store's rows carry no protocol yet, and the protocol is what the bus is derived
// from.** ADR 0129 gives a seat what it accepts, emits and serves; ADR 0122 moved the set into
// a table that has name, scope, delivers and decision and nothing else, and the columns for
// the rest are not there yet. So a row replacing a compiled entry would silently drop the
// protocol, and the roles' work queues would never be raised — found live as "no response
// from stream" the first time a build was submitted over the new bus (2026-09-28). Until the
// table gains the columns, a row without a protocol keeps the compiled one of the same name.
byName := map[string]Seat{}
for _, d := range defaultSeats {
byName[d.Name] = d
}
merged := make([]Seat, 0, len(s))
for _, row := range s {
if len(row.Accepts)+len(row.Emits)+len(row.Serves) == 0 {
if d, known := byName[row.Name]; known {
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
}
}
// What a seat receives, whether it is replicated and the paths it names are never stored
// (novox/hq ADR 0212, ADR 0223, ADR 0287), so they are always the compiled ones.
if d, known := byName[row.Name]; known {
row.Receives = d.Receives
row.Paths = d.Paths
row.Replicated = d.Replicated
row.PulledByKind = d.PulledByKind
row.Yields = d.Yields // never stored (ADR 0553)
row.HeldByTheEngine = d.HeldByTheEngine // never stored (ADR 0563)
row.Serves = optionalAsCompiled(row.Serves, d.Serves)
}
merged = append(merged, row)
}
seats = merged
}
// optionalAsCompiled is a row's verbs with each one the compiled seat marks optional marked so. The mark
// is never stored (Verb.Optional), and a row is what the working set holds once the store is read: a verb
// the seeding added to the row — `failed`, `checks` — would otherwise come back required, and every holder
// not serving it yet would be refused at registration and at handover, which the mark exists to prevent.
//
// **And what a verb replaces is the compiled one** (novox/hq ADR 0245): a row seeded before the field
// existed carries none, and re-seeding widens a verb's arguments only, so read from the row alone the
// `tools` answer — what the mesh MCP server's search and the agent's instructions are built from — would
// say no verb replaces anything on a mesh whose rows exist. The mesh's statement of what a role replaces
// is this binary's, like the optional mark.
func optionalAsCompiled(row, compiled []Verb) []Verb {
optional := map[string]bool{}
replaces := map[string][]string{}
for _, v := range compiled {
if v.Optional {
optional[v.Name] = true
}
if len(v.Replaces) > 0 {
replaces[v.Name] = v.Replaces
}
}
if len(optional) == 0 && len(replaces) == 0 {
return row
}
out := make([]Verb, len(row))
for i, v := range row {
v.Optional = optional[v.Name]
if r, ok := replaces[v.Name]; ok {
v.Replaces = r
}
out[i] = v
}
return out
seats = append([]Seat(nil), s...)
}
// aliases maps a seat's former names to its current canonical name (novox/hq ADR 0122). Loaded from
@@ -325,10 +215,3 @@ func SeatsWithAProtocol() []Seat {
}
return out
}
// BrokerSeat is the mesh's bus. Its holder is the bus, and its account — when it declares one — may
// snapshot the bus's streams and do nothing else (novox/hq ADR 0235).
//
// Deprecated: a whole seat's name is mesh-contracts' (seats.MeshBroker, meshbroker.Seat); kept for its importers
// until their next change takes it from there (testdata/seat-names.txt).
const BrokerSeat = contractseats.MeshBroker
+2 -1
View File
@@ -137,7 +137,8 @@ func TestUseSeatsReplacesTheSetButNeverEmptiesIt(t *testing.T) {
added = append(added, s.Name)
}
}
if want := []string{"go-package-registry", "container-image-registry", "oci-artifact-registry"}; !slices.Equal(added, want) {
slices.Sort(added)
if want := []string{"container-image-registry", "go-package-registry", "oci-artifact-registry"}; !slices.Equal(added, want) {
t.Errorf("the generated set adds %v, want %v", added, want)
}
}
+2 -4
View File
@@ -1,9 +1,7 @@
package catalogue
import contractseats "git.novox.be/novox/mesh-contracts/go/seats"
// NFSServerSeat is the role of the machine that shares directories over NFS (novox/hq ADR 0263).
const NFSServerSeat = contractseats.NodeNfsServer
const NFSServerSeat = "node-nfs-server"
// MountsSeat is the role that mounts a machine's shares and occasional sources (novox/hq ADR 0263).
const MountsSeat = contractseats.NodeMounts
const MountsSeat = "node-mounts"
+38
View File
@@ -0,0 +1,38 @@
package catalogue
import "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
// defaultSeats is the list the tests start from (seatlisttest: the contracts modules' definitions, read as a host reads
// them from the bus, ADR 0635 §1), and DefaultSeats gives a copy of it for a test to restore.
var defaultSeats = func() []Seat {
list := SeatsFromDefinitions(seatlisttest.Definitions())
UseSeats(list)
return list
}()
// DefaultSeats is the tests' seat list.
func DefaultSeats() []Seat { return append([]Seat(nil), defaultSeats...) }
// The whole seats' names, which the tests use and the package does not (ADR 0634 §4; ADR 0613 §5 refuses them in
// non-test code).
const (
CatalogSeat = "mesh-catalog"
CheckerSeat = "mesh-delivery-checker"
EnrolmentSeat = "mesh-enrolment"
PlacementSeat = "mesh-placement"
DeliverySeat = "mesh-delivery"
WantsSeat = "mesh-wants"
HandActsSeat = "mesh-hand-acts"
ConditionsSeat = "mesh-conditions"
WatchdogsSeat = "mesh-watchdogs"
SelfCheckSeat = "mesh-self-check"
HealersSeat = "mesh-healers"
VaultSeat = "mesh-vault"
ProvisionerSeat = "mesh-provisioner"
BrokerSeat = "mesh-broker"
EngineSeat = "node-engine"
NpmPackageRegistrySeat = npmPackageRegistrySeat
GoPackageRegistrySeat = goPackageRegistrySeat
ContainerImageRegistrySeat = containerImageRegistrySeat
OciArtifactRegistrySeat = ociArtifactRegistrySeat
)
-5
View File
@@ -91,9 +91,4 @@ func TestOnlyTheResolverIsReplicated(t *testing.T) {
t.Errorf("%s replicated = %v; only mesh-dns-resolver is (ADR 0223)", s.Name, s.Replicated)
}
}
UseSeats([]Seat{{Name: "mesh-dns-resolver", Scope: ScopeMesh, Delivers: "wildcard-resolution"}})
defer UseSeats(DefaultSeats())
if s, _ := SeatNamed("mesh-dns-resolver"); !s.Replicated {
t.Error("loading the set from the store, which has no column for it, lost the resolver's replication")
}
}
-13
View File
@@ -50,16 +50,3 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) {
t.Fatalf("a verb the seat does not promise was accepted: %v", err)
}
}
func TestTheUplinkVerbsReadBackFromTheRowStayOptional(t *testing.T) {
before := seats
t.Cleanup(func() { seats = before })
// The row as seeding stores it: both verbs, the mark not a column.
UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "resolvers"}, {Name: "links"}}}})
seat := uplinkSeat(t)
holder := Manifest{Module: "systemd-networkd", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}},
Facts: map[string]RosterFile{"resolvers": {Path: ResolverFile, Template: "x"}}}
if err := CanHold(holder, seat); err != nil {
t.Fatalf("read back from the row, the verbs are required: %v", err)
}
}
+2 -2
View File
@@ -114,9 +114,9 @@ func (a BuiltArtifact) referenceProblem() string {
}
}
if seat, path, ok := catalogue.InRegistrySeat(r); ok {
want := catalogue.ContainerImageRegistrySeat
want := "container-image-registry"
if a.Kind == "bundle" || a.Kind == "archive" {
want = catalogue.OciArtifactRegistrySeat
want = "oci-artifact-registry"
}
module, rest, _ := strings.Cut(path, "/")
if a.Kind == "package" || seat != want || module == "" || rest != a.Name+"@"+a.Digest {
+1 -1
View File
@@ -23,7 +23,7 @@ import (
// contract: every reader takes the names from here.
const (
// EnrolmentModule is the module holding the seat mesh-enrolment.
EnrolmentModule = catalogue.EnrolmentSeat
EnrolmentModule = "mesh-enrolment"
// EnrolmentsState is its state, EnrolmentsBucket the bucket, EnrolmentsRead the name a reader's manifest declares.
EnrolmentsState = "enrolments"
EnrolmentsBucket = EnrolmentModule + "_" + EnrolmentsState
+9
View File
@@ -0,0 +1,9 @@
package delivery
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
)
// The seats the tests judge against are given to catalogue as a host gives the ones it read from the bus (ADR 0635 §1).
func init() { catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions())) }
-1
View File
@@ -3,7 +3,6 @@ module git.novox.be/novox/mesh-sdk/go
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
-2
View File
@@ -1,5 +1,3 @@
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+1 -3
View File
@@ -10,13 +10,11 @@ import (
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The seat, its holder and what it keeps (ADR 0565 clause 10).
const (
Seat = catalogue.HandActsSeat
Seat = "mesh-hand-acts"
Module = "mesh-hand-acts"
// ActsLog is the log of every act, kept 400 days; WarrantsState one key `<asker>.<ask>` per warrant, made once.
ActsLog = "acts"
+2 -4
View File
@@ -10,8 +10,6 @@ import (
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The modules holding the four health seats, and the states between them, as `<module>_<state>` buckets.
@@ -274,6 +272,6 @@ type Would struct {
// KeeperSeat and the self-check's run as `checked` on SelfCheckSeat: the controller's events and the take-over key that
// chose between the two sources ended with it (novox/hq ADR 0590 clause 3).
const (
KeeperSeat = catalogue.ConditionsSeat
SelfCheckSeat = catalogue.SelfCheckSeat
KeeperSeat = "mesh-conditions"
SelfCheckSeat = "mesh-self-check"
)
+9
View File
@@ -0,0 +1,9 @@
package overlay
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
)
// The seats the tests judge against are given to catalogue as a host gives the ones it read from the bus (ADR 0635 §1).
func init() { catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions())) }
+24 -30
View File
@@ -15,7 +15,6 @@ import (
"strings"
"testing"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
@@ -27,14 +26,13 @@ var meshSDKPackages = []string{
".", "announce", "announcetest", "buildstep", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "channel",
"composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "kvlimit", "membership",
"objectlink", "outward", "overlay", "parts", "pernode", "process", "provider", "provider/harness", "provisions",
"sdkversion", "seal", "seatdef", "testbus", "views", "views/viewstest",
"sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "testbus", "views", "views/viewstest",
}
// clientSeats are the seats whose logic is their client's in mesh-contracts (novox/hq ADR 0627 §1, §4): mesh-sdk names
// none of them outside catalogue, and holds no package over them.
var clientSeats = []string{contractseats.OperatorChannel, contractseats.MeshVault, "node-engine",
contractseats.NpmPackageRegistry, contractseats.GoPackageRegistry, contractseats.ContainerImageRegistry,
contractseats.OciArtifactRegistry}
var clientSeats = []string{"operator-channel", "mesh-vault", "node-engine", "npm-package-registry", "go-package-registry",
"container-image-registry", "oci-artifact-registry"}
// moduleWord is a module's name in a literal (mesh-<word>[-<word>...], or one of mesh-delivery's parts by its short name
// before -views or _read_), but a seat's that go/views records through (mesh-hand-acts) and an import path's mesh-sdk.
@@ -49,6 +47,10 @@ var notInSDK = []string{"asks", "asker", "publish"}
var untilTheJoin = []string{
"delivery/signers.go: mesh-vault",
"provisions/provisions.go: mesh-vault",
// Names of whole seats in packages that leave in S-7 (delivery, buscompose's grants-part names), with their packages.
"buscompose/users.go: mesh-vault",
"delivery/builder.go: container-image-registry",
"delivery/builder.go: oci-artifact-registry",
}
// computesNot are the functions whose computation is one owner's alone (ADR 0593 §2, §7): the indexer computes a
@@ -68,7 +70,16 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
// name is mesh-contracts' (go/seats), so no string literal here names a defined seat or its subjects, and no exported
// name is given to one, beyond what testdata/seat-names.txt keeps from before the move (each `<file>: <literal or
// name>`), which only shrinks as areas move. A literal naming a module of the same name as a seat counts too.
defined := contractseats.Names()
// The seats the definitions define are the files of seatlist/seatlisttest/seats, one per seat (ADR 0635 §1: mesh-sdk
// imports no contracts module, so the test reads the copy the catalogue's own tests read).
var defined []string
if entries, err := os.ReadDir("seatlist/seatlisttest/seats"); err != nil {
t.Fatal(err)
} else {
for _, e := range entries {
defined = append(defined, strings.TrimSuffix(e.Name(), ".json"))
}
}
namesSeat := func(s string) bool {
for _, seat := range defined {
if s == seat || strings.HasPrefix(s, "mesh.seat."+seat+".") {
@@ -99,8 +110,8 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
if !strings.HasSuffix(path, ".go") {
return nil
}
// A test's import is a requirement of mesh-sdk's go.mod too (ADR 0625 §1, §3): a test may import the seat set,
// which catalogue already requires, and no other contracts module.
// ADR 0635 §1, §6: mesh-sdk imports no contracts module, in a test either (a test's import is a requirement of
// go.mod too, ADR 0625 §1).
if strings.HasSuffix(path, "_test.go") {
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
@@ -108,8 +119,8 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
}
for _, imp := range file.Imports {
p, _ := strconv.Unquote(imp.Path.Value)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts/") && p != contractsSeatSet {
pending = append(pending, filepath.ToSlash(path)+": imports "+p)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts") {
t.Errorf("%s imports %s: mesh-sdk imports no contracts module (ADR 0635 §1)", path, p)
}
}
return nil
@@ -124,11 +135,11 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
}
where := filepath.ToSlash(path) + ": "
inCatalogue := dir == "catalogue"
// ADR 0625 §3: mesh-sdk's only import of a contracts module is catalogue's of the seat set.
// ADR 0635 §1: mesh-sdk imports no contracts module, the seat list being read from the bus.
for _, imp := range file.Imports {
p, _ := strconv.Unquote(imp.Path.Value)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts/") && (!inCatalogue || p != contractsSeatSet) {
pending = append(pending, where+"imports "+p)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts") {
t.Errorf("%simports %s: mesh-sdk imports no contracts module (ADR 0635 §1)", where, p)
}
}
ast.Inspect(file, func(n ast.Node) bool {
@@ -146,20 +157,6 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
pending = append(pending, where+v)
}
}
case *ast.ValueSpec:
for i, name := range n.Names {
if i < len(n.Values) && name.IsExported() {
if sel, ok := n.Values[i].(*ast.SelectorExpr); ok {
if x, ok := sel.X.(*ast.Ident); ok && x.Name == "contractseats" {
seatNames = append(seatNames, where+name.Name)
if !inCatalogue && slices.Contains([]string{"OperatorChannel", "MeshVault", "NodeEngine",
"NpmPackageRegistry", "GoPackageRegistry", "ContainerImageRegistry", "OciArtifactRegistry"}, sel.Sel.Name) {
pending = append(pending, where+name.Name)
}
}
}
}
}
}
return true
})
@@ -317,8 +314,5 @@ func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
}
}
// contractsSeatSet is the seat-set package of mesh-contracts, catalogue's one import of a contracts module.
const contractsSeatSet = "git.novox.be/novox/mesh-contracts/go/seats"
// sdkModule is mesh-sdk's Go module path.
const sdkModule = "git.novox.be/novox/mesh-sdk/go"
+1 -1
View File
@@ -18,7 +18,7 @@ import (
// The seat, its holder and its two states, and the names a reader declares in its `reads`.
const (
Seat = catalogue.ProvisionerSeat
Seat = "mesh-provisioner"
Module = "mesh-provisioner"
ProvisionsState = "provisions"
PlansState = "plans"
+34 -2
View File
@@ -2,14 +2,14 @@ package seatdef
import "strings"
// ResolveRefs is a schema with every `{"$ref": "#/defs/<name>"}` replaced by the definition's named type, as the seat
// ResolveRefs is a schema (its all-string lists as []string, StringLists) with every `{"$ref": "#/defs/<name>"}` replaced by the definition's named type, as the seat
// list's readers need it (a verb's input and answer are one schema each, not a schema and its types). A reference to a
// name the definition does not define stays as it is; nesting deeper than 32 levels stops there.
func ResolveRefs(schema map[string]any, defs map[string]map[string]any) map[string]any {
if schema == nil {
return nil
}
m, _ := resolveValue(schema, defs, 0).(map[string]any)
m, _ := StringLists(resolveValue(schema, defs, 0)).(map[string]any)
return m
}
@@ -38,3 +38,35 @@ func resolveValue(v any, defs map[string]map[string]any, depth int) any {
}
return v
}
// StringLists is a schema with every list whose items are all strings (`required`, `enum`) as a []string, the form the
// compiled seat list wrote them in; a JSON decoder reads them as []any.
func StringLists(v any) any {
switch x := v.(type) {
case map[string]any:
out := make(map[string]any, len(x))
for k, e := range x {
out[k] = StringLists(e)
}
return out
case []any:
strs := make([]string, 0, len(x))
for _, e := range x {
s, ok := e.(string)
if !ok {
strs = nil
break
}
strs = append(strs, s)
}
if strs != nil && len(x) > 0 {
return strs
}
out := make([]any, len(x))
for i, e := range x {
out[i] = StringLists(e)
}
return out
}
return v
}
@@ -0,0 +1,436 @@
{
"format": 1,
"part": "registry",
"description": "The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved.",
"verbs": [
{
"name": "publish",
"description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).",
"callers": "the build modules",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
},
"transfer": {
"type": "string",
"description": "the transfer's name, the caller's own; the same name opens the same transfer again"
},
"parts": {
"type": "array",
"items": {
"$ref": "#/defs/Part"
},
"description": "every part's digest and size; none when the digest is already held"
}
},
"required": [
"transfer",
"name",
"kind",
"digest"
]
},
"answer": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"lacks": {
"type": "array",
"items": {
"type": "string"
},
"description": "the digests of the parts the holder lacks, to send by publish-part"
},
"held": {
"type": "boolean",
"description": "the digest is held already: nothing to send; the outcome follows as an event"
}
},
"required": [
"transfer"
]
},
"optional": true
},
{
"name": "publish-part",
"description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.",
"callers": "the build modules, and only the proved caller that opened the transfer",
"input": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"part": {
"type": "string",
"description": "the part's digest"
},
"sequence": {
"type": "integer",
"description": "the chunk's place in its part, from 0"
},
"last": {
"type": "boolean",
"description": "this chunk is the part's last"
},
"data": {
"type": "string",
"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)"
}
},
"required": [
"transfer",
"part",
"sequence",
"last",
"data"
]
},
"answer": {
"type": "object",
"properties": {
"transfer": {
"type": "string",
"description": "the transfer"
},
"part": {
"type": "string",
"description": "the part"
},
"received": {
"type": "integer",
"description": "the chunks of the part held so far"
},
"complete": {
"type": "boolean",
"description": "the part is whole and its bytes match its digest"
}
},
"required": [
"transfer",
"part"
]
},
"optional": true
},
{
"name": "versions",
"description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"kind": {
"type": "string",
"description": "narrowed to one kind (optional)"
}
},
"required": [
"name"
]
},
"answer": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"versions": {
"type": "array",
"items": {
"$ref": "#/defs/Artifact"
},
"description": "newest first"
}
},
"required": [
"name",
"versions"
]
},
"optional": true
},
{
"name": "remove",
"description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.",
"callers": "the builder, for a tagged version; the artifact sweep's holder, for retention",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"version": {
"type": "string",
"description": "the version"
},
"tag": {
"type": "string",
"description": "the tag (for a tagged version)"
},
"digest": {
"type": "string",
"description": "the digest (for a bundle, archive or copied base)"
},
"reason": {
"type": "string",
"enum": [
"tag-gone",
"retention"
],
"description": "why it goes"
}
},
"required": [
"name",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"removed": {
"type": "boolean",
"description": "it is gone"
},
"kept": {
"type": "boolean",
"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)"
},
"reason": {
"type": "string",
"enum": [
"shared-digest"
],
"description": "why it was kept: shared-digest, another tag names its digest"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "the other tags still naming its digest, when kept"
},
"artifact": {
"$ref": "#/defs/Artifact"
}
},
"required": [
"removed"
]
},
"optional": true
}
],
"events": [
{
"name": "published",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "refused",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "removed",
"body": {
"$ref": "#/defs/Outcome"
}
},
{
"name": "kept",
"body": {
"$ref": "#/defs/Outcome"
}
}
],
"defs": {
"Artifact": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
}
},
"required": [
"name",
"kind",
"digest"
],
"description": "one published thing, as every registry event and answer names it"
},
"Part": {
"type": "object",
"properties": {
"digest": {
"type": "string",
"description": "the part's digest, sha256:<hex>: an image's layer or config, a package's file, a bundle's blob"
},
"size": {
"type": "integer",
"description": "its size in bytes"
}
},
"required": [
"digest",
"size"
],
"description": "one part of a transfer"
},
"Outcome": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)"
},
"module": {
"type": "string",
"description": "the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact"
},
"version": {
"type": "string",
"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone"
},
"kind": {
"type": "string",
"enum": [
"package",
"image",
"bundle",
"archive",
"upstream"
],
"description": "what it is"
},
"tag": {
"type": "string",
"description": "the tag it carries, for a tagged version"
},
"digest": {
"type": "string",
"description": "its digest, sha256:<hex> (a Go package's h1: hash beside it in the version's record)"
},
"fingerprint": {
"type": "string",
"description": "the build's fingerprint"
},
"commit": {
"type": "string",
"description": "the source commit it was built from"
},
"holder": {
"type": "string",
"description": "the module holding the seat that said it"
},
"transfer": {
"type": "string",
"description": "the transfer it ends, when it ended one"
},
"reference": {
"type": "string",
"description": "the reference the seat answers: <registry>/<module>/<artifact>@sha256:<digest>, or <name>@<version> for a package (ADR 0605 §9)"
},
"reason": {
"type": "string",
"description": "why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest)"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "for kept: the other tags still naming its digest"
}
},
"required": [
"name",
"kind",
"digest",
"holder"
],
"description": "the body of published, refused and removed"
}
}
}
+50
View File
@@ -0,0 +1,50 @@
// Package seatlisttest is the seat list for tests (novox/hq ADR 0635 §1): mesh-sdk compiles no seat list, so a test that
// judges a manifest, a claim or a composition against the mesh's seats gives them to catalogue from here, as a host gives
// the ones it read from the bus:
//
// catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions()))
//
// The definitions are copies of the contracts modules' files (seats/<seat>.json, parts/<part>.json), resolved with the
// parts they include. A seat's definition is its contracts module's; these are replaced where a seat moves or changes.
// It imports seatdef alone.
package seatlisttest
import (
"embed"
"fmt"
"sort"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
//go:embed seats/*.json parts/*.json
var files embed.FS
// Definitions are every seat's definition, in seat order. It panics on a file that does not parse.
func Definitions() []seatdef.Definition {
parts := map[string]seatdef.Part{}
pe, _ := files.ReadDir("parts")
for _, e := range pe {
raw, _ := files.ReadFile("parts/" + e.Name())
p, err := seatdef.ParsePart(raw)
if err != nil {
panic(fmt.Sprintf("seatlisttest: parts/%s: %v", e.Name(), err))
}
parts[p.Part] = p
}
var out []seatdef.Definition
se, _ := files.ReadDir("seats")
for _, e := range se {
raw, _ := files.ReadFile("seats/" + e.Name())
d, err := seatdef.Parse(raw)
if err != nil {
panic(fmt.Sprintf("seatlisttest: seats/%s: %v", e.Name(), err))
}
if d, err = seatdef.Resolve(d, parts); err != nil {
panic(fmt.Sprintf("seatlisttest: seats/%s: %v", e.Name(), err))
}
out = append(out, d)
}
sort.Slice(out, func(i, j int) bool { return out[i].Seat < out[j].Seat })
return out
}
@@ -0,0 +1,344 @@
{
"format": 1,
"seat": "container-image-registry",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0605",
"description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.",
"includes": [
"registry"
],
"verbs": [
{
"name": "mirror",
"description": "Copy an outside image into the store by its pinned digest; emits published or refused.",
"callers": "mesh-build-image's holder of the image build seat",
"input": {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "the outside image's reference"
},
"digest": {
"type": "string",
"description": "its pinned digest, sha256:<hex>"
},
"name": {
"type": "string",
"description": "the name it is kept under"
}
},
"required": [
"source",
"digest",
"name"
]
},
"answer": {
"type": "object",
"properties": {
"reference": {
"type": "string",
"description": "the reference the seat answers"
},
"digest": {
"type": "string",
"description": "the digest stored"
}
},
"required": [
"reference",
"digest"
]
},
"optional": true
},
{
"name": "tags",
"description": "The tags of one image and the digest each names.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the image"
}
},
"required": [
"name"
]
},
"answer": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the image"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "the tag"
},
"digest": {
"type": "string",
"description": "its digest"
}
},
"required": [
"tag",
"digest"
]
},
"description": "every tag"
}
},
"required": [
"name",
"tags"
]
},
"optional": true
},
{
"name": "hold",
"description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.",
"callers": "the module that runs the artifact sweep alone",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "which of the sweep's rules keeps it"
}
},
"required": [
"name",
"digest",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"held": {
"type": "boolean",
"description": "it is held"
},
"since": {
"type": "string",
"description": "when the hold began, RFC 3339"
}
},
"required": [
"held"
]
},
"optional": true
},
{
"name": "release",
"description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.",
"callers": "the module that runs the artifact sweep; the operator's session through the holder's tool",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "why it is let go"
}
},
"required": [
"name",
"digest",
"reason"
]
},
"answer": {
"type": "object",
"properties": {
"released": {
"type": "boolean",
"description": "a hold was let go; false when there was none"
}
},
"required": [
"released"
]
},
"optional": true
},
{
"name": "list",
"description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the repository, <module>/<artifact>; every repository of the seat when not given"
}
}
},
"answer": {
"type": "object",
"properties": {
"repositories": {
"type": "array",
"items": {
"$ref": "#/defs/Repository"
},
"description": "each repository named, in order of its name"
}
},
"required": [
"repositories"
]
},
"optional": true
}
],
"events": [
{
"name": "held",
"body": {
"$ref": "#/defs/Hold"
}
},
{
"name": "released",
"body": {
"$ref": "#/defs/Hold"
}
}
],
"family": {
"name": "container-image-registry.events",
"max-age-days": 7
},
"defs": {
"Hold": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the name"
},
"digest": {
"type": "string",
"description": "the digest"
},
"reason": {
"type": "string",
"description": "why"
},
"holder": {
"type": "string",
"description": "the module holding the seat"
},
"by": {
"type": "string",
"description": "the caller the holder proved"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"name",
"digest",
"holder"
],
"description": "the body of held and released"
},
"Repository": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the repository"
},
"manifests": {
"type": "array",
"items": {
"$ref": "#/defs/Listed"
},
"description": "each manifest it holds, by digest"
}
},
"required": [
"name",
"manifests"
]
},
"Listed": {
"type": "object",
"properties": {
"digest": {
"type": "string",
"description": "the manifest's digest, sha256:<hex>"
},
"index": {
"type": "boolean",
"description": "it is an index, naming a manifest per platform"
},
"platforms": {
"type": "array",
"items": {
"type": "string"
},
"description": "for an index, the digests of the manifests it names"
},
"artifact": {
"type": "string",
"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "the tags naming it, none for an untagged one"
},
"size": {
"type": "integer",
"description": "its bytes: its own content and every blob it names"
},
"held": {
"type": "boolean",
"description": "a hold keeps it from retention"
}
},
"required": [
"digest",
"index",
"size",
"held"
]
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"format": 1,
"seat": "git",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0111",
"delivers": "git"
}
@@ -0,0 +1,34 @@
{
"format": 1,
"seat": "go-package-registry",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0605",
"description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.",
"delivers": "go-package-registry",
"includes": [
"registry"
],
"family": {
"name": "go-package-registry.events",
"max-age-days": 7
},
"provisions": [
{
"name": "go-package-registry",
"form": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "the read side's address"
}
},
"required": [
"url"
],
"description": "form none: the Go module proxy's read side, answered anonymously, with no credential (design 51)"
}
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "mesh-artifact-store",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0075"
}
@@ -0,0 +1,103 @@
{
"format": 1,
"seat": "mesh-artifact-sweep",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0571",
"verbs": [
{
"name": "artifacts",
"description": "Every artifact the mesh made that lives in its artifact store, each with its repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.",
"input": {
"properties": {
"collected": {
"description": "\"true\": list the collected references too (counted always)",
"enum": [
"true",
"false"
],
"type": "string"
},
"repository": {
"description": "only this repository's references, as \u003cmodule\u003e/\u003cartifact\u003e (the counts stay whole)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "held",
"description": "Whether the artifact store holds each reference given, asked with one HEAD each: held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. Reads only.",
"input": {
"properties": {
"references": {
"description": "the artifacts' references, artifact-store://\u003cmodule\u003e/\u003cartifact\u003e/blobs/sha256:\u003chex\u003e: a list, or one text separated by commas",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"references"
],
"type": "object"
}
},
{
"name": "collect",
"description": "The artifact store's sweep, now: what is eligible is let go of. Without confirm a dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 seconds; what is left is said. Never touches a digest the mesh did not make.",
"input": {
"properties": {
"confirm": {
"description": "\"true\": let go of what is eligible (needs why); without it nothing is done",
"enum": [
"true",
"false"
],
"type": "string"
},
"most": {
"description": "let go of at most this many (500 by default, at most 5000)",
"type": "string"
},
"why": {
"description": "with confirm: why, required and recorded",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "mirrors",
"description": "The bases the mesh copied into its artifact store, each kept (a kept build stood on it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on it: never removed), by what the builder's log says each build stood on. Records nothing by hand.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "images",
"description": "Every container image one machine's want and have name, each with the resources naming it: what the machine keeps when it prunes its images. Reads only.",
"input": {
"properties": {
"node": {
"description": "the machine's name",
"type": "string"
}
},
"required": [
"node"
],
"type": "object"
}
}
],
"events": [
{
"name": "artifacts-gone"
}
]
}
@@ -0,0 +1,569 @@
{
"format": 1,
"seat": "mesh-broker",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0079",
"delivers": "mesh-bus",
"description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.",
"verbs": [
{
"name": "list",
"description": "Every entry the bus module created, its template, its settings and its fill against its cap.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"answer": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/defs/Held"
},
"description": "every entry"
}
},
"required": [
"entries"
]
},
"optional": true
},
{
"name": "show",
"description": "One entry, its template, settings and fill.",
"callers": "any caller whose role grants it",
"input": {
"$ref": "#/defs/Named"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "check",
"description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"answer": {
"type": "object",
"properties": {
"differences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry"
},
"asked": {
"type": "object",
"description": "asked"
},
"held": {
"type": "object",
"description": "held"
},
"grows": {
"type": "boolean",
"description": "the difference only grows: apply may act on it"
}
},
"required": [
"module",
"local-name"
]
},
"description": "every difference"
}
},
"required": [
"differences"
]
},
"optional": true
},
{
"name": "orphans",
"description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.",
"callers": "any caller whose role grants it",
"input": {
"type": "object",
"properties": {},
"description": "no arguments"
},
"answer": {
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/defs/Held"
},
"description": "every orphan"
}
},
"required": [
"entries"
]
},
"optional": true
},
{
"name": "create",
"description": "Create one entry now.",
"callers": "the bus module's own process only",
"input": {
"$ref": "#/defs/Entry"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "apply",
"description": "Apply a difference that only grows: a larger cap, a longer age.",
"callers": "the bus module's own process only",
"input": {
"$ref": "#/defs/Named"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "recreate",
"description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.",
"callers": "the bus module's process, on the operator's warrant",
"input": {
"$ref": "#/defs/Warranted"
},
"answer": {
"$ref": "#/defs/Held"
},
"optional": true
},
{
"name": "drop",
"description": "Remove one orphan, after a stream snapshot.",
"callers": "the bus module's process, on the operator's warrant",
"input": {
"$ref": "#/defs/Warranted"
},
"answer": {
"type": "object",
"properties": {
"dropped": {
"type": "boolean",
"description": "it is gone"
},
"snapshot": {
"type": "string",
"description": "the snapshot taken first"
}
},
"required": [
"dropped"
]
},
"optional": true
}
],
"events": [
{
"name": "created",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "refused",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "drifted",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "drift-cleared",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "full",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "full-cleared",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "orphaned",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "recreated",
"body": {
"$ref": "#/defs/Said"
}
},
{
"name": "dropped",
"body": {
"$ref": "#/defs/Said"
}
}
],
"family": {
"name": "mesh-broker.events",
"max-age-days": 7
},
"provisions": [
{
"name": "bus-state",
"form": {
"$ref": "#/defs/Binding"
}
}
],
"defs": {
"Entry": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
}
},
"required": [
"module",
"local-name",
"storage-type"
],
"description": "one piece of bus state as a manifest asks it"
},
"Fill": {
"type": "object",
"properties": {
"count": {
"type": "integer",
"description": "messages, keys or objects held"
},
"bytes": {
"type": "integer",
"description": "bytes held"
},
"cap": {
"type": "integer",
"description": "its cap in bytes"
}
},
"required": [
"count",
"bytes",
"cap"
]
},
"Held": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
},
"bus-name": {
"type": "string",
"description": "its name on the bus, <module>_<local name>"
},
"settings": {
"type": "object",
"description": "the settings the bus holds"
},
"fill": {
"$ref": "#/defs/Fill"
},
"state": {
"type": "string",
"enum": [
"held",
"refused",
"waiting",
"drifted",
"full",
"orphaned"
],
"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)"
},
"why": {
"type": "string",
"description": "why it is in its state, in plain words; empty when held as asked"
}
},
"required": [
"module",
"local-name",
"storage-type",
"bus-name"
],
"description": "one entry as the bus holds it"
},
"Said": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module asking it"
},
"local-name": {
"type": "string",
"description": "its local name in the module's manifest"
},
"storage-type": {
"type": "string",
"enum": [
"stream",
"key-value",
"object-store",
"ledger"
],
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"enum": [
"state",
"events",
"work-queue",
"log",
"objects",
"ledger",
""
],
"description": "its template; empty for a configuration of its own"
},
"options": {
"type": "object",
"description": "the template's options the module set, within their bounds"
},
"configuration": {
"type": "object",
"description": "its own configuration, where no template fits"
},
"bus-name": {
"type": "string",
"description": "its name on the bus"
},
"asked": {
"type": "object",
"description": "the settings asked"
},
"held": {
"type": "object",
"description": "the settings held"
},
"why": {
"type": "string",
"description": "why: a refusal's cause, a drift's difference, a drop's or recreate's reason"
},
"fill": {
"$ref": "#/defs/Fill"
},
"snapshot": {
"type": "string",
"description": "the stream snapshot taken before a recreate or drop"
},
"binding": {
"$ref": "#/defs/Binding"
},
"revision": {
"type": "integer",
"description": "the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears"
}
},
"required": [
"module",
"local-name",
"bus-name"
],
"description": "the body of every event of the seat"
},
"Named": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry's local name"
}
},
"required": [
"module",
"local-name"
]
},
"Warranted": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"local-name": {
"type": "string",
"description": "the entry's local name"
},
"warrant": {
"type": "string",
"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)"
},
"why": {
"type": "string",
"description": "why, kept in the record"
}
},
"required": [
"module",
"local-name",
"warrant",
"why"
]
},
"Binding": {
"type": "object",
"properties": {
"bus-name": {
"type": "string",
"description": "the entry's name on the bus"
},
"storage-type": {
"type": "string",
"description": "how the bus keeps it"
},
"template": {
"type": "string",
"description": "its template"
},
"subjects": {
"type": "array",
"items": {
"type": "string"
},
"description": "its subjects, for a stream"
}
},
"required": [
"bus-name",
"storage-type"
],
"description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)"
}
}
}
@@ -0,0 +1,69 @@
{
"format": 1,
"seat": "mesh-catalog",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0604",
"verbs": [
{
"name": "modules",
"description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "module",
"description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.",
"optional": true,
"input": {
"properties": {
"module": {
"description": "the module's name",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
},
{
"name": "provides",
"description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.",
"optional": true,
"input": {
"properties": {
"provision": {
"description": "the provision, such as postgres-database",
"type": "string"
}
},
"required": [
"provision"
],
"type": "object"
}
},
{
"name": "dependents",
"description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.",
"optional": true,
"input": {
"properties": {
"module": {
"description": "the module's name",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,95 @@
{
"format": 1,
"seat": "mesh-conditions",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0564",
"verbs": [
{
"name": "conditions",
"description": "What is wrong with the mesh now: every open condition, urgent first, then oldest, raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition lately.",
"input": {
"properties": {
"days": {
"description": "with history: how many days back (default 7, at most 90)",
"type": "string"
},
"history": {
"description": "\"true\": every raising, change, silence and clearing lately, oldest first",
"enum": [
"true",
"false"
],
"type": "string"
},
"key": {
"description": "a condition's key: that one whole; with history, only its transitions",
"type": "string"
},
"machine": {
"description": "only those about this machine",
"type": "string"
},
"scope": {
"description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module",
"type": "string"
},
"severity": {
"description": "only urgent, or only warning",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "silence",
"description": "Stop one condition's messages for a while, a hand act with why: it stays open and in status, and the silence is kept as its transition in history.",
"input": {
"properties": {
"cause": {
"description": "the cause in a word (the condition's kind when absent)",
"type": "string"
},
"for": {
"description": "how long: 30m, 4h, 2d; at most 7d",
"type": "string"
},
"key": {
"description": "the condition's key",
"type": "string"
},
"why": {
"description": "why, required and recorded",
"type": "string"
}
},
"required": [
"key",
"for",
"why"
],
"type": "object"
}
},
{
"name": "resync",
"description": "Read every observer's findings again and raise and clear what differs from the open conditions: the recovery of a keeper that missed a change.",
"input": {
"properties": {},
"type": "object"
}
}
],
"events": [
{
"name": "condition-raised"
},
{
"name": "condition-changed"
},
{
"name": "condition-cleared"
}
]
}
@@ -0,0 +1,222 @@
{
"format": 1,
"seat": "mesh-config",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0348, ADR 0565",
"verbs": [
{
"name": "config",
"description": "Set, clear, list or read the history of config values: mesh config or node config for a predefined key, or a module's own value on one node, each set with why, validated, and naming each module on each node whose value moves.",
"optional": true,
"input": {
"properties": {
"action": {
"description": "list (when not given), set, clear or history",
"type": "string"
},
"by": {
"description": "who gave the value at the controller's terminal",
"type": "string"
},
"key": {
"description": "the config key",
"type": "string"
},
"limit": {
"description": "for history: at most this many, newest",
"type": "number"
},
"module": {
"description": "a module: its own value, with node",
"type": "string"
},
"node": {
"description": "a node",
"type": "string"
},
"value": {
"description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list"
},
"why": {
"description": "for set and clear: why",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the values asked, or the one set or cleared, or its history"
}
},
{
"name": "renames",
"optional": true,
"description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.",
"input": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "one module (optional)"
}
}
},
"callers": "any caller whose role grants it; the checker asks it",
"answer": {
"type": "object",
"properties": {
"renames": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"from": {
"type": "string",
"description": "the old name"
},
"to": {
"type": "string",
"description": "the new name"
},
"at": {
"type": "string",
"description": "when it moved, RFC 3339"
},
"open": {
"type": "array",
"items": {
"type": "string"
},
"description": "the nodes still holding the old name open"
}
},
"required": [
"module",
"from",
"to"
]
},
"description": "every open rename"
}
},
"required": [
"renames"
]
}
}
],
"description": "The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "config-changed",
"subject": "mesh.mod.<holder>.event.config-changed"
},
{
"name": "renamed",
"subject": "mesh.stream.<holder>.event.renamed",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"from": {
"type": "string",
"description": "the old name"
},
"to": {
"type": "string",
"description": "the new name"
},
"at": {
"type": "string",
"description": "when, RFC 3339"
}
},
"required": [
"module",
"from",
"to",
"at"
]
}
},
{
"name": "retiring",
"subject": "mesh.stream.<holder>.event.retiring",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"key": {
"type": "string",
"description": "the value's or secret's name"
},
"kind": {
"type": "string",
"enum": [
"value",
"secret"
]
},
"since": {
"type": "string",
"description": "since when, RFC 3339"
},
"ask": {
"type": "string",
"description": "the ask's id"
}
},
"required": [
"module",
"key",
"kind",
"since"
]
}
},
{
"name": "retired",
"subject": "mesh.stream.<holder>.event.retired",
"family": "config.events",
"body": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module"
},
"name": {
"type": "string",
"description": "the name"
},
"node": {
"type": "string",
"description": "the node"
},
"ask": {
"type": "string",
"description": "the ask's id"
}
},
"required": [
"module",
"name"
]
}
}
]
}
@@ -0,0 +1,245 @@
{
"format": 1,
"seat": "mesh-delivery-checker",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0547",
"verbs": [
{
"name": "try",
"description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.",
"optional": true,
"replaces": [
"sh merge-check.sh",
"docker run postgres"
],
"input": {
"properties": {
"head": {
"description": "the full commit to try, 40 or 64 hex digits",
"type": "string"
},
"level": {
"description": "unit or integration: only this level; both when absent",
"type": "string"
},
"repository": {
"description": "<owner>/<name>",
"type": "string"
},
"tests": {
"description": "a go test -run pattern: only those tests run, and nothing is kept",
"type": "string"
},
"trunk": {
"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent",
"type": "string"
}
},
"required": [
"repository",
"head"
],
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try's id"
},
"state": {
"type": "string",
"description": "its state"
}
},
"required": [
"id"
]
}
},
{
"name": "try-read",
"description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the try: its state, its steps and its verdict when it has one"
}
},
{
"name": "try-end",
"description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the try's id, try-<digits>",
"type": "string"
},
"why": {
"description": "why it is ended, in words",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
},
"answer": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "the try"
},
"ended": {
"type": "boolean",
"description": "it is ended"
}
},
"required": [
"id",
"ended"
]
}
}
],
"description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "verdict",
"subject": "mesh.stream.<holder>.event.verdict",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request's number, for a pull request's check"
},
"head": {
"type": "string",
"description": "the commit checked"
},
"verdict": {
"type": "string",
"description": "pass or fail"
},
"out-of-step": {
"type": "array",
"items": {
"type": "string"
},
"description": "the generated files out of step with their seat definitions, each path (design 26)"
},
"series": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string",
"description": "the module whose artifact's series changes"
},
"artifact": {
"type": "string",
"description": "its artifact, or the package's ecosystem name as the module's packages name it"
},
"from": {
"type": "string",
"description": "the series on trunk"
},
"to": {
"type": "string",
"description": "the series the checked head gives it"
}
},
"required": [
"module",
"artifact",
"from",
"to"
]
},
"description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none"
}
},
"required": [
"repository",
"head",
"verdict"
]
}
},
{
"name": "tag-wanted",
"subject": "mesh.stream.<holder>.event.tag-wanted",
"family": "checker.asks",
"body": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"description": "owner/repo"
},
"pull": {
"type": "integer",
"description": "the pull request"
},
"head": {
"type": "string",
"description": "its head"
},
"branch": {
"type": "string",
"description": "the pull request's branch: its tag is removed when the branch ends"
},
"module": {
"type": "string",
"description": "the group member"
},
"package": {
"type": "string",
"description": "the package it declares"
},
"tag": {
"type": "string",
"description": "the tag wanted"
}
},
"required": [
"repository",
"head",
"branch",
"module",
"package"
]
}
},
{
"name": "preview.written",
"subject": "mesh.stream.<holder>.event.preview.written",
"family": "checks"
}
]
}
@@ -0,0 +1,226 @@
{
"format": 1,
"seat": "mesh-delivery",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0239",
"verbs": [
{
"name": "deliveries",
"description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.",
"input": {
"properties": {
"all": {
"description": "\"true\": the final ones of the last thirty days too",
"enum": [
"true",
"false"
],
"type": "string"
},
"group": {
"description": "a group's id (its branch name)",
"type": "string"
},
"repository": {
"description": "owner/repository",
"type": "string"
},
"state": {
"description": "one state, e.g. delivering or held",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "times",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "show",
"description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.",
"input": {
"properties": {
"id": {
"description": "a delivery's id, or a group's",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
}
},
{
"name": "groups",
"description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.",
"input": {
"properties": {
"all": {
"description": "\"true\": the groups that ended too",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"type": "object"
}
},
{
"name": "what-if",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "checks",
"description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.",
"optional": true,
"input": {
"properties": {
"commit": {
"description": "a commit's sha, or the start of one, instead of a pull request",
"type": "string"
},
"number": {
"description": "a pull request's number: its head is read",
"type": "string"
},
"repository": {
"description": "owner/repository",
"type": "string"
}
},
"required": [
"repository"
],
"type": "object"
}
},
{
"name": "table",
"description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "stalled",
"description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "recheck",
"description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.",
"input": {
"properties": {
"id": {
"description": "the delivery's id",
"type": "string"
},
"why": {
"description": "why, kept with the transition",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
}
},
{
"name": "release",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "stop",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "close",
"description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.",
"input": {
"properties": {
"id": {
"description": "the delivery's id",
"type": "string"
},
"why": {
"description": "the condition it answers",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
}
},
{
"name": "retire-history",
"description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge",
"optional": true,
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "settle",
"description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.",
"optional": true,
"input": {
"additionalProperties": false,
"properties": {
"ids": {
"description": "the held deliveries' ids (owner/repository@commit)",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"why": {
"description": "why they may be settled, shown to the operator",
"type": "string"
}
},
"required": [
"ids",
"why"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,9 @@
{
"format": 1,
"seat": "mesh-dns-resolver",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0194, ADR 0223",
"delivers": "wildcard-resolution",
"replicated": true
}
@@ -0,0 +1,170 @@
{
"format": 1,
"seat": "mesh-enrolment",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0587",
"verbs": [
{
"name": "issue",
"description": "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.",
"input": {
"properties": {
"expires": {
"description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours",
"type": "string"
},
"node": {
"description": "the new machine's name in the mesh",
"type": "string"
},
"overlay_key": {
"description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it",
"type": "string"
},
"why": {
"description": "why it joins, shown to the operator with the ask",
"type": "string"
}
},
"required": [
"node",
"why",
"overlay_key"
],
"type": "object"
}
},
{
"name": "redeem",
"description": "Spend a token: asked by a joining node-engine dialled as enrol.\u003cnode\u003e alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.",
"input": {
"additionalProperties": false,
"properties": {
"node": {
"description": "the node the token was issued for",
"type": "string"
},
"overlay_key": {
"description": "the private network key's public half",
"type": "string"
},
"profile": {
"description": "what the machine is, as its record says it",
"type": "object"
},
"proof": {
"description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64",
"type": "string"
},
"public_key": {
"description": "the identity key's public half, base64",
"type": "string"
},
"sealing_key": {
"description": "the sealing key's public half, which the account is sealed to",
"type": "string"
},
"serving_key": {
"description": "the serving key's public half, which the certificate binds",
"type": "string"
},
"tunnel": {
"description": "a tunnel found on the machine: everything but its private key",
"type": "object"
}
},
"required": [
"node",
"public_key",
"sealing_key",
"proof"
],
"type": "object"
}
},
{
"name": "confirm",
"description": "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "revoke",
"description": "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.",
"input": {
"properties": {
"node": {
"description": "the node",
"type": "string"
},
"why": {
"description": "why, recorded with the hand act",
"type": "string"
}
},
"required": [
"node",
"why"
],
"type": "object"
}
},
{
"name": "tokens",
"description": "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.",
"input": {
"properties": {
"history": {
"description": "\"true\": the ended enrolments too",
"enum": [
"true",
"false"
],
"type": "string"
},
"node": {
"description": "one node",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "again",
"description": "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.",
"input": {
"properties": {
"node": {
"description": "the node whose ask is raised again",
"type": "string"
}
},
"required": [
"node"
],
"type": "object"
}
}
],
"events": [
{
"name": "enrolment-issued"
},
{
"name": "node-enrolled"
},
{
"name": "enrolment-refused"
},
{
"name": "enrolment-revoked"
},
{
"name": "enrolment-expired"
}
]
}
@@ -0,0 +1,97 @@
{
"format": 1,
"seat": "mesh-hand-acts",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0565",
"verbs": [
{
"name": "hand-act",
"description": "Record one act done by hand: what, why, its cause in a word or a condition's kind (the word a second act for the same reason uses) and the condition it addresses. Who did it is the caller the runtime proves, never said by the caller. Said as act-recorded.",
"input": {
"properties": {
"cause": {
"description": "the cause in a word, or a condition's kind",
"type": "string"
},
"condition": {
"description": "the key of the condition it addressed, if any (optional)",
"type": "string"
},
"verb": {
"description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own",
"type": "string"
},
"what": {
"description": "what was done, in a line",
"type": "string"
},
"why": {
"description": "why it had to be done by hand",
"type": "string"
}
},
"required": [
"what",
"why"
],
"type": "object"
}
},
{
"name": "hand-acts",
"description": "What was done by hand lately, over this log and the controller's: who, why and the cause of each, and the causes recorded twice within a fortnight (a healer is wanted).",
"input": {
"properties": {
"cause": {
"description": "only this cause",
"type": "string"
},
"days": {
"description": "how many days back (default 14)",
"type": "string"
},
"warrants": {
"description": "\"true\": every warrant recorded",
"enum": [
"true",
"false"
],
"type": "string"
},
"who": {
"description": "only acts whose who contains this",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "warranted",
"description": "Record what a module did on the operator's warrant: who chose, through which channel, with which proofs and which answer read from the router's own record of that module's ask, never from the caller; once per ask. Refused for an ask still open, ended without a choice, or not that module's.",
"input": {
"properties": {
"ask": {
"description": "its ask's id",
"type": "string"
},
"asker": {
"description": "the module that asked",
"type": "string"
}
},
"required": [
"asker",
"ask"
],
"type": "object"
}
}
],
"events": [
{
"name": "act-recorded"
}
]
}
@@ -0,0 +1,73 @@
{
"format": 1,
"seat": "mesh-healers",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0564",
"verbs": [
{
"name": "healers",
"description": "The healers: each registered response to one kind of condition, its repair, its budget and what happens when it is spent; every act taken lately with its outcome, and the mesh-wide brake.",
"input": {
"properties": {
"days": {
"description": "how many days of acts back (default 7)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "heal",
"description": "Take one healer's act for one open condition now, within its budget and the brake.",
"input": {
"properties": {
"key": {
"description": "the condition's key",
"type": "string"
},
"why": {
"description": "why now, kept with the act",
"type": "string"
}
},
"required": [
"key",
"why"
],
"type": "object"
}
},
{
"name": "drill",
"description": "Record a drill: something broken on purpose to see the mesh raise and clear it, with what it tests. A drill is never a repair: no healer is wanted for it however often it is run.",
"input": {
"properties": {
"condition": {
"description": "the key of the condition the drill is meant to raise, if any (optional)",
"type": "string"
},
"what": {
"description": "what was done on purpose, in a line",
"type": "string"
},
"why": {
"description": "what the drill tests",
"type": "string"
}
},
"required": [
"what",
"why"
],
"type": "object"
}
}
],
"events": [
{
"name": "healer-acted"
}
]
}
@@ -0,0 +1,137 @@
{
"format": 1,
"seat": "mesh-placement",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0567",
"verbs": [
{
"name": "placement",
"description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the proposal's changes from its placement-changed event.",
"optional": true,
"input": {
"properties": {
"history": {
"description": "\"true\": the changes in order, from the stream and the state's revisions",
"enum": [
"true",
"false"
],
"type": "string"
},
"module": {
"description": "one module: the nodes it is placed on",
"type": "string"
},
"node": {
"description": "one node",
"type": "string"
},
"seats": {
"description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"type": "object"
}
},
{
"name": "proposals",
"description": "Every open proposal of placement: its changes, the revision of each key it was laid over, its candidates, every node's answer, the differences computed and answered, its verdict and its ask. Narrowed to one by id.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "one proposal's id",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "propose",
"description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).",
"optional": true,
"input": {
"additionalProperties": false,
"properties": {
"changes": {
"description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)",
"items": {
"required": [
"op",
"node"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"warrant": {
"description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address",
"required": [
"node",
"address",
"ask",
"warrant"
],
"type": "object"
},
"why": {
"description": "why, shown to the operator with the proposal",
"type": "string"
}
},
"required": [
"why",
"changes"
],
"type": "object"
}
},
{
"name": "withdraw",
"description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the proposal's id",
"type": "string"
},
"why": {
"description": "why, kept in the event",
"type": "string"
}
},
"required": [
"id",
"why"
],
"type": "object"
}
},
{
"name": "again",
"description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.",
"optional": true,
"input": {
"properties": {
"id": {
"description": "the proposal's id",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,35 @@
{
"format": 1,
"seat": "mesh-provisioner",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0349",
"verbs": [
{
"name": "provisioning",
"description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.",
"optional": true,
"input": {
"properties": {
"collection": {
"description": "for collect: a collection",
"type": "string"
},
"module": {
"description": "a module: for collect, a contributor",
"type": "string"
},
"node": {
"description": "a node",
"type": "string"
},
"what": {
"description": "registry (when not given), needs, applied or collect",
"type": "string"
}
},
"type": "object"
}
}
]
}
@@ -0,0 +1,43 @@
{
"format": 1,
"seat": "mesh-self-check",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0564",
"verbs": [
{
"name": "doctor",
"description": "The self-check: the last run's verdict at once, each probe passed, failed, deferred (its facts not on the bus yet, named) or could not run, and how long ago. With run, a run now; with probe, one probe's verdict whole. Runs every five minutes on its own; each failure is a finding.",
"input": {
"properties": {
"probe": {
"description": "one probe's id (D6): its verdict alone, with every finding",
"type": "string"
},
"run": {
"description": "\"true\": run every probe now and answer the verdict",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"type": "object"
}
},
{
"name": "probes",
"description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.",
"input": {
"properties": {},
"type": "object"
}
}
],
"events": [
{
"name": "checked"
}
]
}
@@ -0,0 +1,45 @@
{
"format": 1,
"seat": "mesh-store",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0079",
"delivers": "postgres-database",
"verbs": [
{
"name": "databases",
"description": "Every database the store holds, with its on-disk size.",
"replaces": [
"psql -l"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "query",
"description": "One read-only statement against one database the store holds.",
"replaces": [
"psql"
],
"input": {
"properties": {
"database": {
"description": "the database to query",
"type": "string"
},
"sql": {
"description": "the read-only statement",
"type": "string"
}
},
"required": [
"database",
"sql"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,761 @@
{
"format": 1,
"seat": "mesh-vault",
"scope": "mesh",
"grain": "whole",
"decision": "novox/hq ADR 0161",
"delivers": "secret",
"verbs": [
{
"name": "get",
"description": "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.",
"optional": true,
"input": {
"properties": {
"name": {
"description": "the secret's name",
"type": "string"
},
"public_key": {
"description": "the asker's X25519 public key as SPKI PEM — never a private key",
"type": "string"
}
},
"required": [
"name",
"public_key"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "secret-ask",
"description": "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.",
"optional": true,
"input": {
"properties": {
"asked_by": {
"description": "who asked, as the bus named its caller",
"type": "string"
},
"call": {
"description": "the second step: the call the first step answered",
"type": "string"
},
"desk": {
"description": "the machine whose prompt asks",
"type": "string"
},
"module": {
"description": "the module",
"type": "string"
},
"name": {
"description": "the secret's name in the module",
"type": "string"
},
"node": {
"description": "the machine the secret is for",
"type": "string"
},
"outcome": {
"description": "instead of sealed: how the prompt ended",
"type": "string"
},
"sealed": {
"description": "the second step: the value sealed to the call's key — never the value",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "issued",
"description": "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.",
"optional": true,
"input": {
"properties": {
"call": {
"description": "the second step: the call the first step answered",
"type": "string"
},
"holders": {
"description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone",
"items": {
"properties": {
"module": {
"description": "the consuming module",
"type": "string"
},
"node": {
"description": "the holder's machine",
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"module": {
"description": "the provider's module",
"type": "string"
},
"provider": {
"description": "the provider's machine",
"type": "string"
},
"provision": {
"description": "the provision",
"type": "string"
},
"sealed": {
"description": "the second step: the value sealed to the call's key — never the value",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "rotate",
"description": "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.",
"optional": true,
"input": {
"properties": {
"by": {
"description": "who asked, when the controller carries the act",
"type": "string"
},
"module": {
"description": "the module",
"type": "string"
},
"name": {
"description": "the secret's name in the module",
"type": "string"
},
"node": {
"description": "the machine, for a module's own secret",
"type": "string"
},
"provider": {
"description": "the provider's machine",
"type": "string"
},
"provision": {
"description": "the provision",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "move-in",
"description": "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.",
"optional": true,
"input": {
"properties": {
"call": {
"description": "the second step: the call the first step answered",
"type": "string"
},
"consumer": {
"description": "the consumer's machine",
"type": "string"
},
"consumer_module": {
"description": "the consuming module",
"type": "string"
},
"generation": {
"description": "the controller's generation for the value",
"type": "string"
},
"local": {
"description": "the credential's local name, when it has one",
"type": "string"
},
"made_at": {
"description": "when it was made (RFC 3339)",
"type": "string"
},
"module": {
"description": "its module",
"type": "string"
},
"name": {
"description": "its name in the module",
"type": "string"
},
"node": {
"description": "a module's own secret: its machine",
"type": "string"
},
"origin": {
"description": "made, accepted or issued, as the controller's store says",
"type": "string"
},
"provider": {
"description": "a pair credential: the provider's machine",
"type": "string"
},
"provision": {
"description": "the provision",
"type": "string"
},
"read_back_to": {
"description": "the second step: a raw X25519 public key the read-back is sealed to",
"type": "string"
},
"sealed": {
"description": "the second step: the value sealed to the call's key — never the value",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "node-key",
"description": "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.",
"optional": true,
"input": {
"properties": {
"key": {
"description": "the machine's sealing key, a raw X25519 public key in base64",
"type": "string"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "own",
"description": "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.",
"optional": true,
"input": {
"properties": {
"credentials": {
"description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}",
"items": {
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "issue-account",
"description": "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.",
"optional": true,
"input": {
"properties": {
"user": {
"description": "the bus user, e.g. ace.mesh-build-go",
"type": "string"
}
},
"required": [
"user"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "sign",
"description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.",
"optional": true,
"input": {
"properties": {
"kind": {
"description": "node-certificate or enrolment",
"type": "string"
},
"node": {
"description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols",
"type": "string"
},
"reply": {
"description": "the redeem answer's bytes in base64, for an enrolment",
"type": "string"
},
"serving_key": {
"description": "from mesh-enrolment: the serving key's public half the enrolling node presented",
"type": "string"
}
},
"required": [
"kind"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half",
"properties": {
"kind": {
"type": "string",
"description": "node-certificate or enrolment, as asked"
},
"node": {
"type": "string",
"description": "the machine, for a node certificate"
},
"certificate": {
"type": "string",
"description": "the node certificate as PEM, for a node certificate"
},
"authority": {
"type": "string",
"description": "the authority's certificate that signed it, as PEM"
},
"signature": {
"type": "string",
"description": "the signature in base64, for an enrolment"
},
"signer": {
"type": "string",
"description": "the signer's fingerprint"
},
"digest": {
"type": "string",
"description": "sha256:<hex> of the bytes signed"
}
},
"required": [
"kind",
"signer",
"digest"
]
}
},
{
"name": "enrolment-token",
"description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.<node>. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).",
"optional": true,
"input": {
"properties": {
"drop": {
"description": "\"true\": drop the node's token and its hash",
"enum": [
"true",
"false"
],
"type": "string"
},
"expires": {
"description": "when the token expires (RFC 3339), as the enrolment's key says",
"type": "string"
},
"node": {
"description": "the node the token is for",
"type": "string"
},
"sealed": {
"description": "\"true\": answer the token sealed to the operator key",
"enum": [
"true",
"false"
],
"type": "string"
},
"tunnel": {
"additionalProperties": false,
"description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range",
"properties": {
"address": {
"description": "the joining node's tunnel address",
"type": "string"
},
"hub_endpoint": {
"description": "the hub's endpoint, host:port",
"type": "string"
},
"hub_key": {
"description": "the hub's tunnel public key",
"type": "string"
},
"key": {
"description": "the joining machine's own tunnel public key, as given at issue",
"type": "string"
},
"range": {
"description": "the private network's range, a prefix",
"type": "string"
}
},
"required": [
"address",
"hub_endpoint",
"hub_key",
"key",
"range"
],
"type": "object"
}
},
"required": [
"node"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash",
"properties": {
"node": {
"type": "string",
"description": "the node the token is for"
},
"broker": {
"type": "string",
"description": "the bus's address the machine dials"
},
"fingerprint": {
"type": "string",
"description": "the fingerprint of the bus's certificate"
},
"signer": {
"type": "string",
"description": "the vault's public signing key in base64"
},
"sealed": {
"type": "string",
"description": "the token sealed to the operator key, when asked sealed, relayed as it is"
},
"dropped": {
"type": "boolean",
"description": "a drop left neither the token nor its hash"
}
},
"required": [
"node"
]
}
},
{
"name": "node-account",
"description": "Mint a joining node's bus account node.<node>, sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.<node>. Asked by mesh-enrolment alone. Never the value in the clear.",
"optional": true,
"input": {
"properties": {
"keys_digest": {
"description": "the digest of every public key the node presented (delivery.KeysDigest)",
"type": "string"
},
"node": {
"description": "the joining node",
"type": "string"
},
"retire": {
"description": "\"true\": retire the account minted for this node",
"enum": [
"true",
"false"
],
"type": "string"
},
"sealing_key": {
"description": "the sealing key's public half the node presented, which the account is sealed to",
"type": "string"
}
},
"required": [
"node"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account",
"properties": {
"node": {
"type": "string",
"description": "the joining node"
},
"login": {
"type": "string",
"description": "the login minted"
},
"account": {
"type": "string",
"description": "the account sealed to the node's sealing key, in base64"
},
"retired": {
"type": "boolean",
"description": "a retire left no account"
}
},
"required": [
"node"
]
}
},
{
"name": "rotate-node",
"description": "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.",
"optional": true,
"input": {
"properties": {
"act": {
"description": "the id of the hand act recorded for it in mesh-hand-acts",
"type": "string"
},
"node": {
"description": "the node whose account rotates",
"type": "string"
},
"why": {
"description": "why, in a sentence",
"type": "string"
}
},
"required": [
"node",
"why",
"act"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "node-account-confirm",
"description": "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.",
"optional": true,
"input": {
"properties": {},
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "abandon",
"description": "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.",
"optional": true,
"input": {
"properties": {
"node": {
"description": "the node whose rotation is abandoned",
"type": "string"
}
},
"required": [
"node"
],
"type": "object"
},
"answer": {
"type": "object",
"description": "the holder's answer"
}
},
{
"name": "retire",
"optional": true,
"description": "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.",
"callers": "the holder of the seat mesh-config alone",
"input": {
"type": "object",
"properties": {
"node": {
"type": "string",
"description": "the node"
},
"module": {
"type": "string",
"description": "the module"
},
"name": {
"type": "string",
"description": "the own secret's name"
},
"warrant": {
"type": "string",
"description": "the operator's warrant for this node, module and name: the hand act's id"
},
"ask": {
"type": "string",
"description": "the id of the operator's ask the warrant answers"
}
},
"required": [
"node",
"module",
"name",
"warrant",
"ask"
]
},
"answer": {
"type": "object",
"properties": {
"retired": {
"type": "boolean",
"description": "the secret is gone"
}
},
"required": [
"retired"
]
}
}
],
"description": "The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2).",
"holder-events": [
{
"name": "provisioned",
"subject": "mesh.mod.<holder>.event.provisioned"
},
{
"name": "rotated",
"subject": "mesh.mod.<holder>.event.rotated"
},
{
"name": "secret-changed",
"subject": "mesh.mod.<holder>.event.secret-changed"
},
{
"name": "account-hashed",
"subject": "mesh.mod.<holder>.event.account-hashed"
},
{
"name": "own-replaced",
"subject": "mesh.mod.<holder>.event.own-replaced"
},
{
"name": "account-issued",
"subject": "mesh.mod.<holder>.event.account-issued"
},
{
"name": "signed",
"subject": "mesh.mod.<holder>.event.signed",
"body": {
"type": "object",
"description": "who asked, the kind, the node and the digest of what was signed; never a key",
"properties": {
"caller": {
"type": "string",
"description": "the caller the vault proved"
},
"kind": {
"type": "string",
"description": "node-certificate or enrolment"
},
"node": {
"type": "string",
"description": "the machine, for a node certificate"
},
"digest": {
"type": "string",
"description": "sha256:<hex> of the bytes signed"
},
"signer": {
"type": "string",
"description": "the signer's fingerprint"
},
"at": {
"type": "string",
"description": "when it was signed, RFC 3339"
}
},
"required": [
"caller",
"kind",
"digest",
"signer",
"at"
]
}
},
{
"name": "rotation-begun",
"subject": "mesh.mod.<holder>.event.rotation-begun"
},
{
"name": "node-account-ready",
"subject": "mesh.mod.<holder>.event.node-account-ready"
},
{
"name": "rotation-done",
"subject": "mesh.mod.<holder>.event.rotation-done"
},
{
"name": "own-retired",
"subject": "mesh.mod.<holder>.event.own-retired",
"body": {
"type": "object",
"properties": {
"node": {
"type": "string",
"description": "the node"
},
"module": {
"type": "string",
"description": "the module"
},
"name": {
"type": "string",
"description": "the secret's name"
},
"ask": {
"type": "string",
"description": "the ask's id"
},
"caller": {
"type": "string",
"description": "the caller the vault proved"
}
},
"required": [
"node",
"module",
"name",
"caller"
]
}
}
]
}
@@ -0,0 +1,51 @@
{
"format": 1,
"seat": "mesh-wants",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0565",
"verbs": [
{
"name": "upgrade",
"description": "Whether each module's new builds wait for a person: its policy (roll, together or record) and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.",
"input": {
"properties": {
"compare": {
"description": "\"true\": each node's own composition, by its digest, and the assigner's findings",
"enum": [
"true",
"false"
],
"type": "string"
},
"module": {
"description": "one module",
"type": "string"
},
"policy": {
"description": "with module: roll, together, record, or default to take a person's choice back",
"type": "string"
},
"why": {
"description": "with policy: why, required and kept",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "bus",
"description": "What a bus upgrade would do: the fingerprint of the bus module each node runs against the indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: the step itself is the put on the operator's warrant.",
"input": {
"properties": {},
"type": "object"
}
}
],
"events": [
{
"name": "policy-changed"
}
]
}
@@ -0,0 +1,93 @@
{
"format": 1,
"seat": "mesh-watchdogs",
"scope": "mesh",
"grain": "seat-list",
"decision": "novox/hq ADR 0564",
"verbs": [
{
"name": "signals",
"description": "Every watchdog of the signals table the watchman keeps, each a key that expires unless its signal renews it: its row, what renews it, its bound, the age of its newest signal, and the finding its expiry wrote.",
"input": {
"properties": {
"row": {
"description": "one row, e.g. S1",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "stuck",
"description": "Every want, build or step the watchman says stopped moving past its bound, with since when and the words of its finding.",
"input": {
"properties": {
"machine": {
"description": "only those on this machine",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "presence",
"description": "Every machine and tool runner the watchman sees connected, with its last word, and those it says silent; a machine that said it sleeps is shown as sleeping, not silent.",
"input": {
"properties": {
"machine": {
"description": "one machine",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "dead-letters",
"description": "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one whole. With deliver or drop, a hand act with why.",
"input": {
"properties": {
"cause": {
"description": "with deliver or drop: the cause in a word (dead-letter when absent)",
"type": "string"
},
"consumer": {
"description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on",
"type": "string"
},
"deliver": {
"description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)",
"type": "string"
},
"drop": {
"description": "a dead letter's id: drop it for good (needs why)",
"type": "string"
},
"id": {
"description": "a dead letter's id, as the list gives it: that one whole, with its message",
"type": "string"
},
"limit": {
"description": "how many to list, newest first (default 50); only when listing",
"type": "string"
},
"why": {
"description": "with deliver or drop: why, required and recorded",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "resync",
"description": "Rebuild presence from the bus's connections, and the pulses and findings from the keys the watchman watches, and take every dead-letter notice waiting: the recovery of a watchman that missed a change.",
"input": {
"properties": {},
"type": "object"
}
}
]
}
@@ -0,0 +1,80 @@
{
"format": 1,
"seat": "node-backup",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0214",
"verbs": [
{
"name": "backed-up",
"description": "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.",
"replaces": [
"restic snapshots"
],
"input": {
"properties": {
"module": {
"description": "one module (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "now",
"description": "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.",
"replaces": [
"restic backup"
],
"input": {
"properties": {
"module": {
"description": "one module (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "restore",
"description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as \u003cpath\u003e.restored-\u003cdate\u003e. Swapping it in is a person's act. Lists the restore points when none is named.",
"replaces": [
"restic restore"
],
"input": {
"properties": {
"module": {
"description": "the module",
"type": "string"
},
"path": {
"description": "one of the module's directories (all of them when omitted)",
"type": "string"
},
"snapshot": {
"description": "the restore point (from `backed-up`; the newest when omitted)",
"type": "string"
}
},
"required": [
"module"
],
"type": "object"
}
}
],
"receives": [
{
"kind": "backup",
"comment": "#",
"dirs": true
},
{
"kind": "data",
"comment": "#",
"dirs": true
}
]
}
@@ -0,0 +1,75 @@
{
"format": 1,
"seat": "node-bar",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"receives": [
{
"kind": "block",
"comment": "#",
"offered": true,
"shape": {
"places": [
{
"field": "bar",
"values": [
"bottom",
"top"
]
},
{
"field": "place",
"values": [
"resources",
"status"
]
}
],
"order": "order",
"examples": [
{
"bar": "bottom",
"place": "status",
"shows": "battery"
},
{
"bar": "bottom",
"options": {
"device": "BAT0"
},
"place": "status",
"shows": "battery"
},
{
"bar": "bottom",
"options": {
"command": "echo \"a line\"",
"interval": 5
},
"place": "status",
"shows": "command"
},
{
"bar": "bottom",
"options": {
"state": "power.draw"
},
"place": "status",
"shows": "state"
},
{
"bar": "bottom",
"options": {
"key": "a-machine",
"state": "power.draw"
},
"place": "status",
"shows": "state"
}
],
"text": true
}
}
]
}
@@ -0,0 +1,46 @@
{
"format": 1,
"seat": "node-build-go",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"verbs": [
{
"name": "current",
"description": "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "pause",
"description": "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "resume",
"description": "Take Go asks again on this node.",
"input": {
"properties": {},
"type": "object"
}
}
],
"accepts": [
{
"name": "delivery"
},
{
"name": "check"
},
{
"name": "recheck"
}
]
}
@@ -0,0 +1,46 @@
{
"format": 1,
"seat": "node-build-image",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"verbs": [
{
"name": "current",
"description": "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "pause",
"description": "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "resume",
"description": "Take image asks again on this node.",
"input": {
"properties": {},
"type": "object"
}
}
],
"accepts": [
{
"name": "delivery"
},
{
"name": "check"
},
{
"name": "recheck"
}
]
}
@@ -0,0 +1,46 @@
{
"format": 1,
"seat": "node-build-node",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
"pulled-by-kind": true,
"yields": true,
"verbs": [
{
"name": "current",
"description": "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "pause",
"description": "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "resume",
"description": "Take Node asks again on this node.",
"input": {
"properties": {},
"type": "object"
}
}
],
"accepts": [
{
"name": "delivery"
},
{
"name": "check"
},
{
"name": "recheck"
}
]
}
@@ -0,0 +1,38 @@
{
"format": 1,
"seat": "node-clipboard",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "history",
"description": "What the clipboard held lately, newest first.",
"input": {
"properties": {
"limit": {
"description": "how many (optional, default 20)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "copy",
"description": "Put text on the operator's clipboard.",
"input": {
"properties": {
"text": {
"description": "the text",
"type": "string"
}
},
"required": [
"text"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-compositor",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208"
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-container-runtime",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0166, ADR 0207"
}
@@ -0,0 +1,42 @@
{
"format": 1,
"seat": "node-display-server",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "displays",
"description": "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "layout",
"description": "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.",
"input": {
"properties": {
"action": {
"description": "list, save or apply",
"enum": [
"list",
"save",
"apply"
],
"type": "string"
},
"name": {
"description": "the profile to save or apply (save and apply only)",
"type": "string"
}
},
"required": [
"action"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,44 @@
{
"format": 1,
"seat": "node-display-session",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "reload",
"description": "Reload the session's configuration in place, keeping its windows.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "workspaces",
"description": "The session's workspaces: each one's name, output, and whether it is visible or focused.",
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "windows",
"description": "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.",
"input": {
"properties": {
"workspace": {
"description": "one workspace (optional)",
"type": "string"
}
},
"type": "object"
}
}
],
"receives": [
{
"kind": "config",
"comment": "#"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-environment",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0203"
}
@@ -0,0 +1,589 @@
{
"format": 1,
"seat": "node-files",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0394",
"verbs": [
{
"name": "stat",
"description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"stat",
"ls -ld",
"namei -l",
"findmnt -T"
],
"input": {
"properties": {
"follow": {
"description": "\"true\": also say where the path leads once every link is followed",
"enum": [
"true",
"false"
],
"type": "string"
},
"paths": {
"description": "one absolute path, or several separated by line breaks",
"type": "string"
}
},
"required": [
"paths"
],
"type": "object"
}
},
{
"name": "readlink",
"description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"readlink",
"readlink -f",
"realpath"
],
"input": {
"properties": {
"path": {
"description": "an absolute path",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "list",
"description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"ls",
"ls -la"
],
"input": {
"properties": {
"path": {
"description": "an absolute path to a directory",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "read",
"description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"cat",
"head",
"tail",
"less"
],
"input": {
"properties": {
"bytes": {
"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)",
"type": "string"
},
"from": {
"description": "the first line to answer, counted from 1 (optional)",
"type": "string"
},
"lines": {
"description": "how many lines to answer (optional)",
"type": "string"
},
"path": {
"description": "an absolute path to a file",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "search",
"description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"grep",
"grep -r",
"grep -F"
],
"input": {
"properties": {
"case": {
"description": "\"false\": match whatever the case (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"depth": {
"description": "how many directories deep, up to 8 (optional)",
"type": "string"
},
"matches": {
"description": "the most matches to answer, up to 500 (optional)",
"type": "string"
},
"path": {
"description": "an absolute path to a file or a directory",
"type": "string"
},
"text": {
"description": "the fixed string to find",
"type": "string"
}
},
"required": [
"text",
"path"
],
"type": "object"
}
},
{
"name": "account",
"description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"replaces": [
"getent passwd",
"getent group",
"id",
"groups"
],
"input": {
"properties": {
"group": {
"description": "a group's name or number (optional)",
"type": "string"
},
"name": {
"description": "an account's name or number (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "write",
"description": "Write one text file whole, replacing what it holds; the old content goes to the node's trash. 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.",
"replaces": [
"tee",
"\u003e"
],
"input": {
"properties": {
"content": {
"description": "the text to write",
"type": "string"
},
"mode": {
"description": "the file's mode, such as 0644 (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
}
},
"required": [
"path",
"content",
"reason"
],
"type": "object"
}
},
{
"name": "copy",
"description": "Copy a file or directory to a path on this node. A target on another node (\u003cnode\u003e:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. 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.",
"replaces": [
"cp",
"cp -a",
"ssh \u003cnode\u003e cp"
],
"input": {
"properties": {
"from": {
"description": "an absolute path",
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"to": {
"description": "an absolute path on this node",
"type": "string"
}
},
"required": [
"from",
"to",
"reason"
],
"type": "object"
}
},
{
"name": "move",
"description": "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. 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.",
"replaces": [
"mv"
],
"input": {
"properties": {
"from": {
"description": "an absolute path",
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"to": {
"description": "an absolute path",
"type": "string"
}
},
"required": [
"from",
"to",
"reason"
],
"type": "object"
}
},
{
"name": "delete",
"description": "Delete a file or directory into the node's trash, where restore can bring it back. 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.",
"replaces": [
"rm",
"rm -r"
],
"input": {
"properties": {
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"recursive": {
"description": "\"true\": delete a directory with what it holds",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"required": [
"path",
"reason"
],
"type": "object"
}
},
{
"name": "mkdir",
"description": "Make a directory. 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.",
"replaces": [
"mkdir",
"mkdir -p"
],
"input": {
"properties": {
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"parents": {
"description": "\"true\": make the missing parents too",
"enum": [
"true",
"false"
],
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
}
},
"required": [
"path",
"reason"
],
"type": "object"
}
},
{
"name": "chmod",
"description": "Change the mode of a file or directory. 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.",
"replaces": [
"chmod"
],
"input": {
"properties": {
"mode": {
"description": "the mode, such as 0644 or u+x",
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"recursive": {
"description": "\"true\": also everything under a directory",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"required": [
"path",
"mode",
"reason"
],
"type": "object"
}
},
{
"name": "chown",
"description": "Change the owner and group of a file or directory. 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.",
"replaces": [
"chown",
"chgrp"
],
"input": {
"properties": {
"group": {
"description": "a group's name or number (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"owner": {
"description": "an account's name or number",
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"recursive": {
"description": "\"true\": also everything under a directory",
"enum": [
"true",
"false"
],
"type": "string"
}
},
"required": [
"path",
"owner",
"reason"
],
"type": "object"
}
},
{
"name": "upload",
"description": "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.",
"input": {
"properties": {
"bucket": {
"description": "the bucket the file goes into",
"type": "string"
},
"key": {
"description": "the object's key (optional; the file's SHA-256 when not given)",
"type": "string"
},
"path": {
"description": "an absolute path to a file",
"type": "string"
}
},
"required": [
"path",
"bucket"
],
"type": "object"
}
},
{
"name": "download",
"description": "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.",
"input": {
"properties": {
"bucket": {
"description": "the bucket",
"type": "string"
},
"key": {
"description": "the object's key",
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"type": "string"
},
"path": {
"description": "an absolute path",
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
},
"sha256": {
"description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)",
"type": "string"
}
},
"required": [
"bucket",
"key",
"path",
"reason"
],
"type": "object"
}
},
{
"name": "trash",
"description": "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.",
"replaces": [
"ls ~/.local/share/Trash"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"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.",
"replaces": [
"trash-restore"
],
"input": {
"properties": {
"id": {
"description": "an id from trash",
"type": "string"
},
"override": {
"description": "a reason, to change a path a module owns anyway (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"reason": {
"description": "why the file is changed, for the audit trail",
"type": "string"
}
},
"required": [
"id",
"reason"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,73 @@
{
"format": 1,
"seat": "node-hostname",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0199, ADR 0223",
"verbs": [
{
"name": "entries",
"description": "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.",
"replaces": [
"cat /etc/hosts",
"getent hosts"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "add",
"description": "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.",
"replaces": [
"edit /etc/hosts",
"HOSTALIASES"
],
"input": {
"properties": {
"address": {
"description": "the IPv4 or IPv6 address",
"type": "string"
},
"names": {
"description": "the names for it, separated by spaces",
"type": "string"
}
},
"required": [
"address",
"names"
],
"type": "object"
}
},
{
"name": "remove",
"description": "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.",
"replaces": [
"sed -i /etc/hosts"
],
"input": {
"properties": {
"name": {
"description": "a host name, or an address to remove every line of",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
}
],
"paths": [
{
"path": "/etc/hosts"
},
{
"path": "/etc/hostname"
}
]
}
@@ -0,0 +1,13 @@
{
"format": 1,
"seat": "node-hotkeys",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0212",
"receives": [
{
"kind": "trigger",
"comment": "#"
}
]
}
@@ -0,0 +1,89 @@
{
"format": 1,
"seat": "node-intrusion-prevention",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0121",
"verbs": [
{
"name": "status",
"description": "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.",
"replaces": [
"fail2ban-client status"
],
"input": {
"properties": {
"jail": {
"description": "one jail (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "banned",
"description": "Every address banned on this machine right now, with the jail that holds it and when the ban ends.",
"replaces": [
"fail2ban-client banned"
],
"input": {
"properties": {
"jail": {
"description": "one jail (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "ban",
"description": "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.",
"replaces": [
"fail2ban-client set banip"
],
"input": {
"properties": {
"ip": {
"description": "the address",
"type": "string"
},
"jail": {
"description": "the jail to hold it",
"type": "string"
}
},
"required": [
"ip",
"jail"
],
"type": "object"
}
},
{
"name": "unban",
"description": "Let one address go, from one jail or from every jail when none is named.",
"replaces": [
"fail2ban-client unban",
"fail2ban-client set unbanip"
],
"input": {
"properties": {
"ip": {
"description": "the address",
"type": "string"
},
"jail": {
"description": "one jail (optional)",
"type": "string"
}
},
"required": [
"ip"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,68 @@
{
"format": 1,
"seat": "node-launcher",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "menu",
"description": "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.",
"input": {
"properties": {
"choices": {
"description": "the lines to choose between, in order",
"items": {
"type": "string"
},
"type": "array"
},
"prompt": {
"description": "what the menu asks (optional)",
"type": "string"
}
},
"required": [
"choices"
],
"type": "object"
}
},
{
"name": "secret",
"description": "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.",
"optional": true,
"input": {
"properties": {
"module": {
"description": "the module whose own secret is asked for",
"type": "string"
},
"node": {
"description": "the machine the module runs on",
"type": "string"
},
"seal_to": {
"description": "the asker's public sealing key: the answer is sealed to it",
"type": "string"
},
"secret": {
"description": "the own secret's name",
"type": "string"
},
"timeout_seconds": {
"description": "give up after this long (optional)",
"type": "string"
}
},
"required": [
"module",
"secret",
"node",
"seal_to"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,17 @@
{
"format": 1,
"seat": "node-lock-screen",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "lock",
"description": "Lock the operator's session now.",
"input": {
"properties": {},
"type": "object"
}
}
]
}
@@ -0,0 +1,17 @@
{
"format": 1,
"seat": "node-login-manager",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "sessions",
"description": "The sessions the login manager offers on this machine, and which one the operator account starts by default.",
"input": {
"properties": {},
"type": "object"
}
}
]
}
@@ -0,0 +1,30 @@
{
"format": 1,
"seat": "node-login-shell",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0204",
"verbs": [
{
"name": "execute",
"description": "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.",
"optional": true,
"input": {
"properties": {
"command": {
"description": "the command line, as you would type it",
"type": "string"
},
"timeout_seconds": {
"description": "give up after this long, at most 25 (default 20)",
"type": "string"
}
},
"required": [
"command"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-message-bus",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0215"
}
@@ -0,0 +1,111 @@
{
"format": 1,
"seat": "node-mounts",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0263",
"verbs": [
{
"name": "list",
"description": "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.",
"replaces": [
"cat /etc/fstab",
"findmnt",
"systemctl list-units --type=mount"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "test",
"description": "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.",
"replaces": [
"showmount -e",
"ping"
],
"input": {
"properties": {
"name": {
"description": "the share or source, by its name",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
},
{
"name": "mount",
"description": "Mount one share or occasional source now, rather than at its first access.",
"replaces": [
"mount"
],
"input": {
"properties": {
"name": {
"description": "the share or source, by its name",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
},
{
"name": "unmount",
"description": "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.",
"replaces": [
"umount"
],
"input": {
"properties": {
"name": {
"description": "the share or source, by its name",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
},
{
"name": "adopt",
"description": "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.",
"replaces": [
"sed -i /etc/fstab"
],
"input": {
"properties": {
"confirm": {
"description": "\"true\": change it (needs why); anything else is a dry run",
"enum": [
"true",
"false"
],
"type": "string"
},
"mountpoint": {
"description": "the mount point whose fstab line is taken over",
"type": "string"
},
"why": {
"description": "why, for the record",
"type": "string"
}
},
"required": [
"mountpoint"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,99 @@
{
"format": 1,
"seat": "node-nfs-server",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0263",
"verbs": [
{
"name": "exports",
"description": "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.",
"replaces": [
"exportfs -v",
"cat /etc/exports",
"zfs get sharenfs"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "clients",
"description": "Which machines have mounted which share now, as the NFS server knows its clients.",
"replaces": [
"ss -tn sport = :2049",
"cat /proc/fs/nfsd/clients/*/info"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "test",
"description": "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.",
"replaces": [
"showmount -e"
],
"input": {
"properties": {
"address": {
"description": "a node's private address, to ask whether the share is exported to it (optional)",
"type": "string"
},
"share": {
"description": "the share, by its name",
"type": "string"
}
},
"required": [
"share"
],
"type": "object"
}
},
{
"name": "reload",
"description": "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.",
"replaces": [
"exportfs -ra"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "adopt",
"description": "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.",
"replaces": [
"zfs set sharenfs=off"
],
"input": {
"properties": {
"confirm": {
"description": "\"true\": change it (needs why); anything else is a dry run",
"enum": [
"true",
"false"
],
"type": "string"
},
"path": {
"description": "the shared path whose hand-made export is taken over",
"type": "string"
},
"why": {
"description": "why, for the record",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,51 @@
{
"format": 1,
"seat": "node-notifier",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "send",
"description": "Show the operator a notification.",
"input": {
"properties": {
"body": {
"description": "its text (optional)",
"type": "string"
},
"title": {
"description": "the notification's summary",
"type": "string"
},
"urgency": {
"description": "low, normal (the default) or critical",
"enum": [
"low",
"normal",
"critical"
],
"type": "string"
}
},
"required": [
"title"
],
"type": "object"
}
},
{
"name": "history",
"description": "The notifications shown lately, newest first.",
"input": {
"properties": {
"limit": {
"description": "how many (optional, default 20)",
"type": "string"
}
},
"type": "object"
}
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-package-manager",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0207"
}
@@ -0,0 +1,62 @@
{
"format": 1,
"seat": "node-packet-filter",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0121",
"verbs": [
{
"name": "rules",
"description": "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.",
"replaces": [
"nft list ruleset",
"iptables -L",
"iptables-save"
],
"input": {
"properties": {
"chain": {
"description": "one chain of that table (optional)",
"type": "string"
},
"table": {
"description": "one nftables table, as `family name` (optional)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "reload",
"description": "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.",
"replaces": [
"nft -f"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "remove",
"description": "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.",
"replaces": [
"nft delete",
"iptables -X"
],
"input": {
"properties": {
"where": {
"description": "the rule set, as `node show` lists it",
"type": "string"
}
},
"required": [
"where"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-power",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0211"
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-provisioner",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0349"
}
@@ -0,0 +1,179 @@
{
"format": 1,
"seat": "node-reads",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0310",
"verbs": [
{
"name": "stat",
"description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.",
"replaces": [
"stat",
"ls -ld",
"namei -l",
"findmnt -T"
],
"input": {
"properties": {
"follow": {
"description": "\"true\": also say where the path leads once every link is followed",
"enum": [
"true",
"false"
],
"type": "string"
},
"paths": {
"description": "one absolute path, or several separated by line breaks",
"type": "string"
}
},
"required": [
"paths"
],
"type": "object"
}
},
{
"name": "readlink",
"description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.",
"replaces": [
"readlink",
"readlink -f",
"realpath"
],
"input": {
"properties": {
"path": {
"description": "an absolute path",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "list",
"description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.",
"replaces": [
"ls",
"ls -la"
],
"input": {
"properties": {
"path": {
"description": "an absolute path to a directory",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "read",
"description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.",
"replaces": [
"cat",
"head",
"tail",
"less"
],
"input": {
"properties": {
"bytes": {
"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)",
"type": "string"
},
"from": {
"description": "the first line to answer, counted from 1 (optional)",
"type": "string"
},
"lines": {
"description": "how many lines to answer (optional)",
"type": "string"
},
"path": {
"description": "an absolute path to a file",
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
{
"name": "search",
"description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.",
"replaces": [
"grep",
"grep -r",
"grep -F"
],
"input": {
"properties": {
"case": {
"description": "\"false\": match whatever the case (optional)",
"enum": [
"true",
"false"
],
"type": "string"
},
"depth": {
"description": "how many directories deep, up to 8 (optional)",
"type": "string"
},
"matches": {
"description": "the most matches to answer, up to 500 (optional)",
"type": "string"
},
"path": {
"description": "an absolute path to a file or a directory",
"type": "string"
},
"text": {
"description": "the fixed string to find",
"type": "string"
}
},
"required": [
"text",
"path"
],
"type": "object"
}
},
{
"name": "account",
"description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.",
"replaces": [
"getent passwd",
"getent group",
"id",
"groups"
],
"input": {
"properties": {
"group": {
"description": "a group's name or number (optional)",
"type": "string"
},
"name": {
"description": "an account's name or number (optional)",
"type": "string"
}
},
"type": "object"
}
}
]
}
@@ -0,0 +1,77 @@
{
"format": 1,
"seat": "node-resolver",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0247",
"verbs": [
{
"name": "routes",
"description": "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.",
"replaces": [
"resolvectl status",
"resolvectl dns",
"resolvectl domain"
],
"input": {
"properties": {},
"type": "object"
}
},
{
"name": "route",
"description": "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.",
"replaces": [
"resolvectl dns",
"resolvectl domain",
"resolvectl default-route"
],
"input": {
"properties": {
"domains": {
"description": "the domains to route there, separated by spaces or commas",
"type": "string"
},
"link": {
"description": "the network link the servers are reached over, by name (a VPN's tunnel interface)",
"type": "string"
},
"servers": {
"description": "the servers' addresses, separated by spaces or commas",
"type": "string"
}
},
"required": [
"link",
"domains",
"servers"
],
"type": "object"
}
},
{
"name": "unroute",
"description": "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.",
"replaces": [
"resolvectl revert"
],
"input": {
"properties": {
"link": {
"description": "the network link, by name",
"type": "string"
}
},
"required": [
"link"
],
"type": "object"
}
}
],
"paths": [
{
"path": "/etc/resolv.conf"
}
]
}
@@ -0,0 +1,7 @@
{
"format": 1,
"seat": "node-secret-service",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208"
}
@@ -0,0 +1,305 @@
{
"format": 1,
"seat": "node-service-manager",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0177",
"verbs": [
{
"name": "units",
"description": "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
"replaces": [
"systemctl list-units"
],
"input": {
"properties": {
"pattern": {
"description": "a glob the unit's name must match (optional)",
"type": "string"
},
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "status",
"description": "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.",
"replaces": [
"systemctl status",
"systemctl is-active"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "start",
"description": "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.",
"replaces": [
"systemctl start"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "stop",
"description": "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.",
"replaces": [
"systemctl stop"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "restart",
"description": "Restart one unit.",
"replaces": [
"systemctl restart"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "enable",
"description": "Make one unit start at boot (or at the account's login, in user scope).",
"replaces": [
"systemctl enable"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "disable",
"description": "Stop one unit starting at boot (or at login, in user scope).",
"replaces": [
"systemctl disable"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "journal",
"description": "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: \u003cwhat it was\u003e].",
"replaces": [
"journalctl"
],
"input": {
"properties": {
"lines": {
"description": "how many lines from the end of what matches (default 100, at most 2000)",
"type": "string"
},
"match": {
"description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)",
"type": "string"
},
"priority": {
"description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)",
"type": "string"
},
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"since": {
"description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
},
"until": {
"description": "the window's end, in the same forms (optional; now when absent)",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "failed",
"description": "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.",
"optional": true,
"replaces": [
"systemctl --failed"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" or \"user\": only that manager (both when absent)",
"type": "string"
}
},
"type": "object"
}
},
{
"name": "reset-failed",
"description": "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.",
"replaces": [
"systemctl reset-failed"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "wanted-by",
"description": "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.",
"replaces": [
"systemctl list-dependencies --reverse",
"systemctl show -p WantedBy",
"ls ~/.config/systemd/user/*.wants"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
}
},
"required": [
"unit"
],
"type": "object"
}
},
{
"name": "unlink-dangling",
"description": "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.",
"replaces": [
"rm ~/.config/systemd/user/*.wants/\u003cunit\u003e",
"unlink /etc/systemd/system/*.wants/\u003cunit\u003e"
],
"input": {
"properties": {
"scope": {
"description": "\"system\" (the default) or \"user\": the operator account's own manager",
"type": "string"
},
"unit": {
"description": "the unit's name, as the service manager knows it",
"type": "string"
},
"why": {
"description": "the person's reason for removing the links, kept in the record",
"type": "string"
}
},
"required": [
"unit",
"why"
],
"type": "object"
}
}
]
}
@@ -0,0 +1,26 @@
{
"format": 1,
"seat": "node-terminal-emulator",
"scope": "node",
"grain": "seat-list",
"decision": "novox/hq ADR 0208",
"verbs": [
{
"name": "open",
"description": "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.",
"input": {
"properties": {
"command": {
"description": "what to run in it (optional; the login shell when absent)",
"type": "string"
},
"directory": {
"description": "where it starts (optional; the account's home when absent)",
"type": "string"
}
},
"type": "object"
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More