Accept the release's new manifest words and know its new seats by name, meaning nothing new yet, so delivery 1's readers parse the release's catalogue before any part reads them (issue 774)
mesh/delivery merged: source.main moved to novox/mesh-sdk@044ead7db83e
mesh/delivery-group group fix/774-new-words-first merged: every member is merged
mesh/check pass 78c9ec881771 on 307b6234e6bb | unit 9174 run, 546 kept, 51 skipped | integration 9952 run, 659 kept, 60 skipped | over bound: unit, in…

This commit is contained in:
2026-10-30 00:35:35 +01:00
parent 307b6234e6
commit 78c9ec8817
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 package catalogue
import ( import (
"encoding/json"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -45,6 +46,31 @@ func TestADeclaredStoreParsesAndPassesTheCheck(t *testing.T) {
if it, _ := m.DataItem("dumps"); it.BackedUp() { if it, _ := m.DataItem("dumps"); it.BackedUp() {
t.Fatal("a rebuildable item that says none is backed up") 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. // 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 " + "each hand-over, access act and setuid search, open, done or refused, with who asked, the ask and the " +
"operator's warrant. Read-only.", "operator's warrant. Read-only.",
Input: schema(map[string]string{"limit": "at most this many, newest", "id": "one act, by its id"}, nil)}, 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 // MaxMiB is the log's cap in MiB; zero is LogDefaultMiB. When full, the log refuses new entries
// and never drops old ones. // and never drops old ones.
MaxMiB int `json:"max-mib,omitempty"` 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. // Cap is the log's cap in MiB, the default where none is said.
func (l LogDeclaration) Cap() int { func (l LogDeclaration) Cap() int {
if l.MaxMiB == 0 { if l.MaxMiB == 0 {
@@ -37,20 +44,21 @@ func (l LogDeclaration) Cap() int {
return l.MaxMiB 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 { func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
trimmed := bytes.TrimSpace(raw) trimmed := bytes.TrimSpace(raw)
if len(trimmed) > 0 && trimmed[0] == '"' { if len(trimmed) > 0 && trimmed[0] == '"' {
return json.Unmarshal(trimmed, &l.Name) return json.Unmarshal(trimmed, &l.Name)
} }
var full struct { var full struct {
Name string `json:"name"` Name string `json:"name"`
MaxMiB *int `json:"max-mib"` MaxMiB *int `json:"max-mib"`
MaxAgeDays *int `json:"max-age-days"`
} }
dec := json.NewDecoder(bytes.NewReader(trimmed)) dec := json.NewDecoder(bytes.NewReader(trimmed))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()
if err := dec.Decode(&full); err != nil { 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.Name = full.Name
l.MaxMiB = 0 l.MaxMiB = 0
@@ -61,12 +69,19 @@ func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
} }
l.MaxMiB = *full.MaxMiB 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 return nil
} }
// MarshalJSON writes back the short form when there is nothing else to say. // MarshalJSON writes back the short form when there is nothing else to say.
func (l LogDeclaration) MarshalJSON() ([]byte, error) { func (l LogDeclaration) MarshalJSON() ([]byte, error) {
if l.MaxMiB == 0 { if l.MaxMiB == 0 && l.MaxAgeDays == 0 {
return json.Marshal(l.Name) return json.Marshal(l.Name)
} }
type plain LogDeclaration 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}]` { if string(out) != `["changes",{"name":"moves","max-mib":2048}]` {
t.Fatalf("written back as %s", out) 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}`} { 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 { 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) 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":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":-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","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`}, {`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
} { } {
_, err := ParseManifest([]byte(c.manifest)) _, err := ParseManifest([]byte(c.manifest))
+24 -1
View File
@@ -287,8 +287,18 @@ type OfferCredential struct {
Own string `json:"own,omitempty"` Own string `json:"own,omitempty"`
IssuedBy string `json:"issued-by,omitempty"` IssuedBy string `json:"issued-by,omitempty"`
Tool string `json:"tool,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). // IssuedBySoftware says a provision's credential is issued by its provider's software (novox/hq ADR 0305).
const IssuedBySoftware = "software" const IssuedBySoftware = "software"
@@ -1149,6 +1159,10 @@ type Artifact struct {
// success, and was invisible to the launcher (novox/hq 04-ISSUES/142). // success, and was invisible to the launcher (novox/hq 04-ISSUES/142).
Binary string `json:"binary,omitempty"` 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 // 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). // 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. // 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", "%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)) m.Module, m.Instances, InstancesInterchangeable))
} }
for _, offer := range m.Provides { for i, offer := range m.Provides {
p := offer.Name p := offer.Name
if !name.MatchString(p) { if !name.MatchString(p) {
problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", 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 { } else if offer.Credential != nil {
own, declared := m.OwnSecrets[offer.Credential.Own] own, declared := m.OwnSecrets[offer.Credential.Own]
switch { 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 == "": case offer.Credential.Own == "":
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s provides %q with a credential that names no own secret", m.Module, p)) "%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 " + "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.", "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", 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, " + {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 " + "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.", "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 // 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). // 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). // 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{ Serves: []Verb{
{Name: "config", Optional: true, Description: "Set, clear, list or read the history of config values: " + {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 " + "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 " + "with why, validated, and naming each module on each node whose value moves. A trusted value only " +
"from the controller's terminal.", "from the controller's terminal.",
Input: configInput()}, 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 // 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, // 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"}, {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. // 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"}, {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 // 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. // 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. // 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" { if strings.Join(checkerVerbs, ",") != "try,try-read,try-end" {
t.Errorf("the checker seat serves %v", checkerVerbs) 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 // 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. // 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 { 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) verbs = append(verbs, v.Name)
} }
// acts reads one act by its id too (ADR 0563 §9: the seat's tools are its command line). // 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 { if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[len(engine.Serves)-1].Input) 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" { strings.Join(engine.Emits, ",") != "asked,done,refused" {
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits) 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, // 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 // 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. // 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). // 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 { // fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
t.Errorf("the mesh defines %d seats rather than 49; the set is closed, so a change here is "+ // (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()) "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
} }
} }
@@ -127,7 +150,7 @@ func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
} }
func claimed(claims string) []byte { 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 // **The refusal moved, it did not go** (novox/hq ADR 0118, superseding 0110). A module may now