From 160912280c9c2d7e4895bd8cc5f74f5abe4abc14 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 13 Nov 2026 00:12:06 +0100 Subject: [PATCH 1/4] S1 sdk-if-config-word: the resource key if-config is a reader capability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A resource carrying `if-config` needs `word:resources.if-config`, which ManifestReads states. Builds: design 42 zfs-owns-zfs §4, §2; answer A-13 --- go/catalogue/manifest_reads.go | 11 +++++++++++ go/catalogue/own_manifest_test.go | 8 ++++++++ go/catalogue/terminal_keys.go | 6 ++++++ 3 files changed, 25 insertions(+) diff --git a/go/catalogue/manifest_reads.go b/go/catalogue/manifest_reads.go index fb9add96..805bf130 100644 --- a/go/catalogue/manifest_reads.go +++ b/go/catalogue/manifest_reads.go @@ -38,6 +38,9 @@ const ( var referenceForms = []string{"access", "backup", "bound", "collected", "config", "consumer", "consumers", "contribution", "dir", "machine", "port", "seat", "secret", "setting"} +// ifConfigWord is the reader capability for a resource's `if-config` (IfConfigField). +const ifConfigWord = ReadWord + "resources." + IfConfigField + // ReferenceFormSeat names the reference form a registry seat answers for a build's artifact, // `//@sha256:` (novox/hq ADR 0605 §9; the registry clients' reference forms), as the reader capability // `form:seat-reference` a node-engine built with this contract states (ADR 0618 §1, ADR 0624 §1): the builder writes it @@ -106,6 +109,7 @@ func ManifestReads() []string { set[ReadForm+f] = true } set[ReadForm+ReferenceFormSeatReach] = true + set[ifConfigWord] = true for _, s := range Seats() { set[ReadSeat+s.Name] = true } @@ -171,6 +175,13 @@ func ManifestNeeds(raw []byte) ([]string, error) { if claimsAtMesh(top, storeHolderSeat) { used[ReadForm+ReferenceFormSeatReach] = true } + for _, r := range asList(top["resources"]) { + if obj, ok := r.(map[string]any); ok { + if _, has := obj[IfConfigField]; has { + used[ifConfigWord] = true + } + } + } if _, has := top[ConfigField]; has || takesConfigKey.Match(raw) { used[ReadForm+"config"] = true } diff --git a/go/catalogue/own_manifest_test.go b/go/catalogue/own_manifest_test.go index dae109f6..602cfd58 100644 --- a/go/catalogue/own_manifest_test.go +++ b/go/catalogue/own_manifest_test.go @@ -96,6 +96,11 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { {"a template naming a seat's reach", `{"module":"d","version":"1","resources":[{"id":"daemon","type":"file",` + `"path":"/etc/d.json","template":"files/daemon.json"}]}`, []string{"form:seat", "form:seat-reach"}, []string{"form:seat-reach"}}, + // A resource under a config condition (design 42 zfs-owns-zfs §4, A-13). + {"a resource under if-config", `{"module":"z","version":"1","config":{"k":{"type":"string"}},"resources":[{"id":"f","type":"file",` + + `"if-config":["k"]}]}`, []string{"word:resources.if-config"}, []string{"word:resources.if-config"}}, + {"a resource without if-config", `{"module":"z","version":"1","resources":[{"id":"f","type":"file"}]}`, + []string{"word:resources"}, nil}, {"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil}, } { templates := map[string]map[string]string{ // the templates' text a row's module names @@ -119,6 +124,9 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { 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 slices.Contains(needs, "word:resources.if-config") != (row.why == "a resource under if-config") { + t.Errorf("%s: needs %v, if-config read wrongly", 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) } diff --git a/go/catalogue/terminal_keys.go b/go/catalogue/terminal_keys.go index fc3ff722..02d7ad88 100644 --- a/go/catalogue/terminal_keys.go +++ b/go/catalogue/terminal_keys.go @@ -12,6 +12,12 @@ import ( // catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly. const TrustedField = "trusted" +// IfConfigField is the key any resource may carry to be declared only while a config value of its module holds +// (novox/hq design 42 zfs-owns-zfs §2, §4): a list of conditions, `` or `=`. Said in the catalogue and +// read by the node-engine, which states it as the reader capability `word:resources.if-config` (ManifestReads); a +// manifest whose resource carries it needs that capability (ManifestNeeds). +const IfConfigField = "if-config" + // TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and // accesses, every key its provisions serve and every setting a served value asks for, every setting a file // asks for unless it says `"trusted": false`, and every setting a directory's owner asks for. Places and accesses first, then the rest sorted. -- 2.54.0 From b54d20ac5a732611c0827c91d9eb02ed9d4f9c90 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 13 Nov 2026 00:15:16 +0100 Subject: [PATCH 2/4] S2 sdk-config-forms: the forms config-lines and config-json are reader capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit referenceForms gains config-lines and config-json; textNeeds also finds the exact token ${config-json}, which holds no colon. Builds: design 42 zfs-owns-zfs §4, §3, §9; answers A-13, A-15, A-24, A-25 --- go/catalogue/manifest_reads.go | 11 +++++++++-- go/catalogue/own_manifest_test.go | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/go/catalogue/manifest_reads.go b/go/catalogue/manifest_reads.go index 805bf130..ebc27b7d 100644 --- a/go/catalogue/manifest_reads.go +++ b/go/catalogue/manifest_reads.go @@ -35,12 +35,16 @@ const ( // referenceForms are the reference forms a manifest's values may hold, `${:…}`, 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"} +var referenceForms = []string{"access", "backup", "bound", "collected", "config", "config-json", "config-lines", + "consumer", "consumers", "contribution", "dir", "machine", "port", "seat", "secret", "setting"} // ifConfigWord is the reader capability for a resource's `if-config` (IfConfigField). const ifConfigWord = ReadWord + "resources." + IfConfigField +// configJSONToken is the reference form `${config-json}` whole: it holds no colon, so textNeeds's search for `${
:` +// does not find it (design 42 zfs-owns-zfs, answer A-25). +const configJSONToken = "${config-json}" + // ReferenceFormSeat names the reference form a registry seat answers for a build's artifact, // `//@sha256:` (novox/hq ADR 0605 §9; the registry clients' reference forms), as the reader capability // `form:seat-reference` a node-engine built with this contract states (ADR 0618 §1, ADR 0624 §1): the builder writes it @@ -448,6 +452,9 @@ func textNeeds(text []byte, used map[string]bool) { used[ReadForm+f] = true } } + if bytes.Contains(text, []byte(configJSONToken)) { + used[ReadForm+"config-json"] = true + } if ofSeatReach.Match(text) { used[ReadForm+ReferenceFormSeatReach] = true } diff --git a/go/catalogue/own_manifest_test.go b/go/catalogue/own_manifest_test.go index 602cfd58..5ef0d73a 100644 --- a/go/catalogue/own_manifest_test.go +++ b/go/catalogue/own_manifest_test.go @@ -101,10 +101,20 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { `"if-config":["k"]}]}`, []string{"word:resources.if-config"}, []string{"word:resources.if-config"}}, {"a resource without if-config", `{"module":"z","version":"1","resources":[{"id":"f","type":"file"}]}`, []string{"word:resources"}, nil}, + // The template forms of the config file and the option lines (design 42 zfs-owns-zfs §3, §9; A-13, A-15, A-25). + {"a template writing config lines", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/o.conf"}]}`, + []string{"form:config-lines"}, []string{"form:config-lines"}}, + {"a template writing the config as json", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/c.json"}]}`, + []string{"form:config-json"}, []string{"form:config-json"}}, + {"a template of the config form alone", `{"module":"z","version":"1","resources":[{"id":"f","type":"file","template":"files/k"}]}`, + []string{"form:config"}, nil}, {"a word this contract does not know", `{"module":"u","version":"1","newer-word":{"inside":1}}`, []string{"word:module"}, nil}, } { templates := map[string]map[string]string{ // the templates' text a row's module names - "a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`}, + "a template naming a seat's reach": {"files/daemon.json": `{"x":"${seat:container-image-registry:reach}"}`}, + "a template writing config lines": {"files/o.conf": "${config-lines:arc-max:options zfs zfs_arc_max=:mib-bytes}"}, + "a template writing the config as json": {"files/c.json": "${config-json}"}, + "a template of the config form alone": {"files/k": "${config:k}"}, }[row.why] needs, err := ModuleNeeds([]byte(row.raw), templates) if err != nil { @@ -124,6 +134,12 @@ func TestOwnManifestUsesNoWordTheRunningControllerRefuses(t *testing.T) { 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) } + for _, f := range []string{"form:config-lines", "form:config-json"} { + if slices.Contains(needs, f) != (row.why == map[string]string{"form:config-lines": "a template writing config lines", + "form:config-json": "a template writing the config as json"}[f]) { + t.Errorf("%s: needs %v, %s read wrongly", row.why, needs, f) + } + } if slices.Contains(needs, "word:resources.if-config") != (row.why == "a resource under if-config") { t.Errorf("%s: needs %v, if-config read wrongly", row.why, needs) } -- 2.54.0 From f84fe54c6c8367da5e25bb868eb8036c05663d19 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 13 Nov 2026 01:29:59 +0100 Subject: [PATCH 3/4] mesh-sdk: Account.BusSubject names the subject of a template stream (A-41) Builds: design 42 zfs-owns-zfs A-41 --- go/process/account_test.go | 1 + go/process/module.go | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/go/process/account_test.go b/go/process/account_test.go index 2d6e54e0..bb6b2013 100644 --- a/go/process/account_test.go +++ b/go/process/account_test.go @@ -265,6 +265,7 @@ func TestModuleAccountNamesAndDialOptions(t *testing.T) { for _, c := range []struct{ name, got, want string }{ {"event subject", a.EventSubject("put"), "mesh.mod.mesh-delivery-teller.event.put"}, {"own stream subject", a.StreamSubject("forge.x.pull"), "mesh.stream.mesh-delivery-teller.event.forge.x.pull"}, + {"template stream subject", a.BusSubject("events", "snapshot.taken.person"), "mesh.bus.mesh-delivery-teller.events.snapshot.taken.person"}, {"another module's stream subject", ModuleStreamSubject("mesh-delivery-checker", "check"), "mesh.stream.mesh-delivery-checker.event.check"}, {"durable consumer", a.EventConsumerName(), "n1_mesh-delivery-teller"}, } { diff --git a/go/process/module.go b/go/process/module.go index de682222..6e3d4fe8 100644 --- a/go/process/module.go +++ b/go/process/module.go @@ -71,6 +71,12 @@ func (a Account) EventSubject(event string) string { return "mesh.mod." + a.Modu // (mesh-controller internal/broker/modstreams.go ModuleStreamSubject). func (a Account) StreamSubject(event string) string { return ModuleStreamSubject(a.Module, event) } +// BusSubject is where the module's event lands on a stream it asked for by template: mesh.bus..., +// being the stream's local name in the module's `requires` (the bus module's keeper, PlanOf; A-41). +func (a Account) BusSubject(local, event string) string { + return "mesh.bus." + a.Module + "." + local + "." + event +} + // ModuleStreamSubject is where a module's event lands on that module's own stream, for a part that names another // module's stream (the indexer hears the forge bridge's, the checker's) without holding its credential. func ModuleStreamSubject(module, event string) string { -- 2.54.0 From 2895a21e69cd8e1fb851fc387e48221815493467 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 13 Nov 2026 01:47:29 +0100 Subject: [PATCH 4/4] buscalls: StreamPublish says an event on a module's own bound stream (A-42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 42 zfs-owns-zfs §25, §24, lead answer A-42 --- go/buscalls/buscalls.go | 16 ++++++++++++++++ go/buscalls/buscalls_test.go | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 go/buscalls/buscalls_test.go diff --git a/go/buscalls/buscalls.go b/go/buscalls/buscalls.go index 44e007d4..e24f5613 100644 --- a/go/buscalls/buscalls.go +++ b/go/buscalls/buscalls.go @@ -31,6 +31,8 @@ const ( StreamRead = "mesh/stream.read" StreamFill = "mesh/stream.fill" + // StreamPublish says one event on the module's own bound stream (A-42). + StreamPublish = "mesh/stream.publish" WorkQueueSize = "mesh/workqueue.size" WorkQueueDead = "mesh/workqueue.dead" @@ -153,6 +155,20 @@ type StreamPage struct { Last uint64 `json:"last"` } +// StreamPublishRequest says one event on a module's own stream, by the stream's local name: the tool runner publishes +// Body on the binding's subject plus "." plus Event, acknowledged, with ID as the message id (design 42 zfs-owns-zfs, A-42). +type StreamPublishRequest struct { + Stream string `json:"stream"` + Event string `json:"event"` + Body json.RawMessage `json:"body"` + ID string `json:"id,omitempty"` +} + +// StreamPublished is publish's answer: the sequence the stream acknowledged the event at. +type StreamPublished struct { + Sequence uint64 `json:"sequence"` +} + // StreamFilled is fill's answer: the stream's fill and its first and last sequence. type StreamFilled struct { Fill diff --git a/go/buscalls/buscalls_test.go b/go/buscalls/buscalls_test.go new file mode 100644 index 00000000..01cf18d4 --- /dev/null +++ b/go/buscalls/buscalls_test.go @@ -0,0 +1,21 @@ +package buscalls + +import ( + "encoding/json" + "testing" +) + +// A-42: stream.publish is a method of its own, beside read and fill, and its request and answer keep their field names. +func TestStreamPublishIsAMethodWithItsShapes(t *testing.T) { + if StreamPublish != "mesh/stream.publish" || StreamPublish == StreamRead || StreamPublish == StreamFill { + t.Fatalf("method %q", StreamPublish) + } + b, _ := json.Marshal(StreamPublishRequest{Stream: "events", Event: "scrub.requested", Body: json.RawMessage(`{"a":1}`), ID: "x"}) + if string(b) != `{"stream":"events","event":"scrub.requested","body":{"a":1},"id":"x"}` { + t.Errorf("request %s", b) + } + a, _ := json.Marshal(StreamPublished{Sequence: 7}) + if string(a) != `{"sequence":7}` { + t.Errorf("answer %s", a) + } +} -- 2.54.0