Read the release's new manifest words and seat names, with no change of meaning, so the running parts accept design 47's manifests (issue 774) #132

Merged
mesh-tools merged 1 commits from fix/774-new-words-first into main 2026-10-30 00:10:04 +00:00
11 changed files with 390 additions and 16 deletions
+49
View File
@@ -0,0 +1,49 @@
package catalogue
// **The artifact sweep, out of the controller** (novox/hq ADR 0571 clause 2, issue 1955): what the artifact store keeps
// and lets go of, the seat `mesh-artifact-sweep` (named for its scope, ADR 0121 §1; the operator, 2026-10-29), held
// once for the mesh by artifact-store-tools, serves what the controller served of its store (artifacts, held, mirrors, images, collect) and says `artifacts-gone`. Its keep set is the store's own listing joined
// with every want's fingerprint and each module's five newest builds from the builder's log (ADR 0526 §1); it lets go
// of nothing until the controller's lease names a holder that is no controller.
// ArtifactSweepSeat is the seat artifact-store-tools holds. Not the seat mesh-artifact-store, the store itself held by
// distribution, and not the provision artifact-store it delivers.
const ArtifactSweepSeat = "mesh-artifact-sweep"
// artifactSweepVerbs are the seat's verbs, with the controller's arguments so a caller changes only the seat it asks.
func artifactSweepVerbs() []Verb {
return []Verb{
{Name: "artifacts", Description: "Every artifact the mesh made that lives in its artifact store, each with its " +
"repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five " +
"newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or " +
"unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.",
Input: schema(map[string]string{
"repository": "only this repository's references, as <module>/<artifact> (the counts stay whole)",
"collected": "\"true\": list the collected references too (counted always)",
}, nil, "collected")},
{Name: "held", Description: "Whether the artifact store holds each reference given, asked with one HEAD each: " +
"held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. " +
"Reads only.",
Input: 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 is eligible is let go of. Without confirm a " +
"dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, " +
"then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index " +
"names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 " +
"seconds; what is left is said. Never touches a digest the mesh did not make.",
Input: 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",
"most": "let go of at most this many (500 by default, at most 5000)",
}, nil, "confirm")},
{Name: "mirrors", Description: "The bases the mesh copied into its artifact store, each kept (a kept build stood on " +
"it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on " +
"it: never removed), by what the builder's log says each build stood on. Records nothing by hand.",
Input: schema(map[string]string{}, nil)},
{Name: "images", Description: "Every container image one machine's want and have name, each with the resources " +
"naming it: what the machine keeps when it prunes its images. Reads only.",
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
}
}
+64
View File
@@ -0,0 +1,64 @@
package catalogue
// **The control records, out of the controller** (novox/hq ADR 0565 clause 1, issue 1953): two seats at mesh scope,
// each held once, and the node seat's verb `packages`.
//
// - mesh-wants, held by the assigner (mesh-delivery-assigner): a person's upgrade policy and the bus step (clauses 7,
// 8, 9);
// - mesh-hand-acts, held by the module mesh-hand-acts: the hand-act log and the warrants recorded (clause 10).
//
// Their events are their holders' to say on the seat; their layouts are go/handacts' and go/delivery's.
// The two seats.
const (
WantsSeat = "mesh-wants"
HandActsSeat = "mesh-hand-acts"
)
// controlRecordsDecision is the record that made the two seats.
const controlRecordsDecision = "novox/hq ADR 0565"
// wantsVerbs are the assigner's verbs on mesh-wants (ADR 0565 clauses 7, 8, 9, 15).
func wantsVerbs() []Verb {
return []Verb{
{Name: "upgrade", Description: "Whether each module's new builds wait for a person: its policy (roll, together or record) " +
"and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never " +
"rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; " +
"with policy default, the choice taken back. With compare, every module whose policy differs from the one the " +
"controller writes beside its compositions.",
Input: schema(map[string]string{
"module": "one module",
"policy": "with module: roll, together, record, or default to take a person's choice back",
"why": "with policy: why, required and kept",
"compare": "\"true\": every module whose policy at the assigner differs from the controller's",
}, nil, "compare")},
{Name: "bus", Description: "What a bus upgrade would do: the fingerprint of the bus module each node runs against the " +
"indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: " +
"the step itself is the put on the operator's warrant.",
Input: schema(map[string]string{}, nil)},
}
}
// handActsVerbs are mesh-hand-acts' verbs (ADR 0565 clause 10), as the controller's took them.
func handActsVerbs() []Verb {
return []Verb{
{Name: "hand-act", Description: "Record one act done by hand: what, why, its cause in a word or a condition's kind " +
"(the word a second act for the same reason uses) and the condition it addresses. Who did it is the caller the " +
"runtime proves, never said by the caller. Said as act-recorded.",
Input: schema(map[string]string{
"what": "what was done, in a line",
"why": "why it had to be done by hand",
"cause": "the cause in a word, or a condition's kind",
"condition": "the key of the condition it addressed, if any (optional)",
"verb": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own",
}, []string{"what", "why"})},
{Name: "hand-acts", Description: "What was done by hand lately, over this log and the controller's: who, why and the " +
"cause of each, and the causes recorded twice within a fortnight (a healer is wanted).",
Input: schema(map[string]string{"days": "how many days back (default 14)", "cause": "only this cause",
"who": "only acts whose who contains this", "warrants": "\"true\": every warrant recorded"}, nil, "warrants")},
{Name: "warranted", Description: "Record what a module did on the operator's warrant: who chose, through which channel, " +
"with which proofs and which answer read from the router's own record of that module's ask, never from the " +
"caller; once per ask. Refused for an ask still open, ended without a choice, or not that module's.",
Input: schema(map[string]string{"asker": "the module that asked", "ask": "its ask's id"}, []string{"asker", "ask"})},
}
}
+26
View File
@@ -1,6 +1,7 @@
package catalogue
import (
"encoding/json"
"slices"
"strings"
"testing"
@@ -45,6 +46,31 @@ func TestADeclaredStoreParsesAndPassesTheCheck(t *testing.T) {
if it, _ := m.DataItem("dumps"); it.BackedUp() {
t.Fatal("a rebuildable item that says none is backed up")
}
// Issue 774's new words first: an offer's credential form and an artifact's bus are accepted and change nothing: the
// manifest that says them composes byte-equal to the one that does not, and no form still means a pair.
plain := strings.Replace(declaredStore, `"resources":[`, `"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/x","binary":"x","loads":["x"]}]},"resources":[`, 1)
for _, form := range []string{"pair", "none"} {
worded := strings.Replace(plain, `"scope":"mesh"}]`, `"scope":"mesh","credential":{"form":"`+form+`"}}]`, 1)
worded = strings.Replace(worded, `"loads":["x"]}`, `"loads":["x"],"bus":{"state":["acts"],"removes":["engine-acts"]}}`, 1)
a, b := mustParse(t, plain), mustParse(t, worded)
if b.Provides[0].Credential != nil || b.CredentialFormOf("postgres-database") != a.CredentialFormOf("postgres-database") ||
a.CredentialFormOf("postgres-database") != CredentialFormPair || len(b.Build.Artifacts[0].Bus) == 0 {
t.Fatalf("form %s: offer %+v, form %q against %q", form, b.Provides[0], b.CredentialFormOf("postgres-database"),
a.CredentialFormOf("postgres-database"))
}
b.Build.Artifacts[0].Bus = nil
ra, erra := (Resolution{Node: "anchor", Modules: []Manifest{a}}).Compose(Rendering{})
rb, errb := (Resolution{Node: "anchor", Modules: []Manifest{b}}).Compose(Rendering{})
ja, _ := json.Marshal(ra.Resources)
jb, _ := json.Marshal(rb.Resources)
if erra != nil || errb != nil || string(ja) != string(jb) || len(ra.Resources) == 0 {
t.Fatalf("form %s composes %s against %s (%v, %v)", form, jb, ja, errb, erra)
}
}
if _, err := ParseManifest([]byte(strings.Replace(declaredStore, `"scope":"mesh"}]`, `"scope":"mesh","credential":{"form":"shared"}}]`, 1))); err == nil ||
!strings.Contains(err.Error(), `the form "shared", not "pair" or "none"`) {
t.Fatalf("a form the release does not know: %v", err)
}
}
// Every rule of the section itself, refused at parse in the words of the module.
+6
View File
@@ -43,6 +43,12 @@ func EngineVerbs() []Verb {
"each hand-over, access act and setuid search, open, done or refused, with who asked, the ask and the " +
"operator's warrant. Read-only.",
Input: schema(map[string]string{"limit": "at most this many, newest", "id": "one act, by its id"}, nil)},
// The packages this node's compositions declare (novox/hq ADR 0565 §6, §15): optional until every engine serves it.
{Name: "packages", Optional: true, Description: "The packages this machine's compositions declare, present or absent, " +
"each with the modules that declare it, as the engine keeps them in its key of `packages`; until the controller " +
"hands its lease away, beside the controller's mirror for this machine and their difference. With write, the " +
"key written again from what the engine composed.",
Input: schema(map[string]string{"write": "\"true\": write this machine's key again from what the engine composed"}, nil, "write")},
}
}
+133
View File
@@ -0,0 +1,133 @@
package catalogue
// **The mesh's health, out of the controller** (novox/hq ADR 0564 clause 1, issue 1952): four seats at mesh scope, each
// held once, each holder a process on its own bus credential that writes only its own state.
//
// - mesh-conditions, held by the module mesh-conditions (the condition keeper): the one writer of the open conditions,
// raised from the observers' findings and cleared when they go (clauses 2, 3, 9, 13);
// - mesh-watchdogs, held by the watchman (mesh-delivery-watchman): the signals as keys that expire, its stuck keys and
// presence as findings, and the dead letters from the lease write (clauses 4, 5);
// - mesh-self-check, held by the module mesh-self-check: the probes, every five minutes (clause 6);
// - mesh-healers, held by the module mesh-healers: the healers, acting on the keeper's events (clauses 7, 8).
//
// Their events keep the controller's names and bodies (go/health), so a reader changes only the seat it hears.
// The four health seats.
const (
ConditionsSeat = "mesh-conditions"
WatchdogsSeat = "mesh-watchdogs"
SelfCheckSeat = "mesh-self-check"
HealersSeat = "mesh-healers"
)
// healthDecision is the record that made the four seats.
const healthDecision = "novox/hq ADR 0564"
// conditionsVerbs are the condition keeper's verbs (ADR 0564 clauses 3, 9, 12).
func conditionsVerbs() []Verb {
return []Verb{
{Name: "conditions", Description: "What is wrong with the mesh now: every open condition, urgent first, then oldest, " +
"raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when " +
"the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition " +
"lately, the controller's before the take-over included.",
Input: schema(map[string]string{
"key": "a condition's key: that one whole; with history, only its transitions",
"scope": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module",
"severity": "only urgent, or only warning",
"machine": "only those about this machine",
"history": "\"true\": every raising, change, silence and clearing lately, oldest first",
"days": "with history: how many days back (default 7, at most 90)",
}, nil, "history")},
{Name: "silence", Description: "Stop one condition's messages for a while, a hand act with why: it stays open and in " +
"status, and the silence is kept as its transition in history.",
Input: schema(map[string]string{
"key": "the condition's key",
"for": "how long: 30m, 4h, 2d; at most 7d",
"why": "why, required and recorded",
"cause": "the cause in a word (the condition's kind when absent)",
}, []string{"key", "for", "why"})},
{Name: "compare", Description: "The keeper's open conditions against the controller's: every key open in one and not " +
"the other, or open in both with another severity, each with its reason; the kinds that end with the controller " +
"and the keys held while their module is delivered listed apart and not counted. The take-over waits for it empty.",
Input: schema(map[string]string{}, nil)},
{Name: "take-over", Description: "Ask the operator's Approve to switch who the router, the grants part and " +
"mesh-watcher hear from the controller to the keeper; refused while compare shows a difference. On the warrant " +
"the keeper writes its take-over key.",
Input: schema(map[string]string{"why": "why now, shown to the operator"}, []string{"why"})},
{Name: "hand-back", Description: "Switch the readers back to the controller's conditions, until the controller's " +
"lease is handed away; refused after it.",
Input: schema(map[string]string{"why": "why, kept with the switch"}, []string{"why"})},
{Name: "resync", Description: "Read every observer's findings again and raise and clear what differs from the open " +
"conditions: the recovery of a keeper that missed a change.",
Input: schema(map[string]string{}, nil)},
}
}
// watchdogsVerbs are the watchman's verbs (ADR 0564 clauses 4, 5, 12).
func watchdogsVerbs() []Verb {
return []Verb{
{Name: "signals", Description: "Every watchdog of the signals table the watchman keeps, each a key that expires unless " +
"its signal renews it: its row, what renews it, its bound, the age of its newest signal, and the finding its " +
"expiry wrote.",
Input: schema(map[string]string{"row": "one row, e.g. S1"}, nil)},
{Name: "stuck", Description: "Every want, build or step the watchman says stopped moving past its bound, with since " +
"when and the words of its finding.",
Input: schema(map[string]string{"machine": "only those on this machine"}, nil)},
{Name: "presence", Description: "Every machine and tool runner the watchman sees connected, with its last word, and " +
"those it says silent; a machine that said it sleeps is shown as sleeping, not silent.",
Input: schema(map[string]string{"machine": "one machine"}, nil)},
{Name: "dead-letters", Description: "Every message a consumer on the bus gave up on, kept in DEAD_LETTERS: whose " +
"consumer, the subject, how often it was handed over and when it was given up, newest first. With id: that one " +
"whole. With deliver or drop, a hand act with why, taken only once the controller's lease is handed away; " +
"before, refused naming the controller's verb.",
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",
"cause": "with deliver or drop: the cause in a word (dead-letter when absent)",
}, nil)},
{Name: "resync", Description: "Rebuild presence from the bus's connections, and the pulses and findings from the keys " +
"the watchman watches, and take every dead-letter notice waiting: the recovery of a watchman that missed a change.",
Input: schema(map[string]string{}, nil)},
}
}
// selfCheckVerbs are the self-check's verbs (ADR 0564 clause 6).
func selfCheckVerbs() []Verb {
return []Verb{
{Name: "doctor", Description: "The self-check: the last run's verdict at once, each probe passed, failed, deferred " +
"(its facts not on the bus yet, named) or could not run, and how long ago. With run, a run now; with probe, one " +
"probe's verdict whole. Runs every five minutes on its own; each failure is a finding.",
Input: schema(map[string]string{
"run": "\"true\": run every probe now and answer the verdict",
"probe": "one probe's id (D6): its verdict alone, with every finding",
}, nil, "run")},
{Name: "probes", Description: "The registry: what each probe asserts, where it reads its facts, and the finding it writes.",
Input: schema(map[string]string{}, nil)},
}
}
// healersVerbs are the healers' verbs (ADR 0564 clauses 7, 8).
func healersVerbs() []Verb {
return []Verb{
{Name: "healers", Description: "The healers: each registered response to one kind of condition, its repair, its " +
"budget and what happens when it is spent; every act taken lately with its outcome, the acts it would have " +
"taken while the controller's lease names a controller, and the mesh-wide brake.",
Input: schema(map[string]string{"days": "how many days of acts back (default 7)"}, nil)},
{Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.",
Input: schema(map[string]string{
"key": "the condition's key",
"why": "why now, kept with the act",
}, []string{"key", "why"})},
{Name: "drill", Description: "Record a drill: something broken on purpose to see the mesh raise and clear it, with " +
"what it tests. A drill is never a repair: no healer is wanted for it however often it is run.",
Input: schema(map[string]string{
"what": "what was done on purpose, in a line",
"why": "what the drill tests",
"condition": "the key of the condition the drill is meant to raise, if any (optional)",
}, []string{"what", "why"})},
}
}
+20 -5
View File
@@ -27,8 +27,15 @@ type LogDeclaration struct {
// MaxMiB is the log's cap in MiB; zero is LogDefaultMiB. When full, the log refuses new entries
// and never drops old ones.
MaxMiB int `json:"max-mib,omitempty"`
// MaxAgeDays is how long an entry is kept, in days; zero is as long as the log (novox/hq ADR 0297). The one way an
// entry leaves a log: the server ages it out, never a delete or a purge (novox/hq ADR 0565 §10: the hand-act log
// keeps every act 400 days; issue 1953 S-age).
MaxAgeDays int `json:"max-age-days,omitempty"`
}
// LogMostAgeDays is the longest a log may keep an entry by age: past it, a log keeps its entries as long as it is.
const LogMostAgeDays = 3650
// Cap is the log's cap in MiB, the default where none is said.
func (l LogDeclaration) Cap() int {
if l.MaxMiB == 0 {
@@ -37,20 +44,21 @@ func (l LogDeclaration) Cap() int {
return l.MaxMiB
}
// UnmarshalJSON reads a log as its bare name, or as {name, max-mib}.
// UnmarshalJSON reads a log as its bare name, or as {name, max-mib, max-age-days}.
func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) > 0 && trimmed[0] == '"' {
return json.Unmarshal(trimmed, &l.Name)
}
var full struct {
Name string `json:"name"`
MaxMiB *int `json:"max-mib"`
Name string `json:"name"`
MaxMiB *int `json:"max-mib"`
MaxAgeDays *int `json:"max-age-days"`
}
dec := json.NewDecoder(bytes.NewReader(trimmed))
dec.DisallowUnknownFields()
if err := dec.Decode(&full); err != nil {
return fmt.Errorf("a log is either a name or {name, max-mib}: %w", typedUnknown(err))
return fmt.Errorf("a log is either a name or {name, max-mib, max-age-days}: %w", typedUnknown(err))
}
l.Name = full.Name
l.MaxMiB = 0
@@ -61,12 +69,19 @@ func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
}
l.MaxMiB = *full.MaxMiB
}
l.MaxAgeDays = 0
if full.MaxAgeDays != nil {
if *full.MaxAgeDays < 1 || *full.MaxAgeDays > LogMostAgeDays {
return fmt.Errorf("log %q: max-age-days is between 1 and %d, not %d", full.Name, LogMostAgeDays, *full.MaxAgeDays)
}
l.MaxAgeDays = *full.MaxAgeDays
}
return nil
}
// MarshalJSON writes back the short form when there is nothing else to say.
func (l LogDeclaration) MarshalJSON() ([]byte, error) {
if l.MaxMiB == 0 {
if l.MaxMiB == 0 && l.MaxAgeDays == 0 {
return json.Marshal(l.Name)
}
type plain LogDeclaration
+7 -1
View File
@@ -24,6 +24,11 @@ func TestAManifestMaySayWhatLogsItKeeps(t *testing.T) {
if string(out) != `["changes",{"name":"moves","max-mib":2048}]` {
t.Fatalf("written back as %s", out)
}
// Issue 774's new words first: max-age-days is accepted and written back; nothing reads it yet.
aged, err := ParseManifest([]byte(`{"module":"a","version":"1","logs":[{"name":"acts","max-age-days":400}]}`))
if out, _ := json.Marshal(aged.Logs); err != nil || aged.Logs[0].MaxAgeDays != 400 || string(out) != `[{"name":"acts","max-age-days":400}]` {
t.Fatalf("max-age-days read as %+v, written %s: %v", aged.Logs, out, err)
}
for _, edge := range []string{`{"name":"a","max-mib":1}`, `{"name":"a","max-mib":8192}`} {
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","logs":[` + edge + `]}`)); err != nil {
t.Errorf("%s is inside the caps and was refused: %v", edge, err)
@@ -42,7 +47,8 @@ func TestALogIsNamedLocallyAndCapped(t *testing.T) {
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":8193}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":-1}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":0}]}`, `max-mib is between 1 and 8192, not 0`},
{`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib}`},
{`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib, max-age-days}`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-age-days":0}]}`, `max-age-days is between 1 and 3650`},
{`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
} {
_, err := ParseManifest([]byte(c.manifest))
+24 -1
View File
@@ -287,8 +287,18 @@ type OfferCredential struct {
Own string `json:"own,omitempty"`
IssuedBy string `json:"issued-by,omitempty"`
Tool string `json:"tool,omitempty"`
// Form is the release's word for an offer that names no own secret and no issuer (novox/hq ADR 0570; issue 774's
// new words first): CredentialFormPair or CredentialFormNone. Accepted and read by nothing yet: an offer that says
// only its form is parsed as one that says no credential, which is a pair, as before.
Form string `json:"form,omitempty"`
}
// The forms an offer's credential may say by itself (OfferCredential.Form).
const (
CredentialFormWordPair = "pair"
CredentialFormWordNone = "none"
)
// IssuedBySoftware says a provision's credential is issued by its provider's software (novox/hq ADR 0305).
const IssuedBySoftware = "software"
@@ -1149,6 +1159,10 @@ type Artifact struct {
// success, and was invisible to the launcher (novox/hq 04-ISSUES/142).
Binary string `json:"binary,omitempty"`
// Bus is the release's word for a tools bundle's own bus account (novox/hq ADR 0564 Limits 4; issue 774's new words
// first): accepted as it is written and read by nothing yet.
Bus json.RawMessage `json:"bus,omitempty"`
// URL is where an archive's upstream file is fetched from, for an archive that is a release
// somebody else published rather than a directory of this repository (novox/hq ADR 0316 §2, §6).
// With it come SHA256 and Version, and From stays empty.
@@ -1926,7 +1940,7 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) {
"%s says its instances are %q; the one word is %q, for a module that is the same on every machine",
m.Module, m.Instances, InstancesInterchangeable))
}
for _, offer := range m.Provides {
for i, offer := range m.Provides {
p := offer.Name
if !name.MatchString(p) {
problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p))
@@ -1979,6 +1993,15 @@ func parseManifest(raw []byte, predefined PredefinedList) (Manifest, error) {
} else if offer.Credential != nil {
own, declared := m.OwnSecrets[offer.Credential.Own]
switch {
case offer.Credential.Own == "" && offer.Credential.IssuedBy == "" && offer.Credential.Tool == "" &&
offer.Credential.Form != "":
// The form alone (issue 774's new words first): pair or none, accepted and meaning what no credential
// meant before, a pair (the offer is read as saying none).
if f := offer.Credential.Form; f != CredentialFormWordPair && f != CredentialFormWordNone {
problems = append(problems, fmt.Sprintf("%s provides %q with the form %q, not %q or %q", m.Module, p, f,
CredentialFormWordPair, CredentialFormWordNone))
}
m.Provides[i].Credential = nil
case offer.Credential.Own == "":
problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential that names no own secret", m.Module, p))
+4 -1
View File
@@ -39,7 +39,10 @@ func placementVerbs() []Verb {
"history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the " +
"proposal's changes from its placement-changed event.",
Input: schema(map[string]string{"node": "one node", "module": "one module: the nodes it is placed on",
"history": "\"true\": the changes in order, from the stream and the state's revisions"}, nil, "history")},
"history": "\"true\": the changes in order, from the stream and the state's revisions",
// ADR 0571 clause 4: who holds which seat on which node, from the state seat-holders.
"seats": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's"},
nil, "history", "seats")},
{Name: PlacementVerbProposals, Optional: true, Description: "Every open proposal of placement: its changes, " +
"the revision of each key it was laid over, its candidates, every node's answer, the differences computed and " +
"answered, its verdict and its ask. Narrowed to one by id.",
+27 -1
View File
@@ -245,13 +245,25 @@ var defaultSeats = append([]Seat{
// The owner of every config value (novox/hq ADR 0348 §9, design 57 K3): its holder alone writes the buckets
// mesh-config_values, _resolved, _predefined and _proposals, and serves the one verb `config` (§1, §10).
// Optional until the mesh-config that runs serves it (design 33 §7).
{Name: "mesh-config", Scope: ScopeMesh, Decision: "novox/hq ADR 0348",
{Name: "mesh-config", Scope: ScopeMesh, Decision: "novox/hq ADR 0348, ADR 0565",
Serves: []Verb{
{Name: "config", Optional: true, Description: "Set, clear, list or read the history of config values: " +
"mesh config or node config for a predefined key, or a module's own value on one node, each set " +
"with why, validated, and naming each module on each node whose value moves. A trusted value only " +
"from the controller's terminal.",
Input: configInput()},
// The switch of config to its own sources (novox/hq ADR 0565 §4): optional until every mesh-config serves it.
{Name: "compare", Optional: true, Description: "Every difference between config's own sources and the " +
"controller's records, each with its reason: a module's declared config against the controller's mirror, " +
"the indexer's predefined list against the mirror's, and each open pull request's config candidate against " +
"what the controller told. The take-over waits for none.",
Input: schema(map[string]string{}, nil)},
{Name: "take-over", Optional: true, Description: "Ask the operator's Approve to switch config to its own " +
"sources (the indexer); refused while compare shows a difference.",
Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})},
{Name: "hand-back", Optional: true, Description: "Switch config back to the controller's mirrors, until the " +
"controller hands its lease away; refused after it.",
Input: schema(map[string]string{"why": "why, in a sentence"}, []string{"why"})},
}},
// The provisioner's server (novox/hq ADR 0349 clause 3, design 57 P4): its holder keeps the registry of which
// domain answers which kind of placeholder, and the buckets mesh-provisioner_needs, _applied and _collect,
@@ -280,6 +292,20 @@ var defaultSeats = append([]Seat{
{Name: PlacementSeat, Scope: ScopeMesh, Serves: placementVerbs(), Decision: "novox/hq ADR 0567"},
// The delivery's checker (novox/hq ADR 0547 §10, issue 774): the try is its verb, not the controller's.
{Name: CheckerSeat, Scope: ScopeMesh, Serves: checkerVerbs(), Decision: "novox/hq ADR 0547"},
// The mesh's health, out of the controller (novox/hq ADR 0564 clause 1, health_seats.go): the condition keeper, the
// watchman's watchdogs, the self-check and the healers.
{Name: ConditionsSeat, Scope: ScopeMesh, Decision: healthDecision,
Emits: []string{"condition-raised", "condition-changed", "condition-cleared"}, Serves: conditionsVerbs()},
{Name: WatchdogsSeat, Scope: ScopeMesh, Decision: healthDecision, Serves: watchdogsVerbs()},
{Name: SelfCheckSeat, Scope: ScopeMesh, Decision: healthDecision, Emits: []string{"checked"}, Serves: selfCheckVerbs()},
{Name: HealersSeat, Scope: ScopeMesh, Decision: healthDecision, Emits: []string{"healer-acted"}, Serves: healersVerbs()},
// The control records, out of the controller (novox/hq ADR 0565 clause 1, control_records_seats.go): a person's
// policy and the bus step at the assigner, and the hand-act log.
{Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()},
{Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()},
// The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go).
{Name: ArtifactSweepSeat, Scope: ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"},
Serves: artifactSweepVerbs()},
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
// They keep their names until that migration is done deliberately, apart from the node-* pass.
+30 -7
View File
@@ -64,6 +64,27 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
if strings.Join(checkerVerbs, ",") != "try,try-read,try-end" {
t.Errorf("the checker seat serves %v", checkerVerbs)
}
// The four health seats (novox/hq ADR 0564 clause 1): at mesh scope, each held once, each naming ADR 0564, with the
// verbs and events the record names; the events keep the controller's names (clause 3).
for _, c := range []struct{ seat, verbs, emits string }{
{ConditionsSeat, "conditions,silence,compare,take-over,hand-back,resync", "condition-raised,condition-changed,condition-cleared"},
{WatchdogsSeat, "signals,stuck,presence,dead-letters,resync", ""},
{SelfCheckSeat, "doctor,probes", "checked"},
{HealersSeat, "healers,heal,drill", "healer-acted"},
// And mesh-config's switch (ADR 0565 §4), beside its config.
{"mesh-config", "config,compare,take-over,hand-back", ""},
// And ADR 0565 clause 1's two.
{WantsSeat, "upgrade,bus", "policy-changed"},
{HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"},
// ADR 0571 clause 2: the artifact store's sweep.
{ArtifactSweepSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"},
} {
s, ok := SeatNamed(c.seat)
if !ok || s.Scope != ScopeMesh || s.Replicated || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 05") ||
strings.Join(VerbNames(s.Serves), ",") != c.verbs || strings.Join(s.Emits, ",") != c.emits {
t.Errorf("the health seat %s is %v, %+v", c.seat, ok, s)
}
}
// The node seat node-engine is the node's engine's (novox/hq ADR 0563 §1): at node scope, delivering nothing, its six
// verbs and three events, held by no assignment, and a module's claim of it refused.
if engine, ok := SeatNamed(EngineSeat); !ok || engine.Scope != ScopeNode || engine.Delivers != "" || !engine.HeldByTheEngine {
@@ -74,10 +95,10 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
verbs = append(verbs, v.Name)
}
// acts reads one act by its id too (ADR 0563 §9: the seat's tools are its command line).
if props, _ := engine.Serves[len(engine.Serves)-1].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[len(engine.Serves)-1].Input)
if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[5].Input)
}
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts" ||
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages" ||
strings.Join(engine.Emits, ",") != "asked,done,refused" {
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits)
}
@@ -99,9 +120,11 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
// mesh-build-machine row goes, when no registered manifest claims it.
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1).
if len(Seats()) != 49 {
t.Errorf("the mesh defines %d seats rather than 49; the set is closed, so a change here is "+
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1);
// fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
// (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2).
if len(Seats()) != 56 {
t.Errorf("the mesh defines %d seats rather than 56; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
}
}
@@ -127,7 +150,7 @@ func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
}
func claimed(claims string) []byte {
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh"}],"claims":` + claims + `}`)
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh","credential":{"form":"none"}}],"claims":` + claims + `}`)
}
// **The refusal moved, it did not go** (novox/hq ADR 0118, superseding 0110). A module may now