Compare commits

..
2 Commits
Author SHA1 Message Date
jschoubben 85ba0a9a70 A node record states the reader capabilities its engine reads, a module value the ones its manifest needs, and a want of rule 9 or 10 reads (ADR 0614 §2, §4, issue 774)
mesh/delivery ready: it is merged on the Approve
mesh/check failed (2): unit: mesh-provisioner-client/cmd/mesh-provisioner-client.TestNatsANodeComingBackFil…; and 1 more | 85ba0a9a70d8 on 52e564845e44
mesh/delivery-group group feat/l3-final rejected: composed together: the checker's verdict at the members' heads (278aa5f0ccbe): unit: mesh-provisioner-client/…
NodeRecord.ManifestReads (`manifest-reads`) and Module.Needs (`needs`) are additive at Version 1, read leniently by
every running part. checkRule takes 1 to 10 so a later release may number the verb wait and the capability hold;
nothing writes 9 or 10 in this one. kept_until_delivered.go stays: this release still builds beside mesh-host 798d73e,
and its head comment names the next mesh-sdk release as the one that removes it.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:18:22 +01:00
jschoubben 73c8f15151 The contract says what a reader of manifests reads, what one manifest's own words, forms and seat names need, and the baseline an engine stating none is read as (ADR 0614 §4, issue 774)
A reader capability is a manifest word by its path of keys, a form (the config form, each reference form) or a seat
name of the contract's table. ManifestReads walks the decoder's fields; ManifestNeeds walks a manifest's raw JSON as
the lenient decoder does, naming only what the contract knows. The baseline is ManifestReads generated once from
mesh-sdk f02f142, the contract every running node-engine is built with: it lacks only tool-runner, core-credentials
and the bus-state words, as rows check.

Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-03 23:16:58 +01:00
11 changed files with 832 additions and 10 deletions
+5 -4
View File
@@ -2,10 +2,11 @@ package catalogue
// This file keeps, deprecated, the exported names feat/l3b-bus removed from this package while a running part still
// uses them: an exported name is removed only after every user of it is delivered. The checker builds the running
// node-engine's composer (mesh-host main) beside this mesh-sdk (ADR 0548 §6), and mesh-host main, mesh-delivery main
// (the indexer's judge, the assigner's and mesh-grants' code and tests) and mesh-tools main (node-tools' tests) name
// them. Nothing in L3's own heads uses any of them. The second group removes this file, and its exemption in
// TestNoCatalogueManifestNamesAmqp, once mesh-host a24a1d72 and L3's mesh-delivery and mesh-tools heads run.
// node-engine's composer (mesh-host main) beside this mesh-sdk (ADR 0548 §6), and mesh-host main names them
// (RuntimeModule, CoreCredentialModules, FromBus). Nothing in the final L3 release's heads uses any of them, but that
// release still builds beside mesh-host 798d73e, so it keeps this file. The next release that changes mesh-sdk, once
// the final L3 release's node-engine runs on every node, removes this file and its exemption in
// TestNoCatalogueManifestNamesAmqp.
import (
"fmt"
+394
View File
@@ -0,0 +1,394 @@
package catalogue
import (
"bytes"
_ "embed"
"encoding/json"
"reflect"
"slices"
"sort"
"strings"
)
// A reader capability is one thing of a manifest a reader of manifests (a node-engine) can read (novox/hq ADR 0614 §4):
// a manifest word, a form, or a seat name of the shared contract it is built with. Each is a string of one of three
// kinds:
//
// - `word:<path>`, a key the lenient decoder reads, by its path of keys from the top of the manifest: `word:claims`,
// `word:claims.serves`. The key of a map (a setting's name, a requirement's) is no part of a path, and an entry of
// a list is read as the list: `word:settings.default`, `word:provides.credential`.
// - `form:<name>`, a form a manifest may be written in: the config form (`form:config`, ADR 0348 §1) and each
// reference form a value may hold, `${<name>:…}` (`form:setting`, `form:dir`).
// - `seat:<name>`, a seat of the contract's seat table, as a manifest names one in a claim, a use, an invoke or a
// bucket's share.
//
// ManifestReads is what this contract reads; ManifestNeeds what one manifest's own words, forms and seat names need;
// BaselineManifestReads what a node-engine that states none is read as stating. What a module's build reads once it
// runs is never asked.
const (
ReadWord = "word:"
ReadForm = "form:"
ReadSeat = "seat:"
)
// referenceForms are the reference forms a manifest's values may hold, `${<name>:…}`, as this contract fills them. A new
// reference form is added here in the change that teaches the contract to read it, so a node-engine built with it
// states it (ADR 0612 §2's new reference form is one).
var referenceForms = []string{"access", "backup", "bound", "collected", "config", "consumer", "consumers",
"contribution", "dir", "machine", "port", "seat", "secret", "setting"}
// manifestTopWords are the top-level keys Manifest.UnmarshalJSON reads apart from the fields of manifestFields: each
// read whole, its inside a map of the module's own names (requirements, local names) and values, no words of the
// contract's beneath it. `requires` and `config` are read as their fields are (manifestNeeds).
var manifestTopWords = []string{"secrets", "contributes", OptionalRequiresKey, RequireSharesKey}
// wireKeys are the keys of a block whose decoder reads its object by keys of its own, not by its fields' tags (its
// UnmarshalJSON): the keys as that decoder reads them, none of them a block.
var wireKeys = map[reflect.Type][]string{
reflect.TypeOf(OfferIdentity{}): {"max", "in"},
reflect.TypeOf(DataBackup{}): {"dump", "into"},
reflect.TypeOf(OwnSecret{}): {"path", "taken", "issued-by", "prompt", "owner"},
}
// ManifestReads is every reader capability of this contract, sorted: what a node-engine built with it states in its
// node record (delivery.NodeRecord.ManifestReads).
func ManifestReads() []string {
set := map[string]bool{}
for _, w := range contractWords() {
set[ReadWord+w] = true
}
for _, f := range referenceForms {
set[ReadForm+f] = true
}
for _, s := range DefaultSeats() {
set[ReadSeat+s.Name] = true
}
return sortedSet(set)
}
// baselineManifestReads is the baseline, generated once from mesh-sdk f02f142 (the contract every node-engine ran
// before any stated what it reads) by ManifestReads as this file computes it: one reader capability a line.
//
//go:embed manifest_reads_baseline.txt
var baselineManifestReads string
// BaselineManifestReads is what a node-engine that states no reader capability is read as stating (novox/hq ADR 0614
// §4): the words, forms and seat names of the shared contract as it stood before any node-engine stated them.
func BaselineManifestReads() []string {
var out []string
for _, line := range strings.Split(baselineManifestReads, "\n") {
if line = strings.TrimSpace(line); line != "" {
out = append(out, line)
}
}
return out
}
// StatedManifestReads is what a node-engine's statement is read as: stated itself, or the baseline when it states none
// (an engine built before the statement existed).
func StatedManifestReads(stated []string) []string {
if len(stated) == 0 {
return BaselineManifestReads()
}
return stated
}
// MissingManifestReads is each of needs that stated, read as StatedManifestReads reads it, lacks: sorted, nil when it
// lacks none.
func MissingManifestReads(needs, stated []string) []string {
have := map[string]bool{}
for _, s := range StatedManifestReads(stated) {
have[s] = true
}
var missing []string
for _, n := range needs {
if !have[n] {
missing = append(missing, n)
}
}
sort.Strings(missing)
return slices.Compact(missing)
}
// ManifestNeeds is every reader capability a manifest's own words, forms and seat names need, sorted, read as the
// lenient decoder walks the manifest: only what this contract knows is named, since what it does not know is set aside
// (Manifest.SetAside). An error is only bytes that are no JSON object.
func ManifestNeeds(raw []byte) ([]string, error) {
var top map[string]any
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
if err := dec.Decode(&top); err != nil {
return nil, err
}
used := map[string]bool{}
for _, f := range referenceForms {
if bytes.Contains(raw, []byte("${"+f+":")) {
used[ReadForm+f] = true
}
}
if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) {
used[ReadForm+"config"] = true
}
words := map[string]bool{}
fields := structFields(reflect.TypeOf(manifestFields{}))
for _, k := range sortedAnyKeys(top) {
v := top[k]
switch {
case k == ConfigField:
// The config form is the settings form's words under another key (config.go).
words[SettingsField] = true
walkValue(v, fields[SettingsField].Type, SettingsField, words)
case k == "requires":
words[k] = true
for _, e := range asList(v) {
obj, isObj := e.(map[string]any)
if !isObj {
continue
}
if _, bus := obj["storage"]; bus {
// A bus-state entry of `requires` (BusStateEntry): the stored form's `bus-state`.
words[BusStateField] = true
walkValue(obj, reflect.TypeOf(BusStateEntry{}), BusStateField, words)
continue
}
walkValue(obj, reflect.TypeOf(requirementForm{}), k, words)
}
case slices.Contains(manifestTopWords, k):
words[k] = true
default:
if f, known := fields[k]; known {
words[k] = true
walkValue(v, f.Type, k, words)
}
}
}
for w := range words {
used[ReadWord+w] = true
}
for _, s := range seatNamesUsed(top) {
used[ReadSeat+s] = true
}
return sortedSet(used), nil
}
// seatNamesUsed is each seat of the contract's table the manifest names: in a claim, a use, an invoke of a seat's verb,
// and a bucket's share with a seat.
func seatNamesUsed(top map[string]any) []string {
known := map[string]bool{}
for _, s := range DefaultSeats() {
known[s.Name] = true
}
set := map[string]bool{}
add := func(name string) {
if known[name] {
set[name] = true
}
}
for _, c := range asList(top["claims"]) {
if obj, ok := c.(map[string]any); ok {
name, _ := obj["name"].(string)
add(name)
}
}
for _, u := range asList(top["uses"]) {
name, _ := u.(string)
add(name)
}
for _, i := range asList(top["invokes"]) {
text, _ := i.(string)
if seat, _, ok := SeatInvoke(text); ok {
add(seat)
}
}
for _, r := range asList(top["requires"]) {
obj, _ := r.(map[string]any)
for _, share := range asList(obj["shares"]) {
text, _ := share.(string)
if seat, ok := strings.CutPrefix(text, "seat:"); ok {
add(seat)
}
}
}
return sortedSet(set)
}
// contractWords is every word of this contract, by path (ReadWord): the fields of manifestFields, the keys the
// manifest's own decoder reads beside them, and the long form of a requirement.
func contractWords() []string {
set := map[string]bool{}
walkType(reflect.TypeOf(manifestFields{}), "", set, map[reflect.Type]bool{})
set["requires"] = true
walkType(reflect.TypeOf(requirementForm{}), "requires", set, map[reflect.Type]bool{})
for _, w := range manifestTopWords {
set[w] = true
}
return sortedSet(set)
}
// walkType adds every word of t at path to set: a struct's fields by their keys, a list's and a map's elements as
// themselves; onPath stops a type that holds itself.
func walkType(t reflect.Type, path string, set map[string]bool, onPath map[reflect.Type]bool) {
t = deref(t)
if keys, custom := wireKeys[t]; custom {
for _, k := range keys {
set[joinWord(path, k)] = true
}
return
}
switch t.Kind() {
case reflect.Struct:
if onPath[t] {
return
}
onPath[t] = true
defer delete(onPath, t)
for name, f := range structFields(t) {
word := joinWord(path, name)
set[word] = true
walkType(f.Type, word, set, onPath)
}
case reflect.Slice, reflect.Array, reflect.Map:
if t.Elem().Kind() == reflect.Uint8 {
return // bytes: a value, never a block
}
walkType(t.Elem(), path, set, onPath)
}
}
// walkValue adds to used every word of t that v, a value decoded from JSON, has at path.
func walkValue(v any, t reflect.Type, path string, used map[string]bool) {
t = deref(t)
if keys, custom := wireKeys[t]; custom {
obj, _ := v.(map[string]any)
for _, k := range keys {
if _, has := obj[k]; has {
used[joinWord(path, k)] = true
}
}
return
}
switch t.Kind() {
case reflect.Struct:
obj, isObj := v.(map[string]any)
if !isObj {
return // a short form (a name for an offer): no words inside it
}
fields := structFields(t)
for _, k := range sortedAnyKeys(obj) {
f, known := fields[k]
if !known {
continue // set aside by the decoder, so no word of this contract
}
word := joinWord(path, k)
used[word] = true
walkValue(obj[k], f.Type, word, used)
}
case reflect.Slice, reflect.Array:
if t.Elem().Kind() == reflect.Uint8 {
return
}
for _, item := range asList(v) {
walkValue(item, t.Elem(), path, used)
}
case reflect.Map:
obj, _ := v.(map[string]any)
for _, k := range sortedAnyKeys(obj) {
walkValue(obj[k], t.Elem(), path, used)
}
}
}
// structFields are a struct's fields by the key the JSON decoder reads each by: its tag's name, else its own name; an
// untagged embedded struct's fields as the struct's own; never one tagged "-" or unexported.
func structFields(t reflect.Type) map[string]reflect.StructField {
out := map[string]reflect.StructField{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
tag := f.Tag.Get("json")
name, _, _ := strings.Cut(tag, ",")
if name == "-" {
continue
}
if f.Anonymous && name == "" && deref(f.Type).Kind() == reflect.Struct {
for k, inner := range structFields(deref(f.Type)) {
if _, shadowed := out[k]; !shadowed {
out[k] = inner
}
}
continue
}
if !f.IsExported() {
continue
}
if name == "" {
name = f.Name
}
out[name] = f
}
return out
}
// untaggedBlocks are the struct types among the contract's words whose fields carry no key of their own and which
// wireKeys does not name: their words would be the Go names, which no decoder reads. A row keeps it empty.
func untaggedBlocks() []string {
seen := map[reflect.Type]bool{}
var out []string
var walk func(t reflect.Type)
walk = func(t reflect.Type) {
t = deref(t)
if seen[t] {
return
}
seen[t] = true
if _, custom := wireKeys[t]; custom {
return
}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.IsExported() && !f.Anonymous && f.Tag.Get("json") == "" {
out = append(out, t.String())
break
}
}
for _, f := range structFields(t) {
walk(f.Type)
}
case reflect.Slice, reflect.Array, reflect.Map:
walk(t.Elem())
}
}
walk(reflect.TypeOf(manifestFields{}))
walk(reflect.TypeOf(requirementForm{}))
sort.Strings(out)
return out
}
func deref(t reflect.Type) reflect.Type {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t
}
func joinWord(path, key string) string {
if path == "" {
return key
}
return path + "." + key
}
func asList(v any) []any {
list, _ := v.([]any)
return list
}
func sortedSet(set map[string]bool) []string {
out := make([]string, 0, len(set))
for k := range set {
out = append(out, k)
}
sort.Strings(out)
return out
}
+334
View File
@@ -0,0 +1,334 @@
form:access
form:backup
form:bound
form:collected
form:config
form:consumer
form:consumers
form:contribution
form:dir
form:machine
form:port
form:seat
form:secret
form:setting
seat:git
seat:mesh-artifact-store
seat:mesh-artifact-sweep
seat:mesh-broker
seat:mesh-catalog
seat:mesh-conditions
seat:mesh-config
seat:mesh-delivery
seat:mesh-delivery-checker
seat:mesh-dns-resolver
seat:mesh-enrolment
seat:mesh-hand-acts
seat:mesh-healers
seat:mesh-placement
seat:mesh-provisioner
seat:mesh-self-check
seat:mesh-store
seat:mesh-vault
seat:mesh-wants
seat:mesh-watchdogs
seat:node-backup
seat:node-bar
seat:node-build-go
seat:node-build-image
seat:node-build-node
seat:node-clipboard
seat:node-compositor
seat:node-container-runtime
seat:node-display-server
seat:node-display-session
seat:node-engine
seat:node-environment
seat:node-files
seat:node-hostname
seat:node-hotkeys
seat:node-intrusion-prevention
seat:node-launcher
seat:node-lock-screen
seat:node-login-manager
seat:node-login-shell
seat:node-message-bus
seat:node-mounts
seat:node-nfs-server
seat:node-notifier
seat:node-package-manager
seat:node-packet-filter
seat:node-power
seat:node-provisioner
seat:node-reads
seat:node-resolver
seat:node-secret-service
seat:node-service-manager
seat:node-terminal-emulator
seat:node-uplink
seat:npm-package-registry
seat:the-private-network
word:accesses
word:accesses.id
word:accesses.mode
word:accesses.path
word:binds
word:build
word:build.artifacts
word:build.artifacts.binary
word:build.artifacts.bus
word:build.artifacts.bus.asks
word:build.artifacts.bus.emits
word:build.artifacts.bus.invokes
word:build.artifacts.bus.queues
word:build.artifacts.bus.removes
word:build.artifacts.bus.state
word:build.artifacts.compiles
word:build.artifacts.context
word:build.artifacts.context.ref
word:build.artifacts.context.repository
word:build.artifacts.context.seat
word:build.artifacts.entrypoints
word:build.artifacts.env
word:build.artifacts.external
word:build.artifacts.file
word:build.artifacts.from
word:build.artifacts.kind
word:build.artifacts.language
word:build.artifacts.loads
word:build.artifacts.name
word:build.artifacts.names-on-purpose
word:build.artifacts.paths
word:build.artifacts.sha256
word:build.artifacts.system
word:build.artifacts.target
word:build.artifacts.url
word:build.artifacts.version
word:build.artifacts.within
word:build.on
word:build.on.arg
word:build.on.artifact
word:build.on.image
word:build.on.module
word:bundles
word:bundles.binary
word:bundles.bus
word:bundles.bus.asks
word:bundles.bus.emits
word:bundles.bus.invokes
word:bundles.bus.queues
word:bundles.bus.removes
word:bundles.bus.state
word:bundles.digest
word:bundles.entrypoints
word:bundles.env
word:bundles.language
word:bundles.launchers
word:bundles.loads
word:bundles.name
word:bundles.source
word:bus-users
word:capabilities
word:certificate
word:certificate.authority
word:certificate.into
word:claims
word:claims.capabilities
word:claims.kind
word:claims.name
word:claims.renders
word:claims.scope
word:claims.serves
word:collections
word:collections.comment
word:collections.name
word:computed
word:consumes
word:contributes
word:contributions
word:contributions.content
word:contributions.data
word:contributions.if-capability
word:contributions.kind
word:contributions.seat
word:data
word:data.consumers
word:data.consumers.class
word:data.consumers.in
word:data.consumers.why
word:data.kept-by
word:data.kept-by.class
word:data.kept-by.why
word:data.own
word:data.own.active
word:data.own.backup
word:data.own.backup.dump
word:data.own.backup.into
word:data.own.class
word:data.own.id
word:data.own.measure
word:data.own.path
word:data.own.redundancy
word:data.own.why
word:data.own.within
word:emits
word:environment
word:environment.path
word:environment.path.at
word:environment.path.entry
word:environment.variables
word:facts
word:facts.home
word:facts.path
word:facts.shared
word:facts.template
word:filtering
word:filtering.into
word:grants
word:guards
word:handed
word:handed.digest
word:handed.items
word:handed.source
word:handed.templates
word:instances
word:invokes
word:items
word:items.attributes
word:items.collection
word:items.content
word:items.if-capability
word:items.name
word:items.template
word:jailing
word:jailing.filter-into
word:jailing.into
word:jails
word:jails.failregex
word:jails.jail
word:jails.name
word:keeps
word:listens
word:listens.fixed
word:listens.from
word:listens.name
word:listens.port
word:listens.protocol
word:listens.why
word:logs
word:logs.max-age-days
word:logs.max-mib
word:logs.name
word:module
word:moved
word:objects
word:objects.grant
word:objects.grant.config
word:objects.max-mib
word:objects.name
word:optional-requires
word:own-secrets
word:own-secrets.issued-by
word:own-secrets.owner
word:own-secrets.path
word:own-secrets.prompt
word:own-secrets.taken
word:prepares
word:provides
word:provides.answered-by
word:provides.credential
word:provides.credential.form
word:provides.credential.issued-by
word:provides.credential.own
word:provides.credential.tool
word:provides.identity
word:provides.identity.in
word:provides.identity.max
word:provides.keeps-consumer-data
word:provides.name
word:provides.reach
word:provides.scope
word:reads
word:receives
word:replaces
word:requires
word:requires-shares
word:requires.name
word:requires.optional
word:requires.shares
word:requires.shares.methods
word:requires.shares.to
word:resources
word:runs-as
word:runtime-health
word:seats
word:seats.accepts
word:seats.by-caller
word:seats.emits
word:seats.kinded
word:seats.name
word:seats.needs
word:seats.paths
word:seats.proofs
word:seats.records
word:seats.retain-seconds
word:seats.scope
word:seats.serves
word:seats.serves.description
word:seats.serves.input
word:seats.serves.name
word:seats.serves.output
word:seats.serves.replaces
word:secrets
word:secrets-owner
word:serves
word:settings
word:settings.default
word:settings.description
word:settings.enum
word:settings.kind
word:settings.maximum
word:settings.minimum
word:settings.pattern
word:settings.predefined
word:settings.required
word:settings.runtime
word:settings.trusted
word:settings.type
word:settings.why
word:shell
word:shell.code
word:shell.for
word:shell.slot
word:slug
word:state
word:state.history
word:state.key-ttl
word:state.name
word:state.per-machine
word:state.ttl-seconds
word:streams
word:streams.duplicates
word:streams.events
word:streams.grant
word:streams.grant.config
word:streams.grant.events
word:streams.max-age-days
word:streams.max-deliveries
word:streams.max-mib
word:streams.name
word:streams.retention
word:templates
word:tools
word:unhealthy
word:unhealthy.severity
word:unhealthy.why
word:upgrade
word:upgrade.policy
word:upgrade.why
word:uses
word:version
word:waits
word:zone
word:zone.listen
word:zone.name
+68
View File
@@ -2,6 +2,7 @@ package catalogue
import (
"os"
"slices"
"testing"
)
@@ -26,4 +27,71 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) {
t.Errorf("artifact %q names paths %v: the running controller refuses the word until R4e is delivered", a.Name, a.Paths)
}
}
// What a reader reads (novox/hq ADR 0614 §4): the baseline is the contract of mesh-sdk f02f142, which every running
// node-engine was built with, and this contract reads all of it; the words added since are the only ones it lacks.
reads, baseline := ManifestReads(), BaselineManifestReads()
var added []string
for _, r := range reads {
if !slices.Contains(baseline, r) {
added = append(added, r)
}
}
for _, b := range baseline {
if !slices.Contains(reads, b) {
t.Errorf("the baseline's %s is no reader capability of this contract", b)
}
}
for _, want := range []string{"word:tool-runner", "word:core-credentials", "word:bus-state", "word:bus-state.storage"} {
if !slices.Contains(added, want) {
t.Errorf("%s is in the baseline, or not read: added since it are %v", want, added)
}
}
for _, want := range []string{"word:claims.serves", "word:settings.default", "form:config", "form:setting", "seat:mesh-catalog"} {
if !slices.Contains(baseline, want) {
t.Errorf("the baseline lacks %s", want)
}
}
if blocks := untaggedBlocks(); len(blocks) != 0 {
t.Errorf("blocks whose words are their Go names, no decoder's keys: %v (name their keys in wireKeys)", blocks)
}
// What a manifest's own words, forms and seat names need, beyond the baseline.
for _, row := range []struct {
why, raw string
needs []string // among its needs
missing []string // what the baseline lacks of them
}{
{"a tool runner", `{"module":"r","version":"1","tool-runner":true}`, []string{"word:tool-runner"}, []string{"word:tool-runner"}},
{"a manifest of the baseline's words", `{"module":"a","version":"1","claims":[{"name":"mesh-catalog","scope":"mesh",` +
`"serves":["modules"]}],"invokes":["seat:mesh-delivery.show"],"config":{"x":{"type":"string","default":""}},` +
`"resources":[{"id":"f","type":"file","content":"${config:x} ${count:-0}"}],"provides":["plain",{"name":"p",` +
`"identity":{"max":20,"in":"x"}}]}`,
[]string{"word:claims.serves", "word:settings.type", "form:config", "seat:mesh-catalog", "seat:mesh-delivery",
"word:provides.identity.max", "word:resources"}, nil},
{"a bus-state entry of requires", `{"module":"b","version":"1","requires":["mesh-bus",{"name":"jobs","storage":"kv"}]}`,
[]string{"word:requires", "word:bus-state.storage"}, []string{"word:bus-state", "word:bus-state.name", "word:bus-state.storage"}},
{"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil},
} {
needs, err := ManifestNeeds([]byte(row.raw))
if err != nil {
t.Fatalf("%s: %v", row.why, err)
}
for _, n := range row.needs {
if !slices.Contains(needs, n) {
t.Errorf("%s: needs %v, not %s", row.why, needs, n)
}
}
if slices.ContainsFunc(needs, func(n string) bool { return n == "word:newer-word" || n == "form:count" }) {
t.Errorf("%s: needs %v, naming what the contract does not read", row.why, needs)
}
if got := MissingManifestReads(needs, nil); !slices.Equal(got, row.missing) {
t.Errorf("%s: an engine stating nothing lacks %v, want %v", row.why, got, row.missing)
}
if got := MissingManifestReads(needs, reads); len(got) != 0 {
t.Errorf("%s: this contract lacks %v", row.why, got)
}
}
if got := MissingManifestReads([]string{"seat:mesh-catalog", "word:module"}, []string{"word:module"}); !slices.Equal(got, []string{"seat:mesh-catalog"}) {
t.Errorf("an engine stating its own reads is read by them alone: lacks %v", got)
}
}
+8 -2
View File
@@ -146,9 +146,15 @@ const (
RuleProvisions = 8
)
// ruleRead is the highest rule a reader takes (novox/hq ADR 0614 §2, §4): 9 and 10 are kept for the verb wait and the
// capability hold, which a later release gives numbers of their own, so a want of that release reads on every node
// that runs this one. Nothing writes 9 or 10 yet: the verb wait is held under RuleProvisions and the capability hold
// under RuleComposed, each with words naming its edge.
const ruleRead = 10
func checkRule(rule int, of string) error {
if rule < RuleBuildDone || rule > RuleProvisions {
return fmt.Errorf("delivery: %s names the rule %d, which is not one of the eight (1 to 8)", of, rule)
if rule < RuleBuildDone || rule > ruleRead {
return fmt.Errorf("delivery: %s names the rule %d, which is not one of 1 to %d", of, rule, ruleRead)
}
return nil
}
+4 -2
View File
@@ -79,6 +79,8 @@ func TestAssignerValuesRoundTrip(t *testing.T) {
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"},
{Removed: true, Words: "the module left g14"},
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Hold: true, Words: "put by the operator: the bus's user list"},
// A rule a later release numbers (9, 10: the verb wait, the capability hold, ADR 0614) reads here already.
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 10, Words: "the verb x.y it invokes is served by no running build"},
}
for _, w := range wants {
data, err := EncodeWant(w)
@@ -147,7 +149,7 @@ func TestAssignerValuesRefused(t *testing.T) {
{Removed: true, FP: fp2of('a')},
{FP: fp2of('a'), Newest: fp2of('b')},
{FP: fp2of('a'), Newest: fp2of('a'), Rule: 3},
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 9},
{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 11},
{FP: fp2of('a'), Composed: hashOf('1'), Hold: true},
{FP: fp2of('a'), Composed: hashOf('1'), Put: true, Newest: fp2of('b'), Rule: 3},
{Removed: true, Put: true},
@@ -164,7 +166,7 @@ func TestAssignerValuesRefused(t *testing.T) {
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 3, Unexplained: true},
{Want: fp2of('a'), Sent: fp2of('a'), Rule: 3},
{},
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 9},
{Want: fp2of('a'), Sent: fp2of('b'), Rule: 11},
{WantRemoved: true, Want: fp2of('a'), Rule: 1},
} {
if _, err := EncodeDifference(d); err == nil {
+9
View File
@@ -59,6 +59,10 @@ type NodeRecord struct {
// key until the lease is handed away, and the vault's key and authority once it reads them from `signers`. The gate
// before the lease write reads every node's from the bus.
Trusts []string `json:"trusts,omitempty"`
// ManifestReads are the reader capabilities of the shared contract this machine's node-engine is built with
// (catalogue.ManifestReads, novox/hq ADR 0614 §4): the manifest words, forms and seat names it reads. Empty for an
// engine built before it said them, which a reader takes as the baseline (catalogue.StatedManifestReads).
ManifestReads []string `json:"manifest-reads,omitempty"`
}
func (r NodeRecord) check() error {
@@ -70,6 +74,11 @@ func (r NodeRecord) check() error {
return fmt.Errorf("delivery: the record of %s names an empty capability", r.Node)
}
}
for _, c := range r.ManifestReads {
if c == "" {
return fmt.Errorf("delivery: the record of %s names an empty reader capability", r.Node)
}
}
return nil
}
+3 -1
View File
@@ -21,7 +21,8 @@ var secretNamed = regexp.MustCompile(`(?i)(secret|passw|token|api[-_]?key|privat
func TestNodeRecord(t *testing.T) {
hub := NodeRecord{Node: "novox", TunnelKey: "k", Capabilities: []string{"docker"}, SealingKey: "age1x", ServingKey: "pub",
Certificate: "cert", OutwardLinks: []string{"eth0"}, Networks: []catalogue.AttachedNetwork{{Link: "eth0", Network: "192.0.2.0/24"}},
NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"}}
NetworksSaid: true, Contract: 3, Trusts: []string{"controller key sha256:aa", "vault key sha256:bb"},
ManifestReads: []string{"seat:mesh-catalog", "word:tool-runner"}}
raw, err := EncodeNodeRecord(hub)
if err != nil {
t.Fatal(err)
@@ -45,6 +46,7 @@ func TestNodeRecord(t *testing.T) {
for name, r := range map[string]NodeRecord{
"empty capability": {Node: "ace", Capabilities: []string{""}},
"empty reader capability": {Node: "ace", ManifestReads: []string{""}},
"node that is not a name": {Node: "a.b"},
} {
if _, err := EncodeNodeRecord(r); err == nil {
+4
View File
@@ -443,6 +443,10 @@ type Module struct {
// Warnings are the problems of an accepted manifest a stricter judge outgrew (novox/hq ADR 0579 §1): Verdict is then
// empty (accepted), and the watchman raises delivery.ManifestOutgrown until the manifest is mended.
Warnings []string `json:"warnings,omitempty"`
// Needs are the reader capabilities the manifest's own words, forms and seat names need (catalogue.ManifestNeeds,
// novox/hq ADR 0614 §4), written by the judge with its verdict: the assigner holds a node's want while that node's
// engine does not state one of them. Empty in a value written before, which needs nothing.
Needs []string `json:"needs,omitempty"`
}
// VerdictOf is the verdict a modules value carries, or nil when it carries none (taken as accepted, TakeManifestVerdict).
+2
View File
@@ -181,6 +181,8 @@ func TestSourceValuesRoundTrip(t *testing.T) { // 1200/AC3
for _, m := range []Module{
{FP: "src1:abc", Src: 5, FPSrc: 5, Repository: "novox/mesh-sdk", Commit: "cccc"},
{None: "a build that resolves packages when built", Repository: "novox/mesh-sdk", Commit: "cccc"},
// The reader capabilities its manifest needs (ADR 0614 §4), written with the verdict.
{FP: "src1:abc", Repository: "novox/mesh-sdk", Commit: "cccc", Needs: []string{"seat:mesh-catalog", "word:tool-runner"}},
} {
data, err = EncodeModule(m)
check("module", data, err)
+1 -1
View File
@@ -51,7 +51,7 @@ func TestAWantNamesItsComposition(t *testing.T) {
{Want{Removed: true, Composed: hashOf('1')}, "removed want names no"},
{Want{FP: fp2of('a'), Composed: hashOf('1'), Backup: "later"}, `backup answer "later"`},
{Want{Removed: true, Backup: BackupAgain}, "removed want names no"},
{Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 9}, "rule 9"},
{Want{FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 11}, "rule 11"},
}
for _, r := range refusals {
if _, err := EncodeWant(r.w); err == nil || !strings.Contains(err.Error(), r.why) {