Remove the controller's seat, the lease verbs and placement's adopt and hand-back; refuse any manifest naming the controller
The controller ended by its unassignment (ADR 0590), so its seat, the grants seat's lease write and hand-back and placement's take-over from it serve nothing. The dependence check becomes strict (clause 5): a read of the lease, a claim, a require or the module itself is refused like any other declaration. held and artifacts-gone are the artifact sweep seat's (ADR 0571 §2). Issue 1126 B1, B2. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -1102,10 +1102,7 @@ func healthChecks(t *testing.T) {
|
||||
// It hears its watchdogs' words on core subjects (Limits 3, 11).
|
||||
for _, subject := range []string{"mesh.mod.power.event.sleeping", "mesh.control.g14.tools-alive",
|
||||
"mesh.seat.mesh-self-check.event.checked", "mesh.mod.keycloak.event.provisioner.failing",
|
||||
"mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak",
|
||||
// ADR 0582 clauses 4, 6 (issue 774, 774/S22): the lease write's events, and ASSIGNMENTS deleted.
|
||||
"mesh.seat.mesh-grants.event.lease-altered", "mesh.seat.mesh-grants.event.removal-failed",
|
||||
"mesh.seat.mesh-grants.event.lease-handed", "$JS.EVENT.ADVISORY.STREAM.DELETED.ASSIGNMENTS"} {
|
||||
"mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak"} {
|
||||
if !MaySubscribe(perms, subject) {
|
||||
t.Errorf("the watchman may not hear %s", subject)
|
||||
}
|
||||
|
||||
@@ -129,12 +129,6 @@ func WatchmanHears() []string {
|
||||
"mesh.mod.nats.event.bus.connection-closed",
|
||||
ProvAppliedSubjects,
|
||||
deadLetterPrefix + ">",
|
||||
// And the lease write's (ADR 0582 clauses 4, 6; issue 774): the seat mesh-grants' three events, and the stream
|
||||
// ASSIGNMENTS deleted, which with the controller's consumer deleted clears a removal that failed.
|
||||
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseHanded,
|
||||
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseAltered,
|
||||
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventRemovalFailed,
|
||||
"$JS.EVENT.ADVISORY.STREAM.DELETED." + AssignmentsStream,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,118 +7,62 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// **What still depends on the controller, and the lease write's check** (novox/hq ADR 0571 §13, ADR 0582 clause 2,
|
||||
// issue 774): every declaration of a manifest other than the controller's on the controller (a read of one of its
|
||||
// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, a use of its seat), judged
|
||||
// against the named, temporary list of what another accepted record keeps until the lease write. The grants part's
|
||||
// grants_lease_check runs it over every manifest the indexer holds; ManifestRefusals runs it over a repository's
|
||||
// manifests, in the test ADR 0571 §13 names in mesh-catalog, mesh-delivery and mesh-tools. A read of the lease key is no
|
||||
// dependency: it names the lease's holder, whoever that is, and parts read it to learn that the write happened.
|
||||
// **Nothing may name the controller** (novox/hq ADR 0590 clause 5, issue 1126): the controller ended by its unassignment
|
||||
// on 2026-11-02 and its code left the mesh, so a manifest that reads one of its states (its lease too), invokes one of its
|
||||
// verbs or tools, consumes or waits on one of its events, uses, claims or requires it, or is it, depends on nothing that
|
||||
// answers. ManifestRefusals runs the check over a repository's manifests, in the test each repository of manifests keeps
|
||||
// (mesh-catalog, mesh-delivery, mesh-tools); a later decision that places a controller again must first lift it.
|
||||
|
||||
// ControllerLease is the controller's lease as a reader names it (`<module>.<state>`): no dependency.
|
||||
const ControllerLease = ControllerSeatName + ".lease"
|
||||
// EndedControllerName is the name the ended controller had, as its module and as its seat: no manifest may name it.
|
||||
const EndedControllerName = "mesh-controller"
|
||||
|
||||
// The kinds of a dependency on the controller, as a manifest declares them.
|
||||
// The kinds of a declaration that names the controller.
|
||||
const (
|
||||
DependsRead = "read"
|
||||
DependsInvoke = "invoke"
|
||||
DependsConsume = "consume"
|
||||
DependsUses = "uses"
|
||||
DependsWaits = "waits"
|
||||
DependsRead = "read"
|
||||
DependsInvoke = "invoke"
|
||||
DependsConsume = "consume"
|
||||
DependsUses = "uses"
|
||||
DependsWaits = "waits"
|
||||
DependsClaims = "claims"
|
||||
DependsRequires = "requires"
|
||||
DependsIs = "is"
|
||||
)
|
||||
|
||||
// The phases of the lease write's check (ADR 0571 §13): before the lease write's own delivery the whole named list is
|
||||
// allowed; that delivery drops every entry but the three kept to the write itself, and its check runs with those alone.
|
||||
const (
|
||||
PhaseBefore = "before-delivery"
|
||||
PhaseWrite = "write"
|
||||
)
|
||||
|
||||
// Dependency is one declaration of a manifest other than the controller's on the controller: a read of one of its
|
||||
// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, or a send to its seat.
|
||||
// Dependency is one declaration of a manifest that names the controller.
|
||||
type Dependency struct {
|
||||
Module string `json:"module"`
|
||||
// By is the account declaring it when it is not the module's own: `process <id>` or `tools <binary>`.
|
||||
By string `json:"by,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
What string `json:"what"`
|
||||
// Kept is the record that keeps it until the lease write, from the named list; empty when none does.
|
||||
Kept string `json:"kept,omitempty"`
|
||||
// ToTheWrite says it is one of the three kept to the write itself, which the lease write's own delivery keeps.
|
||||
ToTheWrite bool `json:"to_the_write,omitempty"`
|
||||
}
|
||||
|
||||
// KeptDependency is one entry of the named, temporary list ADR 0571 §13 allows: a read, invoke or consume another
|
||||
// accepted record keeps until the lease write, with that record. ToTheWrite marks the three kept to the write itself.
|
||||
type KeptDependency struct {
|
||||
Module, Kind, What, Record string
|
||||
ToTheWrite bool
|
||||
}
|
||||
|
||||
// KeepList is the named list. Each entry names the clause that keeps it; a dependency not on it fails the check. It
|
||||
// holds the three kept to the write itself and nothing else: the lease write's own delivery dropped every other entry
|
||||
// (ADR 0571 §13; issue 774), so its check runs with these alone in either phase. The third, the bus node agent's
|
||||
// following of the controller's user list (ADR 0571 §8, ADR 0547 §8), is no manifest's declaration but the
|
||||
// node-engine's own, so no manifest matches it; it stays named here so the list reads as the record has it.
|
||||
var KeepList = []KeptDependency{
|
||||
{Module: "mesh-delivery-builder", Kind: DependsConsume, What: "mesh-controller.artifacts-gone", Record: "ADR 0571 §4, §13", ToTheWrite: true},
|
||||
{Module: "artifact-store-tools", Kind: DependsInvoke, What: "seat:mesh-controller.collect", Record: "ADR 0571 §4, §13", ToTheWrite: true},
|
||||
{Module: "node-engine", Kind: DependsRead, What: "the controller's user list of the bus module's composition", Record: "ADR 0571 §8, §13; ADR 0547 §8", ToTheWrite: true},
|
||||
}
|
||||
|
||||
// EndedBy names, for a declaration no entry keeps, the record that already ended it, so a refusal says where it moved.
|
||||
// The controller's `grants` is one: ADR 0571 §9 has no manifest read it from L7's delivery, which changes ADR 0567 §10
|
||||
// and ADR 0547 §5's shadow compare, so neither the assigner's nor the grants part's read of it is kept.
|
||||
var EndedBy = map[string]string{
|
||||
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
|
||||
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
||||
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
|
||||
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)",
|
||||
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
|
||||
}
|
||||
|
||||
// keptFor is the list's entry for one dependency, or nil.
|
||||
func keptFor(d Dependency) *KeptDependency {
|
||||
for i, k := range KeepList {
|
||||
if k.Module == d.Module && k.Kind == d.Kind && k.What == d.What {
|
||||
return &KeepList[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onController says whether a name `<owner>.<rest>` is the controller's.
|
||||
func onController(name string) bool { return strings.HasPrefix(name, ControllerSeatName+".") }
|
||||
func onController(name string) bool { return strings.HasPrefix(name, EndedControllerName+".") }
|
||||
|
||||
// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none:
|
||||
// it is the console's grant to every tool, judged by no record as a dependency.
|
||||
// it is the console's grant to every tool.
|
||||
func invokesController(invoke string) bool {
|
||||
if seat, _, ok := SeatInvoke(invoke); ok {
|
||||
return seat == ControllerSeatName
|
||||
return seat == EndedControllerName
|
||||
}
|
||||
return onController(invoke)
|
||||
}
|
||||
|
||||
// Dependencies are every declaration on the controller in the manifests given, by module, each with the list's entry
|
||||
// that keeps it. The controller's own manifest is not judged, nor a read of its lease key: it names the lease's holder,
|
||||
// whoever that is, and parts read it to learn that the write happened (ADR 0571 §13).
|
||||
// Dependencies are every declaration naming the controller in the manifests given, by module.
|
||||
func Dependencies(manifests map[string]Manifest) []Dependency {
|
||||
var out []Dependency
|
||||
add := func(module, by, kind, what string) {
|
||||
d := Dependency{Module: module, By: by, Kind: kind, What: what}
|
||||
if k := keptFor(d); k != nil {
|
||||
d.Kept, d.ToTheWrite = k.Record, k.ToTheWrite
|
||||
}
|
||||
if !slices.Contains(out, d) {
|
||||
if d := (Dependency{Module: module, By: by, Kind: kind, What: what}); !slices.Contains(out, d) {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
for name, m := range manifests {
|
||||
if name == ControllerSeatName || m.Module == ControllerSeatName {
|
||||
continue
|
||||
if name == EndedControllerName || m.Module == EndedControllerName {
|
||||
add(name, "", DependsIs, EndedControllerName)
|
||||
}
|
||||
for _, r := range m.Reads {
|
||||
if onController(r) && r != ControllerLease {
|
||||
if onController(r) {
|
||||
add(name, "", DependsRead, r)
|
||||
}
|
||||
}
|
||||
@@ -138,10 +82,20 @@ func Dependencies(manifests map[string]Manifest) []Dependency {
|
||||
}
|
||||
}
|
||||
for _, u := range m.Uses {
|
||||
if u == ControllerSeatName {
|
||||
if u == EndedControllerName {
|
||||
add(name, "", DependsUses, u)
|
||||
}
|
||||
}
|
||||
for _, c := range m.Claims {
|
||||
if c.Name == EndedControllerName {
|
||||
add(name, "", DependsClaims, c.Name)
|
||||
}
|
||||
}
|
||||
for _, r := range m.Requires {
|
||||
if r == EndedControllerName || onController(r) {
|
||||
add(name, "", DependsRequires, r)
|
||||
}
|
||||
}
|
||||
for _, a := range m.BusAccounts() {
|
||||
by := "process " + a.ID
|
||||
if a.Tools {
|
||||
@@ -170,69 +124,20 @@ func Dependencies(manifests map[string]Manifest) []Dependency {
|
||||
return out
|
||||
}
|
||||
|
||||
// Verdict is the lease write's check in one phase.
|
||||
type Verdict struct {
|
||||
Phase string `json:"phase"`
|
||||
Passes bool `json:"passes"`
|
||||
Reasons []string `json:"reasons"`
|
||||
}
|
||||
|
||||
// Coverage is how much of the mesh's manifests the dependencies were derived from: the manifests read, the modules the
|
||||
// indexer holds (-1 when its keys could not be listed, which Unread then names), and each manifest that could not be
|
||||
// read.
|
||||
type Coverage struct {
|
||||
Read, Indexed int
|
||||
Unread []Unread
|
||||
}
|
||||
|
||||
// Unread is one manifest, or one listing of them, that could not be read, and why.
|
||||
type Unread struct {
|
||||
Input string `json:"input"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// Judge runs the lease write's check over the dependencies (ADR 0571 §13) in one phase: before the lease write's own
|
||||
// delivery, every dependency must be on the named list; at the write, only the three kept to the write itself may stay.
|
||||
// It passes only over every module the indexer holds: no manifest read, fewer read than the indexer holds, or one that
|
||||
// could not be read fails it, since what was not read could hide a dependency. A lease already handed away leaves
|
||||
// nothing to write.
|
||||
func Judge(deps []Dependency, phase string, cov Coverage, handedAway bool, leaseSays string) Verdict {
|
||||
v := Verdict{Phase: phase, Reasons: []string{}}
|
||||
if handedAway {
|
||||
v.Reasons = append(v.Reasons, "there is nothing to write: "+leaseSays)
|
||||
// ManifestRefusals is the check over a repository's manifests, by module name: one reason per declaration naming the
|
||||
// controller, none when it passes. None given fails it, since nothing read can hide a declaration.
|
||||
func ManifestRefusals(manifests map[string]Manifest) []string {
|
||||
if len(manifests) == 0 {
|
||||
return []string{"no manifest was read: with nothing read, nothing naming the controller can be judged absent"}
|
||||
}
|
||||
switch {
|
||||
case cov.Read == 0:
|
||||
v.Reasons = append(v.Reasons, fmt.Sprintf("no manifest was read from the indexer (it holds %d modules): with nothing read, nothing on the controller can be judged absent", max(cov.Indexed, 0)))
|
||||
case cov.Indexed >= 0 && cov.Read < cov.Indexed:
|
||||
v.Reasons = append(v.Reasons, fmt.Sprintf("only %d of the %d modules the indexer holds had their manifest read, so what the others declare on the controller is not known", cov.Read, cov.Indexed))
|
||||
}
|
||||
for _, d := range deps {
|
||||
var out []string
|
||||
for _, d := range Dependencies(manifests) {
|
||||
who := d.Module
|
||||
if d.By != "" {
|
||||
who += " (" + d.By + ")"
|
||||
}
|
||||
switch {
|
||||
case d.Kept == "":
|
||||
why := fmt.Sprintf("%s declares %s %s, which no record keeps until the lease write (ADR 0571 §13)", who, d.Kind, d.What)
|
||||
if ended := EndedBy[d.Kind+" "+d.What]; ended != "" {
|
||||
why += "; ended by " + ended
|
||||
}
|
||||
v.Reasons = append(v.Reasons, why)
|
||||
case phase == PhaseWrite && !d.ToTheWrite:
|
||||
v.Reasons = append(v.Reasons, fmt.Sprintf("%s declares %s %s, kept by %s only until the lease write's own delivery, which drops it", who, d.Kind, d.What, d.Kept))
|
||||
}
|
||||
out = append(out, fmt.Sprintf("%s declares %s %s: the controller ended by its unassignment and nothing may name it "+
|
||||
"(ADR 0590 clause 5)", who, d.Kind, d.What))
|
||||
}
|
||||
for _, g := range cov.Unread {
|
||||
v.Reasons = append(v.Reasons, fmt.Sprintf("%s could not be read (%s), so what it declares on the controller is not known", g.Input, g.Why))
|
||||
}
|
||||
v.Passes = len(v.Reasons) == 0
|
||||
return v
|
||||
}
|
||||
|
||||
// ManifestRefusals is the lease write's check over a repository's manifests, by module name, in one phase (PhaseBefore
|
||||
// or PhaseWrite): each reason the check gives, none when it passes. Every manifest given counts as read: the caller reads
|
||||
// them from its own tree, and none given fails it.
|
||||
func ManifestRefusals(manifests map[string]Manifest, phase string) []string {
|
||||
return Judge(Dependencies(manifests), phase, Coverage{Read: len(manifests), Indexed: -1}, false, "").Reasons
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -6,98 +6,53 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Issue 774 (ADR 0571 §13, ADR 0582 clause 2): the lease write's check judges every manifest's dependency on the
|
||||
// controller against the named list, at the write (the three kept to it alone) and before its delivery. Moved with the
|
||||
// judge from mesh-grants' leasecheck_test.go (TestTheLeaseCheckJudgesEveryDependencyOnTheController's manifest rows and
|
||||
// TestTheKeepListNamesItsRecordsAndThreeToTheWrite), whose manifests were mesh-grants' fixtures; the conditions row is
|
||||
// mesh-conditions' manifest at mesh-catalog ce3bedd5 (module.json:31-32).
|
||||
func TestTheLeaseWritesCheckJudgesEveryDependencyOnTheController(t *testing.T) {
|
||||
toTheWrite := 0
|
||||
for _, k := range KeepList {
|
||||
if !strings.HasPrefix(k.Record, "ADR ") || k.Module == "" || k.What == "" {
|
||||
t.Errorf("entry %+v names no record, module or declaration", k)
|
||||
}
|
||||
if k.ToTheWrite {
|
||||
toTheWrite++
|
||||
// Issue 1126 B1/AC1, B2/AC1 (ADR 0590 clause 5): the seat set holds no mesh-controller, and the dependence check refuses
|
||||
// every manifest that reads (the lease too), invokes, consumes, waits on, uses, claims, requires or is mesh-controller.
|
||||
func TestNothingMayNameTheController(t *testing.T) {
|
||||
for _, s := range DefaultSeats() {
|
||||
if s.Name == EndedControllerName {
|
||||
t.Errorf("the seat set still holds %s", s.Name)
|
||||
}
|
||||
}
|
||||
if toTheWrite != 3 || len(KeepList) != 3 {
|
||||
t.Errorf("%d entries, %d kept to the write itself: ADR 0571 §13 keeps the three alone from the write's delivery", len(KeepList), toTheWrite)
|
||||
}
|
||||
|
||||
type want struct {
|
||||
deps []string // "<module> <kind> <what> <kept by>", kept by the record or "unkept"
|
||||
write string // a reason the check at the write names, or "" when it passes
|
||||
before string // a reason the check before the delivery names, or "" when it passes
|
||||
if _, ok := SeatNamed(EndedControllerName); ok {
|
||||
t.Errorf("SeatNamed still finds %s", EndedControllerName)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
manifests map[string]string
|
||||
want want
|
||||
name, manifest string
|
||||
want []string // "<kind> <what>"
|
||||
}{
|
||||
// 774/S4/AC2.
|
||||
{"a read of the lease alone is no dependency, and the controller's own manifest is not judged",
|
||||
map[string]string{"mod-l": `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`,
|
||||
"mesh-controller": `{"module":"mesh-controller","version":"1","invokes":["seat:mesh-controller.status"]}`},
|
||||
want{}},
|
||||
// 774/S4/AC1, 774/S30/AC1: the keeper's read of the controller's conditions put back is refused in either phase,
|
||||
// since the lease write's delivery dropped its entry (it was kept by ADR 0564 §9 before).
|
||||
{"the keeper's read of the controller's conditions, put back, is refused",
|
||||
map[string]string{"mesh-conditions": `{"module":"mesh-conditions","version":"1","reads":["mesh-controller.conditions","mesh-controller.lease"]}`},
|
||||
want{deps: []string{"mesh-conditions read mesh-controller.conditions unkept"},
|
||||
write: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write",
|
||||
before: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write"}},
|
||||
// 774/S30/AC1: the three kept to the write pass in either phase.
|
||||
{"the artifact sweep's collect is kept to the write itself",
|
||||
map[string]string{"artifact-store-tools": `{"module":"artifact-store-tools","version":"1","invokes":["seat:mesh-controller.collect"]}`},
|
||||
want{deps: []string{"artifact-store-tools invoke seat:mesh-controller.collect kept by ADR 0571 §4, §13"}}},
|
||||
{"the builder's consume of artifacts-gone is kept to the write itself",
|
||||
map[string]string{"mesh-delivery-builder": `{"module":"mesh-delivery-builder","version":"1","consumes":["mesh-controller.artifacts-gone"]}`},
|
||||
want{deps: []string{"mesh-delivery-builder consume mesh-controller.artifacts-gone kept by ADR 0571 §4, §13"}}},
|
||||
{"a read no record keeps fails both phases, named",
|
||||
map[string]string{"mod-u": `{"module":"mod-u","version":"1","reads":["mesh-controller.declared-packages"]}`},
|
||||
want{deps: []string{"mod-u read mesh-controller.declared-packages unkept"},
|
||||
write: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write",
|
||||
before: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write"}},
|
||||
{"a seat verb and a module tool of the controller's, a consume and a wait are each one; every tool (*) is none",
|
||||
map[string]string{"mod-i": `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` +
|
||||
`"consumes":["mesh-controller.checked","mod-x.thing"],"uses":["mesh-controller"]}`,
|
||||
"node-tools": `{"module":"node-tools","version":"1","waits":["mesh-controller.checked"]}`},
|
||||
want{deps: []string{"mod-i consume mesh-controller.checked unkept", "mod-i invoke mesh-controller.status unkept",
|
||||
"mod-i invoke seat:mesh-controller.root-free unkept", "mod-i uses mesh-controller unkept", "node-tools waits mesh-controller.checked unkept"},
|
||||
write: "mod-i declares invoke seat:mesh-controller.root-free, which no record keeps until the lease write (ADR 0571 §13); ended by ADR 0571 §11",
|
||||
before: "node-tools declares waits mesh-controller.checked"}},
|
||||
{"a process's own invoke of the controller is its module's dependency, named by the process",
|
||||
map[string]string{"mod-p": `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},{"id":"worker","type":"process","run":["./w"],"user":"mod-p",` +
|
||||
`"bus":{"invokes":["seat:mesh-controller.held"]}}]}`},
|
||||
want{deps: []string{"mod-p invoke seat:mesh-controller.held unkept"},
|
||||
write: "mod-p (process worker) declares invoke seat:mesh-controller.held", before: "mod-p (process worker)"}},
|
||||
{"no manifest fails both phases", map[string]string{}, want{write: "no manifest was read", before: "no manifest was read"}},
|
||||
{"a read of the lease", `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`, []string{"read mesh-controller.lease"}},
|
||||
{"the controller's own manifest", `{"module":"mesh-controller","version":"1"}`, []string{"is mesh-controller"}},
|
||||
{"a claim of its seat", `{"module":"a-controller","version":"1","claims":[{"name":"mesh-controller","scope":"mesh"}]}`,
|
||||
[]string{"claims mesh-controller"}},
|
||||
{"each declaration kind; every tool (*) is none",
|
||||
`{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` +
|
||||
`"consumes":["mesh-controller.artifacts-gone","mod-x.thing"],"uses":["mesh-controller"],"waits":["mesh-controller.checked"],` +
|
||||
`"requires":["mesh-controller"]}`,
|
||||
[]string{"consume mesh-controller.artifacts-gone", "invoke mesh-controller.status", "invoke seat:mesh-controller.root-free",
|
||||
"requires mesh-controller", "uses mesh-controller", "waits mesh-controller.checked"}},
|
||||
{"a process's own invoke", `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},` +
|
||||
`{"id":"worker","type":"process","run":["./w"],"user":"mod-p","bus":{"invokes":["seat:mesh-controller.held"]}}]}`,
|
||||
[]string{"invoke seat:mesh-controller.held"}},
|
||||
{"a module on its own seats", `{"module":"mod-ok","version":"1","reads":["mesh-grants.accounts"],"invokes":["*"]}`, nil},
|
||||
} {
|
||||
manifests := map[string]Manifest{}
|
||||
for name, raw := range row.manifests {
|
||||
m, err := Decode([]byte(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %s: %v", row.name, name, err)
|
||||
}
|
||||
manifests[name] = m
|
||||
m, err := Decode([]byte(row.manifest))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", row.name, err)
|
||||
}
|
||||
var deps []string
|
||||
for _, d := range Dependencies(manifests) {
|
||||
kept := "unkept"
|
||||
if d.Kept != "" {
|
||||
kept = "kept by " + d.Kept
|
||||
}
|
||||
deps = append(deps, d.Module+" "+d.Kind+" "+d.What+" "+kept)
|
||||
var got []string
|
||||
for _, d := range Dependencies(map[string]Manifest{m.Module: m}) {
|
||||
got = append(got, d.Kind+" "+d.What)
|
||||
}
|
||||
if !slices.Equal(deps, row.want.deps) {
|
||||
t.Errorf("%s: dependencies %q, want %q", row.name, deps, row.want.deps)
|
||||
if !slices.Equal(got, row.want) {
|
||||
t.Errorf("%s: dependencies %q, want %q", row.name, got, row.want)
|
||||
}
|
||||
for phase, reason := range map[string]string{PhaseWrite: row.want.write, PhaseBefore: row.want.before} {
|
||||
got := ManifestRefusals(manifests, phase)
|
||||
if (reason == "") != (len(got) == 0) || !slices.ContainsFunc(got, func(r string) bool { return strings.Contains(r, reason) }) && reason != "" {
|
||||
t.Errorf("%s: at %s refused %q, want %q", row.name, phase, got, reason)
|
||||
}
|
||||
refused := ManifestRefusals(map[string]Manifest{m.Module: m})
|
||||
if len(refused) != len(row.want) || len(refused) > 0 && !strings.Contains(refused[0], "ADR 0590") {
|
||||
t.Errorf("%s: refused %q", row.name, refused)
|
||||
}
|
||||
}
|
||||
if got := ManifestRefusals(map[string]Manifest{}); len(got) != 1 || !strings.Contains(got[0], "no manifest") {
|
||||
t.Errorf("no manifest given: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
// The delivery seat answers "what did the mesh's checks say of this pull request?" as a verb of its own,
|
||||
// `checks` (novox/hq ADR 0239): read by repository and a pull request's number or a commit.
|
||||
func TestTheDeliverySeatPromisesChecks(t *testing.T) {
|
||||
t.Run("the seat mesh-grants promises the lease write", grantsSeatPromisesTheLeaseWrite)
|
||||
seat, ok := SeatNamed(DeliverySeat)
|
||||
if !ok {
|
||||
t.Fatal("the delivery seat is not in the set")
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
// **The lease write, a verb of the seat `mesh-grants`** (novox/hq ADR 0582 clause 1, issue 774): the grants part
|
||||
// holds the seat once for the mesh and, on the operator's warrant, hands the controller's lease to itself and back.
|
||||
// It serves two verbs and says three events, none carrying a secret:
|
||||
//
|
||||
// - lease-handed: the lease names mesh-grants, written on the operator's warrant (clause 3);
|
||||
// - lease-altered: the lease key or its bucket's expiry differs from what mesh-grants wrote (clause 4);
|
||||
// - removal-failed: the day-later removal of the controller's consumer or stream failed (clause 6).
|
||||
|
||||
// GrantsSeat is the seat the grants part (mesh-grants) holds.
|
||||
const GrantsSeat = "mesh-grants"
|
||||
|
||||
// The seat's verbs.
|
||||
const (
|
||||
GrantsVerbLeaseWrite = "lease-write"
|
||||
GrantsVerbLeaseHandBack = "lease-hand-back"
|
||||
)
|
||||
|
||||
// The seat's events.
|
||||
const (
|
||||
GrantsEventLeaseHanded = "lease-handed"
|
||||
GrantsEventLeaseAltered = "lease-altered"
|
||||
GrantsEventRemovalFailed = "removal-failed"
|
||||
)
|
||||
|
||||
// grantsDecision is the record that made the seat.
|
||||
const grantsDecision = "novox/hq ADR 0582"
|
||||
|
||||
// grantsEvents are the seat's events, in the order the write meets them.
|
||||
func grantsEvents() []string {
|
||||
return []string{GrantsEventLeaseHanded, GrantsEventLeaseAltered, GrantsEventRemovalFailed}
|
||||
}
|
||||
|
||||
// grantsVerbs are the seat's verbs (ADR 0582 clauses 1, 2, 3, 5).
|
||||
func grantsVerbs() []Verb {
|
||||
return []Verb{
|
||||
{Name: GrantsVerbLeaseWrite, Description: "Hand the controller's lease to mesh-grants, kept for good. Refused, " +
|
||||
"naming the check, until the lease check at phase write passes and every condition it does not judge reads " +
|
||||
"green. Otherwise the operator is asked once; on the warrant the checks are read again, the controller's " +
|
||||
"service is stopped, the bus's user list is composed without the controller's write on the lease, the lease's " +
|
||||
"expiry is removed and the key holder names mesh-grants, said as lease-handed and recorded as a hand act. A no " +
|
||||
"or an expiry of the ask acts on nothing.",
|
||||
Input: schema(map[string]string{"why": "why the lease is handed now, shown to the operator and kept"},
|
||||
[]string{"why"})},
|
||||
{Name: GrantsVerbLeaseHandBack, Description: "Give the lease back to the controller, for a day after the write: " +
|
||||
"asked and warranted the same way, it restores the lease's fifteen-second expiry, deletes the key holder, " +
|
||||
"gives the controller its rights back once the list is loaded, and starts the controller. Refused once the " +
|
||||
"day-later removal ran: the controller would start without its consumer and stream.",
|
||||
Input: schema(map[string]string{"why": "why the lease goes back, shown to the operator and kept"},
|
||||
[]string{"why"})},
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The seat mesh-grants (novox/hq ADR 0582 clause 1, issue 774, 774/S1/AC2): its holder serves lease-write and
|
||||
// lease-hand-back, both a condition of holding, and nothing else; the seat says lease-handed, lease-altered and
|
||||
// removal-failed. A case of TestTheDeliverySeatPromisesChecks, the seat-verb test, within the test budget.
|
||||
func grantsSeatPromisesTheLeaseWrite(t *testing.T) {
|
||||
seat, ok := SeatNamed(GrantsSeat)
|
||||
if !ok {
|
||||
t.Fatal("the seat mesh-grants is not in the set")
|
||||
}
|
||||
if seat.Scope != ScopeMesh || seat.Delivers != "" {
|
||||
t.Errorf("mesh-grants is at scope %q delivering %q, want mesh and nothing", seat.Scope, seat.Delivers)
|
||||
}
|
||||
if got, want := VerbNames(seat.Serves), []string{"lease-write", "lease-hand-back"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("mesh-grants serves %v, want %v", got, want)
|
||||
}
|
||||
if want := []string{"lease-handed", "lease-altered", "removal-failed"}; !reflect.DeepEqual(seat.Emits, want) {
|
||||
t.Errorf("mesh-grants says %v, want %v", seat.Emits, want)
|
||||
}
|
||||
for _, v := range seat.Serves {
|
||||
if v.Optional {
|
||||
t.Errorf("%s is optional: the holder ships with the seat, so every verb is a condition of holding", v.Name)
|
||||
}
|
||||
props, _ := v.Input["properties"].(map[string]any)
|
||||
if req, _ := v.Input["required"].([]string); len(props) != 1 || !reflect.DeepEqual(req, []string{"why"}) {
|
||||
t.Errorf("%s takes %v requiring %v, want only a why", v.Name, props, req)
|
||||
}
|
||||
}
|
||||
|
||||
claim := func(serves ...string) Manifest {
|
||||
return Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeMesh, Serves: serves}}}
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
m Manifest
|
||||
refuse string // empty: held
|
||||
}{
|
||||
{"both verbs", claim("lease-write", "lease-hand-back"), ""},
|
||||
{"without the hand-back", claim("lease-write"), "lease-hand-back"},
|
||||
{"a verb the seat does not promise", claim("lease-write", "lease-hand-back", "lease-take"), "lease-take"},
|
||||
{"at node scope", Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeNode,
|
||||
Serves: []string{"lease-write", "lease-hand-back"}}}}, "scope"},
|
||||
} {
|
||||
err := CanHold(c.m, seat)
|
||||
switch {
|
||||
case c.refuse == "" && err != nil:
|
||||
t.Errorf("%s: refused: %v", c.name, err)
|
||||
case c.refuse != "" && (err == nil || !strings.Contains(err.Error(), c.refuse)):
|
||||
t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.refuse)
|
||||
}
|
||||
}
|
||||
|
||||
// No other seat changed: the set is the one before it with mesh-grants added beside mesh-hand-acts.
|
||||
var names []string
|
||||
for _, s := range DefaultSeats() {
|
||||
names = append(names, s.Name)
|
||||
}
|
||||
at := strings.Index(strings.Join(names, " "), HandActsSeat+" "+GrantsSeat+" ")
|
||||
if at < 0 {
|
||||
t.Errorf("mesh-grants does not follow mesh-hand-acts in the seat set: %v", names)
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,13 @@ package catalogue
|
||||
// PlacementSeat is the seat the assigner holds for placement.
|
||||
const PlacementSeat = "mesh-placement"
|
||||
|
||||
// The seat's seven verbs (ADR 0567 §10).
|
||||
// The seat's five verbs (ADR 0567 §10).
|
||||
const (
|
||||
PlacementVerbPlacement = "placement"
|
||||
PlacementVerbProposals = "proposals"
|
||||
PlacementVerbPropose = "propose"
|
||||
PlacementVerbWithdraw = "withdraw"
|
||||
PlacementVerbAgain = "again"
|
||||
PlacementVerbAdopt = "adopt"
|
||||
PlacementVerbHandBack = "hand-back"
|
||||
)
|
||||
|
||||
// placementVerbs are the seat's verbs: what reads all it keeps and does by hand all it does by itself (ADR 0567 §10).
|
||||
@@ -74,13 +72,5 @@ func placementVerbs() []Verb {
|
||||
"stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. " +
|
||||
"Nothing more: the recovery of a proposal whose judgement or ask was lost.",
|
||||
Input: schema(map[string]string{"id": "the proposal's id"}, []string{"id"})},
|
||||
{Name: PlacementVerbAdopt, Optional: true, Description: "Take placement over from the controller: its " +
|
||||
"placement-export is proposed whole as the seed, every node's key, which every node must answer as the same. " +
|
||||
"Its Approve is the take-over; at the warrant the export is read again and a changed one is proposed anew.",
|
||||
Input: schema(map[string]string{"why": "why, shown to the operator"}, []string{"why"})},
|
||||
{Name: PlacementVerbHandBack, Optional: true, Description: "The operator's alone, recorded as a hand act: " +
|
||||
"delete every placement key, the history kept, so every node returns to the controller's compositions. " +
|
||||
"Refused after the lease write.",
|
||||
Input: schema(map[string]string{"why": "why, recorded with the hand act"}, []string{"why"})},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ func TestTheVerbsWorkedAroundSayWhatTheyReplace(t *testing.T) {
|
||||
ServiceManagerSeat + ".restart": "systemctl restart",
|
||||
ServiceManagerSeat + ".status": "systemctl status",
|
||||
"node-hostname.add": "edit /etc/hosts",
|
||||
ControllerSeatName + ".node": "hostnamectl",
|
||||
}
|
||||
for _, s := range DefaultSeats() {
|
||||
for _, v := range s.Serves {
|
||||
|
||||
@@ -100,37 +100,6 @@ type Receivable struct {
|
||||
//
|
||||
// In the order a person reads it: the mesh's own, then a node's.
|
||||
var defaultSeats = append([]Seat{
|
||||
// The control plane states what it did under the seat it holds (novox/hq ADR 0134): a role's
|
||||
// events belong to the role, so they keep their address while the holder is replaced. No accepts,
|
||||
// so no work queue is raised for it — only what its holder may say.
|
||||
// And it serves the mesh's own verbs as the seat's tools (novox/hq ADR 0154): `status`, `push`,
|
||||
// `assign` and the rest are a role's interface, not a container's, and stay addressable while
|
||||
// the control plane is replaced.
|
||||
{Name: ControllerSeatName, Scope: ScopeMesh, Decision: "novox/hq ADR 0079",
|
||||
// And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4).
|
||||
Emits: []string{"applied", "refused", "built-before",
|
||||
"condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat",
|
||||
// A value given by hand, replaced after its module's first good start (novox/hq ADR 0228).
|
||||
"secret-replaced",
|
||||
// Every act a healer takes (novox/hq to-be 45 §7).
|
||||
"healer-acted",
|
||||
// A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8).
|
||||
"rolled-back",
|
||||
// A pull request's merge check, judged (novox/hq to-be 45 §9).
|
||||
"checked",
|
||||
// A walk kept in a new state, and a tier sent to a machine: kept until the catalogue's main consumes them no
|
||||
// more; nothing emits them (ADR 0539).
|
||||
"plan-moved",
|
||||
"sent",
|
||||
// A key of a record mirror written or removed (novox/hq ADR 0289, design 54 §3; design 57 F3).
|
||||
"assignment-changed", "declared-config-changed", "predefined-changed",
|
||||
// A builder's key not taken in because the artifact store does not hold an artifact it names (novox/hq
|
||||
// issue 1937): the builder builds it again.
|
||||
"artifacts-gone",
|
||||
// A bucket of a state this release deleted, deleted at the controller's first start (novox/hq ADR 0539
|
||||
// clause 13): the merge window's, the silent sends' and the building plans' hold, each said once.
|
||||
"bucket-deleted"},
|
||||
Serves: ControllerVerbs},
|
||||
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
|
||||
// served by whichever module holds the seat with tools of these names.
|
||||
{Name: "mesh-store", Scope: ScopeMesh, Delivers: "postgres-database", Decision: "novox/hq ADR 0079",
|
||||
@@ -348,9 +317,6 @@ var defaultSeats = append([]Seat{
|
||||
// policy and the bus step at the assigner, and the hand-act log.
|
||||
{Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()},
|
||||
{Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()},
|
||||
// The lease write (novox/hq ADR 0582 clause 1, grants_seat.go): the grants part hands the controller's lease to
|
||||
// itself and back on the operator's warrant.
|
||||
{Name: GrantsSeat, Scope: ScopeMesh, Decision: grantsDecision, Emits: grantsEvents(), Serves: grantsVerbs()},
|
||||
// Joining a machine (novox/hq ADR 0587 §1, enrolment_seat.go): held once by the module mesh-enrolment.
|
||||
{Name: EnrolmentSeat, Scope: ScopeMesh, Decision: enrolmentDecision, Emits: EnrolmentEvents, Serves: enrolmentVerbs()},
|
||||
// The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go).
|
||||
|
||||
@@ -76,521 +76,6 @@ func VerbNames(verbs []Verb) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// RemovedControllerVerbs are the controller seat's verbs release 6 removed (novox/hq ADR 0539, ADR 0429 §8): no
|
||||
// delivery plan, batch, tier or push exists, so nothing starts, stops, retries, follows or reads one. Each stays in
|
||||
// ControllerVerbs as a row with no input, so a caller is answered by name with its replacement, given here.
|
||||
var RemovedControllerVerbs = map[string]string{
|
||||
"plans": "the delivery view (nox-delivery): each machine's want and have, read from the assigner's wants and the " +
|
||||
"node-engine's haves",
|
||||
"plan": "the node's compositions: mesh-controller_composed holds each module's digest and judgement per node, and " +
|
||||
"mesh-hand-compose prints one module's composition for one node",
|
||||
"push": "a want: the assigner writes want.<node>.<module> by its rules, and by hand the operator's credential writes " +
|
||||
"it, naming a kept composition (ADR 0539 clause 10)",
|
||||
"deliver": "nothing to start: a merged change reaches each machine by its want",
|
||||
"delivery-stop": "a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)",
|
||||
"delivery-backups-again": "the assigner's backup ask, answered on the phone or in the delivery view (ADR 0539 clause 16)",
|
||||
"delivery-walks": "the delivery view (nox-delivery) and the wants, haves and compositions on the bus",
|
||||
"delivery-order": "no order to keep: each module reaches its machines by its own want, its providers first by rule 5",
|
||||
}
|
||||
|
||||
// removedInR6 is the row of a verb release 6 removed from the controller's seat: no input, refused by name.
|
||||
func removedInR6(name string) Verb {
|
||||
return Verb{Name: name, Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: " +
|
||||
RemovedControllerVerbs[name], Input: schema(nil, nil)}
|
||||
}
|
||||
|
||||
// ControllerSeatName is the seat the control plane holds, whose tools are the mesh's own verbs.
|
||||
const ControllerSeatName = "mesh-controller"
|
||||
|
||||
// ControllerVerbs are the mesh's own verbs, as the `mesh-controller` seat's tools (novox/hq ADR 0154).
|
||||
//
|
||||
// **The same function the command line calls, and nothing the tool adds** (ADR 0035): each of these
|
||||
// is a command the controller's binary already answers, run by the holder of the seat with the
|
||||
// arguments below and answered with what the command printed. A verb here is a contract every future
|
||||
// holder must implement, which is why the list is short and made of what an operator asks weekly.
|
||||
// Additive within a version (design 33 §7); a verb that would break a caller takes a new version.
|
||||
var ControllerVerbs = []Verb{
|
||||
{Name: "tools", Description: "Every seat's tools, from the mesh's own records: what each role " +
|
||||
"answers, whether or not its holder is up. The mesh's own verbs are the mesh-controller seat's.",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "calls", Description: "The calls this controller answered lately and what came of each — " +
|
||||
"one still running, one that finished after its caller was told it was running, one whose answer " +
|
||||
"the bus refused — and, given a call's id, its whole answer (novox/hq issue 265). A call that has " +
|
||||
"not finished within ten seconds answers that it is running, with its id; this is where it ends.",
|
||||
Input: schema(map[string]string{
|
||||
"call": "a call's id, as a running answer or `calls` gives it: that call and its whole answer",
|
||||
}, nil)},
|
||||
{Name: "status", Description: "What is wrong, what is quiet, what is out of date, and which " +
|
||||
"machines are behind what the mesh would send them.",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "nodes", Description: "Every machine the mesh knows, with whether it is converged or adopted.",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "node", Description: "What one machine reported it can do, what it is assigned, and why.",
|
||||
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"}),
|
||||
Replaces: []string{"hostnamectl", "uptime"}},
|
||||
{Name: "placement-export", Description: "Each machine's placement as the catalogue's placement file says it (novox/hq ADR 0555 " +
|
||||
"§6, issue 1126): what a person decided of the machine (endpoint, site, hub, address on the private network, public " +
|
||||
"domain, accounts, adoption, the tunnel found at adoption) and where its modules run (in order, with host ports, " +
|
||||
"bindings, config placement layers, pins and holdings), read by the controller's own code from its own store, as JSON. " +
|
||||
"Read-only; the one verb the controller gains for its exit (the operator, 2026-10-26 19:06:40), read once by the seed " +
|
||||
"of the placement files.",
|
||||
Input: schema(map[string]string{"node": "one machine's name; every machine when absent"}, nil)},
|
||||
{Name: "modules", Description: "Every module the mesh holds: version, the commit it was built from, " +
|
||||
"and which machines run it.",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "seats", Description: "Every seat the mesh defines, what it delivers, and who holds it.",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "builds", Description: "What has been built lately and what came of it, for every module or for one; " +
|
||||
"or, given a build's id, everything the build machine said while building it, line by line, from the bus.",
|
||||
Input: schema(map[string]string{
|
||||
"module": "one module's name; every module when absent",
|
||||
"log": "a build's id (as `builds` lists it): print what the build machine said, line by line",
|
||||
"limit": "how many builds to list (default 20); not with log",
|
||||
}, nil)},
|
||||
removedInR6("plans"),
|
||||
// The verbs the delivery's owner asks with (novox/hq ADR 0239): the planner's answer, a group's order and
|
||||
// composed check, and the start and end of a walk. Callable by a person too; mesh-delivery is who needs them.
|
||||
{Name: "delivery-plan", Description: "Removed in R4e (ADR 0501 §5 / ADR 0503 §3): refused by name, naming its replacement",
|
||||
Input: schema(nil, nil)},
|
||||
removedInR6("delivery-order"),
|
||||
{Name: "delivery-check", Description: "Answers that the checker (mesh-delivery-checker) judges a group (novox/hq " +
|
||||
"ADR 0503 §3); asks nothing. Its inputs stay, since hq's repository check still names it (ADR 0513 §4).",
|
||||
Input: schema(map[string]string{"group": "the group's id (none for one head checked again)",
|
||||
"members": "the members, as JSON, as delivery-order takes them",
|
||||
"ready": "\"true\" when the pull request is marked ready: its check is queued before every other check (novox/hq ADR 0406 §3)"},
|
||||
[]string{"members"})},
|
||||
removedInR6("deliver"),
|
||||
removedInR6("delivery-stop"),
|
||||
removedInR6("delivery-backups-again"),
|
||||
removedInR6("delivery-walks"),
|
||||
removedInR6("plan"),
|
||||
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there, " +
|
||||
"or when a seat its resources are applied through is held by nothing on the machine (novox/hq ADR 0207). " +
|
||||
"A module not registered yet is looked for (novox/hq issue 325, ADR 0261): while its build runs — a merge " +
|
||||
"that adds a module asks for one — the assignment is kept as pending and the controller makes it when the " +
|
||||
"build registers it; `status` lists it, and if the build fails it expires with why and raises a condition. " +
|
||||
"A module known and not built is said, and build asks for its build; a name with no module and no build " +
|
||||
"request kept is refused with the closest names.",
|
||||
Input: schema(map[string]string{"node": "the machine's name",
|
||||
"module": "the module's name; several comma-separated are judged together",
|
||||
"build": "\"true\": for a module known and not built (its last build failed, or was never asked), ask " +
|
||||
"for its build and keep this assignment pending until the build registers it"},
|
||||
[]string{"node", "module"}, "build")},
|
||||
{Name: "unassign", Description: "Take a module off a machine. Refused when it holds a seat a module left there depends on.",
|
||||
Input: schema(map[string]string{"node": "the machine's name",
|
||||
"module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})},
|
||||
{Name: "pin", Description: "Tell a machine which provider answers a provision for it — the module, and the node " +
|
||||
"it runs on, both. Asked for when more than one could answer; the refusal lists them.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine's name",
|
||||
"provision": "the provision, as the consumer requires it",
|
||||
"from": "the node the chosen provider runs on",
|
||||
"module": "the module providing it there",
|
||||
}, []string{"node", "provision", "from", "module"})},
|
||||
{Name: "unpin", Description: "Take that choice back, putting the question to the mesh again.",
|
||||
Input: schema(map[string]string{"node": "the machine's name", "provision": "the provision"}, []string{"node", "provision"})},
|
||||
removedInR6("push"),
|
||||
{Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine and module, " +
|
||||
"else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " +
|
||||
"name: made anew and the machine sent, so the module starts again on it — for a secret its definition " +
|
||||
"says it reads at start, whether the mesh made the value or a person gave it (novox/hq ADR 0228); one " +
|
||||
"an outside party issued, or one the module applies to a backend, is refused with the reason.",
|
||||
Input: schema(map[string]string{
|
||||
"provision": "a pair credential: the provision whose credential to replace",
|
||||
"consumer": "with provision: only the holder on this machine (optional)",
|
||||
"node": "an own secret: the machine",
|
||||
"module": "an own secret: the module; with provision: only this consuming module's credential (optional)",
|
||||
"secret": "an own secret: its name in the module's definition",
|
||||
"why": "an own secret: why it is rotated — recorded in the hand-act log (optional)",
|
||||
"cause": "with why: the cause in a word, the word a second rotation for the same reason uses (optional)",
|
||||
}, nil)},
|
||||
{Name: "give", Description: "Take a module's own secret from the operator at their desk (novox/hq ADR 0259 " +
|
||||
"§10): the same as secret-ask with the desk named. A prompt that does not show what is typed opens on " +
|
||||
"the machine named by at, its answer comes back sealed to this call alone, and is sealed to the " +
|
||||
"module's machine as `secret accept` seals it. The value is never an argument and never in the answer: " +
|
||||
"the answer says it was taken, or why not. Recorded in the hand-act log as a value given at the desk. " +
|
||||
"The prompt waits 25 seconds; dismissed or unanswered, nothing changes. Then push the machine.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine the module runs on, which the secret is sealed to",
|
||||
"module": "the module's name",
|
||||
"secret": "the own secret's name in the module's definition",
|
||||
"at": "the machine the operator sits at, where the prompt opens",
|
||||
}, []string{"node", "module", "secret", "at"})},
|
||||
{Name: "issued", Description: "Take the credential a provider's software issued for a provision — a media " +
|
||||
"server's sign-in token — and keep it as that provision's one credential for every consumer (novox/hq ADR " +
|
||||
"0305): the controller asks the provider's own tool on its machine, in the controller's own name, for the " +
|
||||
"value sealed to a key made for this call alone, seals it to every holder and forgets it, and sends their " +
|
||||
"machines when it changed. Carries no value and answers none; a value every holder already holds is not " +
|
||||
"sealed again, so asking again restarts nothing. Refused for a provision no offer says its software issues.",
|
||||
Input: schema(map[string]string{
|
||||
"provision": "the provision whose provider's software issues its credential, e.g. plex-api",
|
||||
}, []string{"provision"})},
|
||||
{Name: "secret-move", Description: "Move every secret the controller holds into the vault, as it is (novox/hq " +
|
||||
"ADR 0349 clause 7, design 57's V3): each machine's node-engine opens what was sealed to it and hands it to " +
|
||||
"the vault, which takes it from that machine alone, never over a value it holds, and reads it back; the " +
|
||||
"record of asks at a desk is copied once. No value is rotated, in the arguments or in the answer, and " +
|
||||
"nothing points at the vault because of it. Answers, per machine, what moved, what was there already and " +
|
||||
"what did not move and why; a machine not answering is moved when it is next heard from. Run again, it " +
|
||||
"moves what is left.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "only this machine's secrets (optional; every machine's without it)",
|
||||
}, nil)},
|
||||
{Name: "secret-ask", Description: "Ask the operator for a module's own secret (novox/hq ADR 0277): a prompt " +
|
||||
"that shows nothing of what is typed opens at the desk — the module's machine, or the one at names — " +
|
||||
"naming the module, the secret and who asked; the operator types the value there, never on a channel " +
|
||||
"and never in a verb's argument; the answer comes back sealed to this call alone and is sealed to the " +
|
||||
"module's machine as `secret accept` seals it. The answer says the value was taken, or why not. " +
|
||||
"Refused for a secret the mesh issues itself (the bus account, one the mesh may make) and for a module " +
|
||||
"that runs as an account of its own, whose value is typed at the controller's terminal. Bounded: one " +
|
||||
"open prompt per secret, three asks an hour. Recorded in the hand-act log, with who asked, and " +
|
||||
"announced on every channel. Then push the machine.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine the module runs on, which the secret is sealed to",
|
||||
"module": "the module's name",
|
||||
"secret": "the own secret's name in the module's definition",
|
||||
"at": "the machine the operator sits at, where the prompt opens; the module's machine when absent",
|
||||
}, []string{"node", "module", "secret"})},
|
||||
{Name: "issue", Description: "Give a module on a machine its account on the bus: minted, and sealed to the " +
|
||||
"machine as the module's own secret named broker, read at the next push of that machine. For a module " +
|
||||
"whose definition declares that secret; refused with the reason otherwise. Issued again, it replaces the account.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine that runs the module",
|
||||
"module": "the module's name",
|
||||
}, []string{"node", "module"})},
|
||||
{Name: "token", Description: "Refused through a verb since novox/hq ADR 0266: the one-time token a machine " +
|
||||
"joins with is answered to its caller, and whoever may call a verb includes agents. Issue it at the " +
|
||||
"controller's terminal: `mesh-controller token issue --new <name> --overlay-key <public half>`.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "a machine the mesh already has a record for",
|
||||
"new": "or the name of a machine to create the record for",
|
||||
"overlay_key": "the public half of the machine's tunnel key",
|
||||
"for": "how long it may be used, as a duration (default 1h)",
|
||||
"adopted": "\"true\" when the machine is in use and joins adopted",
|
||||
}, nil, "adopted"),
|
||||
Replaces: []string{"mesh-controller token issue", "wg set"}},
|
||||
{Name: "settings", Description: "Read or set what an assignment is configured with: a module's settings for the whole " +
|
||||
"mesh, or for one machine. Without values or clear, answers the layer as it stands — read it before setting it — " +
|
||||
"then every value the module gives a default or a layer sets, with where it comes from: the module's default, " +
|
||||
"the mesh, or the machine (novox/hq ADR 0262); with list \"preferences\", or with no module, every " +
|
||||
"module's preferences and each machine's value; " +
|
||||
"with history, the layers it replaced. Setting replaces that layer whole and answers each key it adds (+), " +
|
||||
"changes (~) and removes (-); a set that would remove a key is refused unless replace says it is meant " +
|
||||
"(novox/hq ADR 0217). Takes effect at the next push. With clear, removes the layer and the module is back to " +
|
||||
"what its definition says; a cleared or replaced layer is kept in the history. A trusted setting — " +
|
||||
"places, accesses, what a provider serves, what a trusted file asks for, the whole layer of a module " +
|
||||
"with a trusted mergeable file — is refused through this verb (novox/hq issue 339, 340) and PROPOSED " +
|
||||
"instead (novox/hq ADR 0277): with propose, the values (or clear) are put to the operator on a channel " +
|
||||
"that proves who answers, with every key and its exact new value, and the layer is set only on their " +
|
||||
"Approve; the answer names the proposal, its fingerprint and when it expires, and nothing changes until " +
|
||||
"then. With proposals, every proposal and where each stands; with proposal, one whole. Once move-config has " +
|
||||
"run, a module in the config form forwards to mesh-config (issue 664): show lists what config lists, set and " +
|
||||
"clear set and clear each value as the module's own on the machine (none for the whole mesh), placement " +
|
||||
"staying beside the assignment, and propose proposes each value through mesh-config.",
|
||||
Input: schema(map[string]string{
|
||||
"module": "the module's name; with list, only that module's",
|
||||
"values": "the settings as a JSON object, for set or propose",
|
||||
"node": "one machine; the whole mesh when absent",
|
||||
"clear": "\"true\" to remove the layer instead of setting it, or to propose its removal; not with values",
|
||||
"replace": "\"true\": with values, the set (or the proposal) is meant to remove the keys the layer had and it does not name",
|
||||
"history": "\"true\": without values or clear, the layers this one replaced, the latest first",
|
||||
"list": "\"preferences\": every module's preferences — key, default and why — and the value on each " +
|
||||
"machine it is assigned to with where it comes from; module and node narrow it (novox/hq ADR 0262)",
|
||||
"propose": "\"true\": propose the values (or clear) to the operator instead of setting them (novox/hq ADR 0277)",
|
||||
"proposals": "\"true\": every settings proposal, newest first, and where each stands",
|
||||
"proposal": "a proposal's id: that proposal whole, its values and the layer it was shown against",
|
||||
}, nil, "clear", "replace", "history", "propose", "proposals")},
|
||||
{Name: "command", Description: "Run one reading command line of the controller's own, as you would type it at " +
|
||||
"its shell — `node show ace`, `module list`, `plans`, `conditions show <key>` — and answer what it " +
|
||||
"printed. The generic verb beside the named ones (novox/hq ADR 0154), and since ADR 0266 it only reads: " +
|
||||
"status, version, help, seats, healers, hand-acts, durations, collection, images, artifacts, data, builds, " +
|
||||
"plan, plans (not stop/close/go), doctor (not run), conditions list/show/history, node list/show, " +
|
||||
"module list, settings show/preferences, retire list, cleanup list, delivery plan/walks, bus, mirrors (not " +
|
||||
"--record/--confirm). What writes has its named verb; what sets a key, issues a credential or a token, or " +
|
||||
"accepts, recovers or exports a secret is the controller's terminal's alone.",
|
||||
Input: schema(map[string]string{
|
||||
"command": "the command line, as the controller's binary takes it; quotes group a word with spaces",
|
||||
}, []string{"command"})},
|
||||
// The build queue went with build-agent (novox/hq ADR 0525 §9): asks are the build seats' streams, their backlog the
|
||||
// streams' own state, shown in the delivery view. The verbs are refused by name for one release, naming that.
|
||||
{Name: "queue", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: the build seats' backlog in the delivery view",
|
||||
Input: schema(nil, nil)},
|
||||
{Name: "cancel", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)},
|
||||
{Name: "clear", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)},
|
||||
{Name: "replay", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement", Input: schema(nil, nil)},
|
||||
// novox/hq issue 686: a builder ran a branch's store tests for the first time in its pull request's merge check.
|
||||
{Name: "try", Description: "Try a pushed branch or commit before its pull request (novox/hq issue 686): the " +
|
||||
"checker (mesh-delivery-checker) merges the head onto the repository's trunk and runs its levels on the merge, " +
|
||||
"as it judges a pull request (ADR 0503 §3), its levels asks on the build seats (ADR 0525 §1); no merge-check.sh is run. Answers at " +
|
||||
"once with an id; asked again with that id it waits up to 20 s and answers pass, fail or error, how long it " +
|
||||
"took and the lines naming what failed — or that it is still waiting or running, to ask again. Sets no " +
|
||||
"status on the forge, registers and builds nothing for delivery. One try of a commit at a time — a second " +
|
||||
"ask answers the first's id. Only for a repository of the mesh; the ref must be on the forge.",
|
||||
Input: schema(map[string]string{
|
||||
"repository": "the repository on the forge, owner/name; with ref",
|
||||
"ref": "the branch or commit to try, as pushed to the forge",
|
||||
"base": "the branch it is merged into (the repository's default branch when none is named)",
|
||||
"tests": "only the tests this `go test -run` pattern names",
|
||||
"store-only": "\"true\": only the integration level",
|
||||
"id": "instead of a repository: the id a try answered, to read what came of it",
|
||||
}, nil, "store-only"),
|
||||
Replaces: []string{"sh merge-check.sh", "docker run postgres"}},
|
||||
{Name: "kill", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: a build seat's ask ends where it runs", Input: schema(nil, nil)},
|
||||
{Name: "pause", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: <node>/node-build-<language>.pause", Input: schema(nil, nil)},
|
||||
{Name: "resume", Description: "Removed in R4g (ADR 0525 §9): refused by name, naming its replacement: <node>/node-build-<language>.resume", Input: schema(nil, nil)},
|
||||
// Acts done by hand, and what the bounds are set from (novox/hq to-be 45 §7, Phase 0).
|
||||
{Name: "hand-act", Description: "Record an act done by hand outside the mesh — a container restarted, a file " +
|
||||
"edited, a service started on a machine — with why and its cause, in the hand-act log beside the pushes and " +
|
||||
"plans ended by hand (novox/hq to-be 45 §7). A cause recorded twice in a fortnight is a healer wanted.",
|
||||
Input: schema(map[string]string{
|
||||
"what": "what was done, in a line",
|
||||
"why": "why it had to be done by hand",
|
||||
"cause": "the cause in a word, or a condition's kind — the word a second act for the same reason uses",
|
||||
"condition": "the key of the condition it addressed, if any (optional)",
|
||||
}, []string{"what", "why", "cause"})},
|
||||
{Name: "drill", Description: "Record a drill — something broken on purpose to see the mesh raise and clear it: " +
|
||||
"a module stopped, a process killed — with what it tests, in the hand-act log. A drill is a person's " +
|
||||
"deliberate test, never a repair: no healer is wanted for it however often it is run (S15).",
|
||||
Input: schema(map[string]string{
|
||||
"what": "what was done on purpose, in a line",
|
||||
"why": "what the drill tests",
|
||||
"condition": "the key of the condition the drill is meant to raise, if any (optional)",
|
||||
}, []string{"what", "why"})},
|
||||
{Name: "warranted", Description: "Record in the hand-act log what a module did on the operator's warrant (novox/hq " +
|
||||
"ADR 0274, ADR 0259): who chose, through which channel, with which proofs and which answer are read from the " +
|
||||
"router's own record of that module's ask, never from the caller; recorded once per ask however often it is " +
|
||||
"asked; the caller names the ask and says nothing else. Refused for an ask still open, ended without a choice, " +
|
||||
"or not that module's.",
|
||||
Input: schema(map[string]string{
|
||||
"asker": "the module that asked, e.g. claude-code",
|
||||
"ask": "its ask's id",
|
||||
}, []string{"asker", "ask"})},
|
||||
{Name: "hand-acts", Description: "What was done by hand lately — pushes, plans ended, consumers re-made, acts " +
|
||||
"recorded — who, why and the cause of each, and which causes repeat: each repeat is a healer the mesh lacks.",
|
||||
Input: schema(map[string]string{"days": "how many days back (default 14)"}, nil)},
|
||||
{Name: "durations", Description: "How long things take, as the controller measured them: a send to its machine's " +
|
||||
"report (apply), a plan's tier, a build, and each phase of an " +
|
||||
"ended walk per class (walk-phase, novox/hq ADR 0282) — per machine, repository, module or class/phase, with " +
|
||||
"median, p90 and max. What the core's bounds are set from (novox/hq to-be 45 Phase 0).",
|
||||
Input: schema(map[string]string{
|
||||
"kind": "one kind: apply, plan-tier, build or walk-phase; every kind when absent",
|
||||
"days": "how many days back (default 14)",
|
||||
}, nil)},
|
||||
// What is wrong, and the self-check (novox/hq to-be 45 §2, §4).
|
||||
{Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, " +
|
||||
"then oldest — raised by the watchdogs of the signals table, the self-check's probes and the providers' " +
|
||||
"own words, and cleared when observation says it is resolved, never by hand. Given a key, that one " +
|
||||
"whole with its evidence; with history, every transition lately; with silence, stop one's messages " +
|
||||
"for a while — a hand act, which says why (novox/hq to-be 45 §2).",
|
||||
Input: schema(map[string]string{
|
||||
"key": "a condition's key: that one whole; with history, only its transitions",
|
||||
"scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe or mesh",
|
||||
"severity": "only urgent, or only warning",
|
||||
"machine": "only those about this machine",
|
||||
"history": "\"true\": every raising, change, silence and clearing lately, oldest first",
|
||||
"days": "with history: how many days back (default 7, at most 90)",
|
||||
"silence": "a condition's key: send no message for it for a while; it stays open and in status",
|
||||
"for": "with silence: how long — 30m, 4h, 2d; at most 7d",
|
||||
"why": "with silence: why — required, and recorded in the hand-act log",
|
||||
"cause": "with silence: the cause in a word (the condition's kind when absent)",
|
||||
}, nil, "history")},
|
||||
// What the healers did (novox/hq to-be 45 §7).
|
||||
{Name: "healers", Description: "The healers (novox/hq to-be 45 §7): each registered response to one kind " +
|
||||
"of condition — its repair (the ordinary path again), its budget, what happens when it is spent — every act " +
|
||||
"they took lately with its outcome, and the mesh-wide brake. A heal is never a hand act; whether it " +
|
||||
"repaired anything is the condition's clearing to say. Each act is also in its condition's tried.",
|
||||
Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)},
|
||||
{Name: "doctor", Description: "The self-check (novox/hq to-be 45 §4): the last run's verdict at once — " +
|
||||
"each probe of the design's live invariants passed, failed or could not run, and how long ago. With " +
|
||||
"run, a run now; with probes, the registry; with signals, every row of the signals table and the age " +
|
||||
"of its newest signal. Runs every five minutes on its own; each failure is an open condition.",
|
||||
Input: schema(map[string]string{
|
||||
"run": "\"true\": run every probe now and answer the verdict",
|
||||
"probes": "\"true\": the registry — what each probe asserts, and the condition it raises",
|
||||
"signals": "\"true\": the signals table, each row with the age of its newest signal",
|
||||
"probe": "one probe's id (D14): its verdict alone, with every finding — the verdict shows at most twenty a probe",
|
||||
}, nil, "run", "probes", "signals")},
|
||||
// How a module's new builds reach its machines, and the bus's planned step (novox/hq ADR 0236).
|
||||
{Name: "upgrade", Description: "Whether each module's new builds wait for a person (novox/hq ADR 0236, ADR 0539 clause 14): " +
|
||||
"for a module of policy record, the assigner writes no want naming a composition judged to recreate a container or move " +
|
||||
"data until the operator answers its ask; with where the policy comes from (a person, the module, the bus, its " +
|
||||
"irreplaceable data, the default) and why. With module, that one; with policy, a person's choice for it — roll-out, " +
|
||||
"record (with why) or default to take the choice back. The bus is never moved by the assigner (ADR 0539 clause 15).",
|
||||
Input: schema(map[string]string{
|
||||
"module": "one module",
|
||||
"policy": "with module: roll-out, record or default",
|
||||
"why": "with policy: why — required for record, kept",
|
||||
}, nil)},
|
||||
{Name: "bus", Description: "The bus as a person's step (novox/hq to-be 45 §8, ADR 0236, ADR 0539 clause 15): what a bus " +
|
||||
"upgrade would do — the bus's build on each machine against the one the mesh holds — and how the last step went. " +
|
||||
"With upgrade, check one: a person's act with why, after the streams are snapshotted (snapshot-taken says where, " +
|
||||
"while the mesh takes none itself), saying first whether it can be reverted; then it answers the want the operator " +
|
||||
"writes with the operator's credential, want.<node>.<bus module>, recorded — nothing is sent, and the assigner never " +
|
||||
"moves the bus.",
|
||||
Input: schema(map[string]string{
|
||||
"upgrade": "\"true\": start the bus's upgrade",
|
||||
"why": "with upgrade: why — required, recorded in the hand-act log",
|
||||
"cause": "with upgrade: the cause in a word (default bus-upgrade)",
|
||||
"reversible": "\"true\": with upgrade, the new version can be undone by putting the old one back",
|
||||
"irreversible": "\"true\": with upgrade, it cannot — your explicit word that it runs anyway",
|
||||
"snapshot-taken": "with upgrade: where the streams' snapshot you took is",
|
||||
}, nil, "upgrade", "reversible", "irreversible")},
|
||||
// A consumer the mesh stopped asking for: retired, waiting for a person, deleted only by one
|
||||
// (novox/hq ADR 0230).
|
||||
{Name: "retire", Description: "A consumer the mesh stops asking for is retired by its provider — access " +
|
||||
"disabled, data kept — after the same answer in five passes; more than three at once, or more than half " +
|
||||
"of those held, waits for a person. With no answer, every provider that waits and what it would retire. " +
|
||||
"With answer approve or reject, a node and a module: retire exactly what that provider waits with, or " +
|
||||
"keep it active — a hand act, which says why (novox/hq ADR 0230).",
|
||||
Input: schema(map[string]string{
|
||||
"answer": "approve or reject: answer what the provider waits with (needs node, module and why)",
|
||||
"node": "with answer: the machine the provider runs on",
|
||||
"module": "with answer: the provider module",
|
||||
"why": "with answer: why — required, and recorded in the hand-act log",
|
||||
"cause": "with answer: the cause in a word (retire-waiting when absent)",
|
||||
}, nil)},
|
||||
{Name: "cleanup", Description: "Every consumer a provider holds retired — its age, its size where the " +
|
||||
"backend knows, and why it was retired — and every module's own data retired on its machine (ADR 0233). " +
|
||||
"With consumer (and node, module): the provider deletes that one retired consumer — never an active one; " +
|
||||
"for a module's own retired item, consumer names the item and the machine's backup holder takes a last " +
|
||||
"restore point of it, then deletes it. With older-than: every retired consumer older than that many " +
|
||||
"days, listed; deleted only with confirm. Deleting is a hand act, which says why (novox/hq ADR 0230).",
|
||||
Input: schema(map[string]string{
|
||||
"node": "with consumer: the machine the provider runs on",
|
||||
"module": "with consumer: the provider module",
|
||||
"consumer": "delete this retired consumer (needs node, module and why)",
|
||||
"older-than": "delete every consumer retired more than this many days (needs why; lists only without confirm)",
|
||||
"confirm": "\"true\": with older-than, delete what is listed",
|
||||
"why": "with consumer or older-than: why — required, and recorded in the hand-act log",
|
||||
"cause": "with consumer or older-than: the cause in a word (cleanup-waiting when absent)",
|
||||
}, nil, "confirm")},
|
||||
{Name: "judge-directories", Description: "Read-only: which of the directories one machine is declared its " +
|
||||
"node-engine's walk that follows no link another account could make would refuse (novox/hq issue 358), one line " +
|
||||
"each: placed, would be refused and why (the link and who could replace it), refused already by the placement " +
|
||||
"guard, or below a home. The engine judges the declaration it keeps and makes, chmods and chowns nothing.",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine's name",
|
||||
}, []string{"node"})},
|
||||
// What a consumer gave up on (novox/hq issue 330): kept in DEAD_LETTERS until a person acts on it.
|
||||
{Name: "root-free", Description: "Whether each machine named is root-free now (novox/hq ADR 0259 §8): no agent " +
|
||||
"there can become root without a person. Judged when asked, never from a condition: free only when the machine " +
|
||||
"names an account its agents run as, its node-engine judged that account unable to become root within the " +
|
||||
"last 15 minutes, and the login shell's execute is not served there. Anything else, a read that failed " +
|
||||
"included, is not free and says why. The router asks it before an answer from a channel proving its sender " +
|
||||
"may approve. Only reads.",
|
||||
Input: listed(schema(map[string]string{
|
||||
"machines": "the machines to judge, by name: a list, or one text separated by commas",
|
||||
}, []string{"machines"}), "machines")},
|
||||
{Name: "dead-letters", Description: "Every message a consumer on the bus gave up on after handing it over " +
|
||||
"as often as it may, 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 what it said. With deliver: hand it " +
|
||||
"again to the consumer that gave it up, and nobody else; with drop: let it go for good. Delivering and " +
|
||||
"dropping are hand acts, which say why (novox/hq issue 330).",
|
||||
Input: schema(map[string]string{
|
||||
"id": "a dead letter's id, as the list gives it: that one whole, with its message",
|
||||
"consumer": "a consumer's name, or <stream>.<consumer>: only what that one gave up on",
|
||||
"limit": "how many to list, newest first (default 50); only when listing",
|
||||
"deliver": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)",
|
||||
"drop": "a dead letter's id: drop it for good (needs why)",
|
||||
"why": "with deliver or drop: why — required, and recorded in the hand-act log",
|
||||
"cause": "with deliver or drop: the cause in a word (dead-letter when absent)",
|
||||
}, nil)},
|
||||
// The data every machine declares (novox/hq ADR 0233).
|
||||
{Name: "data", Description: "Every item of data every machine declares, as the self-check last measured it: " +
|
||||
"its class (irreplaceable, rebuildable, cache), where it is, its size, its newest write, its newest good " +
|
||||
"backup and the bound on it — and what is retired: kept after its module left the machine, removed only by " +
|
||||
"`cleanup delete` (novox/hq ADR 0233).",
|
||||
Input: schema(map[string]string{
|
||||
"machine": "one machine (optional)",
|
||||
"retired": "\"true\": only what is retired",
|
||||
}, nil, "retired")},
|
||||
{Name: "build", Description: "Removed in R4e (ADR 0501 §5 / ADR 0503 §3): refused by name, naming its replacement",
|
||||
Input: schema(nil, nil)},
|
||||
// What the records say the registries may keep, asked on demand (novox/hq ADR 0251, to-be 51).
|
||||
{Name: "artifacts", Description: "Every artifact the mesh recorded making that lives in its artifact store, " +
|
||||
"each with its repository, digest, kind (image or archive) and state: kept — and why: a definition names " +
|
||||
"it, or one of the five most recent builds of a module the mesh holds — or eligible: made, kept for no " +
|
||||
"reason, not yet let go. Collected ones are counted, and listed only with collected. Reads only " +
|
||||
"(novox/hq ADR 0251).",
|
||||
Input: schema(map[string]string{
|
||||
"repository": "only this repository's references, as <module>/<artifact> (the counts stay whole)",
|
||||
"collected": "\"true\": list the collected references too (they only grow; counted always)",
|
||||
}, nil, "collected")},
|
||||
// Whether the artifact store holds what a builder's done key names, before the builder reuses it (novox/hq issue
|
||||
// 1937): the store's collector deletes a blob nothing holds, and a key the controller never took in holds nothing.
|
||||
{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, and " +
|
||||
"builds a key whose artifact is gone again. Reads only: nothing is held or written (novox/hq issue 1937).",
|
||||
Input: listed(schema(map[string]string{
|
||||
"references": "the artifacts' references, artifact-store://<module>/<artifact>/blobs/sha256:<hex>: a list, " +
|
||||
"or one text separated by commas",
|
||||
}, []string{"references"}), "references")},
|
||||
{Name: "collect", Description: "The artifact store's sweep, now: what the mesh made and keeps for no reason " +
|
||||
"is let go of. Without confirm a dry run — every kept archive asked about with HEADs, what would be let go " +
|
||||
"listed, nothing held or deleted. With confirm and why: every kept archive held first, then each eligible " +
|
||||
"artifact let go of, oldest first, and recorded collected — a hand act, recorded with its why. A confirmed " +
|
||||
"collect lets an index go with the platform manifests no kept index names (read first; any read that " +
|
||||
"fails lets nothing go); the sweep after a build leaves an eligible index for such a collect (novox/hq ADR " +
|
||||
"0257). Bounded by " +
|
||||
"most (500 by default, at most 5000) and 45 seconds; what is left is said. Bytes are reclaimed by the " +
|
||||
"store's nightly collector. Never touches a digest the mesh did not record (novox/hq ADR 0189, ADR 0251).",
|
||||
Input: schema(map[string]string{
|
||||
"confirm": "\"true\": let go of what is eligible (needs why); without it nothing is done",
|
||||
"why": "with confirm: why — required, and recorded in the hand-act log",
|
||||
"most": "let go of at most this many (500 by default, at most 5000)",
|
||||
}, nil, "confirm")},
|
||||
{Name: "move-placement", Description: "The move of placement out of the settings layers, run once after the " +
|
||||
"delivery that holds it (novox/hq ADR 0348 clause 11, issue 736): every placement key a settings layer still " +
|
||||
"holds — endpoints, expose, reach and ports, which a layer set before then keeps among its settings — is put " +
|
||||
"beside the assignment, where places, accesses and networks already are. Without confirm it says which layers " +
|
||||
"hold which keys and moves nothing. With confirm and why: in one transaction, each such layer is kept whole " +
|
||||
"in its history first (why: moved to placement), its keys are laid over the placement beside it, and the " +
|
||||
"layer keeps its other keys, when it was set and who set it — and the act is recorded in the hand-act log. " +
|
||||
"Answers each layer moved: its machine (empty for the whole mesh), its module and its keys. Nothing a " +
|
||||
"machine is told changes: composition reads the keys as placement wherever they are. Asked again, it " +
|
||||
"moves nothing.",
|
||||
Input: schema(map[string]string{
|
||||
"confirm": "\"true\": move them (needs why); without it nothing is moved",
|
||||
"why": "with confirm: why — required, and recorded in the hand-act log",
|
||||
}, nil, "confirm")},
|
||||
{Name: "move-config", Description: "The move of config: every config value a settings layer holds is set in " +
|
||||
"mesh-config, run once after the delivery that holds it and after move-placement (novox/hq ADR 0348 §9, " +
|
||||
"issue 664). A machine's layer becomes the module's own value on that machine; a whole-mesh layer becomes " +
|
||||
"mesh config for the predefined key its value falls back to where that module alone falls back to it, and " +
|
||||
"otherwise the module's own value on each machine it is assigned to, laid under that machine's layer; a key " +
|
||||
"a layer says by its file's name is set under its config key. Without confirm it says each value it would " +
|
||||
"set, where from, each layer that stays (its module is in the settings form) and anything that refuses the " +
|
||||
"move, and sets nothing. With confirm and why: each value is set through mesh-config in the controller's " +
|
||||
"name, every module's resolved values are read back and compared with what its layers lay today, and only " +
|
||||
"then, in one transaction, is each layer moved kept whole in its history first (why: moved to mesh-config) " +
|
||||
"and emptied, and the controller switched to read mesh-config where it read the layers — the act recorded " +
|
||||
"in the hand-act log. Nothing a machine is told changes. Asked again, it sets nothing set already.",
|
||||
Input: schema(map[string]string{
|
||||
"confirm": "\"true\": set the values and switch (needs why); without it nothing is set",
|
||||
"why": "with confirm: why — required, and recorded in the hand-act log and with each value",
|
||||
}, nil, "confirm")},
|
||||
{Name: "mirrors", Description: "The bases the mesh copied into its artifact store for builds to stand on, as " +
|
||||
"the records hold them: each with its state — kept because a kept build stood on it, or eligible — and " +
|
||||
"which build or person recorded it. With record: references of copies no record names (a copy made " +
|
||||
"before copies were recorded, or by a build that failed before then) to record as the mesh's, so the " +
|
||||
"sweep keeps or lets go of them like any other; only a repository the mirror writes is taken " +
|
||||
"(upstream/<host>/<path>, or a module's <module>/on-<argument>), and each must be in the store. Without " +
|
||||
"confirm a dry run; with confirm and why, recorded — a hand act. A recorded copy is eligible: the next " +
|
||||
"sweep lets it go unless a kept build stood on it. Records only: this verb deletes nothing " +
|
||||
"(novox/hq ADR 0257).",
|
||||
Input: schema(map[string]string{
|
||||
"record": "references to record, artifact-store://<repository>@sha256:<hex>, separated by spaces or commas",
|
||||
"confirm": "\"true\": record them (needs why); without it nothing is recorded",
|
||||
"why": "with confirm: why — required, and recorded with each copy and in the hand-act log",
|
||||
}, nil, "confirm")},
|
||||
{Name: "images", Description: "Every container image one machine's declaration names — the declaration the " +
|
||||
"mesh would send it now and the one it was last sent — each with the resources naming it: what the " +
|
||||
"machine keeps when it prunes its images. sent_known is false when the last one sent is not kept with " +
|
||||
"its images. Reads only (novox/hq ADR 0251).",
|
||||
Input: schema(map[string]string{
|
||||
"node": "the machine's name",
|
||||
}, []string{"node"})},
|
||||
}
|
||||
|
||||
// schema is a JSON schema for an object of string properties, which is every argument the verbs
|
||||
// above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call. The
|
||||
// switches are the properties that are "true" or "false" and nothing else, said in the schema as an
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 1887/A9b: the build queue's verbs are refused by name, naming what replaced them, and the verbs R4e removed are gone.
|
||||
func TestTheBuildQueueVerbsAreRefusedByName(t *testing.T) {
|
||||
rows := map[string]Verb{}
|
||||
for _, v := range ControllerVerbs {
|
||||
rows[v.Name] = v
|
||||
}
|
||||
for _, gone := range []string{"rebuild", "forget-checks"} {
|
||||
if _, ok := rows[gone]; ok {
|
||||
t.Errorf("the controller still lists %s", gone)
|
||||
}
|
||||
}
|
||||
for _, refused := range []string{"queue", "cancel", "clear", "kill", "pause", "resume", "replay"} {
|
||||
v, ok := rows[refused]
|
||||
if !ok || !strings.HasPrefix(v.Description, "Removed in R4g (ADR 0525 §9): refused by name") {
|
||||
t.Errorf("%s = %q, want refused by name", refused, v.Description)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rows["queue"].Description, "delivery view") || !strings.Contains(rows["pause"].Description, "node-build-") {
|
||||
t.Errorf("queue and pause do not name their replacements: %q, %q", rows["queue"].Description, rows["pause"].Description)
|
||||
}
|
||||
if strings.Contains(rows["try"].Description, "build-agent") {
|
||||
t.Errorf("try still names build-agent: %q", rows["try"].Description)
|
||||
}
|
||||
}
|
||||
@@ -5,58 +5,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 1943/AC4: the controller's seat no longer offers a verb that starts, stops, retries, follows or reads a delivery
|
||||
// plan, nor a push: each is a row with no input whose description refuses it by name and names its replacement, and
|
||||
// each is in the removed list the holder answers from. upgrade keeps its policy and drops the plan's backlog.
|
||||
func TestTheControllerSeatHasNoPlanVerbs(t *testing.T) {
|
||||
rows := map[string]Verb{}
|
||||
for _, v := range ControllerVerbs {
|
||||
rows[v.Name] = v
|
||||
}
|
||||
gone := []string{"plans", "plan", "push", "deliver", "delivery-stop", "delivery-backups-again", "delivery-walks", "delivery-order"}
|
||||
for _, name := range gone {
|
||||
v, ok := rows[name]
|
||||
if !ok {
|
||||
t.Errorf("%s is not in the table, so a caller is not answered by name", name)
|
||||
continue
|
||||
}
|
||||
replacement, removed := RemovedControllerVerbs[name]
|
||||
if !removed || replacement == "" {
|
||||
t.Errorf("%s is not in the removed list with its replacement", name)
|
||||
}
|
||||
if !strings.HasPrefix(v.Description, "Removed in R6 (ADR 0539): refused by name") || !strings.Contains(v.Description, replacement) {
|
||||
t.Errorf("%s = %q, want refused by name, naming %q", name, v.Description, replacement)
|
||||
}
|
||||
if props, _ := v.Input["properties"].(map[string]any); len(props) > 0 {
|
||||
t.Errorf("%s still takes %v", name, props)
|
||||
}
|
||||
}
|
||||
props, _ := rows["upgrade"].Input["properties"].(map[string]any)
|
||||
for _, arg := range []string{"together", "backlog", "release-backlog", "cause"} {
|
||||
if _, has := props[arg]; has {
|
||||
t.Errorf("upgrade still takes %s, a plan's", arg)
|
||||
}
|
||||
}
|
||||
for _, arg := range []string{"module", "policy", "why"} {
|
||||
if _, has := props[arg]; !has {
|
||||
t.Errorf("upgrade no longer takes %s", arg)
|
||||
}
|
||||
}
|
||||
for _, word := range []string{"rolled", "gate", "backlog", "push"} {
|
||||
if strings.Contains(rows["upgrade"].Description, word) {
|
||||
t.Errorf("upgrade's description still speaks of %q: %q", word, rows["upgrade"].Description)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rows["bus"].Description, "want") {
|
||||
t.Errorf("bus does not say the operator writes the bus's want: %q", rows["bus"].Description)
|
||||
}
|
||||
for name := range RemovedControllerVerbs {
|
||||
if v := rows[name]; !strings.HasPrefix(v.Description, "Removed in R6") {
|
||||
t.Errorf("%s is in the removed list and its row says %q", name, v.Description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1943/AC4: the delivery seat keeps the merge's verbs and no longer offers times, what-if, release, stop or
|
||||
// retire-history: each is a row with no input, optional so the holder of before and the holder of release 6 both hold
|
||||
// the seat, refusing it by name; mark-ready, served by nobody since issue 1022, is gone.
|
||||
@@ -101,15 +49,3 @@ func TestTheDeliverySeatHasNoPlanVerbs(t *testing.T) {
|
||||
t.Errorf("the gate of release 5 cannot hold the seat: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1943 (for 1945/AC14): the controller's seat says a bucket it deleted at its first start as its event, bucket-deleted
|
||||
// (ADR 0539 clause 13).
|
||||
func TestTheControllerSaysABucketItDeleted(t *testing.T) {
|
||||
seat, ok := SeatNamed(ControllerSeatName)
|
||||
if !ok {
|
||||
t.Fatal("no controller seat")
|
||||
}
|
||||
if !SeatSays(seat.Emits, "bucket-deleted") {
|
||||
t.Errorf("the controller's seat does not emit bucket-deleted: %v", seat.Emits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,21 +68,3 @@ func TestHoldingAMeshSeatRequiresServingItsVerbs(t *testing.T) {
|
||||
t.Fatalf("a holder serving every verb was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The mesh's own verbs are declared in full: an agent cannot call a name without a schema.
|
||||
func TestEveryControllerVerbIsDescribedWithASchema(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, v := range ControllerVerbs {
|
||||
if v.Description == "" || v.Input == nil || v.Input["type"] != "object" {
|
||||
t.Errorf("%s: no description or no object schema", v.Name)
|
||||
}
|
||||
if seen[v.Name] {
|
||||
t.Errorf("%s declared twice", v.Name)
|
||||
}
|
||||
seen[v.Name] = true
|
||||
}
|
||||
seat, _ := SeatNamed(ControllerSeatName)
|
||||
if len(seat.Serves) != len(ControllerVerbs) {
|
||||
t.Fatalf("the compiled mesh-controller seat serves %d verbs, the table has %d", len(seat.Serves), len(ControllerVerbs))
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -8,19 +8,19 @@ import (
|
||||
)
|
||||
|
||||
// Whether the artifact store still holds what a done key names (novox/hq issue 1937). A build module pushes a bundle or
|
||||
// an archive as a bare blob (ADR 0498 §1), and the store's nightly collector deletes every blob no manifest names; only
|
||||
// the controller's take-in and sweep hold one by its manifest. A done key the controller has not taken in can therefore
|
||||
// name a blob that is gone, and reusing it sends a definition every machine fails on. So the builder asks before it
|
||||
// reuses a done key, and the controller says when a take-in found a gone blob.
|
||||
// an archive as a bare blob (ADR 0498 §1), and the store's nightly collector deletes every blob no manifest names. A done
|
||||
// key can therefore name a blob that is gone, and reusing it sends a definition every machine fails on. So the builder
|
||||
// asks before it reuses a done key, and the artifact sweep says when it found a gone blob. Both are the seat
|
||||
// mesh-artifact-sweep's (novox/hq ADR 0571 §2, ADR 0590): the controller that served them first has ended.
|
||||
const (
|
||||
// HeldVerb is the controller seat's verb the builder asks, as a module's invokes names it: references in, each
|
||||
// HeldVerb is the artifact sweep seat's verb the builder asks, as a module's invokes names it: references in, each
|
||||
// answered held or gone.
|
||||
HeldVerb = "seat:mesh-controller.held"
|
||||
// ArtifactsGoneEvent is the controller seat's event said when a take-in is refused because the store does not hold
|
||||
// an artifact the key names: as a consumer names it, `mesh-controller.artifacts-gone`.
|
||||
HeldVerb = "seat:mesh-artifact-sweep.held"
|
||||
// ArtifactsGoneEvent is the artifact sweep seat's event said when the store does not hold an artifact a key names:
|
||||
// as a consumer names it, `mesh-artifact-sweep.artifacts-gone`.
|
||||
ArtifactsGoneEvent = "artifacts-gone"
|
||||
// ArtifactsGoneSubject is where it is published (the seat's event subject).
|
||||
ArtifactsGoneSubject = "mesh.seat.mesh-controller.event." + ArtifactsGoneEvent
|
||||
ArtifactsGoneSubject = "mesh.seat.mesh-artifact-sweep.event." + ArtifactsGoneEvent
|
||||
)
|
||||
|
||||
// HeldAnswer is the answer of HeldVerb: the references the store holds, and those it answered 404 for. A reference the
|
||||
@@ -69,7 +69,7 @@ func DecodeHeldAnswer(raw []byte, asked []string) (HeldAnswer, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ArtifactsGone is the body of ArtifactsGoneEvent: the module whose builder's key the controller would not take in,
|
||||
// ArtifactsGone is the body of ArtifactsGoneEvent: the module whose builder's key names a gone artifact,
|
||||
// the key and its fingerprint, and every reference of it the store does not hold.
|
||||
type ArtifactsGone struct {
|
||||
Event string `json:"event"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -55,30 +56,13 @@ func TestArtifactsGoneRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The verb and the event are on the controller seat's contract under the names the builder uses.
|
||||
func TestHeldOnTheControllerSeat(t *testing.T) {
|
||||
if HeldVerb != "seat:"+catalogue.ControllerSeatName+".held" {
|
||||
t.Fatalf("HeldVerb %q", HeldVerb)
|
||||
// The verb and the event are on the artifact sweep seat's contract under the names the builder uses (ADR 0571 §2).
|
||||
func TestHeldOnTheArtifactSweepSeat(t *testing.T) {
|
||||
seat, ok := catalogue.SeatNamed(catalogue.ArtifactSweepSeat)
|
||||
if !ok || HeldVerb != "seat:"+seat.Name+".held" || ArtifactsGoneSubject != "mesh.seat."+seat.Name+".event."+ArtifactsGoneEvent {
|
||||
t.Fatalf("HeldVerb %q, ArtifactsGoneSubject %q", HeldVerb, ArtifactsGoneSubject)
|
||||
}
|
||||
found := false
|
||||
for _, v := range catalogue.ControllerVerbs {
|
||||
if v.Name == "held" {
|
||||
found = true
|
||||
}
|
||||
if !slices.ContainsFunc(seat.Serves, func(v catalogue.Verb) bool { return v.Name == "held" }) || !slices.Contains(seat.Emits, ArtifactsGoneEvent) {
|
||||
t.Fatalf("the sweep seat serves %v and emits %v", seat.Serves, seat.Emits)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("the controller seat declares no verb held")
|
||||
}
|
||||
for _, s := range catalogue.DefaultSeats() {
|
||||
if s.Name != catalogue.ControllerSeatName {
|
||||
continue
|
||||
}
|
||||
for _, e := range s.Emits {
|
||||
if e == ArtifactsGoneEvent {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("the controller seat emits %v, not %s", s.Emits, ArtifactsGoneEvent)
|
||||
}
|
||||
t.Fatal("no controller seat")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user