diff --git a/go/catalogue/artifact_sweep_seat.go b/go/catalogue/artifact_sweep_seat.go new file mode 100644 index 0000000..cf8cefc --- /dev/null +++ b/go/catalogue/artifact_sweep_seat.go @@ -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 / (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:////blobs/sha256:: 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"})}, + } +} diff --git a/go/catalogue/control_records_seats.go b/go/catalogue/control_records_seats.go new file mode 100644 index 0000000..5d30fd0 --- /dev/null +++ b/go/catalogue/control_records_seats.go @@ -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"})}, + } +} diff --git a/go/catalogue/data_test.go b/go/catalogue/data_test.go index 9511cda..979c552 100644 --- a/go/catalogue/data_test.go +++ b/go/catalogue/data_test.go @@ -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. diff --git a/go/catalogue/engine_seat.go b/go/catalogue/engine_seat.go index b3b695b..b924506 100644 --- a/go/catalogue/engine_seat.go +++ b/go/catalogue/engine_seat.go @@ -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")}, } } diff --git a/go/catalogue/health_seats.go b/go/catalogue/health_seats.go new file mode 100644 index 0000000..79ae2c6 --- /dev/null +++ b/go/catalogue/health_seats.go @@ -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 .: 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"})}, + } +} diff --git a/go/catalogue/logs.go b/go/catalogue/logs.go index 7856b8c..d20aa1c 100644 --- a/go/catalogue/logs.go +++ b/go/catalogue/logs.go @@ -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 diff --git a/go/catalogue/logs_test.go b/go/catalogue/logs_test.go index 76e7ee0..cbe86d4 100644 --- a/go/catalogue/logs_test.go +++ b/go/catalogue/logs_test.go @@ -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)) diff --git a/go/catalogue/manifest.go b/go/catalogue/manifest.go index b40dbad..0c96848 100644 --- a/go/catalogue/manifest.go +++ b/go/catalogue/manifest.go @@ -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)) diff --git a/go/catalogue/placement_seat.go b/go/catalogue/placement_seat.go index c400d08..7fa69de 100644 --- a/go/catalogue/placement_seat.go +++ b/go/catalogue/placement_seat.go @@ -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.", diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 56637b8..e8e68b0 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -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. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index 9113bd7..94dba0c 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -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