From f9c3be614f5d104c4de5099a3f06f68206737d58 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:39:11 +0100 Subject: [PATCH 01/94] =?UTF-8?q?Start=20the=20seat=20contracts'=20reposit?= =?UTF-8?q?ory=20(ADR=200608=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..fca5468 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# mesh-contracts + +The mesh's seat contracts (novox/hq ADR 0608 §5, ADR 0613, ADR 0620; design 26 *A seat's definition*): one +language-neutral definition per seat, and the Go and TypeScript types generated from it. + +Every file under `go/` and `ts/` is written by the generators of the Go and the Node build modules, never by hand. +This repository holds contracts only and no seat: a seat's holder implements its contract. -- 2.54.0 From 166aad0a99eb31f5e110df49f3c1ab8198d881c6 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:41:29 +0100 Subject: [PATCH 02/94] =?UTF-8?q?SD-M1:=20the=20seat=20definition's=20form?= =?UTF-8?q?at=20and=20its=20own=20schema=20(ADR=200608=20=C2=A71,=20design?= =?UTF-8?q?=2026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One seat.json per seat at the whole grain or the seat list's, parts taken in whole, fixtures as data, an index for the seat list's reading order; read strictly, at most 256 KiB. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- format/seat-definition.schema.json | 419 +++++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 format/seat-definition.schema.json diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json new file mode 100644 index 0000000..965beeb --- /dev/null +++ b/format/seat-definition.schema.json @@ -0,0 +1,419 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.novox.be/novox/mesh-contracts/format/seat-definition.schema.json", + "title": "A seat's definition (novox/hq ADR 0608 \u00a71, design 26), format 1", + "description": "One file per seat, seats//seat.json. A whole definition (grain whole) gives every verb's input and answer, every message's body and where its events are kept; one at the seat list's grain (seat-list) holds what mesh-sdk's compiled seat list held (ADR 0613 \u00a72). At most 256 KiB. Read strictly: a field not named here is refused.", + "type": "object", + "additionalProperties": false, + "required": [ + "format", + "seat", + "scope", + "grain", + "decision" + ], + "properties": { + "format": { + "const": 1 + }, + "seat": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*[a-z0-9]$" + }, + "scope": { + "enum": [ + "mesh", + "node" + ] + }, + "grain": { + "enum": [ + "whole", + "seat-list" + ] + }, + "decision": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "holder-declares": { + "type": "boolean", + "description": "the seat is not in the compiled seat list; its holder's manifest declares it" + }, + "delivers": { + "type": "string" + }, + "replicated": { + "type": "boolean" + }, + "pulled-by-kind": { + "type": "boolean" + }, + "yields": { + "type": "boolean" + }, + "held-by-the-engine": { + "type": "boolean" + }, + "includes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": "parts/.json taken in whole" + }, + "verbs": { + "type": "array", + "items": { + "$ref": "#/$defs/verb" + } + }, + "accepts": { + "type": "array", + "items": { + "$ref": "#/$defs/message" + } + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/message" + }, + "description": "published on mesh.seat..event." + }, + "family": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "max-age-days" + ], + "properties": { + "name": { + "type": "string" + }, + "max-age-days": { + "type": "integer", + "minimum": 1 + }, + "max-mib": { + "type": "integer", + "minimum": 1 + } + } + }, + "holder-events": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "subject" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "subject": { + "type": "string" + }, + "family": { + "type": "string" + }, + "body": { + "type": "object", + "description": "a JSON Schema" + } + } + }, + "description": "the holder's own events listed beside the seat, keeping the holder's subjects (ADR 0620 \u00a72)" + }, + "records": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "key": { + "type": "string" + }, + "value": { + "type": "object", + "description": "a JSON Schema" + } + } + } + }, + "provisions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "form" + ], + "properties": { + "name": { + "type": "string" + }, + "form": { + "type": "object", + "description": "a JSON Schema" + } + } + } + }, + "receives": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "dirs": { + "type": "boolean" + }, + "offered": { + "type": "boolean" + }, + "shape": { + "type": "object", + "additionalProperties": false, + "properties": { + "places": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "order": { + "type": "string" + }, + "examples": { + "type": "array", + "items": { + "type": "object" + } + }, + "text": { + "type": "boolean" + } + } + } + } + } + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "pattern": "^/" + }, + "steps-back-for": { + "type": "string" + } + } + } + }, + "defs": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "a JSON Schema" + } + } + }, + "$defs": { + "verb": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "replaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "input": { + "type": "object", + "description": "a JSON Schema" + }, + "answer": { + "type": "object", + "description": "a JSON Schema" + }, + "callers": { + "type": "string" + } + } + }, + "message": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "body": { + "type": "object", + "description": "a JSON Schema" + }, + "by-caller": { + "type": "boolean" + } + } + }, + "part": { + "description": "parts/.json", + "type": "object", + "additionalProperties": false, + "required": [ + "format", + "part" + ], + "properties": { + "format": { + "const": 1 + }, + "part": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": { + "type": "string" + }, + "verbs": { + "type": "array", + "items": { + "$ref": "#/$defs/verb" + } + }, + "events": { + "type": "array", + "items": { + "$ref": "#/$defs/message" + } + }, + "defs": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "a JSON Schema" + } + } + } + }, + "fixtures": { + "description": "seats//fixtures/*.json: data, never functions", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "of", + "given" + ], + "properties": { + "name": { + "type": "string" + }, + "of": { + "type": "string", + "pattern": "^(verb:[a-z][a-z0-9-]*\\.(input|answer)|accept:[a-z][a-z0-9-]*|event:[a-z][a-z0-9-]*|holder-event:[a-z][a-z0-9-]*)$" + }, + "subject": { + "type": "string" + }, + "given": {}, + "wire": {}, + "why": { + "type": "string" + } + } + } + }, + "index": { + "description": "seats/index.json: the seat list's reading order", + "type": "object", + "additionalProperties": false, + "required": [ + "format", + "seats" + ], + "properties": { + "format": { + "const": 1 + }, + "seats": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + } + } +} -- 2.54.0 From 2fade8d603ce7f282c75fbe34da3ccc731040430 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 03/94] =?UTF-8?q?SD-M2a:=20the=20four=20registry=20seats?= =?UTF-8?q?=20whole,=20their=20shared=20verbs=20written=20once=20(design?= =?UTF-8?q?=2051,=20ADR=200605,=20ADR=200611,=20ADR=200617=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publish, publish-part, versions and remove in the part registry; mirror and tags on the image seat; hold and release on the two seats distribution holds; published, refused, removed, held and released on each seat's own family. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- parts/registry.json | 397 ++++++++++++++++++ .../fixtures/events.json | 78 ++++ .../fixtures/verbs.json | 97 +++++ seats/container-image-registry/seat.json | 264 ++++++++++++ .../go-package-registry/fixtures/events.json | 52 +++ seats/go-package-registry/fixtures/verbs.json | 97 +++++ seats/go-package-registry/seat.json | 34 ++ .../npm-package-registry/fixtures/events.json | 52 +++ .../npm-package-registry/fixtures/verbs.json | 97 +++++ seats/npm-package-registry/seat.json | 34 ++ .../fixtures/events.json | 78 ++++ .../oci-artifact-registry/fixtures/verbs.json | 97 +++++ seats/oci-artifact-registry/seat.json | 169 ++++++++ 13 files changed, 1546 insertions(+) create mode 100644 parts/registry.json create mode 100644 seats/container-image-registry/fixtures/events.json create mode 100644 seats/container-image-registry/fixtures/verbs.json create mode 100644 seats/container-image-registry/seat.json create mode 100644 seats/go-package-registry/fixtures/events.json create mode 100644 seats/go-package-registry/fixtures/verbs.json create mode 100644 seats/go-package-registry/seat.json create mode 100644 seats/npm-package-registry/fixtures/events.json create mode 100644 seats/npm-package-registry/fixtures/verbs.json create mode 100644 seats/npm-package-registry/seat.json create mode 100644 seats/oci-artifact-registry/fixtures/events.json create mode 100644 seats/oci-artifact-registry/fixtures/verbs.json create mode 100644 seats/oci-artifact-registry/seat.json diff --git a/parts/registry.json b/parts/registry.json new file mode 100644 index 0000000..af29f85 --- /dev/null +++ b/parts/registry.json @@ -0,0 +1,397 @@ +{ + "format": 1, + "part": "registry", + "description": "The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved.", + "verbs": [ + { + "name": "publish", + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "callers": "the build modules", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + }, + "transfer": { + "type": "string", + "description": "the transfer's name, the caller's own; the same name opens the same transfer again" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/defs/Part" + }, + "description": "every part's digest and size; none when the digest is already held" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ] + }, + "answer": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "lacks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the digests of the parts the holder lacks, to send by publish-part" + }, + "held": { + "type": "boolean", + "description": "the digest is held already: nothing to send; the outcome follows as an event" + } + }, + "required": [ + "transfer" + ] + } + }, + { + "name": "publish-part", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "callers": "the build modules, and only the proved caller that opened the transfer", + "input": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "part": { + "type": "string", + "description": "the part's digest" + }, + "sequence": { + "type": "integer", + "description": "the chunk's place in its part, from 0" + }, + "last": { + "type": "boolean", + "description": "this chunk is the part's last" + }, + "data": { + "type": "string", + "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ] + }, + "answer": { + "type": "object", + "properties": { + "transfer": { + "type": "string", + "description": "the transfer" + }, + "part": { + "type": "string", + "description": "the part" + }, + "received": { + "type": "integer", + "description": "the chunks of the part held so far" + }, + "complete": { + "type": "boolean", + "description": "the part is whole and its bytes match its digest" + } + }, + "required": [ + "transfer", + "part" + ] + } + }, + { + "name": "versions", + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "kind": { + "type": "string", + "description": "narrowed to one kind (optional)" + } + }, + "required": [ + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/defs/Artifact" + }, + "description": "newest first" + } + }, + "required": [ + "name", + "versions" + ] + } + }, + { + "name": "remove", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "callers": "the builder, for a tagged version; the artifact sweep's holder, for retention", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "version": { + "type": "string", + "description": "the version" + }, + "tag": { + "type": "string", + "description": "the tag (for a tagged version)" + }, + "digest": { + "type": "string", + "description": "the digest (for a bundle, archive or copied base)" + }, + "reason": { + "type": "string", + "enum": [ + "tag-gone", + "retention" + ], + "description": "why it goes" + } + }, + "required": [ + "name", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "removed": { + "type": "boolean", + "description": "it is gone" + }, + "artifact": { + "$ref": "#/defs/Artifact" + } + }, + "required": [ + "removed" + ] + } + } + ], + "events": [ + { + "name": "published", + "body": { + "$ref": "#/defs/Outcome" + } + }, + { + "name": "refused", + "body": { + "$ref": "#/defs/Outcome" + } + }, + { + "name": "removed", + "body": { + "$ref": "#/defs/Outcome" + } + } + ], + "defs": { + "Artifact": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "description": "one published thing, as every registry event and answer names it" + }, + "Part": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob" + }, + "size": { + "type": "integer", + "description": "its size in bytes" + } + }, + "required": [ + "digest", + "size" + ], + "description": "one part of a transfer" + }, + "Outcome": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the package's, image's, bundle's or archive's name" + }, + "version": { + "type": "string", + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" + }, + "kind": { + "type": "string", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "description": "what it is" + }, + "tag": { + "type": "string", + "description": "the tag it carries, for a tagged version" + }, + "digest": { + "type": "string", + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" + }, + "fingerprint": { + "type": "string", + "description": "the build's fingerprint" + }, + "commit": { + "type": "string", + "description": "the source commit it was built from" + }, + "holder": { + "type": "string", + "description": "the module holding the seat that said it" + }, + "transfer": { + "type": "string", + "description": "the transfer it ends, when it ended one" + }, + "reference": { + "type": "string", + "description": "the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9)" + }, + "reason": { + "type": "string", + "description": "why: a refusal's cause, or a removal's (retention, tag-gone, by-hand)" + } + }, + "required": [ + "name", + "kind", + "digest", + "holder" + ], + "description": "the body of published, refused and removed" + } + } +} diff --git a/seats/container-image-registry/fixtures/events.json b/seats/container-image-registry/fixtures/events.json new file mode 100644 index 0000000..8bd60c9 --- /dev/null +++ b/seats/container-image-registry/fixtures/events.json @@ -0,0 +1,78 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.container-image-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.container-image-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.container-image-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "held on the seat's subject", + "of": "event:held", + "subject": "mesh.seat.container-image-registry.event.held", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "released on the seat's subject", + "of": "event:released", + "subject": "mesh.seat.container-image-registry.event.released", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + } +] diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json new file mode 100644 index 0000000..2c03cdc --- /dev/null +++ b/seats/container-image-registry/fixtures/verbs.json @@ -0,0 +1,97 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + } +] diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json new file mode 100644 index 0000000..c289aad --- /dev/null +++ b/seats/container-image-registry/seat.json @@ -0,0 +1,264 @@ +{ + "format": 1, + "seat": "container-image-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.", + "delivers": "artifact-store", + "includes": [ + "registry" + ], + "verbs": [ + { + "name": "mirror", + "description": "Copy an outside image into the store by its pinned digest; emits published or refused.", + "callers": "mesh-build-image's holder of the image build seat", + "input": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "the outside image's reference" + }, + "digest": { + "type": "string", + "description": "its pinned digest, sha256:" + }, + "name": { + "type": "string", + "description": "the name it is kept under" + } + }, + "required": [ + "source", + "digest", + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "the reference the seat answers" + }, + "digest": { + "type": "string", + "description": "the digest stored" + } + }, + "required": [ + "reference", + "digest" + ] + } + }, + { + "name": "tags", + "description": "The tags of one image and the digest each names.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the image" + } + }, + "required": [ + "name" + ] + }, + "answer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the image" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tag": { + "type": "string", + "description": "the tag" + }, + "digest": { + "type": "string", + "description": "its digest" + } + }, + "required": [ + "tag", + "digest" + ] + }, + "description": "every tag" + } + }, + "required": [ + "name", + "tags" + ] + } + }, + { + "name": "hold", + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "callers": "the module that runs the artifact sweep alone", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "which of the sweep's rules keeps it" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "held": { + "type": "boolean", + "description": "it is held" + }, + "since": { + "type": "string", + "description": "when the hold began, RFC 3339" + } + }, + "required": [ + "held" + ] + } + }, + { + "name": "release", + "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why it is let go" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "released": { + "type": "boolean", + "description": "the hold is gone" + } + }, + "required": [ + "released" + ] + } + } + ], + "events": [ + { + "name": "held", + "body": { + "$ref": "#/defs/Hold" + } + }, + { + "name": "released", + "body": { + "$ref": "#/defs/Hold" + } + } + ], + "family": { + "name": "container-image-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "artifact-store", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the store's address" + } + }, + "required": [ + "url" + ], + "description": "the artifact store's read side, the OCI distribution protocol's GET and HEAD, open on the mesh network (design 51)" + } + } + ], + "defs": { + "Hold": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why" + }, + "holder": { + "type": "string", + "description": "the module holding the seat" + }, + "by": { + "type": "string", + "description": "the caller the holder proved" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "name", + "digest", + "holder" + ], + "description": "the body of held and released" + } + } +} diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json new file mode 100644 index 0000000..fde91ce --- /dev/null +++ b/seats/go-package-registry/fixtures/events.json @@ -0,0 +1,52 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.go-package-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.go-package-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.go-package-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19", + "reason": "retention" + } + } +] diff --git a/seats/go-package-registry/fixtures/verbs.json b/seats/go-package-registry/fixtures/verbs.json new file mode 100644 index 0000000..338bf36 --- /dev/null +++ b/seats/go-package-registry/fixtures/verbs.json @@ -0,0 +1,97 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + } +] diff --git a/seats/go-package-registry/seat.json b/seats/go-package-registry/seat.json new file mode 100644 index 0000000..edf3998 --- /dev/null +++ b/seats/go-package-registry/seat.json @@ -0,0 +1,34 @@ +{ + "format": 1, + "seat": "go-package-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.", + "delivers": "go-package-registry", + "includes": [ + "registry" + ], + "family": { + "name": "go-package-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "go-package-registry", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the read side's address" + } + }, + "required": [ + "url" + ], + "description": "form none: the Go module proxy's read side, answered anonymously, with no credential (design 51)" + } + } + ] +} diff --git a/seats/npm-package-registry/fixtures/events.json b/seats/npm-package-registry/fixtures/events.json new file mode 100644 index 0000000..f7cca82 --- /dev/null +++ b/seats/npm-package-registry/fixtures/events.json @@ -0,0 +1,52 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.npm-package-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.npm-package-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.npm-package-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "transfer": "t1", + "reference": "mesh-sdk@0.1.19", + "reason": "retention" + } + } +] diff --git a/seats/npm-package-registry/fixtures/verbs.json b/seats/npm-package-registry/fixtures/verbs.json new file mode 100644 index 0000000..338bf36 --- /dev/null +++ b/seats/npm-package-registry/fixtures/verbs.json @@ -0,0 +1,97 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + } +] diff --git a/seats/npm-package-registry/seat.json b/seats/npm-package-registry/seat.json new file mode 100644 index 0000000..3330edd --- /dev/null +++ b/seats/npm-package-registry/seat.json @@ -0,0 +1,34 @@ +{ + "format": 1, + "seat": "npm-package-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0109; novox/hq ADR 0605", + "description": "The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.", + "delivers": "npm-package-registry", + "includes": [ + "registry" + ], + "family": { + "name": "npm-package-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "npm-package-registry", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the read side's address" + } + }, + "required": [ + "url" + ], + "description": "form none: the npm registry's read side, answered anonymously, with no credential (design 51)" + } + } + ] +} diff --git a/seats/oci-artifact-registry/fixtures/events.json b/seats/oci-artifact-registry/fixtures/events.json new file mode 100644 index 0000000..0928933 --- /dev/null +++ b/seats/oci-artifact-registry/fixtures/events.json @@ -0,0 +1,78 @@ +[ + { + "name": "published on the seat's subject", + "of": "event:published", + "subject": "mesh.seat.oci-artifact-registry.event.published", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.oci-artifact-registry.event.refused", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "removed on the seat's subject", + "of": "event:removed", + "subject": "mesh.seat.oci-artifact-registry.event.removed", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "transfer": "t1", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "retention" + } + }, + { + "name": "held on the seat's subject", + "of": "event:held", + "subject": "mesh.seat.oci-artifact-registry.event.held", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "released on the seat's subject", + "of": "event:released", + "subject": "mesh.seat.oci-artifact-registry.event.released", + "given": { + "name": "mesh-sdk", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "reason": "kept by the sweep's rule", + "holder": "distribution", + "by": "mesh-artifact-sweep", + "at": "2026-11-04T12:00:00Z" + } + } +] diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json new file mode 100644 index 0000000..697b2ca --- /dev/null +++ b/seats/oci-artifact-registry/fixtures/verbs.json @@ -0,0 +1,97 @@ +[ + { + "name": "publish opens a transfer", + "of": "verb:publish.input", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "a field the reader does not know is dropped", + "of": "verb:publish.input", + "why": "ADR 0608 §6: a reader ignores a field it does not know", + "given": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ], + "later": { + "x": 1 + } + }, + "wire": { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "transfer": "t1", + "parts": [ + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 1048576 + } + ] + } + }, + { + "name": "publish answers what it lacks", + "of": "verb:publish.answer", + "given": { + "transfer": "t1", + "lacks": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ] + } + }, + { + "name": "a chunk of a part", + "of": "verb:publish-part.input", + "given": { + "transfer": "t1", + "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sequence": 0, + "last": true, + "data": "aGVsbG8=" + } + }, + { + "name": "the versions held", + "of": "verb:versions.answer", + "given": { + "name": "mesh-sdk", + "versions": [ + { + "name": "mesh-sdk", + "version": "0.1.19", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc" + } + ] + } + } +] diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json new file mode 100644 index 0000000..52921d3 --- /dev/null +++ b/seats/oci-artifact-registry/seat.json @@ -0,0 +1,169 @@ +{ + "format": 1, + "seat": "oci-artifact-registry", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0605", + "description": "The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.", + "delivers": "artifact-store", + "includes": [ + "registry" + ], + "verbs": [ + { + "name": "hold", + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "callers": "the module that runs the artifact sweep alone", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "which of the sweep's rules keeps it" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "held": { + "type": "boolean", + "description": "it is held" + }, + "since": { + "type": "string", + "description": "when the hold began, RFC 3339" + } + }, + "required": [ + "held" + ] + } + }, + { + "name": "release", + "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why it is let go" + } + }, + "required": [ + "name", + "digest", + "reason" + ] + }, + "answer": { + "type": "object", + "properties": { + "released": { + "type": "boolean", + "description": "the hold is gone" + } + }, + "required": [ + "released" + ] + } + } + ], + "events": [ + { + "name": "held", + "body": { + "$ref": "#/defs/Hold" + } + }, + { + "name": "released", + "body": { + "$ref": "#/defs/Hold" + } + } + ], + "family": { + "name": "oci-artifact-registry.events", + "max-age-days": 7 + }, + "provisions": [ + { + "name": "artifact-store", + "form": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "the store's address" + } + }, + "required": [ + "url" + ], + "description": "the artifact store's read side, the OCI distribution protocol's GET and HEAD, open on the mesh network (design 51)" + } + } + ], + "defs": { + "Hold": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the name" + }, + "digest": { + "type": "string", + "description": "the digest" + }, + "reason": { + "type": "string", + "description": "why" + }, + "holder": { + "type": "string", + "description": "the module holding the seat" + }, + "by": { + "type": "string", + "description": "the caller the holder proved" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "name", + "digest", + "holder" + ], + "description": "the body of held and released" + } + } +} -- 2.54.0 From 4f37cc2db919e66114d5b6da9897f1bee80df8de Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 04/94] =?UTF-8?q?SD-M2b:=20mesh-broker=20whole:=20its=20ei?= =?UTF-8?q?ght=20verbs,=20nine=20events=20and=20the=20bus-state=20binding?= =?UTF-8?q?=20(design=2025=20=C2=A7=2012.3,=20ADR=200595=20=C2=A75,=20?= =?UTF-8?q?=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- seats/mesh-broker/fixtures/events.json | 213 ++++++++++ seats/mesh-broker/fixtures/verbs.json | 51 +++ seats/mesh-broker/seat.json | 536 +++++++++++++++++++++++++ 3 files changed, 800 insertions(+) create mode 100644 seats/mesh-broker/fixtures/events.json create mode 100644 seats/mesh-broker/fixtures/verbs.json create mode 100644 seats/mesh-broker/seat.json diff --git a/seats/mesh-broker/fixtures/events.json b/seats/mesh-broker/fixtures/events.json new file mode 100644 index 0000000..374448d --- /dev/null +++ b/seats/mesh-broker/fixtures/events.json @@ -0,0 +1,213 @@ +[ + { + "name": "created on the seat's subject", + "of": "event:created", + "subject": "mesh.seat.mesh-broker.event.created", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + }, + { + "name": "refused on the seat's subject", + "of": "event:refused", + "subject": "mesh.seat.mesh-broker.event.refused", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + }, + "why": "the bus refused it" + } + }, + { + "name": "drifted on the seat's subject", + "of": "event:drifted", + "subject": "mesh.seat.mesh-broker.event.drifted", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + }, + "why": "the bus refused it" + } + }, + { + "name": "drift-cleared on the seat's subject", + "of": "event:drift-cleared", + "subject": "mesh.seat.mesh-broker.event.drift-cleared", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + }, + { + "name": "full on the seat's subject", + "of": "event:full", + "subject": "mesh.seat.mesh-broker.event.full", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + }, + { + "name": "full-cleared on the seat's subject", + "of": "event:full-cleared", + "subject": "mesh.seat.mesh-broker.event.full-cleared", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + }, + { + "name": "orphaned on the seat's subject", + "of": "event:orphaned", + "subject": "mesh.seat.mesh-broker.event.orphaned", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + }, + { + "name": "recreated on the seat's subject", + "of": "event:recreated", + "subject": "mesh.seat.mesh-broker.event.recreated", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + }, + "why": "the bus refused it" + } + }, + { + "name": "dropped on the seat's subject", + "of": "event:dropped", + "subject": "mesh.seat.mesh-broker.event.dropped", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "bus-name": "mesh-knowledge_ledger", + "asked": { + "max-mib": 64 + }, + "held": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + }, + "why": "the bus refused it" + } + } +] diff --git a/seats/mesh-broker/fixtures/verbs.json b/seats/mesh-broker/fixtures/verbs.json new file mode 100644 index 0000000..a1d5c41 --- /dev/null +++ b/seats/mesh-broker/fixtures/verbs.json @@ -0,0 +1,51 @@ +[ + { + "name": "create one entry", + "of": "verb:create.input", + "given": { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "options": { + "max-mib": 64 + } + } + }, + { + "name": "drop on the operator's warrant", + "of": "verb:drop.input", + "given": { + "module": "mesh-knowledge", + "local-name": "old", + "warrant": "w-1", + "why": "the operator approved the drop" + } + }, + { + "name": "list answers every entry", + "of": "verb:list.answer", + "given": { + "entries": [ + { + "module": "mesh-knowledge", + "local-name": "ledger", + "storage-type": "ledger", + "template": "ledger", + "options": { + "max-mib": 64 + }, + "bus-name": "mesh-knowledge_ledger", + "settings": { + "max-mib": 64 + }, + "fill": { + "count": 10, + "bytes": 2048, + "cap": 67108864 + } + } + ] + } + } +] diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json new file mode 100644 index 0000000..59df817 --- /dev/null +++ b/seats/mesh-broker/seat.json @@ -0,0 +1,536 @@ +{ + "format": 1, + "seat": "mesh-broker", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0079; novox/hq ADR 0595", + "delivers": "mesh-bus", + "description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.", + "verbs": [ + { + "name": "list", + "description": "Every entry the bus module created, its template, its settings and its fill against its cap.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "answer": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/defs/Held" + }, + "description": "every entry" + } + }, + "required": [ + "entries" + ] + } + }, + { + "name": "show", + "description": "One entry, its template, settings and fill.", + "callers": "any caller whose role grants it", + "input": { + "$ref": "#/defs/Named" + }, + "answer": { + "$ref": "#/defs/Held" + } + }, + { + "name": "check", + "description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "answer": { + "type": "object", + "properties": { + "differences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry" + }, + "asked": { + "type": "object", + "description": "asked" + }, + "held": { + "type": "object", + "description": "held" + }, + "grows": { + "type": "boolean", + "description": "the difference only grows: apply may act on it" + } + }, + "required": [ + "module", + "local-name" + ] + }, + "description": "every difference" + } + }, + "required": [ + "differences" + ] + } + }, + { + "name": "orphans", + "description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": {}, + "description": "no arguments" + }, + "answer": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/defs/Held" + }, + "description": "every orphan" + } + }, + "required": [ + "entries" + ] + } + }, + { + "name": "create", + "description": "Create one entry now.", + "callers": "the bus module's own process only", + "input": { + "$ref": "#/defs/Entry" + }, + "answer": { + "$ref": "#/defs/Held" + } + }, + { + "name": "apply", + "description": "Apply a difference that only grows: a larger cap, a longer age.", + "callers": "the bus module's own process only", + "input": { + "$ref": "#/defs/Named" + }, + "answer": { + "$ref": "#/defs/Held" + } + }, + { + "name": "recreate", + "description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", + "callers": "the bus module's process, on the operator's warrant", + "input": { + "$ref": "#/defs/Warranted" + }, + "answer": { + "$ref": "#/defs/Held" + } + }, + { + "name": "drop", + "description": "Remove one orphan, after a stream snapshot.", + "callers": "the bus module's process, on the operator's warrant", + "input": { + "$ref": "#/defs/Warranted" + }, + "answer": { + "type": "object", + "properties": { + "dropped": { + "type": "boolean", + "description": "it is gone" + }, + "snapshot": { + "type": "string", + "description": "the snapshot taken first" + } + }, + "required": [ + "dropped" + ] + } + } + ], + "events": [ + { + "name": "created", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "refused", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "drifted", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "drift-cleared", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "full", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "full-cleared", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "orphaned", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "recreated", + "body": { + "$ref": "#/defs/Said" + } + }, + { + "name": "dropped", + "body": { + "$ref": "#/defs/Said" + } + } + ], + "family": { + "name": "mesh-broker.events", + "max-age-days": 30, + "max-mib": 256 + }, + "provisions": [ + { + "name": "bus-state", + "form": { + "type": "object", + "properties": { + "bus-name": { + "type": "string", + "description": "the entry's name on the bus" + }, + "storage-type": { + "type": "string", + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "description": "its template" + }, + "subjects": { + "type": "array", + "items": { + "type": "string" + }, + "description": "its subjects, for a stream" + } + }, + "required": [ + "bus-name", + "storage-type" + ], + "description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)" + } + } + ], + "defs": { + "Entry": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + } + }, + "required": [ + "module", + "local-name", + "storage-type" + ], + "description": "one piece of bus state as a manifest asks it" + }, + "Fill": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "messages, keys or objects held" + }, + "bytes": { + "type": "integer", + "description": "bytes held" + }, + "cap": { + "type": "integer", + "description": "its cap in bytes" + } + }, + "required": [ + "count", + "bytes", + "cap" + ] + }, + "Held": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + }, + "bus-name": { + "type": "string", + "description": "its name on the bus, _" + }, + "settings": { + "type": "object", + "description": "the settings the bus holds" + }, + "fill": { + "$ref": "#/defs/Fill" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "description": "one entry as the bus holds it" + }, + "Said": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module asking it" + }, + "local-name": { + "type": "string", + "description": "its local name in the module's manifest" + }, + "storage-type": { + "type": "string", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "description": "its template; empty for a configuration of its own" + }, + "options": { + "type": "object", + "description": "the template's options the module set, within their bounds" + }, + "configuration": { + "type": "object", + "description": "its own configuration, where no template fits" + }, + "bus-name": { + "type": "string", + "description": "its name on the bus" + }, + "asked": { + "type": "object", + "description": "the settings asked" + }, + "held": { + "type": "object", + "description": "the settings held" + }, + "why": { + "type": "string", + "description": "why: a refusal's cause, a drift's difference, a drop's or recreate's reason" + }, + "fill": { + "$ref": "#/defs/Fill" + }, + "snapshot": { + "type": "string", + "description": "the stream snapshot taken before a recreate or drop" + } + }, + "required": [ + "module", + "local-name", + "bus-name" + ], + "description": "the body of every event of the seat" + }, + "Named": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry's local name" + } + }, + "required": [ + "module", + "local-name" + ] + }, + "Warranted": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "local-name": { + "type": "string", + "description": "the entry's local name" + }, + "warrant": { + "type": "string", + "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)" + }, + "why": { + "type": "string", + "description": "why, kept in the record" + } + }, + "required": [ + "module", + "local-name", + "warrant", + "why" + ] + } + } +} -- 2.54.0 From f7b3744a90079512bf9ea7296fb21c35e98e29a7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 05/94] =?UTF-8?q?SD-M2c:=20mesh-config=20and=20mesh-vault?= =?UTF-8?q?=20whole,=20renames=20and=20retire=20new;=20the=20holders'=20ow?= =?UTF-8?q?n=20events=20beside=20them=20(ADR=200620=20=C2=A71-=C2=A73,=20d?= =?UTF-8?q?esign=2032)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vault's definition leaves out accept and move-asks, which no holder served and nothing called. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- seats/mesh-config/fixtures/events.json | 44 ++ seats/mesh-config/fixtures/verbs.json | 19 + seats/mesh-config/seat.json | 222 +++++++++ seats/mesh-vault/fixtures/events.json | 74 +++ seats/mesh-vault/fixtures/verbs.json | 12 + seats/mesh-vault/seat.json | 634 +++++++++++++++++++++++++ 6 files changed, 1005 insertions(+) create mode 100644 seats/mesh-config/fixtures/events.json create mode 100644 seats/mesh-config/fixtures/verbs.json create mode 100644 seats/mesh-config/seat.json create mode 100644 seats/mesh-vault/fixtures/events.json create mode 100644 seats/mesh-vault/fixtures/verbs.json create mode 100644 seats/mesh-vault/seat.json diff --git a/seats/mesh-config/fixtures/events.json b/seats/mesh-config/fixtures/events.json new file mode 100644 index 0000000..01c3f98 --- /dev/null +++ b/seats/mesh-config/fixtures/events.json @@ -0,0 +1,44 @@ +[ + { + "name": "config-changed, unchanged", + "of": "holder-event:config-changed", + "subject": "mesh.mod.mesh-config.event.config-changed", + "given": { + "key": "x" + } + }, + { + "name": "renamed", + "of": "holder-event:renamed", + "subject": "mesh.mod.mesh-config.event.renamed", + "given": { + "module": "messenger", + "from": "old-key", + "to": "new-key", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "retiring", + "of": "holder-event:retiring", + "subject": "mesh.mod.mesh-config.event.retiring", + "given": { + "module": "messenger", + "key": "old-key", + "kind": "value", + "since": "2026-11-04T12:00:00Z", + "ask": "a1" + } + }, + { + "name": "retired", + "of": "holder-event:retired", + "subject": "mesh.mod.mesh-config.event.retired", + "given": { + "module": "messenger", + "name": "old-key", + "node": "ace", + "ask": "a1" + } + } +] diff --git a/seats/mesh-config/fixtures/verbs.json b/seats/mesh-config/fixtures/verbs.json new file mode 100644 index 0000000..aab449b --- /dev/null +++ b/seats/mesh-config/fixtures/verbs.json @@ -0,0 +1,19 @@ +[ + { + "name": "renames answers the open windows", + "of": "verb:renames.answer", + "given": { + "renames": [ + { + "module": "messenger", + "from": "old-key", + "to": "new-key", + "at": "2026-11-04T12:00:00Z", + "open": [ + "g14" + ] + } + ] + } + } +] diff --git a/seats/mesh-config/seat.json b/seats/mesh-config/seat.json new file mode 100644 index 0000000..7ddce60 --- /dev/null +++ b/seats/mesh-config/seat.json @@ -0,0 +1,222 @@ +{ + "format": 1, + "seat": "mesh-config", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0348, ADR 0565", + "verbs": [ + { + "name": "config", + "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.", + "optional": true, + "input": { + "properties": { + "action": { + "description": "list (when not given), set, clear or history", + "type": "string" + }, + "by": { + "description": "who gave the value at the controller's terminal", + "type": "string" + }, + "key": { + "description": "the config key", + "type": "string" + }, + "limit": { + "description": "for history: at most this many, newest", + "type": "number" + }, + "module": { + "description": "a module: its own value, with node", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "value": { + "description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list" + }, + "why": { + "description": "for set and clear: why", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the values asked, or the one set or cleared, or its history" + } + }, + { + "name": "renames", + "optional": true, + "description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", + "input": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "one module (optional)" + } + } + }, + "callers": "any caller whose role grants it; the checker asks it", + "answer": { + "type": "object", + "properties": { + "renames": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "from": { + "type": "string", + "description": "the old name" + }, + "to": { + "type": "string", + "description": "the new name" + }, + "at": { + "type": "string", + "description": "when it moved, RFC 3339" + }, + "open": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the nodes still holding the old name open" + } + }, + "required": [ + "module", + "from", + "to" + ] + }, + "description": "every open rename" + } + }, + "required": [ + "renames" + ] + } + } + ], + "description": "The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "config-changed", + "subject": "mesh.mod..event.config-changed" + }, + { + "name": "renamed", + "subject": "mesh.mod..event.renamed", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "from": { + "type": "string", + "description": "the old name" + }, + "to": { + "type": "string", + "description": "the new name" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "module", + "from", + "to", + "at" + ] + } + }, + { + "name": "retiring", + "subject": "mesh.mod..event.retiring", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "key": { + "type": "string", + "description": "the value's or secret's name" + }, + "kind": { + "type": "string", + "enum": [ + "value", + "secret" + ] + }, + "since": { + "type": "string", + "description": "since when, RFC 3339" + }, + "ask": { + "type": "string", + "description": "the ask's id" + } + }, + "required": [ + "module", + "key", + "kind", + "since" + ] + } + }, + { + "name": "retired", + "subject": "mesh.mod..event.retired", + "family": "config.events", + "body": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module" + }, + "name": { + "type": "string", + "description": "the name" + }, + "node": { + "type": "string", + "description": "the node" + }, + "ask": { + "type": "string", + "description": "the ask's id" + } + }, + "required": [ + "module", + "name" + ] + } + } + ] +} diff --git a/seats/mesh-vault/fixtures/events.json b/seats/mesh-vault/fixtures/events.json new file mode 100644 index 0000000..342fd91 --- /dev/null +++ b/seats/mesh-vault/fixtures/events.json @@ -0,0 +1,74 @@ +[ + { + "name": "provisioned under the vault's own subjects", + "of": "holder-event:provisioned", + "subject": "mesh.mod.mesh-vault.event.provisioned", + "given": {} + }, + { + "name": "rotated under the vault's own subjects", + "of": "holder-event:rotated", + "subject": "mesh.mod.mesh-vault.event.rotated", + "given": {} + }, + { + "name": "secret-changed under the vault's own subjects", + "of": "holder-event:secret-changed", + "subject": "mesh.mod.mesh-vault.event.secret-changed", + "given": {} + }, + { + "name": "account-hashed under the vault's own subjects", + "of": "holder-event:account-hashed", + "subject": "mesh.mod.mesh-vault.event.account-hashed", + "given": {} + }, + { + "name": "own-replaced under the vault's own subjects", + "of": "holder-event:own-replaced", + "subject": "mesh.mod.mesh-vault.event.own-replaced", + "given": {} + }, + { + "name": "account-issued under the vault's own subjects", + "of": "holder-event:account-issued", + "subject": "mesh.mod.mesh-vault.event.account-issued", + "given": {} + }, + { + "name": "signed under the vault's own subjects", + "of": "holder-event:signed", + "subject": "mesh.mod.mesh-vault.event.signed", + "given": {} + }, + { + "name": "rotation-begun under the vault's own subjects", + "of": "holder-event:rotation-begun", + "subject": "mesh.mod.mesh-vault.event.rotation-begun", + "given": {} + }, + { + "name": "node-account-ready under the vault's own subjects", + "of": "holder-event:node-account-ready", + "subject": "mesh.mod.mesh-vault.event.node-account-ready", + "given": {} + }, + { + "name": "rotation-done under the vault's own subjects", + "of": "holder-event:rotation-done", + "subject": "mesh.mod.mesh-vault.event.rotation-done", + "given": {} + }, + { + "name": "own-retired", + "of": "holder-event:own-retired", + "subject": "mesh.mod.mesh-vault.event.own-retired", + "given": { + "node": "ace", + "module": "messenger", + "name": "token", + "ask": "a1", + "caller": "mesh-config" + } + } +] diff --git a/seats/mesh-vault/fixtures/verbs.json b/seats/mesh-vault/fixtures/verbs.json new file mode 100644 index 0000000..4f2608e --- /dev/null +++ b/seats/mesh-vault/fixtures/verbs.json @@ -0,0 +1,12 @@ +[ + { + "name": "retire on a warrant", + "of": "verb:retire.input", + "given": { + "node": "ace", + "module": "messenger", + "name": "token", + "warrant": "w-1" + } + } +] diff --git a/seats/mesh-vault/seat.json b/seats/mesh-vault/seat.json new file mode 100644 index 0000000..a6a753d --- /dev/null +++ b/seats/mesh-vault/seat.json @@ -0,0 +1,634 @@ +{ + "format": 1, + "seat": "mesh-vault", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0161", + "delivers": "secret", + "verbs": [ + { + "name": "get", + "description": "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.", + "optional": true, + "input": { + "properties": { + "name": { + "description": "the secret's name", + "type": "string" + }, + "public_key": { + "description": "the asker's X25519 public key as SPKI PEM — never a private key", + "type": "string" + } + }, + "required": [ + "name", + "public_key" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "secret-ask", + "description": "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", + "optional": true, + "input": { + "properties": { + "asked_by": { + "description": "who asked, as the bus named its caller", + "type": "string" + }, + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "desk": { + "description": "the machine whose prompt asks", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "name": { + "description": "the secret's name in the module", + "type": "string" + }, + "node": { + "description": "the machine the secret is for", + "type": "string" + }, + "outcome": { + "description": "instead of sealed: how the prompt ended", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "issued", + "description": "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.", + "optional": true, + "input": { + "properties": { + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "holders": { + "description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone", + "items": { + "properties": { + "module": { + "description": "the consuming module", + "type": "string" + }, + "node": { + "description": "the holder's machine", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "module": { + "description": "the provider's module", + "type": "string" + }, + "provider": { + "description": "the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "rotate", + "description": "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.", + "optional": true, + "input": { + "properties": { + "by": { + "description": "who asked, when the controller carries the act", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "name": { + "description": "the secret's name in the module", + "type": "string" + }, + "node": { + "description": "the machine, for a module's own secret", + "type": "string" + }, + "provider": { + "description": "the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "move-in", + "description": "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.", + "optional": true, + "input": { + "properties": { + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "consumer": { + "description": "the consumer's machine", + "type": "string" + }, + "consumer_module": { + "description": "the consuming module", + "type": "string" + }, + "generation": { + "description": "the controller's generation for the value", + "type": "string" + }, + "local": { + "description": "the credential's local name, when it has one", + "type": "string" + }, + "made_at": { + "description": "when it was made (RFC 3339)", + "type": "string" + }, + "module": { + "description": "its module", + "type": "string" + }, + "name": { + "description": "its name in the module", + "type": "string" + }, + "node": { + "description": "a module's own secret: its machine", + "type": "string" + }, + "origin": { + "description": "made, accepted or issued, as the controller's store says", + "type": "string" + }, + "provider": { + "description": "a pair credential: the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + }, + "read_back_to": { + "description": "the second step: a raw X25519 public key the read-back is sealed to", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "node-key", + "description": "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.", + "optional": true, + "input": { + "properties": { + "key": { + "description": "the machine's sealing key, a raw X25519 public key in base64", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "own", + "description": "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.", + "optional": true, + "input": { + "properties": { + "credentials": { + "description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}", + "items": { + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "issue-account", + "description": "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", + "optional": true, + "input": { + "properties": { + "user": { + "description": "the bus user, e.g. ace.mesh-build-go", + "type": "string" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "sign", + "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", + "optional": true, + "input": { + "properties": { + "kind": { + "description": "node-certificate or enrolment", + "type": "string" + }, + "node": { + "description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", + "type": "string" + }, + "reply": { + "description": "the redeem answer's bytes in base64, for an enrolment", + "type": "string" + }, + "serving_key": { + "description": "from mesh-enrolment: the serving key's public half the enrolling node presented", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "enrolment-token", + "description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", + "optional": true, + "input": { + "properties": { + "drop": { + "description": "\"true\": drop the node's token and its hash", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "expires": { + "description": "when the token expires (RFC 3339), as the enrolment's key says", + "type": "string" + }, + "node": { + "description": "the node the token is for", + "type": "string" + }, + "sealed": { + "description": "\"true\": answer the token sealed to the operator key", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "tunnel": { + "additionalProperties": false, + "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", + "properties": { + "address": { + "description": "the joining node's tunnel address", + "type": "string" + }, + "hub_endpoint": { + "description": "the hub's endpoint, host:port", + "type": "string" + }, + "hub_key": { + "description": "the hub's tunnel public key", + "type": "string" + }, + "key": { + "description": "the joining machine's own tunnel public key, as given at issue", + "type": "string" + }, + "range": { + "description": "the private network's range, a prefix", + "type": "string" + } + }, + "required": [ + "address", + "hub_endpoint", + "hub_key", + "key", + "range" + ], + "type": "object" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "node-account", + "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", + "optional": true, + "input": { + "properties": { + "keys_digest": { + "description": "the digest of every public key the node presented (delivery.KeysDigest)", + "type": "string" + }, + "node": { + "description": "the joining node", + "type": "string" + }, + "retire": { + "description": "\"true\": retire the account minted for this node", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "sealing_key": { + "description": "the sealing key's public half the node presented, which the account is sealed to", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "rotate-node", + "description": "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", + "optional": true, + "input": { + "properties": { + "act": { + "description": "the id of the hand act recorded for it in mesh-hand-acts", + "type": "string" + }, + "node": { + "description": "the node whose account rotates", + "type": "string" + }, + "why": { + "description": "why, in a sentence", + "type": "string" + } + }, + "required": [ + "node", + "why", + "act" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "node-account-confirm", + "description": "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "abandon", + "description": "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.", + "optional": true, + "input": { + "properties": { + "node": { + "description": "the node whose rotation is abandoned", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the holder's answer" + } + }, + { + "name": "retire", + "optional": true, + "description": "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", + "callers": "the holder of the seat mesh-config alone", + "input": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "module": { + "type": "string", + "description": "the module" + }, + "name": { + "type": "string", + "description": "the own secret's name" + }, + "warrant": { + "type": "string", + "description": "the operator's warrant for this node, module and name" + } + }, + "required": [ + "node", + "module", + "name", + "warrant" + ] + }, + "answer": { + "type": "object", + "properties": { + "retired": { + "type": "boolean", + "description": "the secret is gone" + } + }, + "required": [ + "retired" + ] + } + } + ], + "description": "The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "provisioned", + "subject": "mesh.mod..event.provisioned" + }, + { + "name": "rotated", + "subject": "mesh.mod..event.rotated" + }, + { + "name": "secret-changed", + "subject": "mesh.mod..event.secret-changed" + }, + { + "name": "account-hashed", + "subject": "mesh.mod..event.account-hashed" + }, + { + "name": "own-replaced", + "subject": "mesh.mod..event.own-replaced" + }, + { + "name": "account-issued", + "subject": "mesh.mod..event.account-issued" + }, + { + "name": "signed", + "subject": "mesh.mod..event.signed" + }, + { + "name": "rotation-begun", + "subject": "mesh.mod..event.rotation-begun" + }, + { + "name": "node-account-ready", + "subject": "mesh.mod..event.node-account-ready" + }, + { + "name": "rotation-done", + "subject": "mesh.mod..event.rotation-done" + }, + { + "name": "own-retired", + "subject": "mesh.mod..event.own-retired", + "body": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "module": { + "type": "string", + "description": "the module" + }, + "name": { + "type": "string", + "description": "the secret's name" + }, + "ask": { + "type": "string", + "description": "the ask's id" + }, + "caller": { + "type": "string", + "description": "the caller the vault proved" + } + }, + "required": [ + "node", + "module", + "name", + "caller" + ] + } + } + ] +} -- 2.54.0 From bf880e9f7f93152b3b687f204806e89c9bb2f893 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 06/94] =?UTF-8?q?SD-M2d:=20mesh-delivery-checker=20and=20o?= =?UTF-8?q?perator-channel=20whole=20(ADR=200620=20=C2=A71,=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checker's verdict and tag-wanted on its own family checker.asks, preview.written on its stream checks; operator-channel written from the messenger's seat entry, decided its only seat event, offered and closed beside it. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../fixtures/events.json | 35 ++ seats/mesh-delivery-checker/seat.json | 209 +++++++ seats/operator-channel/fixtures/events.json | 98 +++ seats/operator-channel/seat.json | 560 ++++++++++++++++++ 4 files changed, 902 insertions(+) create mode 100644 seats/mesh-delivery-checker/fixtures/events.json create mode 100644 seats/mesh-delivery-checker/seat.json create mode 100644 seats/operator-channel/fixtures/events.json create mode 100644 seats/operator-channel/seat.json diff --git a/seats/mesh-delivery-checker/fixtures/events.json b/seats/mesh-delivery-checker/fixtures/events.json new file mode 100644 index 0000000..46ef4d8 --- /dev/null +++ b/seats/mesh-delivery-checker/fixtures/events.json @@ -0,0 +1,35 @@ +[ + { + "name": "verdict on the checker's own family", + "of": "holder-event:verdict", + "subject": "mesh.mod.mesh-delivery-checker.event.verdict", + "given": { + "repository": "novox/mesh-contracts", + "pull": 3, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "verdict": "fail", + "out-of-step": [ + "go/seats/meshbroker/contract.go" + ] + } + }, + { + "name": "tag-wanted on the checker's own family", + "of": "holder-event:tag-wanted", + "subject": "mesh.mod.mesh-delivery-checker.event.tag-wanted", + "given": { + "repository": "novox/mesh-sdk", + "pull": 170, + "head": "cccccccccccccccccccccccccccccccccccccccc", + "module": "mesh-sdk", + "package": "go", + "tag": "registry-1966" + } + }, + { + "name": "preview.written on the checker's stream", + "of": "holder-event:preview.written", + "subject": "mesh.mod.mesh-delivery-checker.event.preview.written", + "given": {} + } +] diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json new file mode 100644 index 0000000..e671f2e --- /dev/null +++ b/seats/mesh-delivery-checker/seat.json @@ -0,0 +1,209 @@ +{ + "format": 1, + "seat": "mesh-delivery-checker", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0547", + "verbs": [ + { + "name": "try", + "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", + "optional": true, + "replaces": [ + "sh merge-check.sh", + "docker run postgres" + ], + "input": { + "properties": { + "head": { + "description": "the full commit to try, 40 or 64 hex digits", + "type": "string" + }, + "level": { + "description": "unit or integration: only this level; both when absent", + "type": "string" + }, + "repository": { + "description": "/", + "type": "string" + }, + "tests": { + "description": "a go test -run pattern: only those tests run, and nothing is kept", + "type": "string" + }, + "trunk": { + "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", + "type": "string" + } + }, + "required": [ + "repository", + "head" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try's id" + }, + "state": { + "type": "string", + "description": "its state" + } + }, + "required": [ + "id" + ] + } + }, + { + "name": "try-read", + "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the try: its state, its steps and its verdict when it has one" + } + }, + { + "name": "try-end", + "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + }, + "why": { + "description": "why it is ended, in words", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the try" + }, + "ended": { + "type": "boolean", + "description": "it is ended" + } + }, + "required": [ + "id", + "ended" + ] + } + } + ], + "description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).", + "holder-events": [ + { + "name": "verdict", + "subject": "mesh.mod..event.verdict", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request's number, for a pull request's check" + }, + "head": { + "type": "string", + "description": "the commit checked" + }, + "verdict": { + "type": "string", + "description": "pass or fail" + }, + "out-of-step": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the generated files out of step with their seat definitions, each path (design 26)" + } + }, + "required": [ + "repository", + "head", + "verdict" + ] + } + }, + { + "name": "tag-wanted", + "subject": "mesh.mod..event.tag-wanted", + "family": "checker.asks", + "body": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "description": "owner/repo" + }, + "pull": { + "type": "integer", + "description": "the pull request" + }, + "head": { + "type": "string", + "description": "its head" + }, + "module": { + "type": "string", + "description": "the group member" + }, + "package": { + "type": "string", + "description": "the package it declares" + }, + "tag": { + "type": "string", + "description": "the tag wanted" + } + }, + "required": [ + "repository", + "head", + "module", + "package" + ] + } + }, + { + "name": "preview.written", + "subject": "mesh.mod..event.preview.written", + "family": "checks" + } + ] +} diff --git a/seats/operator-channel/fixtures/events.json b/seats/operator-channel/fixtures/events.json new file mode 100644 index 0000000..971ce14 --- /dev/null +++ b/seats/operator-channel/fixtures/events.json @@ -0,0 +1,98 @@ +[ + { + "name": "decided for its asker", + "of": "event:decided", + "subject": "mesh.seat.operator-channel.event.decided.mesh-delivery", + "given": { + "ask": "a1", + "asker": "mesh-delivery", + "outcome": "chosen", + "option": "ok", + "label": "Release", + "level": "approve", + "by": { + "who": "operator", + "kind": "telegram", + "identity": "1", + "verified": "user id" + }, + "channel": "telegram", + "at": "2026-11-04T12:00:00Z", + "ask-digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + } + }, + { + "name": "refused under the holder's own subjects", + "of": "holder-event:refused", + "subject": "mesh.mod.messenger.event.refused", + "given": {} + }, + { + "name": "offered on the router's restricted stream", + "of": "holder-event:offered", + "subject": "mesh.mod.messenger.event.offered", + "given": { + "id": "a1", + "asker": "mesh-delivery", + "headline": "release held", + "channels": [ + "telegram" + ] + } + }, + { + "name": "closed once per end", + "of": "holder-event:closed", + "subject": "mesh.mod.messenger.event.closed", + "given": { + "id": "a1", + "asker": "mesh-delivery", + "how": "answered" + } + }, + { + "name": "an ask by its asker", + "of": "accept:ask", + "subject": "mesh.seat.operator-channel.accept.ask.mesh-delivery", + "given": { + "id": "a1", + "headline": "release held", + "explanation": "A delivery waits on your word.", + "options": [ + { + "id": "ok", + "label": "Release", + "does": "releases it", + "level": "approve", + "binds": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + } + ], + "who": "operator", + "expires": "2026-11-05T12:00:00Z", + "on-expiry": "the delivery stays held" + } + }, + { + "name": "a cancel by its asker", + "of": "accept:cancel", + "subject": "mesh.seat.operator-channel.accept.cancel.mesh-delivery", + "given": { + "id": "a1" + } + }, + { + "name": "a condition by its publisher", + "of": "accept:condition", + "subject": "mesh.seat.operator-channel.accept.condition.mesh-watcher", + "given": { + "v": 1, + "key": "backup.ace.failed", + "subject": { + "scope": "node", + "id": "ace" + }, + "state": "raised", + "severity": "warning" + } + } +] diff --git a/seats/operator-channel/seat.json b/seats/operator-channel/seat.json new file mode 100644 index 0000000..26d0e95 --- /dev/null +++ b/seats/operator-channel/seat.json @@ -0,0 +1,560 @@ +{ + "format": 1, + "seat": "operator-channel", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0259", + "holder-declares": true, + "description": "Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26).", + "verbs": [ + { + "name": "open", + "description": "What is open now: every message the operator was sent about something still wrong, urgent first, oldest first.", + "input": { + "type": "object", + "properties": {}, + "description": "no arguments" + }, + "answer": { + "type": "object", + "description": "the open messages" + } + }, + { + "name": "history", + "description": "What was said to the operator lately, newest first, the refusals, and what waits to be sent on each channel.", + "input": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "description": "at most this many of each (default 50)" + } + } + }, + "answer": { + "type": "object", + "description": "the sends, refusals and what waits" + } + }, + { + "name": "notify", + "description": "Tell the operator something, as a module that uses the seat: a key, urgent or warning, one line, and what it is about; clear says it is over.", + "input": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "the message's key" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "subject": { + "type": "string", + "description": "what it is about" + }, + "headline": { + "type": "string", + "description": "plain words" + }, + "explanation": { + "type": "string", + "description": "one or two plain sentences" + }, + "resolved": { + "type": "string", + "description": "the line said when it is over" + }, + "clear": { + "type": "boolean", + "description": "it is over" + } + } + }, + "answer": { + "type": "object", + "description": "what was sent" + } + }, + { + "name": "tell", + "description": "Tell the operator a plain notice that needs no answer and opens no condition (ADR 0380).", + "input": { + "type": "object", + "properties": { + "headline": { + "type": "string", + "description": "plain words" + }, + "lines": { + "type": "array", + "items": { + "type": "string" + }, + "description": "one or two plain lines" + }, + "subject": { + "type": "string", + "description": "what it is about" + } + } + }, + "answer": { + "type": "object", + "description": "what was sent" + } + }, + { + "name": "asks", + "description": "The asks the router holds: open first, then the last month's, each with how it ended.", + "input": { + "type": "object", + "properties": {}, + "description": "no arguments" + }, + "answer": { + "type": "object", + "description": "the asks" + } + } + ], + "accepts": [ + { + "name": "ask", + "by-caller": true, + "body": { + "$ref": "#/defs/Ask" + } + }, + { + "name": "cancel", + "by-caller": true, + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the ask's id" + } + }, + "required": [ + "id" + ] + } + }, + { + "name": "condition", + "by-caller": true, + "body": { + "$ref": "#/defs/Condition" + } + } + ], + "events": [ + { + "name": "decided", + "by-caller": true, + "body": { + "$ref": "#/defs/Warrant" + } + } + ], + "family": { + "name": "operator-channel.decided", + "max-age-days": 30 + }, + "records": [ + { + "name": "asks", + "key": ".", + "value": { + "type": "object", + "description": "one ask the router holds and how it stands" + } + } + ], + "holder-events": [ + { + "name": "refused", + "subject": "mesh.mod..event.refused" + }, + { + "name": "offered", + "subject": "mesh.mod..event.offered", + "family": "the router's restricted stream (design 55 § 11)", + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the ask's id" + }, + "asker": { + "type": "string", + "description": "its asker" + }, + "headline": { + "type": "string", + "description": "its headline" + }, + "channels": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the channels it was offered on" + } + }, + "required": [ + "id", + "asker" + ] + } + }, + { + "name": "closed", + "subject": "mesh.mod..event.closed", + "family": "the router's restricted stream (design 55 § 11)", + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the ask's id" + }, + "asker": { + "type": "string", + "description": "its asker" + }, + "how": { + "type": "string", + "enum": [ + "answered", + "expired", + "cancelled", + "replaced", + "unlinked" + ] + } + }, + "required": [ + "id", + "asker", + "how" + ] + } + } + ], + "defs": { + "Ask": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the asker's own id" + }, + "headline": { + "type": "string", + "description": "a few plain words" + }, + "explanation": { + "type": "string", + "description": "what happened and what it means" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/defs/Option" + }, + "description": "the answers it offers" + }, + "who": { + "type": "string", + "description": "who may answer: operator" + }, + "expires": { + "type": "string", + "description": "when it ends unanswered, RFC 3339" + }, + "on-expiry": { + "type": "string", + "description": "what the asker then does" + }, + "about": { + "type": "string", + "description": "what it is about: a newer ask about it replaces the older" + }, + "urgent": { + "type": "boolean", + "description": "urgent" + }, + "whole": { + "type": "string", + "description": "the explanation with its values whole" + }, + "details": { + "type": "string", + "description": "its secondary words" + }, + "whole-details": { + "type": "string", + "description": "the details whole" + } + }, + "required": [ + "id", + "headline", + "explanation", + "options", + "who", + "expires", + "on-expiry" + ] + }, + "Option": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the option's id" + }, + "label": { + "type": "string", + "description": "the button's label" + }, + "does": { + "type": "string", + "description": "what it does, in plain words" + }, + "level": { + "type": "string", + "description": "the proof its answer needs" + }, + "binds": { + "type": "string", + "description": "the digest of exactly what the asker performs when it is chosen" + } + }, + "required": [ + "id", + "label", + "does", + "level" + ] + }, + "Warrant": { + "type": "object", + "properties": { + "ask": { + "type": "string", + "description": "the ask" + }, + "asker": { + "type": "string", + "description": "its asker" + }, + "about": { + "type": "string", + "description": "what it was about" + }, + "outcome": { + "type": "string", + "enum": [ + "chosen", + "expired", + "cancelled", + "replaced", + "refused" + ] + }, + "option": { + "type": "string", + "description": "the option chosen" + }, + "label": { + "type": "string", + "description": "its label" + }, + "level": { + "type": "string", + "description": "its level" + }, + "by": { + "$ref": "#/defs/Person" + }, + "channel": { + "type": "string", + "description": "the module it came through" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the proofs present" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + }, + "ask-digest": { + "type": "string", + "description": "the digest of the ask as the router took it" + }, + "words": { + "type": "string", + "description": "why it ended without a choice, or what refused it" + } + }, + "required": [ + "ask", + "asker", + "outcome", + "at" + ] + }, + "Person": { + "type": "object", + "properties": { + "who": { + "type": "string", + "description": "the role" + }, + "kind": { + "type": "string", + "description": "the channel kind" + }, + "identity": { + "type": "string", + "description": "the account" + }, + "display": { + "type": "string", + "description": "the name shown" + }, + "verified": { + "type": "string", + "description": "how the router knew" + } + }, + "required": [ + "who", + "kind", + "identity", + "verified" + ] + }, + "Condition": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the form's version, 1" + }, + "key": { + "type": "string", + "description": "the condition's key" + }, + "severity": { + "type": "string", + "description": "urgent or warning" + }, + "subject": { + "type": "object", + "properties": { + "scope": { + "type": "string", + "description": "scope" + }, + "id": { + "type": "string", + "description": "id" + }, + "machine": { + "type": "string", + "description": "machine" + } + }, + "required": [ + "scope", + "id" + ] + }, + "state": { + "type": "string", + "description": "raised or cleared" + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string", + "description": "plain words" + }, + "needs": { + "type": "string", + "description": "what it needs" + }, + "explanation": { + "type": "string", + "description": "plain words" + }, + "resolved": { + "type": "string", + "description": "the line said when it is over" + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "label" + }, + "verb": { + "type": "string", + "description": "verb" + }, + "machine": { + "type": "string", + "description": "machine" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "level": { + "type": "string", + "description": "level" + } + }, + "required": [ + "label", + "verb" + ] + }, + "description": "the answers it offers" + } + }, + "required": [ + "v", + "key", + "subject", + "state" + ] + } + } +} -- 2.54.0 From df74b712c0fb55fd81b1b087d62e72e38baaf8f0 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 07/94] =?UTF-8?q?SD-M3:=20every=20other=20seat=20at=20the?= =?UTF-8?q?=20seat=20list's=20grain,=20in=20its=20reading=20order=20(ADR?= =?UTF-8?q?=200613=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converted mechanically from mesh-sdk's compiled list at 5a38a12 by a program kept out of the repository; the three new registry seats follow npm-package-registry, operator-channel the mesh's own seats. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- format/seat-definition.schema.json | 4 +- seats/git/seat.json | 8 + seats/index.json | 65 +++ seats/mesh-artifact-store/seat.json | 8 + seats/mesh-artifact-sweep/seat.json | 103 ++++ seats/mesh-catalog/seat.json | 69 +++ seats/mesh-conditions/seat.json | 95 ++++ seats/mesh-delivery/seat.json | 226 +++++++++ seats/mesh-dns-resolver/seat.json | 9 + seats/mesh-enrolment/seat.json | 170 +++++++ seats/mesh-hand-acts/seat.json | 97 ++++ seats/mesh-healers/seat.json | 73 +++ seats/mesh-placement/seat.json | 137 +++++ seats/mesh-provisioner/seat.json | 35 ++ seats/mesh-self-check/seat.json | 43 ++ seats/mesh-store/seat.json | 45 ++ seats/mesh-wants/seat.json | 51 ++ seats/mesh-watchdogs/seat.json | 93 ++++ seats/node-backup/seat.json | 80 +++ seats/node-bar/seat.json | 75 +++ seats/node-build-go/seat.json | 46 ++ seats/node-build-image/seat.json | 46 ++ seats/node-build-node/seat.json | 46 ++ seats/node-clipboard/seat.json | 38 ++ seats/node-compositor/seat.json | 7 + seats/node-container-runtime/seat.json | 7 + seats/node-display-server/seat.json | 42 ++ seats/node-display-session/seat.json | 44 ++ seats/node-engine/seat.json | 194 +++++++ seats/node-environment/seat.json | 7 + seats/node-files/seat.json | 589 ++++++++++++++++++++++ seats/node-hostname/seat.json | 73 +++ seats/node-hotkeys/seat.json | 13 + seats/node-intrusion-prevention/seat.json | 89 ++++ seats/node-launcher/seat.json | 68 +++ seats/node-lock-screen/seat.json | 17 + seats/node-login-manager/seat.json | 17 + seats/node-login-shell/seat.json | 30 ++ seats/node-message-bus/seat.json | 7 + seats/node-mounts/seat.json | 111 ++++ seats/node-nfs-server/seat.json | 99 ++++ seats/node-notifier/seat.json | 51 ++ seats/node-package-manager/seat.json | 7 + seats/node-packet-filter/seat.json | 62 +++ seats/node-power/seat.json | 7 + seats/node-provisioner/seat.json | 7 + seats/node-reads/seat.json | 179 +++++++ seats/node-resolver/seat.json | 77 +++ seats/node-secret-service/seat.json | 7 + seats/node-service-manager/seat.json | 305 +++++++++++ seats/node-terminal-emulator/seat.json | 26 + seats/node-uplink/seat.json | 33 ++ seats/the-private-network/seat.json | 7 + 53 files changed, 3842 insertions(+), 2 deletions(-) create mode 100644 seats/git/seat.json create mode 100644 seats/index.json create mode 100644 seats/mesh-artifact-store/seat.json create mode 100644 seats/mesh-artifact-sweep/seat.json create mode 100644 seats/mesh-catalog/seat.json create mode 100644 seats/mesh-conditions/seat.json create mode 100644 seats/mesh-delivery/seat.json create mode 100644 seats/mesh-dns-resolver/seat.json create mode 100644 seats/mesh-enrolment/seat.json create mode 100644 seats/mesh-hand-acts/seat.json create mode 100644 seats/mesh-healers/seat.json create mode 100644 seats/mesh-placement/seat.json create mode 100644 seats/mesh-provisioner/seat.json create mode 100644 seats/mesh-self-check/seat.json create mode 100644 seats/mesh-store/seat.json create mode 100644 seats/mesh-wants/seat.json create mode 100644 seats/mesh-watchdogs/seat.json create mode 100644 seats/node-backup/seat.json create mode 100644 seats/node-bar/seat.json create mode 100644 seats/node-build-go/seat.json create mode 100644 seats/node-build-image/seat.json create mode 100644 seats/node-build-node/seat.json create mode 100644 seats/node-clipboard/seat.json create mode 100644 seats/node-compositor/seat.json create mode 100644 seats/node-container-runtime/seat.json create mode 100644 seats/node-display-server/seat.json create mode 100644 seats/node-display-session/seat.json create mode 100644 seats/node-engine/seat.json create mode 100644 seats/node-environment/seat.json create mode 100644 seats/node-files/seat.json create mode 100644 seats/node-hostname/seat.json create mode 100644 seats/node-hotkeys/seat.json create mode 100644 seats/node-intrusion-prevention/seat.json create mode 100644 seats/node-launcher/seat.json create mode 100644 seats/node-lock-screen/seat.json create mode 100644 seats/node-login-manager/seat.json create mode 100644 seats/node-login-shell/seat.json create mode 100644 seats/node-message-bus/seat.json create mode 100644 seats/node-mounts/seat.json create mode 100644 seats/node-nfs-server/seat.json create mode 100644 seats/node-notifier/seat.json create mode 100644 seats/node-package-manager/seat.json create mode 100644 seats/node-packet-filter/seat.json create mode 100644 seats/node-power/seat.json create mode 100644 seats/node-provisioner/seat.json create mode 100644 seats/node-reads/seat.json create mode 100644 seats/node-resolver/seat.json create mode 100644 seats/node-secret-service/seat.json create mode 100644 seats/node-service-manager/seat.json create mode 100644 seats/node-terminal-emulator/seat.json create mode 100644 seats/node-uplink/seat.json create mode 100644 seats/the-private-network/seat.json diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index 965beeb..d78ab51 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -118,7 +118,7 @@ "properties": { "name": { "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" + "pattern": "^[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*$" }, "subject": { "type": "string" @@ -381,7 +381,7 @@ }, "of": { "type": "string", - "pattern": "^(verb:[a-z][a-z0-9-]*\\.(input|answer)|accept:[a-z][a-z0-9-]*|event:[a-z][a-z0-9-]*|holder-event:[a-z][a-z0-9-]*)$" + "pattern": "^(verb:[a-z][a-z0-9-]*\\.(input|answer)|accept:[a-z][a-z0-9-]*|event:[a-z][a-z0-9-]*|holder-event:[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*)$" }, "subject": { "type": "string" diff --git a/seats/git/seat.json b/seats/git/seat.json new file mode 100644 index 0000000..836285a --- /dev/null +++ b/seats/git/seat.json @@ -0,0 +1,8 @@ +{ + "format": 1, + "seat": "git", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0111", + "delivers": "git" +} diff --git a/seats/index.json b/seats/index.json new file mode 100644 index 0000000..28eb0cf --- /dev/null +++ b/seats/index.json @@ -0,0 +1,65 @@ +{ + "format": 1, + "seats": [ + "mesh-store", + "mesh-broker", + "mesh-vault", + "mesh-config", + "mesh-provisioner", + "mesh-artifact-store", + "mesh-catalog", + "mesh-delivery", + "mesh-placement", + "mesh-delivery-checker", + "mesh-conditions", + "mesh-watchdogs", + "mesh-self-check", + "mesh-healers", + "mesh-wants", + "mesh-hand-acts", + "mesh-enrolment", + "mesh-artifact-sweep", + "operator-channel", + "npm-package-registry", + "go-package-registry", + "container-image-registry", + "oci-artifact-registry", + "git", + "node-build-go", + "node-build-node", + "node-build-image", + "mesh-dns-resolver", + "node-hostname", + "node-intrusion-prevention", + "node-packet-filter", + "node-service-manager", + "node-package-manager", + "node-container-runtime", + "node-environment", + "node-provisioner", + "node-login-shell", + "node-power", + "node-hotkeys", + "node-message-bus", + "node-backup", + "the-private-network", + "node-uplink", + "node-resolver", + "node-nfs-server", + "node-mounts", + "node-reads", + "node-files", + "node-engine", + "node-login-manager", + "node-display-server", + "node-display-session", + "node-terminal-emulator", + "node-launcher", + "node-notifier", + "node-lock-screen", + "node-clipboard", + "node-bar", + "node-compositor", + "node-secret-service" + ] +} diff --git a/seats/mesh-artifact-store/seat.json b/seats/mesh-artifact-store/seat.json new file mode 100644 index 0000000..b1efa95 --- /dev/null +++ b/seats/mesh-artifact-store/seat.json @@ -0,0 +1,8 @@ +{ + "format": 1, + "seat": "mesh-artifact-store", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0075", + "delivers": "artifact-store" +} diff --git a/seats/mesh-artifact-sweep/seat.json b/seats/mesh-artifact-sweep/seat.json new file mode 100644 index 0000000..602a5f8 --- /dev/null +++ b/seats/mesh-artifact-sweep/seat.json @@ -0,0 +1,103 @@ +{ + "format": 1, + "seat": "mesh-artifact-sweep", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0571", + "verbs": [ + { + "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": { + "properties": { + "collected": { + "description": "\"true\": list the collected references too (counted always)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "repository": { + "description": "only this repository's references, as \u003cmodule\u003e/\u003cartifact\u003e (the counts stay whole)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "references": { + "description": "the artifacts' references, artifact-store://\u003cmodule\u003e/\u003cartifact\u003e/blobs/sha256:\u003chex\u003e: a list, or one text separated by commas", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "references" + ], + "type": "object" + } + }, + { + "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": { + "properties": { + "confirm": { + "description": "\"true\": let go of what is eligible (needs why); without it nothing is done", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "most": { + "description": "let go of at most this many (500 by default, at most 5000)", + "type": "string" + }, + "why": { + "description": "with confirm: why, required and recorded", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": {}, + "type": "object" + } + }, + { + "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": { + "properties": { + "node": { + "description": "the machine's name", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "artifacts-gone" + } + ] +} diff --git a/seats/mesh-catalog/seat.json b/seats/mesh-catalog/seat.json new file mode 100644 index 0000000..40b6618 --- /dev/null +++ b/seats/mesh-catalog/seat.json @@ -0,0 +1,69 @@ +{ + "format": 1, + "seat": "mesh-catalog", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0604", + "verbs": [ + { + "name": "modules", + "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "module", + "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + }, + { + "name": "provides", + "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", + "optional": true, + "input": { + "properties": { + "provision": { + "description": "the provision, such as postgres-database", + "type": "string" + } + }, + "required": [ + "provision" + ], + "type": "object" + } + }, + { + "name": "dependents", + "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + } + ] +} diff --git a/seats/mesh-conditions/seat.json b/seats/mesh-conditions/seat.json new file mode 100644 index 0000000..5513eba --- /dev/null +++ b/seats/mesh-conditions/seat.json @@ -0,0 +1,95 @@ +{ + "format": 1, + "seat": "mesh-conditions", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0564", + "verbs": [ + { + "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.", + "input": { + "properties": { + "days": { + "description": "with history: how many days back (default 7, at most 90)", + "type": "string" + }, + "history": { + "description": "\"true\": every raising, change, silence and clearing lately, oldest first", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "key": { + "description": "a condition's key: that one whole; with history, only its transitions", + "type": "string" + }, + "machine": { + "description": "only those about this machine", + "type": "string" + }, + "scope": { + "description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", + "type": "string" + }, + "severity": { + "description": "only urgent, or only warning", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word (the condition's kind when absent)", + "type": "string" + }, + "for": { + "description": "how long: 30m, 4h, 2d; at most 7d", + "type": "string" + }, + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why, required and recorded", + "type": "string" + } + }, + "required": [ + "key", + "for", + "why" + ], + "type": "object" + } + }, + { + "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": { + "properties": {}, + "type": "object" + } + } + ], + "events": [ + { + "name": "condition-raised" + }, + { + "name": "condition-changed" + }, + { + "name": "condition-cleared" + } + ] +} diff --git a/seats/mesh-delivery/seat.json b/seats/mesh-delivery/seat.json new file mode 100644 index 0000000..bafd85c --- /dev/null +++ b/seats/mesh-delivery/seat.json @@ -0,0 +1,226 @@ +{ + "format": 1, + "seat": "mesh-delivery", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0239", + "verbs": [ + { + "name": "deliveries", + "description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", + "input": { + "properties": { + "all": { + "description": "\"true\": the final ones of the last thirty days too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "group": { + "description": "a group's id (its branch name)", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + }, + "state": { + "description": "one state, e.g. delivering or held", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "times", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "show", + "description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", + "input": { + "properties": { + "id": { + "description": "a delivery's id, or a group's", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + }, + { + "name": "groups", + "description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", + "input": { + "properties": { + "all": { + "description": "\"true\": the groups that ended too", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "what-if", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "checks", + "description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", + "optional": true, + "input": { + "properties": { + "commit": { + "description": "a commit's sha, or the start of one, instead of a pull request", + "type": "string" + }, + "number": { + "description": "a pull request's number: its head is read", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + } + }, + "required": [ + "repository" + ], + "type": "object" + } + }, + { + "name": "table", + "description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "stalled", + "description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "recheck", + "description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "why, kept with the transition", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + } + }, + { + "name": "release", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "stop", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "close", + "description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "the condition it answers", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + } + }, + { + "name": "retire-history", + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "settle", + "description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", + "optional": true, + "input": { + "additionalProperties": false, + "properties": { + "ids": { + "description": "the held deliveries' ids (owner/repository@commit)", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "why": { + "description": "why they may be settled, shown to the operator", + "type": "string" + } + }, + "required": [ + "ids", + "why" + ], + "type": "object" + } + } + ] +} diff --git a/seats/mesh-dns-resolver/seat.json b/seats/mesh-dns-resolver/seat.json new file mode 100644 index 0000000..14480ac --- /dev/null +++ b/seats/mesh-dns-resolver/seat.json @@ -0,0 +1,9 @@ +{ + "format": 1, + "seat": "mesh-dns-resolver", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0194, ADR 0223", + "delivers": "wildcard-resolution", + "replicated": true +} diff --git a/seats/mesh-enrolment/seat.json b/seats/mesh-enrolment/seat.json new file mode 100644 index 0000000..b611558 --- /dev/null +++ b/seats/mesh-enrolment/seat.json @@ -0,0 +1,170 @@ +{ + "format": 1, + "seat": "mesh-enrolment", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0587", + "verbs": [ + { + "name": "issue", + "description": "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.", + "input": { + "properties": { + "expires": { + "description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", + "type": "string" + }, + "node": { + "description": "the new machine's name in the mesh", + "type": "string" + }, + "overlay_key": { + "description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", + "type": "string" + }, + "why": { + "description": "why it joins, shown to the operator with the ask", + "type": "string" + } + }, + "required": [ + "node", + "why", + "overlay_key" + ], + "type": "object" + } + }, + { + "name": "redeem", + "description": "Spend a token: asked by a joining node-engine dialled as enrol.\u003cnode\u003e alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", + "input": { + "additionalProperties": false, + "properties": { + "node": { + "description": "the node the token was issued for", + "type": "string" + }, + "overlay_key": { + "description": "the private network key's public half", + "type": "string" + }, + "profile": { + "description": "what the machine is, as its record says it", + "type": "object" + }, + "proof": { + "description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64", + "type": "string" + }, + "public_key": { + "description": "the identity key's public half, base64", + "type": "string" + }, + "sealing_key": { + "description": "the sealing key's public half, which the account is sealed to", + "type": "string" + }, + "serving_key": { + "description": "the serving key's public half, which the certificate binds", + "type": "string" + }, + "tunnel": { + "description": "a tunnel found on the machine: everything but its private key", + "type": "object" + } + }, + "required": [ + "node", + "public_key", + "sealing_key", + "proof" + ], + "type": "object" + } + }, + { + "name": "confirm", + "description": "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "revoke", + "description": "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.", + "input": { + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "why": { + "description": "why, recorded with the hand act", + "type": "string" + } + }, + "required": [ + "node", + "why" + ], + "type": "object" + } + }, + { + "name": "tokens", + "description": "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.", + "input": { + "properties": { + "history": { + "description": "\"true\": the ended enrolments too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "again", + "description": "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.", + "input": { + "properties": { + "node": { + "description": "the node whose ask is raised again", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "enrolment-issued" + }, + { + "name": "node-enrolled" + }, + { + "name": "enrolment-refused" + }, + { + "name": "enrolment-revoked" + }, + { + "name": "enrolment-expired" + } + ] +} diff --git a/seats/mesh-hand-acts/seat.json b/seats/mesh-hand-acts/seat.json new file mode 100644 index 0000000..a789dbb --- /dev/null +++ b/seats/mesh-hand-acts/seat.json @@ -0,0 +1,97 @@ +{ + "format": 1, + "seat": "mesh-hand-acts", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0565", + "verbs": [ + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word, or a condition's kind", + "type": "string" + }, + "condition": { + "description": "the key of the condition it addressed, if any (optional)", + "type": "string" + }, + "verb": { + "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", + "type": "string" + }, + "what": { + "description": "what was done, in a line", + "type": "string" + }, + "why": { + "description": "why it had to be done by hand", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + } + }, + { + "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": { + "properties": { + "cause": { + "description": "only this cause", + "type": "string" + }, + "days": { + "description": "how many days back (default 14)", + "type": "string" + }, + "warrants": { + "description": "\"true\": every warrant recorded", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "who": { + "description": "only acts whose who contains this", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "ask": { + "description": "its ask's id", + "type": "string" + }, + "asker": { + "description": "the module that asked", + "type": "string" + } + }, + "required": [ + "asker", + "ask" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "act-recorded" + } + ] +} diff --git a/seats/mesh-healers/seat.json b/seats/mesh-healers/seat.json new file mode 100644 index 0000000..0f463da --- /dev/null +++ b/seats/mesh-healers/seat.json @@ -0,0 +1,73 @@ +{ + "format": 1, + "seat": "mesh-healers", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0564", + "verbs": [ + { + "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, and the mesh-wide brake.", + "input": { + "properties": { + "days": { + "description": "how many days of acts back (default 7)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "heal", + "description": "Take one healer's act for one open condition now, within its budget and the brake.", + "input": { + "properties": { + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why now, kept with the act", + "type": "string" + } + }, + "required": [ + "key", + "why" + ], + "type": "object" + } + }, + { + "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": { + "properties": { + "condition": { + "description": "the key of the condition the drill is meant to raise, if any (optional)", + "type": "string" + }, + "what": { + "description": "what was done on purpose, in a line", + "type": "string" + }, + "why": { + "description": "what the drill tests", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "healer-acted" + } + ] +} diff --git a/seats/mesh-placement/seat.json b/seats/mesh-placement/seat.json new file mode 100644 index 0000000..cc3d44c --- /dev/null +++ b/seats/mesh-placement/seat.json @@ -0,0 +1,137 @@ +{ + "format": 1, + "seat": "mesh-placement", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0567", + "verbs": [ + { + "name": "placement", + "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", + "optional": true, + "input": { + "properties": { + "history": { + "description": "\"true\": the changes in order, from the stream and the state's revisions", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module: the nodes it is placed on", + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + }, + "seats": { + "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "proposals", + "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.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "one proposal's id", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "propose", + "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", + "optional": true, + "input": { + "additionalProperties": false, + "properties": { + "changes": { + "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", + "items": { + "required": [ + "op", + "node" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "warrant": { + "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", + "required": [ + "node", + "address", + "ask", + "warrant" + ], + "type": "object" + }, + "why": { + "description": "why, shown to the operator with the proposal", + "type": "string" + } + }, + "required": [ + "why", + "changes" + ], + "type": "object" + } + }, + { + "name": "withdraw", + "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + }, + "why": { + "description": "why, kept in the event", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + } + }, + { + "name": "again", + "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + } + } + ] +} diff --git a/seats/mesh-provisioner/seat.json b/seats/mesh-provisioner/seat.json new file mode 100644 index 0000000..fe16d81 --- /dev/null +++ b/seats/mesh-provisioner/seat.json @@ -0,0 +1,35 @@ +{ + "format": 1, + "seat": "mesh-provisioner", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0349", + "verbs": [ + { + "name": "provisioning", + "description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", + "optional": true, + "input": { + "properties": { + "collection": { + "description": "for collect: a collection", + "type": "string" + }, + "module": { + "description": "a module: for collect, a contributor", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "what": { + "description": "registry (when not given), needs, applied or collect", + "type": "string" + } + }, + "type": "object" + } + } + ] +} diff --git a/seats/mesh-self-check/seat.json b/seats/mesh-self-check/seat.json new file mode 100644 index 0000000..56ba074 --- /dev/null +++ b/seats/mesh-self-check/seat.json @@ -0,0 +1,43 @@ +{ + "format": 1, + "seat": "mesh-self-check", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0564", + "verbs": [ + { + "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": { + "properties": { + "probe": { + "description": "one probe's id (D6): its verdict alone, with every finding", + "type": "string" + }, + "run": { + "description": "\"true\": run every probe now and answer the verdict", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "probes", + "description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", + "input": { + "properties": {}, + "type": "object" + } + } + ], + "events": [ + { + "name": "checked" + } + ] +} diff --git a/seats/mesh-store/seat.json b/seats/mesh-store/seat.json new file mode 100644 index 0000000..f02517a --- /dev/null +++ b/seats/mesh-store/seat.json @@ -0,0 +1,45 @@ +{ + "format": 1, + "seat": "mesh-store", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0079", + "delivers": "postgres-database", + "verbs": [ + { + "name": "databases", + "description": "Every database the store holds, with its on-disk size.", + "replaces": [ + "psql -l" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "query", + "description": "One read-only statement against one database the store holds.", + "replaces": [ + "psql" + ], + "input": { + "properties": { + "database": { + "description": "the database to query", + "type": "string" + }, + "sql": { + "description": "the read-only statement", + "type": "string" + } + }, + "required": [ + "database", + "sql" + ], + "type": "object" + } + } + ] +} diff --git a/seats/mesh-wants/seat.json b/seats/mesh-wants/seat.json new file mode 100644 index 0000000..8b50c1a --- /dev/null +++ b/seats/mesh-wants/seat.json @@ -0,0 +1,51 @@ +{ + "format": 1, + "seat": "mesh-wants", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0565", + "verbs": [ + { + "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, each node's own composition and the assigner's findings.", + "input": { + "properties": { + "compare": { + "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module", + "type": "string" + }, + "policy": { + "description": "with module: roll, together, record, or default to take a person's choice back", + "type": "string" + }, + "why": { + "description": "with policy: why, required and kept", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": {}, + "type": "object" + } + } + ], + "events": [ + { + "name": "policy-changed" + } + ] +} diff --git a/seats/mesh-watchdogs/seat.json b/seats/mesh-watchdogs/seat.json new file mode 100644 index 0000000..3ad9f3c --- /dev/null +++ b/seats/mesh-watchdogs/seat.json @@ -0,0 +1,93 @@ +{ + "format": 1, + "seat": "mesh-watchdogs", + "scope": "mesh", + "grain": "seat-list", + "decision": "novox/hq ADR 0564", + "verbs": [ + { + "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": { + "properties": { + "row": { + "description": "one row, e.g. S1", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "only those on this machine", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "one machine", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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.", + "input": { + "properties": { + "cause": { + "description": "with deliver or drop: the cause in a word (dead-letter when absent)", + "type": "string" + }, + "consumer": { + "description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on", + "type": "string" + }, + "deliver": { + "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", + "type": "string" + }, + "drop": { + "description": "a dead letter's id: drop it for good (needs why)", + "type": "string" + }, + "id": { + "description": "a dead letter's id, as the list gives it: that one whole, with its message", + "type": "string" + }, + "limit": { + "description": "how many to list, newest first (default 50); only when listing", + "type": "string" + }, + "why": { + "description": "with deliver or drop: why, required and recorded", + "type": "string" + } + }, + "type": "object" + } + }, + { + "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": { + "properties": {}, + "type": "object" + } + } + ] +} diff --git a/seats/node-backup/seat.json b/seats/node-backup/seat.json new file mode 100644 index 0000000..465f68f --- /dev/null +++ b/seats/node-backup/seat.json @@ -0,0 +1,80 @@ +{ + "format": 1, + "seat": "node-backup", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0214", + "verbs": [ + { + "name": "backed-up", + "description": "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.", + "replaces": [ + "restic snapshots" + ], + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "now", + "description": "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.", + "replaces": [ + "restic backup" + ], + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "restore", + "description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as \u003cpath\u003e.restored-\u003cdate\u003e. Swapping it in is a person's act. Lists the restore points when none is named.", + "replaces": [ + "restic restore" + ], + "input": { + "properties": { + "module": { + "description": "the module", + "type": "string" + }, + "path": { + "description": "one of the module's directories (all of them when omitted)", + "type": "string" + }, + "snapshot": { + "description": "the restore point (from `backed-up`; the newest when omitted)", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + } + ], + "receives": [ + { + "kind": "backup", + "comment": "#", + "dirs": true + }, + { + "kind": "data", + "comment": "#", + "dirs": true + } + ] +} diff --git a/seats/node-bar/seat.json b/seats/node-bar/seat.json new file mode 100644 index 0000000..dc73191 --- /dev/null +++ b/seats/node-bar/seat.json @@ -0,0 +1,75 @@ +{ + "format": 1, + "seat": "node-bar", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "receives": [ + { + "kind": "block", + "comment": "#", + "offered": true, + "shape": { + "places": [ + { + "field": "bar", + "values": [ + "bottom", + "top" + ] + }, + { + "field": "place", + "values": [ + "resources", + "status" + ] + } + ], + "order": "order", + "examples": [ + { + "bar": "bottom", + "place": "status", + "shows": "battery" + }, + { + "bar": "bottom", + "options": { + "device": "BAT0" + }, + "place": "status", + "shows": "battery" + }, + { + "bar": "bottom", + "options": { + "command": "echo \"a line\"", + "interval": 5 + }, + "place": "status", + "shows": "command" + }, + { + "bar": "bottom", + "options": { + "state": "power.draw" + }, + "place": "status", + "shows": "state" + }, + { + "bar": "bottom", + "options": { + "key": "a-machine", + "state": "power.draw" + }, + "place": "status", + "shows": "state" + } + ], + "text": true + } + } + ] +} diff --git a/seats/node-build-go/seat.json b/seats/node-build-go/seat.json new file mode 100644 index 0000000..5beead8 --- /dev/null +++ b/seats/node-build-go/seat.json @@ -0,0 +1,46 @@ +{ + "format": 1, + "seat": "node-build-go", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "verbs": [ + { + "name": "current", + "description": "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "pause", + "description": "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "resume", + "description": "Take Go asks again on this node.", + "input": { + "properties": {}, + "type": "object" + } + } + ], + "accepts": [ + { + "name": "delivery" + }, + { + "name": "check" + }, + { + "name": "recheck" + } + ] +} diff --git a/seats/node-build-image/seat.json b/seats/node-build-image/seat.json new file mode 100644 index 0000000..96065eb --- /dev/null +++ b/seats/node-build-image/seat.json @@ -0,0 +1,46 @@ +{ + "format": 1, + "seat": "node-build-image", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "verbs": [ + { + "name": "current", + "description": "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "pause", + "description": "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "resume", + "description": "Take image asks again on this node.", + "input": { + "properties": {}, + "type": "object" + } + } + ], + "accepts": [ + { + "name": "delivery" + }, + { + "name": "check" + }, + { + "name": "recheck" + } + ] +} diff --git a/seats/node-build-node/seat.json b/seats/node-build-node/seat.json new file mode 100644 index 0000000..d2b8fb7 --- /dev/null +++ b/seats/node-build-node/seat.json @@ -0,0 +1,46 @@ +{ + "format": 1, + "seat": "node-build-node", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "pulled-by-kind": true, + "yields": true, + "verbs": [ + { + "name": "current", + "description": "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "pause", + "description": "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "resume", + "description": "Take Node asks again on this node.", + "input": { + "properties": {}, + "type": "object" + } + } + ], + "accepts": [ + { + "name": "delivery" + }, + { + "name": "check" + }, + { + "name": "recheck" + } + ] +} diff --git a/seats/node-clipboard/seat.json b/seats/node-clipboard/seat.json new file mode 100644 index 0000000..0f21e51 --- /dev/null +++ b/seats/node-clipboard/seat.json @@ -0,0 +1,38 @@ +{ + "format": 1, + "seat": "node-clipboard", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "history", + "description": "What the clipboard held lately, newest first.", + "input": { + "properties": { + "limit": { + "description": "how many (optional, default 20)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "copy", + "description": "Put text on the operator's clipboard.", + "input": { + "properties": { + "text": { + "description": "the text", + "type": "string" + } + }, + "required": [ + "text" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-compositor/seat.json b/seats/node-compositor/seat.json new file mode 100644 index 0000000..4ce95ee --- /dev/null +++ b/seats/node-compositor/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-compositor", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208" +} diff --git a/seats/node-container-runtime/seat.json b/seats/node-container-runtime/seat.json new file mode 100644 index 0000000..4a68219 --- /dev/null +++ b/seats/node-container-runtime/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-container-runtime", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0166, ADR 0207" +} diff --git a/seats/node-display-server/seat.json b/seats/node-display-server/seat.json new file mode 100644 index 0000000..8523e84 --- /dev/null +++ b/seats/node-display-server/seat.json @@ -0,0 +1,42 @@ +{ + "format": 1, + "seat": "node-display-server", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "displays", + "description": "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "layout", + "description": "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.", + "input": { + "properties": { + "action": { + "description": "list, save or apply", + "enum": [ + "list", + "save", + "apply" + ], + "type": "string" + }, + "name": { + "description": "the profile to save or apply (save and apply only)", + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-display-session/seat.json b/seats/node-display-session/seat.json new file mode 100644 index 0000000..6edac04 --- /dev/null +++ b/seats/node-display-session/seat.json @@ -0,0 +1,44 @@ +{ + "format": 1, + "seat": "node-display-session", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "reload", + "description": "Reload the session's configuration in place, keeping its windows.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "workspaces", + "description": "The session's workspaces: each one's name, output, and whether it is visible or focused.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "windows", + "description": "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.", + "input": { + "properties": { + "workspace": { + "description": "one workspace (optional)", + "type": "string" + } + }, + "type": "object" + } + } + ], + "receives": [ + { + "kind": "config", + "comment": "#" + } + ] +} diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json new file mode 100644 index 0000000..20ee202 --- /dev/null +++ b/seats/node-engine/seat.json @@ -0,0 +1,194 @@ +{ + "format": 1, + "seat": "node-engine", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0563", + "held-by-the-engine": true, + "verbs": [ + { + "name": "hand-over", + "description": "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", + "input": { + "properties": { + "path": { + "description": "the directory's absolute path, as the engine states it", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "access-act", + "description": "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", + "input": { + "properties": { + "access": { + "description": "the access, library:\u003cname\u003e", + "type": "string" + }, + "act": { + "description": "the act: take", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "act", + "access" + ], + "type": "object" + } + }, + { + "name": "judge-directories", + "description": "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "setuid-search", + "description": "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", + "input": { + "properties": { + "why": { + "description": "why it is asked, in words", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "root-free", + "description": "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "acts", + "description": "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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": { + "properties": { + "id": { + "description": "one act, by its id", + "type": "string" + }, + "limit": { + "description": "at most this many, newest", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "packages", + "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.", + "optional": true, + "input": { + "properties": { + "write": { + "description": "\"true\": write this machine's key again from what the engine composed", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "composition", + "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed-\u003cnode\u003e (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", + "optional": true, + "input": { + "properties": { + "diff": { + "description": "\"true\": list the resources that differ from what runs", + "type": "string" + }, + "digest": { + "description": "a composition's digest (sha256:…), to read that revision rather than the latest", + "type": "string" + }, + "module": { + "description": "the module, as placed on this machine; node for the node's own", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + } + }, + { + "name": "compose", + "description": "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "a module placed on this machine whose apply is also tried again (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "compose-candidate", + "description": "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", + "optional": true, + "input": { + "properties": { + "candidate": { + "description": "the key of the indexer's candidate, \u003cgroup\u003e.\u003cheads hash\u003e", + "type": "string" + }, + "input": { + "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "candidate" + ], + "type": "object" + } + } + ], + "events": [ + { + "name": "asked" + }, + { + "name": "done" + }, + { + "name": "refused" + } + ] +} diff --git a/seats/node-environment/seat.json b/seats/node-environment/seat.json new file mode 100644 index 0000000..064beb0 --- /dev/null +++ b/seats/node-environment/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-environment", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0203" +} diff --git a/seats/node-files/seat.json b/seats/node-files/seat.json new file mode 100644 index 0000000..7ad768f --- /dev/null +++ b/seats/node-files/seat.json @@ -0,0 +1,589 @@ +{ + "format": 1, + "seat": "node-files", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0394", + "verbs": [ + { + "name": "stat", + "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "stat", + "ls -ld", + "namei -l", + "findmnt -T" + ], + "input": { + "properties": { + "follow": { + "description": "\"true\": also say where the path leads once every link is followed", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "paths": { + "description": "one absolute path, or several separated by line breaks", + "type": "string" + } + }, + "required": [ + "paths" + ], + "type": "object" + } + }, + { + "name": "readlink", + "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "readlink", + "readlink -f", + "realpath" + ], + "input": { + "properties": { + "path": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "list", + "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "ls", + "ls -la" + ], + "input": { + "properties": { + "path": { + "description": "an absolute path to a directory", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "read", + "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "cat", + "head", + "tail", + "less" + ], + "input": { + "properties": { + "bytes": { + "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", + "type": "string" + }, + "from": { + "description": "the first line to answer, counted from 1 (optional)", + "type": "string" + }, + "lines": { + "description": "how many lines to answer (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "search", + "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "grep", + "grep -r", + "grep -F" + ], + "input": { + "properties": { + "case": { + "description": "\"false\": match whatever the case (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "depth": { + "description": "how many directories deep, up to 8 (optional)", + "type": "string" + }, + "matches": { + "description": "the most matches to answer, up to 500 (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file or a directory", + "type": "string" + }, + "text": { + "description": "the fixed string to find", + "type": "string" + } + }, + "required": [ + "text", + "path" + ], + "type": "object" + } + }, + { + "name": "account", + "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "replaces": [ + "getent passwd", + "getent group", + "id", + "groups" + ], + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "type": "string" + }, + "name": { + "description": "an account's name or number (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "write", + "description": "Write one text file whole, replacing what it holds; the old content goes to the node's trash. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "tee", + "\u003e" + ], + "input": { + "properties": { + "content": { + "description": "the text to write", + "type": "string" + }, + "mode": { + "description": "the file's mode, such as 0644 (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "path", + "content", + "reason" + ], + "type": "object" + } + }, + { + "name": "copy", + "description": "Copy a file or directory to a path on this node. A target on another node (\u003cnode\u003e:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "cp", + "cp -a", + "ssh \u003cnode\u003e cp" + ], + "input": { + "properties": { + "from": { + "description": "an absolute path", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "to": { + "description": "an absolute path on this node", + "type": "string" + } + }, + "required": [ + "from", + "to", + "reason" + ], + "type": "object" + } + }, + { + "name": "move", + "description": "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "mv" + ], + "input": { + "properties": { + "from": { + "description": "an absolute path", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "to": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "from", + "to", + "reason" + ], + "type": "object" + } + }, + { + "name": "delete", + "description": "Delete a file or directory into the node's trash, where restore can bring it back. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "rm", + "rm -r" + ], + "input": { + "properties": { + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": delete a directory with what it holds", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "reason" + ], + "type": "object" + } + }, + { + "name": "mkdir", + "description": "Make a directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "mkdir", + "mkdir -p" + ], + "input": { + "properties": { + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "parents": { + "description": "\"true\": make the missing parents too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "path", + "reason" + ], + "type": "object" + } + }, + { + "name": "chmod", + "description": "Change the mode of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "chmod" + ], + "input": { + "properties": { + "mode": { + "description": "the mode, such as 0644 or u+x", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": also everything under a directory", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "mode", + "reason" + ], + "type": "object" + } + }, + { + "name": "chown", + "description": "Change the owner and group of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "chown", + "chgrp" + ], + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "owner": { + "description": "an account's name or number", + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": also everything under a directory", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "owner", + "reason" + ], + "type": "object" + } + }, + { + "name": "upload", + "description": "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "bucket": { + "description": "the bucket the file goes into", + "type": "string" + }, + "key": { + "description": "the object's key (optional; the file's SHA-256 when not given)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path", + "bucket" + ], + "type": "object" + } + }, + { + "name": "download", + "description": "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "bucket": { + "description": "the bucket", + "type": "string" + }, + "key": { + "description": "the object's key", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "sha256": { + "description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", + "type": "string" + } + }, + "required": [ + "bucket", + "key", + "path", + "reason" + ], + "type": "object" + } + }, + { + "name": "trash", + "description": "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.", + "replaces": [ + "ls ~/.local/share/Trash" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "restore", + "description": "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "replaces": [ + "trash-restore" + ], + "input": { + "properties": { + "id": { + "description": "an id from trash", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "id", + "reason" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-hostname/seat.json b/seats/node-hostname/seat.json new file mode 100644 index 0000000..0f646af --- /dev/null +++ b/seats/node-hostname/seat.json @@ -0,0 +1,73 @@ +{ + "format": 1, + "seat": "node-hostname", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0199, ADR 0223", + "verbs": [ + { + "name": "entries", + "description": "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", + "replaces": [ + "cat /etc/hosts", + "getent hosts" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "add", + "description": "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", + "replaces": [ + "edit /etc/hosts", + "HOSTALIASES" + ], + "input": { + "properties": { + "address": { + "description": "the IPv4 or IPv6 address", + "type": "string" + }, + "names": { + "description": "the names for it, separated by spaces", + "type": "string" + } + }, + "required": [ + "address", + "names" + ], + "type": "object" + } + }, + { + "name": "remove", + "description": "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", + "replaces": [ + "sed -i /etc/hosts" + ], + "input": { + "properties": { + "name": { + "description": "a host name, or an address to remove every line of", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + } + ], + "paths": [ + { + "path": "/etc/hosts" + }, + { + "path": "/etc/hostname" + } + ] +} diff --git a/seats/node-hotkeys/seat.json b/seats/node-hotkeys/seat.json new file mode 100644 index 0000000..cedd627 --- /dev/null +++ b/seats/node-hotkeys/seat.json @@ -0,0 +1,13 @@ +{ + "format": 1, + "seat": "node-hotkeys", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0212", + "receives": [ + { + "kind": "trigger", + "comment": "#" + } + ] +} diff --git a/seats/node-intrusion-prevention/seat.json b/seats/node-intrusion-prevention/seat.json new file mode 100644 index 0000000..a987a70 --- /dev/null +++ b/seats/node-intrusion-prevention/seat.json @@ -0,0 +1,89 @@ +{ + "format": 1, + "seat": "node-intrusion-prevention", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0121", + "verbs": [ + { + "name": "status", + "description": "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", + "replaces": [ + "fail2ban-client status" + ], + "input": { + "properties": { + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "banned", + "description": "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", + "replaces": [ + "fail2ban-client banned" + ], + "input": { + "properties": { + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "ban", + "description": "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", + "replaces": [ + "fail2ban-client set banip" + ], + "input": { + "properties": { + "ip": { + "description": "the address", + "type": "string" + }, + "jail": { + "description": "the jail to hold it", + "type": "string" + } + }, + "required": [ + "ip", + "jail" + ], + "type": "object" + } + }, + { + "name": "unban", + "description": "Let one address go, from one jail or from every jail when none is named.", + "replaces": [ + "fail2ban-client unban", + "fail2ban-client set unbanip" + ], + "input": { + "properties": { + "ip": { + "description": "the address", + "type": "string" + }, + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "required": [ + "ip" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-launcher/seat.json b/seats/node-launcher/seat.json new file mode 100644 index 0000000..3eab2eb --- /dev/null +++ b/seats/node-launcher/seat.json @@ -0,0 +1,68 @@ +{ + "format": 1, + "seat": "node-launcher", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "menu", + "description": "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", + "input": { + "properties": { + "choices": { + "description": "the lines to choose between, in order", + "items": { + "type": "string" + }, + "type": "array" + }, + "prompt": { + "description": "what the menu asks (optional)", + "type": "string" + } + }, + "required": [ + "choices" + ], + "type": "object" + } + }, + { + "name": "secret", + "description": "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.", + "optional": true, + "input": { + "properties": { + "module": { + "description": "the module whose own secret is asked for", + "type": "string" + }, + "node": { + "description": "the machine the module runs on", + "type": "string" + }, + "seal_to": { + "description": "the asker's public sealing key: the answer is sealed to it", + "type": "string" + }, + "secret": { + "description": "the own secret's name", + "type": "string" + }, + "timeout_seconds": { + "description": "give up after this long (optional)", + "type": "string" + } + }, + "required": [ + "module", + "secret", + "node", + "seal_to" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-lock-screen/seat.json b/seats/node-lock-screen/seat.json new file mode 100644 index 0000000..4cd19ed --- /dev/null +++ b/seats/node-lock-screen/seat.json @@ -0,0 +1,17 @@ +{ + "format": 1, + "seat": "node-lock-screen", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "lock", + "description": "Lock the operator's session now.", + "input": { + "properties": {}, + "type": "object" + } + } + ] +} diff --git a/seats/node-login-manager/seat.json b/seats/node-login-manager/seat.json new file mode 100644 index 0000000..e0a70ba --- /dev/null +++ b/seats/node-login-manager/seat.json @@ -0,0 +1,17 @@ +{ + "format": 1, + "seat": "node-login-manager", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "sessions", + "description": "The sessions the login manager offers on this machine, and which one the operator account starts by default.", + "input": { + "properties": {}, + "type": "object" + } + } + ] +} diff --git a/seats/node-login-shell/seat.json b/seats/node-login-shell/seat.json new file mode 100644 index 0000000..c868e5b --- /dev/null +++ b/seats/node-login-shell/seat.json @@ -0,0 +1,30 @@ +{ + "format": 1, + "seat": "node-login-shell", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0204", + "verbs": [ + { + "name": "execute", + "description": "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.", + "optional": true, + "input": { + "properties": { + "command": { + "description": "the command line, as you would type it", + "type": "string" + }, + "timeout_seconds": { + "description": "give up after this long, at most 25 (default 20)", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-message-bus/seat.json b/seats/node-message-bus/seat.json new file mode 100644 index 0000000..83c6744 --- /dev/null +++ b/seats/node-message-bus/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-message-bus", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0215" +} diff --git a/seats/node-mounts/seat.json b/seats/node-mounts/seat.json new file mode 100644 index 0000000..43f1655 --- /dev/null +++ b/seats/node-mounts/seat.json @@ -0,0 +1,111 @@ +{ + "format": 1, + "seat": "node-mounts", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0263", + "verbs": [ + { + "name": "list", + "description": "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.", + "replaces": [ + "cat /etc/fstab", + "findmnt", + "systemctl list-units --type=mount" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "test", + "description": "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.", + "replaces": [ + "showmount -e", + "ping" + ], + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + { + "name": "mount", + "description": "Mount one share or occasional source now, rather than at its first access.", + "replaces": [ + "mount" + ], + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + { + "name": "unmount", + "description": "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.", + "replaces": [ + "umount" + ], + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + { + "name": "adopt", + "description": "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", + "replaces": [ + "sed -i /etc/fstab" + ], + "input": { + "properties": { + "confirm": { + "description": "\"true\": change it (needs why); anything else is a dry run", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "mountpoint": { + "description": "the mount point whose fstab line is taken over", + "type": "string" + }, + "why": { + "description": "why, for the record", + "type": "string" + } + }, + "required": [ + "mountpoint" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-nfs-server/seat.json b/seats/node-nfs-server/seat.json new file mode 100644 index 0000000..4cedbde --- /dev/null +++ b/seats/node-nfs-server/seat.json @@ -0,0 +1,99 @@ +{ + "format": 1, + "seat": "node-nfs-server", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0263", + "verbs": [ + { + "name": "exports", + "description": "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.", + "replaces": [ + "exportfs -v", + "cat /etc/exports", + "zfs get sharenfs" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "clients", + "description": "Which machines have mounted which share now, as the NFS server knows its clients.", + "replaces": [ + "ss -tn sport = :2049", + "cat /proc/fs/nfsd/clients/*/info" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "test", + "description": "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.", + "replaces": [ + "showmount -e" + ], + "input": { + "properties": { + "address": { + "description": "a node's private address, to ask whether the share is exported to it (optional)", + "type": "string" + }, + "share": { + "description": "the share, by its name", + "type": "string" + } + }, + "required": [ + "share" + ], + "type": "object" + } + }, + { + "name": "reload", + "description": "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.", + "replaces": [ + "exportfs -ra" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "adopt", + "description": "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.", + "replaces": [ + "zfs set sharenfs=off" + ], + "input": { + "properties": { + "confirm": { + "description": "\"true\": change it (needs why); anything else is a dry run", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "the shared path whose hand-made export is taken over", + "type": "string" + }, + "why": { + "description": "why, for the record", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-notifier/seat.json b/seats/node-notifier/seat.json new file mode 100644 index 0000000..f391310 --- /dev/null +++ b/seats/node-notifier/seat.json @@ -0,0 +1,51 @@ +{ + "format": 1, + "seat": "node-notifier", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "send", + "description": "Show the operator a notification.", + "input": { + "properties": { + "body": { + "description": "its text (optional)", + "type": "string" + }, + "title": { + "description": "the notification's summary", + "type": "string" + }, + "urgency": { + "description": "low, normal (the default) or critical", + "enum": [ + "low", + "normal", + "critical" + ], + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + } + }, + { + "name": "history", + "description": "The notifications shown lately, newest first.", + "input": { + "properties": { + "limit": { + "description": "how many (optional, default 20)", + "type": "string" + } + }, + "type": "object" + } + } + ] +} diff --git a/seats/node-package-manager/seat.json b/seats/node-package-manager/seat.json new file mode 100644 index 0000000..c2b32b0 --- /dev/null +++ b/seats/node-package-manager/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-package-manager", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0207" +} diff --git a/seats/node-packet-filter/seat.json b/seats/node-packet-filter/seat.json new file mode 100644 index 0000000..fb69e1c --- /dev/null +++ b/seats/node-packet-filter/seat.json @@ -0,0 +1,62 @@ +{ + "format": 1, + "seat": "node-packet-filter", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0121", + "verbs": [ + { + "name": "rules", + "description": "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", + "replaces": [ + "nft list ruleset", + "iptables -L", + "iptables-save" + ], + "input": { + "properties": { + "chain": { + "description": "one chain of that table (optional)", + "type": "string" + }, + "table": { + "description": "one nftables table, as `family name` (optional)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "reload", + "description": "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", + "replaces": [ + "nft -f" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "remove", + "description": "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", + "replaces": [ + "nft delete", + "iptables -X" + ], + "input": { + "properties": { + "where": { + "description": "the rule set, as `node show` lists it", + "type": "string" + } + }, + "required": [ + "where" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-power/seat.json b/seats/node-power/seat.json new file mode 100644 index 0000000..762a79f --- /dev/null +++ b/seats/node-power/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-power", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0211" +} diff --git a/seats/node-provisioner/seat.json b/seats/node-provisioner/seat.json new file mode 100644 index 0000000..9752f8b --- /dev/null +++ b/seats/node-provisioner/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-provisioner", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0349" +} diff --git a/seats/node-reads/seat.json b/seats/node-reads/seat.json new file mode 100644 index 0000000..938a71a --- /dev/null +++ b/seats/node-reads/seat.json @@ -0,0 +1,179 @@ +{ + "format": 1, + "seat": "node-reads", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0310", + "verbs": [ + { + "name": "stat", + "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.", + "replaces": [ + "stat", + "ls -ld", + "namei -l", + "findmnt -T" + ], + "input": { + "properties": { + "follow": { + "description": "\"true\": also say where the path leads once every link is followed", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "paths": { + "description": "one absolute path, or several separated by line breaks", + "type": "string" + } + }, + "required": [ + "paths" + ], + "type": "object" + } + }, + { + "name": "readlink", + "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.", + "replaces": [ + "readlink", + "readlink -f", + "realpath" + ], + "input": { + "properties": { + "path": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "list", + "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.", + "replaces": [ + "ls", + "ls -la" + ], + "input": { + "properties": { + "path": { + "description": "an absolute path to a directory", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "read", + "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.", + "replaces": [ + "cat", + "head", + "tail", + "less" + ], + "input": { + "properties": { + "bytes": { + "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", + "type": "string" + }, + "from": { + "description": "the first line to answer, counted from 1 (optional)", + "type": "string" + }, + "lines": { + "description": "how many lines to answer (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + { + "name": "search", + "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.", + "replaces": [ + "grep", + "grep -r", + "grep -F" + ], + "input": { + "properties": { + "case": { + "description": "\"false\": match whatever the case (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "depth": { + "description": "how many directories deep, up to 8 (optional)", + "type": "string" + }, + "matches": { + "description": "the most matches to answer, up to 500 (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file or a directory", + "type": "string" + }, + "text": { + "description": "the fixed string to find", + "type": "string" + } + }, + "required": [ + "text", + "path" + ], + "type": "object" + } + }, + { + "name": "account", + "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.", + "replaces": [ + "getent passwd", + "getent group", + "id", + "groups" + ], + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "type": "string" + }, + "name": { + "description": "an account's name or number (optional)", + "type": "string" + } + }, + "type": "object" + } + } + ] +} diff --git a/seats/node-resolver/seat.json b/seats/node-resolver/seat.json new file mode 100644 index 0000000..8689bcd --- /dev/null +++ b/seats/node-resolver/seat.json @@ -0,0 +1,77 @@ +{ + "format": 1, + "seat": "node-resolver", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0247", + "verbs": [ + { + "name": "routes", + "description": "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", + "replaces": [ + "resolvectl status", + "resolvectl dns", + "resolvectl domain" + ], + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "route", + "description": "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", + "replaces": [ + "resolvectl dns", + "resolvectl domain", + "resolvectl default-route" + ], + "input": { + "properties": { + "domains": { + "description": "the domains to route there, separated by spaces or commas", + "type": "string" + }, + "link": { + "description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", + "type": "string" + }, + "servers": { + "description": "the servers' addresses, separated by spaces or commas", + "type": "string" + } + }, + "required": [ + "link", + "domains", + "servers" + ], + "type": "object" + } + }, + { + "name": "unroute", + "description": "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", + "replaces": [ + "resolvectl revert" + ], + "input": { + "properties": { + "link": { + "description": "the network link, by name", + "type": "string" + } + }, + "required": [ + "link" + ], + "type": "object" + } + } + ], + "paths": [ + { + "path": "/etc/resolv.conf" + } + ] +} diff --git a/seats/node-secret-service/seat.json b/seats/node-secret-service/seat.json new file mode 100644 index 0000000..fd5b84b --- /dev/null +++ b/seats/node-secret-service/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "node-secret-service", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208" +} diff --git a/seats/node-service-manager/seat.json b/seats/node-service-manager/seat.json new file mode 100644 index 0000000..9270cc2 --- /dev/null +++ b/seats/node-service-manager/seat.json @@ -0,0 +1,305 @@ +{ + "format": 1, + "seat": "node-service-manager", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0177", + "verbs": [ + { + "name": "units", + "description": "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", + "replaces": [ + "systemctl list-units" + ], + "input": { + "properties": { + "pattern": { + "description": "a glob the unit's name must match (optional)", + "type": "string" + }, + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "status", + "description": "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", + "replaces": [ + "systemctl status", + "systemctl is-active" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "start", + "description": "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", + "replaces": [ + "systemctl start" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "stop", + "description": "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", + "replaces": [ + "systemctl stop" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "restart", + "description": "Restart one unit.", + "replaces": [ + "systemctl restart" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "enable", + "description": "Make one unit start at boot (or at the account's login, in user scope).", + "replaces": [ + "systemctl enable" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "disable", + "description": "Stop one unit starting at boot (or at login, in user scope).", + "replaces": [ + "systemctl disable" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "journal", + "description": "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: \u003cwhat it was\u003e].", + "replaces": [ + "journalctl" + ], + "input": { + "properties": { + "lines": { + "description": "how many lines from the end of what matches (default 100, at most 2000)", + "type": "string" + }, + "match": { + "description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", + "type": "string" + }, + "priority": { + "description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", + "type": "string" + }, + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "since": { + "description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + }, + "until": { + "description": "the window's end, in the same forms (optional; now when absent)", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "failed", + "description": "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.", + "optional": true, + "replaces": [ + "systemctl --failed" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" or \"user\": only that manager (both when absent)", + "type": "string" + } + }, + "type": "object" + } + }, + { + "name": "reset-failed", + "description": "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.", + "replaces": [ + "systemctl reset-failed" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "wanted-by", + "description": "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.", + "replaces": [ + "systemctl list-dependencies --reverse", + "systemctl show -p WantedBy", + "ls ~/.config/systemd/user/*.wants" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + } + }, + { + "name": "unlink-dangling", + "description": "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.", + "replaces": [ + "rm ~/.config/systemd/user/*.wants/\u003cunit\u003e", + "unlink /etc/systemd/system/*.wants/\u003cunit\u003e" + ], + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + }, + "why": { + "description": "the person's reason for removing the links, kept in the record", + "type": "string" + } + }, + "required": [ + "unit", + "why" + ], + "type": "object" + } + } + ] +} diff --git a/seats/node-terminal-emulator/seat.json b/seats/node-terminal-emulator/seat.json new file mode 100644 index 0000000..732a5ba --- /dev/null +++ b/seats/node-terminal-emulator/seat.json @@ -0,0 +1,26 @@ +{ + "format": 1, + "seat": "node-terminal-emulator", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0208", + "verbs": [ + { + "name": "open", + "description": "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.", + "input": { + "properties": { + "command": { + "description": "what to run in it (optional; the login shell when absent)", + "type": "string" + }, + "directory": { + "description": "where it starts (optional; the account's home when absent)", + "type": "string" + } + }, + "type": "object" + } + } + ] +} diff --git a/seats/node-uplink/seat.json b/seats/node-uplink/seat.json new file mode 100644 index 0000000..0ecfbf1 --- /dev/null +++ b/seats/node-uplink/seat.json @@ -0,0 +1,33 @@ +{ + "format": 1, + "seat": "node-uplink", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0117", + "verbs": [ + { + "name": "resolvers", + "description": "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + }, + { + "name": "links", + "description": "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", + "optional": true, + "input": { + "properties": {}, + "type": "object" + } + } + ], + "paths": [ + { + "path": "/etc/resolv.conf", + "steps-back-for": "node-resolver" + } + ] +} diff --git a/seats/the-private-network/seat.json b/seats/the-private-network/seat.json new file mode 100644 index 0000000..858f02e --- /dev/null +++ b/seats/the-private-network/seat.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seat": "the-private-network", + "scope": "node", + "grain": "seat-list", + "decision": "novox/hq ADR 0110" +} -- 2.54.0 From c7fb8d6689f63b1291c6f3c3c4bffec1edd22f32 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:50:54 +0100 Subject: [PATCH 08/94] SD-M3: the Go and TypeScript generated from the definitions (design 26 *The generator*) go/seats the seat list every reader compiles and every seat's name; a package per whole seat with its names, subjects, types and a test of its fixtures; ts/ the same as a TypeScript package. Written by seatgen, never by hand. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/go.mod | 3 + go/parts/registry/contract.go | 173 + go/seats/containerimageregistry/contract.go | 195 + .../containerimageregistry/contract_test.go | 66 + go/seats/gopackageregistry/contract.go | 87 + go/seats/gopackageregistry/contract_test.go | 62 + go/seats/meshbroker/contract.go | 280 + go/seats/meshbroker/contract_test.go | 70 + go/seats/meshconfig/contract.go | 120 + go/seats/meshconfig/contract_test.go | 56 + go/seats/meshdeliverychecker/contract.go | 108 + go/seats/meshdeliverychecker/contract_test.go | 52 + go/seats/meshvault/contract.go | 355 + go/seats/meshvault/contract_test.go | 70 + go/seats/npmpackageregistry/contract.go | 87 + go/seats/npmpackageregistry/contract_test.go | 62 + go/seats/ociartifactregistry/contract.go | 153 + go/seats/ociartifactregistry/contract_test.go | 66 + go/seats/operatorchannel/contract.go | 298 + go/seats/operatorchannel/contract_test.go | 60 + go/seats/seats.go | 746 ++ ts/package.json | 19 + ts/src/container-image-registry.ts | 155 + ts/src/go-package-registry.ts | 50 + ts/src/index.ts | 12 + ts/src/mesh-broker.ts | 261 + ts/src/mesh-config.ts | 119 + ts/src/mesh-delivery-checker.ts | 107 + ts/src/mesh-vault.ts | 345 + ts/src/npm-package-registry.ts | 50 + ts/src/oci-artifact-registry.ts | 114 + ts/src/operator-channel.ts | 282 + ts/src/parts/registry.ts | 173 + ts/src/seats.ts | 6889 +++++++++++++++++ ts/tsconfig.json | 14 + 35 files changed, 11759 insertions(+) create mode 100644 go/go.mod create mode 100644 go/parts/registry/contract.go create mode 100644 go/seats/containerimageregistry/contract.go create mode 100644 go/seats/containerimageregistry/contract_test.go create mode 100644 go/seats/gopackageregistry/contract.go create mode 100644 go/seats/gopackageregistry/contract_test.go create mode 100644 go/seats/meshbroker/contract.go create mode 100644 go/seats/meshbroker/contract_test.go create mode 100644 go/seats/meshconfig/contract.go create mode 100644 go/seats/meshconfig/contract_test.go create mode 100644 go/seats/meshdeliverychecker/contract.go create mode 100644 go/seats/meshdeliverychecker/contract_test.go create mode 100644 go/seats/meshvault/contract.go create mode 100644 go/seats/meshvault/contract_test.go create mode 100644 go/seats/npmpackageregistry/contract.go create mode 100644 go/seats/npmpackageregistry/contract_test.go create mode 100644 go/seats/ociartifactregistry/contract.go create mode 100644 go/seats/ociartifactregistry/contract_test.go create mode 100644 go/seats/operatorchannel/contract.go create mode 100644 go/seats/operatorchannel/contract_test.go create mode 100644 go/seats/seats.go create mode 100644 ts/package.json create mode 100644 ts/src/container-image-registry.ts create mode 100644 ts/src/go-package-registry.ts create mode 100644 ts/src/index.ts create mode 100644 ts/src/mesh-broker.ts create mode 100644 ts/src/mesh-config.ts create mode 100644 ts/src/mesh-delivery-checker.ts create mode 100644 ts/src/mesh-vault.ts create mode 100644 ts/src/npm-package-registry.ts create mode 100644 ts/src/oci-artifact-registry.ts create mode 100644 ts/src/operator-channel.ts create mode 100644 ts/src/parts/registry.ts create mode 100644 ts/src/seats.ts create mode 100644 ts/tsconfig.json diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..efb2a0d --- /dev/null +++ b/go/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go + +go 1.26.0 diff --git a/go/parts/registry/contract.go b/go/parts/registry/contract.go new file mode 100644 index 0000000..81d4e3b --- /dev/null +++ b/go/parts/registry/contract.go @@ -0,0 +1,173 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package registry is the part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. +package registry + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" +) + +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 768 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it is gone + Removed bool `json:"removed"` +} + +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go new file mode 100644 index 0000000..d14c33e --- /dev/null +++ b/go/seats/containerimageregistry/contract.go @@ -0,0 +1,195 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package containerimageregistry is the contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. +package containerimageregistry + +import ( + "git.novox.be/novox/mesh-contracts/go/parts/registry" +) + +// Seat is the seat's name. +const Seat = "container-image-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" + VerbMirror = "mirror" + VerbTags = "tags" + VerbHold = "hold" + VerbRelease = "release" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventHeld = "held" + EventReleased = "released" +) + +// Family is where the seat's events are kept. +const ( + Family = "container-image-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.container-image-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.container-image-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.container-image-registry.event.removed" + +// SubjectHeld is where held is published. +const SubjectHeld = "mesh.seat.container-image-registry.event.held" + +// SubjectReleased is where released is published. +const SubjectReleased = "mesh.seat.container-image-registry.event.released" + +// Artifact is the part registry's. +type Artifact = registry.Artifact + +// Outcome is the part registry's. +type Outcome = registry.Outcome + +// Part is the part registry's. +type Part = registry.Part + +// PublishAnswer is the part registry's. +type PublishAnswer = registry.PublishAnswer + +// PublishInput is the part registry's. +type PublishInput = registry.PublishInput + +// PublishPartAnswer is the part registry's. +type PublishPartAnswer = registry.PublishPartAnswer + +// PublishPartInput is the part registry's. +type PublishPartInput = registry.PublishPartInput + +// PublishedEvent is the part registry's. +type PublishedEvent = registry.PublishedEvent + +// RefusedEvent is the part registry's. +type RefusedEvent = registry.RefusedEvent + +// RemoveAnswer is the part registry's. +type RemoveAnswer = registry.RemoveAnswer + +// RemoveInput is the part registry's. +type RemoveInput = registry.RemoveInput + +// RemovedEvent is the part registry's. +type RemovedEvent = registry.RemovedEvent + +// VersionsAnswer is the part registry's. +type VersionsAnswer = registry.VersionsAnswer + +// VersionsInput is the part registry's. +type VersionsInput = registry.VersionsInput + +// Hold is the body of held and released. +type Hold struct { + // when, RFC 3339 + At string `json:"at,omitempty"` + // the caller the holder proved + By string `json:"by,omitempty"` + // the digest + Digest string `json:"digest"` + // the module holding the seat + Holder string `json:"holder"` + // the name + Name string `json:"name"` + // why + Reason string `json:"reason,omitempty"` +} + +// MirrorInput is what mirror takes. +type MirrorInput struct { + // its pinned digest, sha256: + Digest string `json:"digest"` + // the name it is kept under + Name string `json:"name"` + // the outside image's reference + Source string `json:"source"` +} + +// MirrorAnswer is what mirror answers. +type MirrorAnswer struct { + // the digest stored + Digest string `json:"digest"` + // the reference the seat answers + Reference string `json:"reference"` +} + +// TagsInput is what tags takes. +type TagsInput struct { + // the image + Name string `json:"name"` +} + +// TagsAnswerTagsItem is a type of the contract. +type TagsAnswerTagsItem struct { + // its digest + Digest string `json:"digest"` + // the tag + Tag string `json:"tag"` +} + +// TagsAnswer is what tags answers. +type TagsAnswer struct { + // the image + Name string `json:"name"` + // every tag + Tags []TagsAnswerTagsItem `json:"tags"` +} + +// HoldInput is what hold takes. +type HoldInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // which of the sweep's rules keeps it + Reason string `json:"reason"` +} + +// HoldAnswer is what hold answers. +type HoldAnswer struct { + // it is held + Held bool `json:"held"` + // when the hold began, RFC 3339 + Since string `json:"since,omitempty"` +} + +// ReleaseInput is what release takes. +type ReleaseInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // why it is let go + Reason string `json:"reason"` +} + +// ReleaseAnswer is what release answers. +type ReleaseAnswer struct { + // the hold is gone + Released bool `json:"released"` +} + +// HeldEvent is the body of the event held. +type HeldEvent = Hold + +// ReleasedEvent is the body of the event released. +type ReleasedEvent = Hold + +// ArtifactStoreForm is the form of the provision artifact-store. +type ArtifactStoreForm struct { + // the store's address + URL string `json:"url"` +} diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go new file mode 100644 index 0000000..7141141 --- /dev/null +++ b/go/seats/containerimageregistry/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package containerimageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/container-image-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.container-image-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.container-image-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.container-image-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.removed$")}, + {name: "held on the seat's subject", subject: "mesh.seat.container-image-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.held$")}, + {name: "released on the seat's subject", subject: "mesh.seat.container-image-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.released$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go new file mode 100644 index 0000000..39f58cf --- /dev/null +++ b/go/seats/gopackageregistry/contract.go @@ -0,0 +1,87 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package gopackageregistry is the contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. +package gopackageregistry + +import ( + "git.novox.be/novox/mesh-contracts/go/parts/registry" +) + +// Seat is the seat's name. +const Seat = "go-package-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" +) + +// Family is where the seat's events are kept. +const ( + Family = "go-package-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.go-package-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.go-package-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" + +// Artifact is the part registry's. +type Artifact = registry.Artifact + +// Outcome is the part registry's. +type Outcome = registry.Outcome + +// Part is the part registry's. +type Part = registry.Part + +// PublishAnswer is the part registry's. +type PublishAnswer = registry.PublishAnswer + +// PublishInput is the part registry's. +type PublishInput = registry.PublishInput + +// PublishPartAnswer is the part registry's. +type PublishPartAnswer = registry.PublishPartAnswer + +// PublishPartInput is the part registry's. +type PublishPartInput = registry.PublishPartInput + +// PublishedEvent is the part registry's. +type PublishedEvent = registry.PublishedEvent + +// RefusedEvent is the part registry's. +type RefusedEvent = registry.RefusedEvent + +// RemoveAnswer is the part registry's. +type RemoveAnswer = registry.RemoveAnswer + +// RemoveInput is the part registry's. +type RemoveInput = registry.RemoveInput + +// RemovedEvent is the part registry's. +type RemovedEvent = registry.RemovedEvent + +// VersionsAnswer is the part registry's. +type VersionsAnswer = registry.VersionsAnswer + +// VersionsInput is the part registry's. +type VersionsInput = registry.VersionsInput + +// GoPackageRegistryForm is the form of the provision go-package-registry. +type GoPackageRegistryForm struct { + // the read side's address + URL string `json:"url"` +} diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go new file mode 100644 index 0000000..f52946b --- /dev/null +++ b/go/seats/gopackageregistry/contract_test.go @@ -0,0 +1,62 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package gopackageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/go-package-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go new file mode 100644 index 0000000..46907d3 --- /dev/null +++ b/go/seats/meshbroker/contract.go @@ -0,0 +1,280 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshbroker is the contract of the seat "mesh-broker", defined whole (seats/mesh-broker/seat.json): The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone. +package meshbroker + +// Seat is the seat's name. +const Seat = "mesh-broker" + +const ( + VerbList = "list" + VerbShow = "show" + VerbCheck = "check" + VerbOrphans = "orphans" + VerbCreate = "create" + VerbApply = "apply" + VerbRecreate = "recreate" + VerbDrop = "drop" +) + +const ( + EventCreated = "created" + EventRefused = "refused" + EventDrifted = "drifted" + EventDriftCleared = "drift-cleared" + EventFull = "full" + EventFullCleared = "full-cleared" + EventOrphaned = "orphaned" + EventRecreated = "recreated" + EventDropped = "dropped" +) + +// Family is where the seat's events are kept. +const ( + Family = "mesh-broker.events" + FamilyMaxAgeDays = 30 + FamilyMaxMiB = 256 +) + +// SubjectCreated is where created is published. +const SubjectCreated = "mesh.seat.mesh-broker.event.created" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.mesh-broker.event.refused" + +// SubjectDrifted is where drifted is published. +const SubjectDrifted = "mesh.seat.mesh-broker.event.drifted" + +// SubjectDriftCleared is where drift-cleared is published. +const SubjectDriftCleared = "mesh.seat.mesh-broker.event.drift-cleared" + +// SubjectFull is where full is published. +const SubjectFull = "mesh.seat.mesh-broker.event.full" + +// SubjectFullCleared is where full-cleared is published. +const SubjectFullCleared = "mesh.seat.mesh-broker.event.full-cleared" + +// SubjectOrphaned is where orphaned is published. +const SubjectOrphaned = "mesh.seat.mesh-broker.event.orphaned" + +// SubjectRecreated is where recreated is published. +const SubjectRecreated = "mesh.seat.mesh-broker.event.recreated" + +// SubjectDropped is where dropped is published. +const SubjectDropped = "mesh.seat.mesh-broker.event.dropped" + +// Entry is one piece of bus state as a manifest asks it. +type Entry struct { + // its own configuration, where no template fits + Configuration map[string]any `json:"configuration,omitempty"` + // its local name in the module's manifest + LocalName string `json:"local-name"` + // the module asking it + Module string `json:"module"` + // the template's options the module set, within their bounds + Options map[string]any `json:"options,omitempty"` + // how the bus keeps it + StorageType string `json:"storage-type"` + // its template; empty for a configuration of its own + Template string `json:"template,omitempty"` +} + +// Fill is a type of the contract. +type Fill struct { + // bytes held + Bytes int64 `json:"bytes"` + // its cap in bytes + Cap int64 `json:"cap"` + // messages, keys or objects held + Count int64 `json:"count"` +} + +// Held is one entry as the bus holds it. +type Held struct { + // its name on the bus, _ + BusName string `json:"bus-name"` + // its own configuration, where no template fits + Configuration map[string]any `json:"configuration,omitempty"` + Fill *Fill `json:"fill,omitempty"` + // its local name in the module's manifest + LocalName string `json:"local-name"` + // the module asking it + Module string `json:"module"` + // the template's options the module set, within their bounds + Options map[string]any `json:"options,omitempty"` + // the settings the bus holds + Settings map[string]any `json:"settings,omitempty"` + // how the bus keeps it + StorageType string `json:"storage-type"` + // its template; empty for a configuration of its own + Template string `json:"template,omitempty"` +} + +// Named is a type of the contract. +type Named struct { + // the entry's local name + LocalName string `json:"local-name"` + // the module + Module string `json:"module"` +} + +// Said is the body of every event of the seat. +type Said struct { + // the settings asked + Asked map[string]any `json:"asked,omitempty"` + // its name on the bus + BusName string `json:"bus-name"` + // its own configuration, where no template fits + Configuration map[string]any `json:"configuration,omitempty"` + Fill *Fill `json:"fill,omitempty"` + // the settings held + Held map[string]any `json:"held,omitempty"` + // its local name in the module's manifest + LocalName string `json:"local-name"` + // the module asking it + Module string `json:"module"` + // the template's options the module set, within their bounds + Options map[string]any `json:"options,omitempty"` + // the stream snapshot taken before a recreate or drop + Snapshot string `json:"snapshot,omitempty"` + // how the bus keeps it + StorageType string `json:"storage-type,omitempty"` + // its template; empty for a configuration of its own + Template string `json:"template,omitempty"` + // why: a refusal's cause, a drift's difference, a drop's or recreate's reason + Why string `json:"why,omitempty"` +} + +// Warranted is a type of the contract. +type Warranted struct { + // the entry's local name + LocalName string `json:"local-name"` + // the module + Module string `json:"module"` + // the operator's warrant, as the operator channel decided it (ADR 0259 §6) + Warrant string `json:"warrant"` + // why, kept in the record + Why string `json:"why"` +} + +// ListInput is what list takes. +type ListInput struct { + // one module (optional) + Module string `json:"module,omitempty"` +} + +// ListAnswer is what list answers. +type ListAnswer struct { + // every entry + Entries []Held `json:"entries"` +} + +// ShowInput is what show takes. +type ShowInput = Named + +// ShowAnswer is what show answers. +type ShowAnswer = Held + +// CheckInput is what check takes. +type CheckInput struct { + // one module (optional) + Module string `json:"module,omitempty"` +} + +// CheckAnswerDifferencesItem is a type of the contract. +type CheckAnswerDifferencesItem struct { + // asked + Asked map[string]any `json:"asked,omitempty"` + // the difference only grows: apply may act on it + Grows bool `json:"grows,omitempty"` + // held + Held map[string]any `json:"held,omitempty"` + // the entry + LocalName string `json:"local-name"` + // the module + Module string `json:"module"` +} + +// CheckAnswer is what check answers. +type CheckAnswer struct { + // every difference + Differences []CheckAnswerDifferencesItem `json:"differences"` +} + +// OrphansInput is what orphans takes. +type OrphansInput struct { +} + +// OrphansAnswer is what orphans answers. +type OrphansAnswer struct { + // every orphan + Entries []Held `json:"entries"` +} + +// CreateInput is what create takes. +type CreateInput = Entry + +// CreateAnswer is what create answers. +type CreateAnswer = Held + +// ApplyInput is what apply takes. +type ApplyInput = Named + +// ApplyAnswer is what apply answers. +type ApplyAnswer = Held + +// RecreateInput is what recreate takes. +type RecreateInput = Warranted + +// RecreateAnswer is what recreate answers. +type RecreateAnswer = Held + +// DropInput is what drop takes. +type DropInput = Warranted + +// DropAnswer is what drop answers. +type DropAnswer struct { + // it is gone + Dropped bool `json:"dropped"` + // the snapshot taken first + Snapshot string `json:"snapshot,omitempty"` +} + +// CreatedEvent is the body of the event created. +type CreatedEvent = Said + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Said + +// DriftedEvent is the body of the event drifted. +type DriftedEvent = Said + +// DriftClearedEvent is the body of the event drift-cleared. +type DriftClearedEvent = Said + +// FullEvent is the body of the event full. +type FullEvent = Said + +// FullClearedEvent is the body of the event full-cleared. +type FullClearedEvent = Said + +// OrphanedEvent is the body of the event orphaned. +type OrphanedEvent = Said + +// RecreatedEvent is the body of the event recreated. +type RecreatedEvent = Said + +// DroppedEvent is the body of the event dropped. +type DroppedEvent = Said + +// BusStateForm is the form of the provision bus-state. +type BusStateForm struct { + // the entry's name on the bus + BusName string `json:"bus-name"` + // how the bus keeps it + StorageType string `json:"storage-type"` + // its subjects, for a stream + Subjects []string `json:"subjects,omitempty"` + // its template + Template string `json:"template,omitempty"` +} diff --git a/go/seats/meshbroker/contract_test.go b/go/seats/meshbroker/contract_test.go new file mode 100644 index 0000000..03c1f9c --- /dev/null +++ b/go/seats/meshbroker/contract_test.go @@ -0,0 +1,70 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshbroker + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-broker/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "created on the seat's subject", subject: "mesh.seat.mesh-broker.event.created", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v CreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.created$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.mesh-broker.event.refused", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.refused$")}, + {name: "drifted on the seat's subject", subject: "mesh.seat.mesh-broker.event.drifted", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DriftedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drifted$")}, + {name: "drift-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.drift-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v DriftClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drift-cleared$")}, + {name: "full on the seat's subject", subject: "mesh.seat.mesh-broker.event.full", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full$")}, + {name: "full-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.full-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full-cleared$")}, + {name: "orphaned on the seat's subject", subject: "mesh.seat.mesh-broker.event.orphaned", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v OrphanedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.orphaned$")}, + {name: "recreated on the seat's subject", subject: "mesh.seat.mesh-broker.event.recreated", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.recreated$")}, + {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.event.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.dropped$")}, + {name: "create one entry", subject: "", given: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v CreateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "drop on the operator's warrant", subject: "", given: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", wire: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", + decode: func(b []byte) (any, error) { var v DropInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "list answers every entry", subject: "", given: "{\"entries\":[{\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"settings\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}]}", wire: "{\"entries\":[{\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"settings\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}]}", + decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go new file mode 100644 index 0000000..ff82d22 --- /dev/null +++ b/go/seats/meshconfig/contract.go @@ -0,0 +1,120 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshconfig is the contract of the seat "mesh-config", defined whole (seats/mesh-config/seat.json): The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2). +package meshconfig + +// Seat is the seat's name. +const Seat = "mesh-config" + +const ( + VerbConfig = "config" + VerbRenames = "renames" +) + +// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). +const ( + HolderEventConfigChanged = "config-changed" + HolderEventRenamed = "renamed" + HolderEventRetiring = "retiring" + HolderEventRetired = "retired" +) + +// HolderSubjectConfigChanged is where the holder publishes config-changed. +func HolderSubjectConfigChanged(holder string) string { + return "mesh.mod." + holder + ".event.config-changed" +} + +// HolderSubjectRenamed is where the holder publishes renamed, kept in config.events. +func HolderSubjectRenamed(holder string) string { return "mesh.mod." + holder + ".event.renamed" } + +// HolderSubjectRetiring is where the holder publishes retiring, kept in config.events. +func HolderSubjectRetiring(holder string) string { return "mesh.mod." + holder + ".event.retiring" } + +// HolderSubjectRetired is where the holder publishes retired, kept in config.events. +func HolderSubjectRetired(holder string) string { return "mesh.mod." + holder + ".event.retired" } + +// ConfigInput is what config takes. +type ConfigInput struct { + // list (when not given), set, clear or history + Action string `json:"action,omitempty"` + // who gave the value at the controller's terminal + By string `json:"by,omitempty"` + // the config key + Key string `json:"key,omitempty"` + // for history: at most this many, newest + Limit float64 `json:"limit,omitempty"` + // a module: its own value, with node + Module string `json:"module,omitempty"` + // a node + Node string `json:"node,omitempty"` + // for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list + Value any `json:"value,omitempty"` + // for set and clear: why + Why string `json:"why,omitempty"` +} + +// ConfigAnswer is what config answers. +type ConfigAnswer = map[string]any + +// RenamesInput is what renames takes. +type RenamesInput struct { + // one module (optional) + Module string `json:"module,omitempty"` +} + +// RenamesAnswerRenamesItem is a type of the contract. +type RenamesAnswerRenamesItem struct { + // when it moved, RFC 3339 + At string `json:"at,omitempty"` + // the old name + From string `json:"from"` + // the module + Module string `json:"module"` + // the nodes still holding the old name open + Open []string `json:"open,omitempty"` + // the new name + To string `json:"to"` +} + +// RenamesAnswer is what renames answers. +type RenamesAnswer struct { + // every open rename + Renames []RenamesAnswerRenamesItem `json:"renames"` +} + +// RenamedHolderEvent is the body of the holder's event renamed. +type RenamedHolderEvent struct { + // when, RFC 3339 + At string `json:"at"` + // the old name + From string `json:"from"` + // the module + Module string `json:"module"` + // the new name + To string `json:"to"` +} + +// RetiringHolderEvent is the body of the holder's event retiring. +type RetiringHolderEvent struct { + // the ask's id + Ask string `json:"ask,omitempty"` + // the value's or secret's name + Key string `json:"key"` + Kind string `json:"kind"` + // the module + Module string `json:"module"` + // since when, RFC 3339 + Since string `json:"since"` +} + +// RetiredHolderEvent is the body of the holder's event retired. +type RetiredHolderEvent struct { + // the ask's id + Ask string `json:"ask,omitempty"` + // the module + Module string `json:"module"` + // the name + Name string `json:"name"` + // the node + Node string `json:"node,omitempty"` +} diff --git a/go/seats/meshconfig/contract_test.go b/go/seats/meshconfig/contract_test.go new file mode 100644 index 0000000..eb8839a --- /dev/null +++ b/go/seats/meshconfig/contract_test.go @@ -0,0 +1,56 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshconfig + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-config/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "config-changed, unchanged", subject: "mesh.mod.mesh-config.event.config-changed", given: "{\"key\":\"x\"}", wire: "{\"key\":\"x\"}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.config-changed$")}, + {name: "renamed", subject: "mesh.mod.mesh-config.event.renamed", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", + decode: func(b []byte) (any, error) { var v RenamedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.renamed$")}, + {name: "retiring", subject: "mesh.mod.mesh-config.event.retiring", given: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", wire: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", + decode: func(b []byte) (any, error) { var v RetiringHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.retiring$")}, + {name: "retired", subject: "mesh.mod.mesh-config.event.retired", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v RetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.retired$")}, + {name: "renames answers the open windows", subject: "", given: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", wire: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", + decode: func(b []byte) (any, error) { var v RenamesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go new file mode 100644 index 0000000..da03e1a --- /dev/null +++ b/go/seats/meshdeliverychecker/contract.go @@ -0,0 +1,108 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshdeliverychecker is the contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). +package meshdeliverychecker + +// Seat is the seat's name. +const Seat = "mesh-delivery-checker" + +const ( + VerbTry = "try" + VerbTryRead = "try-read" + VerbTryEnd = "try-end" +) + +// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). +const ( + HolderEventVerdict = "verdict" + HolderEventTagWanted = "tag-wanted" + HolderEventPreviewWritten = "preview.written" +) + +// HolderSubjectVerdict is where the holder publishes verdict, kept in checker.asks. +func HolderSubjectVerdict(holder string) string { return "mesh.mod." + holder + ".event.verdict" } + +// HolderSubjectTagWanted is where the holder publishes tag-wanted, kept in checker.asks. +func HolderSubjectTagWanted(holder string) string { return "mesh.mod." + holder + ".event.tag-wanted" } + +// HolderSubjectPreviewWritten is where the holder publishes preview.written, kept in checks. +func HolderSubjectPreviewWritten(holder string) string { + return "mesh.mod." + holder + ".event.preview.written" +} + +// TryInput is what try takes. +type TryInput struct { + // the full commit to try, 40 or 64 hex digits + Head string `json:"head"` + // unit or integration: only this level; both when absent + Level string `json:"level,omitempty"` + // / + Repository string `json:"repository"` + // a go test -run pattern: only those tests run, and nothing is kept + Tests string `json:"tests,omitempty"` + // the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent + Trunk string `json:"trunk,omitempty"` +} + +// TryAnswer is what try answers. +type TryAnswer struct { + // the try's id + ID string `json:"id"` + // its state + State string `json:"state,omitempty"` +} + +// TryReadInput is what try-read takes. +type TryReadInput struct { + // the try's id, try- + ID string `json:"id"` +} + +// TryReadAnswer is what try-read answers. +type TryReadAnswer = map[string]any + +// TryEndInput is what try-end takes. +type TryEndInput struct { + // the try's id, try- + ID string `json:"id"` + // why it is ended, in words + Why string `json:"why"` +} + +// TryEndAnswer is what try-end answers. +type TryEndAnswer struct { + // it is ended + Ended bool `json:"ended"` + // the try + ID string `json:"id"` +} + +// VerdictHolderEvent is the body of the holder's event verdict. +type VerdictHolderEvent struct { + // the commit checked + Head string `json:"head"` + // the generated files out of step with their seat definitions, each path (design 26) + OutOfStep []string `json:"out-of-step,omitempty"` + // the pull request's number, for a pull request's check + Pull int64 `json:"pull,omitempty"` + // owner/repo + Repository string `json:"repository"` + // pass or fail + Verdict string `json:"verdict"` +} + +// TagWantedHolderEvent is the body of the holder's event tag-wanted. +type TagWantedHolderEvent struct { + // its head + Head string `json:"head"` + // the group member + Module string `json:"module"` + // the package it declares + Package string `json:"package"` + // the pull request + Pull int64 `json:"pull,omitempty"` + // owner/repo + Repository string `json:"repository"` + // the tag wanted + Tag string `json:"tag,omitempty"` +} diff --git a/go/seats/meshdeliverychecker/contract_test.go b/go/seats/meshdeliverychecker/contract_test.go new file mode 100644 index 0000000..0e1514f --- /dev/null +++ b/go/seats/meshdeliverychecker/contract_test.go @@ -0,0 +1,52 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshdeliverychecker + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-delivery-checker/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "verdict on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", + decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.verdict$")}, + {name: "tag-wanted on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.tag-wanted", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", + decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, + {name: "preview.written on the checker's stream", subject: "mesh.mod.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/meshvault/contract.go b/go/seats/meshvault/contract.go new file mode 100644 index 0000000..b0231b3 --- /dev/null +++ b/go/seats/meshvault/contract.go @@ -0,0 +1,355 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshvault is the contract of the seat "mesh-vault", defined whole (seats/mesh-vault/seat.json): The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2). +package meshvault + +// Seat is the seat's name. +const Seat = "mesh-vault" + +const ( + VerbGet = "get" + VerbSecretAsk = "secret-ask" + VerbIssued = "issued" + VerbRotate = "rotate" + VerbMoveIn = "move-in" + VerbNodeKey = "node-key" + VerbOwn = "own" + VerbIssueAccount = "issue-account" + VerbSign = "sign" + VerbEnrolmentToken = "enrolment-token" + VerbNodeAccount = "node-account" + VerbRotateNode = "rotate-node" + VerbNodeAccountConfirm = "node-account-confirm" + VerbAbandon = "abandon" + VerbRetire = "retire" +) + +// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). +const ( + HolderEventProvisioned = "provisioned" + HolderEventRotated = "rotated" + HolderEventSecretChanged = "secret-changed" + HolderEventAccountHashed = "account-hashed" + HolderEventOwnReplaced = "own-replaced" + HolderEventAccountIssued = "account-issued" + HolderEventSigned = "signed" + HolderEventRotationBegun = "rotation-begun" + HolderEventNodeAccountReady = "node-account-ready" + HolderEventRotationDone = "rotation-done" + HolderEventOwnRetired = "own-retired" +) + +// HolderSubjectProvisioned is where the holder publishes provisioned. +func HolderSubjectProvisioned(holder string) string { + return "mesh.mod." + holder + ".event.provisioned" +} + +// HolderSubjectRotated is where the holder publishes rotated. +func HolderSubjectRotated(holder string) string { return "mesh.mod." + holder + ".event.rotated" } + +// HolderSubjectSecretChanged is where the holder publishes secret-changed. +func HolderSubjectSecretChanged(holder string) string { + return "mesh.mod." + holder + ".event.secret-changed" +} + +// HolderSubjectAccountHashed is where the holder publishes account-hashed. +func HolderSubjectAccountHashed(holder string) string { + return "mesh.mod." + holder + ".event.account-hashed" +} + +// HolderSubjectOwnReplaced is where the holder publishes own-replaced. +func HolderSubjectOwnReplaced(holder string) string { + return "mesh.mod." + holder + ".event.own-replaced" +} + +// HolderSubjectAccountIssued is where the holder publishes account-issued. +func HolderSubjectAccountIssued(holder string) string { + return "mesh.mod." + holder + ".event.account-issued" +} + +// HolderSubjectSigned is where the holder publishes signed. +func HolderSubjectSigned(holder string) string { return "mesh.mod." + holder + ".event.signed" } + +// HolderSubjectRotationBegun is where the holder publishes rotation-begun. +func HolderSubjectRotationBegun(holder string) string { + return "mesh.mod." + holder + ".event.rotation-begun" +} + +// HolderSubjectNodeAccountReady is where the holder publishes node-account-ready. +func HolderSubjectNodeAccountReady(holder string) string { + return "mesh.mod." + holder + ".event.node-account-ready" +} + +// HolderSubjectRotationDone is where the holder publishes rotation-done. +func HolderSubjectRotationDone(holder string) string { + return "mesh.mod." + holder + ".event.rotation-done" +} + +// HolderSubjectOwnRetired is where the holder publishes own-retired. +func HolderSubjectOwnRetired(holder string) string { + return "mesh.mod." + holder + ".event.own-retired" +} + +// GetInput is what get takes. +type GetInput struct { + // the secret's name + Name string `json:"name"` + // the asker's X25519 public key as SPKI PEM — never a private key + PublicKey string `json:"public_key"` +} + +// GetAnswer is what get answers. +type GetAnswer = map[string]any + +// SecretAskInput is what secret-ask takes. +type SecretAskInput struct { + // who asked, as the bus named its caller + AskedBy string `json:"asked_by,omitempty"` + // the second step: the call the first step answered + Call string `json:"call,omitempty"` + // the machine whose prompt asks + Desk string `json:"desk,omitempty"` + // the module + Module string `json:"module,omitempty"` + // the secret's name in the module + Name string `json:"name,omitempty"` + // the machine the secret is for + Node string `json:"node,omitempty"` + // instead of sealed: how the prompt ended + Outcome string `json:"outcome,omitempty"` + // the second step: the value sealed to the call's key — never the value + Sealed string `json:"sealed,omitempty"` +} + +// SecretAskAnswer is what secret-ask answers. +type SecretAskAnswer = map[string]any + +// IssuedInputHoldersItem is a type of the contract. +type IssuedInputHoldersItem struct { + // the consuming module + Module string `json:"module,omitempty"` + // the holder's machine + Node string `json:"node,omitempty"` +} + +// IssuedInput is what issued takes. +type IssuedInput struct { + // the second step: the call the first step answered + Call string `json:"call,omitempty"` + // the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone + Holders []IssuedInputHoldersItem `json:"holders,omitempty"` + // the provider's module + Module string `json:"module,omitempty"` + // the provider's machine + Provider string `json:"provider,omitempty"` + // the provision + Provision string `json:"provision,omitempty"` + // the second step: the value sealed to the call's key — never the value + Sealed string `json:"sealed,omitempty"` +} + +// IssuedAnswer is what issued answers. +type IssuedAnswer = map[string]any + +// RotateInput is what rotate takes. +type RotateInput struct { + // who asked, when the controller carries the act + By string `json:"by,omitempty"` + // the module + Module string `json:"module,omitempty"` + // the secret's name in the module + Name string `json:"name,omitempty"` + // the machine, for a module's own secret + Node string `json:"node,omitempty"` + // the provider's machine + Provider string `json:"provider,omitempty"` + // the provision + Provision string `json:"provision,omitempty"` +} + +// RotateAnswer is what rotate answers. +type RotateAnswer = map[string]any + +// MoveInInput is what move-in takes. +type MoveInInput struct { + // the second step: the call the first step answered + Call string `json:"call,omitempty"` + // the consumer's machine + Consumer string `json:"consumer,omitempty"` + // the consuming module + ConsumerModule string `json:"consumer_module,omitempty"` + // the controller's generation for the value + Generation string `json:"generation,omitempty"` + // the credential's local name, when it has one + Local string `json:"local,omitempty"` + // when it was made (RFC 3339) + MadeAt string `json:"made_at,omitempty"` + // its module + Module string `json:"module,omitempty"` + // its name in the module + Name string `json:"name,omitempty"` + // a module's own secret: its machine + Node string `json:"node,omitempty"` + // made, accepted or issued, as the controller's store says + Origin string `json:"origin,omitempty"` + // a pair credential: the provider's machine + Provider string `json:"provider,omitempty"` + // the provision + Provision string `json:"provision,omitempty"` + // the second step: a raw X25519 public key the read-back is sealed to + ReadBackTo string `json:"read_back_to,omitempty"` + // the second step: the value sealed to the call's key — never the value + Sealed string `json:"sealed,omitempty"` +} + +// MoveInAnswer is what move-in answers. +type MoveInAnswer = map[string]any + +// NodeKeyInput is what node-key takes. +type NodeKeyInput struct { + // the machine's sealing key, a raw X25519 public key in base64 + Key string `json:"key,omitempty"` +} + +// NodeKeyAnswer is what node-key answers. +type NodeKeyAnswer = map[string]any + +// OwnInput is what own takes. +type OwnInput struct { + // the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local} + Credentials []map[string]any `json:"credentials,omitempty"` +} + +// OwnAnswer is what own answers. +type OwnAnswer = map[string]any + +// IssueAccountInput is what issue-account takes. +type IssueAccountInput struct { + // the bus user, e.g. ace.mesh-build-go + User string `json:"user"` +} + +// IssueAccountAnswer is what issue-account answers. +type IssueAccountAnswer = map[string]any + +// SignInput is what sign takes. +type SignInput struct { + // node-certificate or enrolment + Kind string `json:"kind"` + // the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols + Node string `json:"node,omitempty"` + // the redeem answer's bytes in base64, for an enrolment + Reply string `json:"reply,omitempty"` + // from mesh-enrolment: the serving key's public half the enrolling node presented + ServingKey string `json:"serving_key,omitempty"` +} + +// SignAnswer is what sign answers. +type SignAnswer = map[string]any + +// EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. +type EnrolmentTokenInputTunnel struct { + // the joining node's tunnel address + Address string `json:"address"` + // the hub's endpoint, host:port + HubEndpoint string `json:"hub_endpoint"` + // the hub's tunnel public key + HubKey string `json:"hub_key"` + // the joining machine's own tunnel public key, as given at issue + Key string `json:"key"` + // the private network's range, a prefix + Range string `json:"range"` +} + +// EnrolmentTokenInput is what enrolment-token takes. +type EnrolmentTokenInput struct { + // "true": drop the node's token and its hash + Drop string `json:"drop,omitempty"` + // when the token expires (RFC 3339), as the enrolment's key says + Expires string `json:"expires,omitempty"` + // the node the token is for + Node string `json:"node"` + // "true": answer the token sealed to the operator key + Sealed string `json:"sealed,omitempty"` + // the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range + Tunnel *EnrolmentTokenInputTunnel `json:"tunnel,omitempty"` +} + +// EnrolmentTokenAnswer is what enrolment-token answers. +type EnrolmentTokenAnswer = map[string]any + +// NodeAccountInput is what node-account takes. +type NodeAccountInput struct { + // the digest of every public key the node presented (delivery.KeysDigest) + KeysDigest string `json:"keys_digest,omitempty"` + // the joining node + Node string `json:"node"` + // "true": retire the account minted for this node + Retire string `json:"retire,omitempty"` + // the sealing key's public half the node presented, which the account is sealed to + SealingKey string `json:"sealing_key,omitempty"` +} + +// NodeAccountAnswer is what node-account answers. +type NodeAccountAnswer = map[string]any + +// RotateNodeInput is what rotate-node takes. +type RotateNodeInput struct { + // the id of the hand act recorded for it in mesh-hand-acts + Act string `json:"act"` + // the node whose account rotates + Node string `json:"node"` + // why, in a sentence + Why string `json:"why"` +} + +// RotateNodeAnswer is what rotate-node answers. +type RotateNodeAnswer = map[string]any + +// NodeAccountConfirmInput is what node-account-confirm takes. +type NodeAccountConfirmInput struct { +} + +// NodeAccountConfirmAnswer is what node-account-confirm answers. +type NodeAccountConfirmAnswer = map[string]any + +// AbandonInput is what abandon takes. +type AbandonInput struct { + // the node whose rotation is abandoned + Node string `json:"node"` +} + +// AbandonAnswer is what abandon answers. +type AbandonAnswer = map[string]any + +// RetireInput is what retire takes. +type RetireInput struct { + // the module + Module string `json:"module"` + // the own secret's name + Name string `json:"name"` + // the node + Node string `json:"node"` + // the operator's warrant for this node, module and name + Warrant string `json:"warrant"` +} + +// RetireAnswer is what retire answers. +type RetireAnswer struct { + // the secret is gone + Retired bool `json:"retired"` +} + +// OwnRetiredHolderEvent is the body of the holder's event own-retired. +type OwnRetiredHolderEvent struct { + // the ask's id + Ask string `json:"ask,omitempty"` + // the caller the vault proved + Caller string `json:"caller"` + // the module + Module string `json:"module"` + // the secret's name + Name string `json:"name"` + // the node + Node string `json:"node"` +} diff --git a/go/seats/meshvault/contract_test.go b/go/seats/meshvault/contract_test.go new file mode 100644 index 0000000..56dbe3b --- /dev/null +++ b/go/seats/meshvault/contract_test.go @@ -0,0 +1,70 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshvault + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-vault/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "provisioned under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.provisioned", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.provisioned$")}, + {name: "rotated under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotated", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotated$")}, + {name: "secret-changed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.secret-changed", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.secret-changed$")}, + {name: "account-hashed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.account-hashed", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.account-hashed$")}, + {name: "own-replaced under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.own-replaced", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-replaced$")}, + {name: "account-issued under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.account-issued", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.account-issued$")}, + {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.signed$")}, + {name: "rotation-begun under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotation-begun", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-begun$")}, + {name: "node-account-ready under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.node-account-ready", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.node-account-ready$")}, + {name: "rotation-done under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotation-done", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-done$")}, + {name: "own-retired", subject: "mesh.mod.mesh-vault.event.own-retired", given: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v OwnRetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-retired$")}, + {name: "retire on a warrant", subject: "", given: "{\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", + decode: func(b []byte) (any, error) { var v RetireInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go new file mode 100644 index 0000000..690946c --- /dev/null +++ b/go/seats/npmpackageregistry/contract.go @@ -0,0 +1,87 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package npmpackageregistry is the contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. +package npmpackageregistry + +import ( + "git.novox.be/novox/mesh-contracts/go/parts/registry" +) + +// Seat is the seat's name. +const Seat = "npm-package-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" +) + +// Family is where the seat's events are kept. +const ( + Family = "npm-package-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.npm-package-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" + +// Artifact is the part registry's. +type Artifact = registry.Artifact + +// Outcome is the part registry's. +type Outcome = registry.Outcome + +// Part is the part registry's. +type Part = registry.Part + +// PublishAnswer is the part registry's. +type PublishAnswer = registry.PublishAnswer + +// PublishInput is the part registry's. +type PublishInput = registry.PublishInput + +// PublishPartAnswer is the part registry's. +type PublishPartAnswer = registry.PublishPartAnswer + +// PublishPartInput is the part registry's. +type PublishPartInput = registry.PublishPartInput + +// PublishedEvent is the part registry's. +type PublishedEvent = registry.PublishedEvent + +// RefusedEvent is the part registry's. +type RefusedEvent = registry.RefusedEvent + +// RemoveAnswer is the part registry's. +type RemoveAnswer = registry.RemoveAnswer + +// RemoveInput is the part registry's. +type RemoveInput = registry.RemoveInput + +// RemovedEvent is the part registry's. +type RemovedEvent = registry.RemovedEvent + +// VersionsAnswer is the part registry's. +type VersionsAnswer = registry.VersionsAnswer + +// VersionsInput is the part registry's. +type VersionsInput = registry.VersionsInput + +// NpmPackageRegistryForm is the form of the provision npm-package-registry. +type NpmPackageRegistryForm struct { + // the read side's address + URL string `json:"url"` +} diff --git a/go/seats/npmpackageregistry/contract_test.go b/go/seats/npmpackageregistry/contract_test.go new file mode 100644 index 0000000..66b39b2 --- /dev/null +++ b/go/seats/npmpackageregistry/contract_test.go @@ -0,0 +1,62 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package npmpackageregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/npm-package-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go new file mode 100644 index 0000000..e602f28 --- /dev/null +++ b/go/seats/ociartifactregistry/contract.go @@ -0,0 +1,153 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package ociartifactregistry is the contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. +package ociartifactregistry + +import ( + "git.novox.be/novox/mesh-contracts/go/parts/registry" +) + +// Seat is the seat's name. +const Seat = "oci-artifact-registry" + +const ( + VerbPublish = "publish" + VerbPublishPart = "publish-part" + VerbVersions = "versions" + VerbRemove = "remove" + VerbHold = "hold" + VerbRelease = "release" +) + +const ( + EventPublished = "published" + EventRefused = "refused" + EventRemoved = "removed" + EventHeld = "held" + EventReleased = "released" +) + +// Family is where the seat's events are kept. +const ( + Family = "oci-artifact-registry.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPublished is where published is published. +const SubjectPublished = "mesh.seat.oci-artifact-registry.event.published" + +// SubjectRefused is where refused is published. +const SubjectRefused = "mesh.seat.oci-artifact-registry.event.refused" + +// SubjectRemoved is where removed is published. +const SubjectRemoved = "mesh.seat.oci-artifact-registry.event.removed" + +// SubjectHeld is where held is published. +const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" + +// SubjectReleased is where released is published. +const SubjectReleased = "mesh.seat.oci-artifact-registry.event.released" + +// Artifact is the part registry's. +type Artifact = registry.Artifact + +// Outcome is the part registry's. +type Outcome = registry.Outcome + +// Part is the part registry's. +type Part = registry.Part + +// PublishAnswer is the part registry's. +type PublishAnswer = registry.PublishAnswer + +// PublishInput is the part registry's. +type PublishInput = registry.PublishInput + +// PublishPartAnswer is the part registry's. +type PublishPartAnswer = registry.PublishPartAnswer + +// PublishPartInput is the part registry's. +type PublishPartInput = registry.PublishPartInput + +// PublishedEvent is the part registry's. +type PublishedEvent = registry.PublishedEvent + +// RefusedEvent is the part registry's. +type RefusedEvent = registry.RefusedEvent + +// RemoveAnswer is the part registry's. +type RemoveAnswer = registry.RemoveAnswer + +// RemoveInput is the part registry's. +type RemoveInput = registry.RemoveInput + +// RemovedEvent is the part registry's. +type RemovedEvent = registry.RemovedEvent + +// VersionsAnswer is the part registry's. +type VersionsAnswer = registry.VersionsAnswer + +// VersionsInput is the part registry's. +type VersionsInput = registry.VersionsInput + +// Hold is the body of held and released. +type Hold struct { + // when, RFC 3339 + At string `json:"at,omitempty"` + // the caller the holder proved + By string `json:"by,omitempty"` + // the digest + Digest string `json:"digest"` + // the module holding the seat + Holder string `json:"holder"` + // the name + Name string `json:"name"` + // why + Reason string `json:"reason,omitempty"` +} + +// HoldInput is what hold takes. +type HoldInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // which of the sweep's rules keeps it + Reason string `json:"reason"` +} + +// HoldAnswer is what hold answers. +type HoldAnswer struct { + // it is held + Held bool `json:"held"` + // when the hold began, RFC 3339 + Since string `json:"since,omitempty"` +} + +// ReleaseInput is what release takes. +type ReleaseInput struct { + // the digest + Digest string `json:"digest"` + // the name + Name string `json:"name"` + // why it is let go + Reason string `json:"reason"` +} + +// ReleaseAnswer is what release answers. +type ReleaseAnswer struct { + // the hold is gone + Released bool `json:"released"` +} + +// HeldEvent is the body of the event held. +type HeldEvent = Hold + +// ReleasedEvent is the body of the event released. +type ReleasedEvent = Hold + +// ArtifactStoreForm is the form of the provision artifact-store. +type ArtifactStoreForm struct { + // the store's address + URL string `json:"url"` +} diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go new file mode 100644 index 0000000..f8e04e6 --- /dev/null +++ b/go/seats/ociartifactregistry/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package ociartifactregistry + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/oci-artifact-registry/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "published on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.published$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.refused$")}, + {name: "removed on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.removed$")}, + {name: "held on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.held$")}, + {name: "released on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", + decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.released$")}, + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", + decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go new file mode 100644 index 0000000..49fdc1e --- /dev/null +++ b/go/seats/operatorchannel/contract.go @@ -0,0 +1,298 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package operatorchannel is the contract of the seat "operator-channel", defined whole (seats/operator-channel/seat.json): Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26). +package operatorchannel + +// Seat is the seat's name. +const Seat = "operator-channel" + +const ( + VerbOpen = "open" + VerbHistory = "history" + VerbNotify = "notify" + VerbTell = "tell" + VerbAsks = "asks" +) + +const ( + AcceptAsk = "ask" + AcceptCancel = "cancel" + AcceptCondition = "condition" +) + +const ( + EventDecided = "decided" +) + +// Family is where the seat's events are kept. +const ( + Family = "operator-channel.decided" + FamilyMaxAgeDays = 30 +) + +// SubjectDecided is where decided is published for one caller. +func SubjectDecided(caller string) string { + return "mesh.seat.operator-channel.event.decided." + caller +} + +// AcceptSubjectAsk is where one caller sends ask. +func AcceptSubjectAsk(caller string) string { return "mesh.seat.operator-channel.accept.ask." + caller } + +// AcceptSubjectCancel is where one caller sends cancel. +func AcceptSubjectCancel(caller string) string { + return "mesh.seat.operator-channel.accept.cancel." + caller +} + +// AcceptSubjectCondition is where one caller sends condition. +func AcceptSubjectCondition(caller string) string { + return "mesh.seat.operator-channel.accept.condition." + caller +} + +// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). +const ( + HolderEventRefused = "refused" + HolderEventOffered = "offered" + HolderEventClosed = "closed" +) + +// HolderSubjectRefused is where the holder publishes refused. +func HolderSubjectRefused(holder string) string { return "mesh.mod." + holder + ".event.refused" } + +// HolderSubjectOffered is where the holder publishes offered, kept in the router's restricted stream (design 55 § 11). +func HolderSubjectOffered(holder string) string { return "mesh.mod." + holder + ".event.offered" } + +// HolderSubjectClosed is where the holder publishes closed, kept in the router's restricted stream (design 55 § 11). +func HolderSubjectClosed(holder string) string { return "mesh.mod." + holder + ".event.closed" } + +const ( + RecordAsks = "asks" +) + +// Ask is a type of the contract. +type Ask struct { + // what it is about: a newer ask about it replaces the older + About string `json:"about,omitempty"` + // its secondary words + Details string `json:"details,omitempty"` + // when it ends unanswered, RFC 3339 + Expires string `json:"expires"` + // what happened and what it means + Explanation string `json:"explanation"` + // a few plain words + Headline string `json:"headline"` + // the asker's own id + ID string `json:"id"` + // what the asker then does + OnExpiry string `json:"on-expiry"` + // the answers it offers + Options []Option `json:"options"` + // urgent + Urgent bool `json:"urgent,omitempty"` + // who may answer: operator + Who string `json:"who"` + // the explanation with its values whole + Whole string `json:"whole,omitempty"` + // the details whole + WholeDetails string `json:"whole-details,omitempty"` +} + +// ConditionActionsItem is a type of the contract. +type ConditionActionsItem struct { + Arguments map[string]string `json:"arguments,omitempty"` + // label + Label string `json:"label"` + // level + Level string `json:"level,omitempty"` + // machine + Machine string `json:"machine,omitempty"` + // verb + Verb string `json:"verb"` +} + +// ConditionSubject is a type of the contract. +type ConditionSubject struct { + // id + ID string `json:"id"` + // machine + Machine string `json:"machine,omitempty"` + // scope + Scope string `json:"scope"` +} + +// Condition is a type of the contract. +type Condition struct { + // the answers it offers + Actions []ConditionActionsItem `json:"actions,omitempty"` + // plain words + Explanation string `json:"explanation,omitempty"` + // plain words + Headline string `json:"headline,omitempty"` + // the condition's key + Key string `json:"key"` + // what it needs + Needs string `json:"needs,omitempty"` + // the line said when it is over + Resolved string `json:"resolved,omitempty"` + // urgent or warning + Severity string `json:"severity,omitempty"` + // raised or cleared + State string `json:"state"` + Subject ConditionSubject `json:"subject"` + // one line + Summary string `json:"summary,omitempty"` + // the form's version, 1 + V int64 `json:"v"` +} + +// Option is a type of the contract. +type Option struct { + // the digest of exactly what the asker performs when it is chosen + Binds string `json:"binds,omitempty"` + // what it does, in plain words + Does string `json:"does"` + // the option's id + ID string `json:"id"` + // the button's label + Label string `json:"label"` + // the proof its answer needs + Level string `json:"level"` +} + +// Person is a type of the contract. +type Person struct { + // the name shown + Display string `json:"display,omitempty"` + // the account + Identity string `json:"identity"` + // the channel kind + Kind string `json:"kind"` + // how the router knew + Verified string `json:"verified"` + // the role + Who string `json:"who"` +} + +// Warrant is a type of the contract. +type Warrant struct { + // what it was about + About string `json:"about,omitempty"` + // the ask + Ask string `json:"ask"` + // the digest of the ask as the router took it + AskDigest string `json:"ask-digest,omitempty"` + // its asker + Asker string `json:"asker"` + // when, RFC 3339 + At string `json:"at"` + By *Person `json:"by,omitempty"` + // the module it came through + Channel string `json:"channel,omitempty"` + // its label + Label string `json:"label,omitempty"` + // its level + Level string `json:"level,omitempty"` + // the option chosen + Option string `json:"option,omitempty"` + Outcome string `json:"outcome"` + // the proofs present + Proofs []string `json:"proofs,omitempty"` + // why it ended without a choice, or what refused it + Words string `json:"words,omitempty"` +} + +// OpenInput is what open takes. +type OpenInput struct { +} + +// OpenAnswer is what open answers. +type OpenAnswer = map[string]any + +// HistoryInput is what history takes. +type HistoryInput struct { + // at most this many of each (default 50) + Limit int64 `json:"limit,omitempty"` +} + +// HistoryAnswer is what history answers. +type HistoryAnswer = map[string]any + +// NotifyInput is what notify takes. +type NotifyInput struct { + // it is over + Clear bool `json:"clear,omitempty"` + // one or two plain sentences + Explanation string `json:"explanation,omitempty"` + // plain words + Headline string `json:"headline,omitempty"` + // the message's key + Key string `json:"key,omitempty"` + // the line said when it is over + Resolved string `json:"resolved,omitempty"` + Severity string `json:"severity,omitempty"` + // what it is about + Subject string `json:"subject,omitempty"` + // one line + Summary string `json:"summary,omitempty"` +} + +// NotifyAnswer is what notify answers. +type NotifyAnswer = map[string]any + +// TellInput is what tell takes. +type TellInput struct { + // plain words + Headline string `json:"headline,omitempty"` + // one or two plain lines + Lines []string `json:"lines,omitempty"` + // what it is about + Subject string `json:"subject,omitempty"` +} + +// TellAnswer is what tell answers. +type TellAnswer = map[string]any + +// AsksInput is what asks takes. +type AsksInput struct { +} + +// AsksAnswer is what asks answers. +type AsksAnswer = map[string]any + +// AskAccept is the body of ask. +type AskAccept = Ask + +// CancelAccept is the body of cancel. +type CancelAccept struct { + // the ask's id + ID string `json:"id"` +} + +// ConditionAccept is the body of condition. +type ConditionAccept = Condition + +// DecidedEvent is the body of the event decided. +type DecidedEvent = Warrant + +// OfferedHolderEvent is the body of the holder's event offered. +type OfferedHolderEvent struct { + // its asker + Asker string `json:"asker"` + // the channels it was offered on + Channels []string `json:"channels,omitempty"` + // its headline + Headline string `json:"headline,omitempty"` + // the ask's id + ID string `json:"id"` +} + +// ClosedHolderEvent is the body of the holder's event closed. +type ClosedHolderEvent struct { + // its asker + Asker string `json:"asker"` + How string `json:"how"` + // the ask's id + ID string `json:"id"` +} + +// AsksRecord is one value of the record asks. +type AsksRecord = map[string]any diff --git a/go/seats/operatorchannel/contract_test.go b/go/seats/operatorchannel/contract_test.go new file mode 100644 index 0000000..2a8c9ce --- /dev/null +++ b/go/seats/operatorchannel/contract_test.go @@ -0,0 +1,60 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package operatorchannel + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/operator-channel/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "decided for its asker", subject: "mesh.seat.operator-channel.event.decided.mesh-delivery", given: "{\"ask\":\"a1\",\"ask-digest\":\"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"asker\":\"mesh-delivery\",\"at\":\"2026-11-04T12:00:00Z\",\"by\":{\"identity\":\"1\",\"kind\":\"telegram\",\"verified\":\"user id\",\"who\":\"operator\"},\"channel\":\"telegram\",\"label\":\"Release\",\"level\":\"approve\",\"option\":\"ok\",\"outcome\":\"chosen\"}", wire: "{\"ask\":\"a1\",\"ask-digest\":\"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee\",\"asker\":\"mesh-delivery\",\"at\":\"2026-11-04T12:00:00Z\",\"by\":{\"identity\":\"1\",\"kind\":\"telegram\",\"verified\":\"user id\",\"who\":\"operator\"},\"channel\":\"telegram\",\"label\":\"Release\",\"level\":\"approve\",\"option\":\"ok\",\"outcome\":\"chosen\"}", + decode: func(b []byte) (any, error) { var v DecidedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.event\\.decided\\.[A-Za-z0-9_-]+$")}, + {name: "refused under the holder's own subjects", subject: "mesh.mod.messenger.event.refused", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.refused$")}, + {name: "offered on the router's restricted stream", subject: "mesh.mod.messenger.event.offered", given: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", + decode: func(b []byte) (any, error) { var v OfferedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.offered$")}, + {name: "closed once per end", subject: "mesh.mod.messenger.event.closed", given: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", + decode: func(b []byte) (any, error) { var v ClosedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.closed$")}, + {name: "an ask by its asker", subject: "mesh.seat.operator-channel.accept.ask.mesh-delivery", given: "{\"expires\":\"2026-11-05T12:00:00Z\",\"explanation\":\"A delivery waits on your word.\",\"headline\":\"release held\",\"id\":\"a1\",\"on-expiry\":\"the delivery stays held\",\"options\":[{\"binds\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"does\":\"releases it\",\"id\":\"ok\",\"label\":\"Release\",\"level\":\"approve\"}],\"who\":\"operator\"}", wire: "{\"expires\":\"2026-11-05T12:00:00Z\",\"explanation\":\"A delivery waits on your word.\",\"headline\":\"release held\",\"id\":\"a1\",\"on-expiry\":\"the delivery stays held\",\"options\":[{\"binds\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"does\":\"releases it\",\"id\":\"ok\",\"label\":\"Release\",\"level\":\"approve\"}],\"who\":\"operator\"}", + decode: func(b []byte) (any, error) { var v AskAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.ask\\.[A-Za-z0-9_-]+$")}, + {name: "a cancel by its asker", subject: "mesh.seat.operator-channel.accept.cancel.mesh-delivery", given: "{\"id\":\"a1\"}", wire: "{\"id\":\"a1\"}", + decode: func(b []byte) (any, error) { var v CancelAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.cancel\\.[A-Za-z0-9_-]+$")}, + {name: "a condition by its publisher", subject: "mesh.seat.operator-channel.accept.condition.mesh-watcher", given: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", wire: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v ConditionAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.condition\\.[A-Za-z0-9_-]+$")}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/seats.go b/go/seats/seats.go new file mode 100644 index 0000000..fb55e4e --- /dev/null +++ b/go/seats/seats.go @@ -0,0 +1,746 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package seats is the mesh's seat list, generated from every seat's definition (design 26 *The seat set has one +// source*): what each seat is, where it is held, the provision it delivers, its verbs with their inputs, what it +// accepts and emits, what it receives and the paths it owns. mesh-sdk's runtime reads it; a seat its holder declares +// itself has a name here and no entry in the list. +package seats + +// Seat is one role the mesh defines. +type Seat struct { + Name string + Scope string + Grain string + Replicated bool + Delivers string + Accepts []string + Emits []string + PulledByKind bool + Yields bool + Serves []Verb + Receives []Receivable + Paths []Path + HeldByTheEngine bool + Decision string +} + +// Verb is one verb a seat serves: its input and answer as JSON Schema. +type Verb struct { + Name string + Description string + Input map[string]any + Output map[string]any + Optional bool + Replaces []string +} + +// Receivable is one kind of contribution a seat receives. +type Receivable struct { + Kind string + Comment string + Dirs bool + Shape *Shape + Offered bool +} + +// Shape is a kind given as data. Its checks are the runtime's. +type Shape struct { + Places []PlaceField + Order string + Examples []map[string]any + Text bool +} + +// PlaceField is one field a holder places by, and the values it takes. +type PlaceField struct { + Field string + Values []string +} + +// Path is one path on its machine a seat's holder owns. +type Path struct { + Path string + StepsBackFor string +} + +// The name of every seat defined. +const ( + MeshStore = "mesh-store" + MeshBroker = "mesh-broker" + MeshVault = "mesh-vault" + MeshConfig = "mesh-config" + MeshProvisioner = "mesh-provisioner" + MeshArtifactStore = "mesh-artifact-store" + MeshCatalog = "mesh-catalog" + MeshDelivery = "mesh-delivery" + MeshPlacement = "mesh-placement" + MeshDeliveryChecker = "mesh-delivery-checker" + MeshConditions = "mesh-conditions" + MeshWatchdogs = "mesh-watchdogs" + MeshSelfCheck = "mesh-self-check" + MeshHealers = "mesh-healers" + MeshWants = "mesh-wants" + MeshHandActs = "mesh-hand-acts" + MeshEnrolment = "mesh-enrolment" + MeshArtifactSweep = "mesh-artifact-sweep" + OperatorChannel = "operator-channel" + NpmPackageRegistry = "npm-package-registry" + GoPackageRegistry = "go-package-registry" + ContainerImageRegistry = "container-image-registry" + OciArtifactRegistry = "oci-artifact-registry" + Git = "git" + NodeBuildGo = "node-build-go" + NodeBuildNode = "node-build-node" + NodeBuildImage = "node-build-image" + MeshDnsResolver = "mesh-dns-resolver" + NodeHostname = "node-hostname" + NodeIntrusionPrevention = "node-intrusion-prevention" + NodePacketFilter = "node-packet-filter" + NodeServiceManager = "node-service-manager" + NodePackageManager = "node-package-manager" + NodeContainerRuntime = "node-container-runtime" + NodeEnvironment = "node-environment" + NodeProvisioner = "node-provisioner" + NodeLoginShell = "node-login-shell" + NodePower = "node-power" + NodeHotkeys = "node-hotkeys" + NodeMessageBus = "node-message-bus" + NodeBackup = "node-backup" + ThePrivateNetwork = "the-private-network" + NodeUplink = "node-uplink" + NodeResolver = "node-resolver" + NodeNfsServer = "node-nfs-server" + NodeMounts = "node-mounts" + NodeReads = "node-reads" + NodeFiles = "node-files" + NodeEngine = "node-engine" + NodeLoginManager = "node-login-manager" + NodeDisplayServer = "node-display-server" + NodeDisplaySession = "node-display-session" + NodeTerminalEmulator = "node-terminal-emulator" + NodeLauncher = "node-launcher" + NodeNotifier = "node-notifier" + NodeLockScreen = "node-lock-screen" + NodeClipboard = "node-clipboard" + NodeBar = "node-bar" + NodeCompositor = "node-compositor" + NodeSecretService = "node-secret-service" +) + +// Names is every seat defined, in reading order, the ones their holders declare included. +func Names() []string { + return []string{MeshStore, MeshBroker, MeshVault, MeshConfig, MeshProvisioner, MeshArtifactStore, MeshCatalog, MeshDelivery, MeshPlacement, MeshDeliveryChecker, MeshConditions, MeshWatchdogs, MeshSelfCheck, MeshHealers, MeshWants, MeshHandActs, MeshEnrolment, MeshArtifactSweep, OperatorChannel, NpmPackageRegistry, GoPackageRegistry, ContainerImageRegistry, OciArtifactRegistry, Git, NodeBuildGo, NodeBuildNode, NodeBuildImage, MeshDnsResolver, NodeHostname, NodeIntrusionPrevention, NodePacketFilter, NodeServiceManager, NodePackageManager, NodeContainerRuntime, NodeEnvironment, NodeProvisioner, NodeLoginShell, NodePower, NodeHotkeys, NodeMessageBus, NodeBackup, ThePrivateNetwork, NodeUplink, NodeResolver, NodeNfsServer, NodeMounts, NodeReads, NodeFiles, NodeEngine, NodeLoginManager, NodeDisplayServer, NodeDisplaySession, NodeTerminalEmulator, NodeLauncher, NodeNotifier, NodeLockScreen, NodeClipboard, NodeBar, NodeCompositor, NodeSecretService} +} + +// List is the seat list, in reading order: a new copy each call. +func List() []Seat { + return []Seat{ + {Name: MeshStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0079", Delivers: "postgres-database", + Serves: []Verb{ + {Name: "databases", Description: "Every database the store holds, with its on-disk size.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"psql -l"}}, + {Name: "query", Description: "One read-only statement against one database the store holds.", + Input: map[string]any{"properties": map[string]any{"database": map[string]any{"description": "the database to query", "type": "string"}, "sql": map[string]any{"description": "the read-only statement", "type": "string"}}, "required": []string{"database", "sql"}, "type": "object"}, Replaces: []string{"psql"}}, + }}, + {Name: MeshBroker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0079; novox/hq ADR 0595", Delivers: "mesh-bus", Emits: []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"}, + Serves: []Verb{ + {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every entry", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, + {Name: "show", Description: "One entry, its template, settings and fill.", + Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + {Name: "check", Description: "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"differences": map[string]any{"description": "every difference", "items": map[string]any{"properties": map[string]any{"asked": map[string]any{"description": "asked", "type": "object"}, "grows": map[string]any{"description": "the difference only grows: apply may act on it", "type": "boolean"}, "held": map[string]any{"description": "held", "type": "object"}, "local-name": map[string]any{"description": "the entry", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, "type": "array"}}, "required": []string{"differences"}, "type": "object"}}, + {Name: "orphans", Description: "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", + Input: map[string]any{"description": "no arguments", "properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every orphan", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, + {Name: "create", Description: "Create one entry now.", + Input: map[string]any{"description": "one piece of bus state as a manifest asks it", "properties": map[string]any{"configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type"}, "type": "object"}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + {Name: "apply", Description: "Apply a difference that only grows: a larger cap, a longer age.", + Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + {Name: "recreate", Description: "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", + Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + {Name: "drop", Description: "Remove one orphan, after a stream snapshot.", + Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"dropped": map[string]any{"description": "it is gone", "type": "boolean"}, "snapshot": map[string]any{"description": "the snapshot taken first", "type": "string"}}, "required": []string{"dropped"}, "type": "object"}}, + }}, + {Name: MeshVault, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0161", Delivers: "secret", + Serves: []Verb{ + {Name: "get", Description: "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.", Optional: true, + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the secret's name", "type": "string"}, "public_key": map[string]any{"description": "the asker's X25519 public key as SPKI PEM — never a private key", "type": "string"}}, "required": []string{"name", "public_key"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "secret-ask", Description: "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", Optional: true, + Input: map[string]any{"properties": map[string]any{"asked_by": map[string]any{"description": "who asked, as the bus named its caller", "type": "string"}, "call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "desk": map[string]any{"description": "the machine whose prompt asks", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the secret's name in the module", "type": "string"}, "node": map[string]any{"description": "the machine the secret is for", "type": "string"}, "outcome": map[string]any{"description": "instead of sealed: how the prompt ended", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "issued", Description: "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.", Optional: true, + Input: map[string]any{"properties": map[string]any{"call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "holders": map[string]any{"description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone", "items": map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the consuming module", "type": "string"}, "node": map[string]any{"description": "the holder's machine", "type": "string"}}, "type": "object"}, "type": "array"}, "module": map[string]any{"description": "the provider's module", "type": "string"}, "provider": map[string]any{"description": "the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "rotate", Description: "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.", Optional: true, + Input: map[string]any{"properties": map[string]any{"by": map[string]any{"description": "who asked, when the controller carries the act", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the secret's name in the module", "type": "string"}, "node": map[string]any{"description": "the machine, for a module's own secret", "type": "string"}, "provider": map[string]any{"description": "the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "move-in", Description: "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.", Optional: true, + Input: map[string]any{"properties": map[string]any{"call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "consumer": map[string]any{"description": "the consumer's machine", "type": "string"}, "consumer_module": map[string]any{"description": "the consuming module", "type": "string"}, "generation": map[string]any{"description": "the controller's generation for the value", "type": "string"}, "local": map[string]any{"description": "the credential's local name, when it has one", "type": "string"}, "made_at": map[string]any{"description": "when it was made (RFC 3339)", "type": "string"}, "module": map[string]any{"description": "its module", "type": "string"}, "name": map[string]any{"description": "its name in the module", "type": "string"}, "node": map[string]any{"description": "a module's own secret: its machine", "type": "string"}, "origin": map[string]any{"description": "made, accepted or issued, as the controller's store says", "type": "string"}, "provider": map[string]any{"description": "a pair credential: the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}, "read_back_to": map[string]any{"description": "the second step: a raw X25519 public key the read-back is sealed to", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "node-key", Description: "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.", Optional: true, + Input: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the machine's sealing key, a raw X25519 public key in base64", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "own", Description: "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.", Optional: true, + Input: map[string]any{"properties": map[string]any{"credentials": map[string]any{"description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}", "items": map[string]any{"type": "object"}, "type": "array"}}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "issue-account", Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", Optional: true, + Input: map[string]any{"properties": map[string]any{"user": map[string]any{"description": "the bus user, e.g. ace.mesh-build-go", "type": "string"}}, "required": []string{"user"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "sign", Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", Optional: true, + Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "node-certificate or enrolment", "type": "string"}, "node": map[string]any{"description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", "type": "string"}, "reply": map[string]any{"description": "the redeem answer's bytes in base64, for an enrolment", "type": "string"}, "serving_key": map[string]any{"description": "from mesh-enrolment: the serving key's public half the enrolling node presented", "type": "string"}}, "required": []string{"kind"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "enrolment-token", Description: "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", Optional: true, + Input: map[string]any{"properties": map[string]any{"drop": map[string]any{"description": "\"true\": drop the node's token and its hash", "enum": []string{"true", "false"}, "type": "string"}, "expires": map[string]any{"description": "when the token expires (RFC 3339), as the enrolment's key says", "type": "string"}, "node": map[string]any{"description": "the node the token is for", "type": "string"}, "sealed": map[string]any{"description": "\"true\": answer the token sealed to the operator key", "enum": []string{"true", "false"}, "type": "string"}, "tunnel": map[string]any{"additionalProperties": false, "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", "properties": map[string]any{"address": map[string]any{"description": "the joining node's tunnel address", "type": "string"}, "hub_endpoint": map[string]any{"description": "the hub's endpoint, host:port", "type": "string"}, "hub_key": map[string]any{"description": "the hub's tunnel public key", "type": "string"}, "key": map[string]any{"description": "the joining machine's own tunnel public key, as given at issue", "type": "string"}, "range": map[string]any{"description": "the private network's range, a prefix", "type": "string"}}, "required": []string{"address", "hub_endpoint", "hub_key", "key", "range"}, "type": "object"}}, "required": []string{"node"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "node-account", Description: "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", Optional: true, + Input: map[string]any{"properties": map[string]any{"keys_digest": map[string]any{"description": "the digest of every public key the node presented (delivery.KeysDigest)", "type": "string"}, "node": map[string]any{"description": "the joining node", "type": "string"}, "retire": map[string]any{"description": "\"true\": retire the account minted for this node", "enum": []string{"true", "false"}, "type": "string"}, "sealing_key": map[string]any{"description": "the sealing key's public half the node presented, which the account is sealed to", "type": "string"}}, "required": []string{"node"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "rotate-node", Description: "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", Optional: true, + Input: map[string]any{"properties": map[string]any{"act": map[string]any{"description": "the id of the hand act recorded for it in mesh-hand-acts", "type": "string"}, "node": map[string]any{"description": "the node whose account rotates", "type": "string"}, "why": map[string]any{"description": "why, in a sentence", "type": "string"}}, "required": []string{"node", "why", "act"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "node-account-confirm", Description: "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "abandon", Description: "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.", Optional: true, + Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node whose rotation is abandoned", "type": "string"}}, "required": []string{"node"}, "type": "object"}, + Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + {Name: "retire", Description: "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the own secret's name", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant for this node, module and name", "type": "string"}}, "required": []string{"node", "module", "name", "warrant"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"retired": map[string]any{"description": "the secret is gone", "type": "boolean"}}, "required": []string{"retired"}, "type": "object"}}, + }}, + {Name: MeshConfig, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0348, ADR 0565", + Serves: []Verb{ + {Name: "config", 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.", Optional: true, + Input: map[string]any{"properties": map[string]any{"action": map[string]any{"description": "list (when not given), set, clear or history", "type": "string"}, "by": map[string]any{"description": "who gave the value at the controller's terminal", "type": "string"}, "key": map[string]any{"description": "the config key", "type": "string"}, "limit": map[string]any{"description": "for history: at most this many, newest", "type": "number"}, "module": map[string]any{"description": "a module: its own value, with node", "type": "string"}, "node": map[string]any{"description": "a node", "type": "string"}, "value": map[string]any{"description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list"}, "why": map[string]any{"description": "for set and clear: why", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the values asked, or the one set or cleared, or its history", "type": "object"}}, + {Name: "renames", Description: "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"renames": map[string]any{"description": "every open rename", "items": map[string]any{"properties": map[string]any{"at": map[string]any{"description": "when it moved, RFC 3339", "type": "string"}, "from": map[string]any{"description": "the old name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "open": map[string]any{"description": "the nodes still holding the old name open", "items": map[string]any{"type": "string"}, "type": "array"}, "to": map[string]any{"description": "the new name", "type": "string"}}, "required": []string{"module", "from", "to"}, "type": "object"}, "type": "array"}}, "required": []string{"renames"}, "type": "object"}}, + }}, + {Name: MeshProvisioner, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0349", + Serves: []Verb{ + {Name: "provisioning", Description: "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", Optional: true, + Input: map[string]any{"properties": map[string]any{"collection": map[string]any{"description": "for collect: a collection", "type": "string"}, "module": map[string]any{"description": "a module: for collect, a contributor", "type": "string"}, "node": map[string]any{"description": "a node", "type": "string"}, "what": map[string]any{"description": "registry (when not given), needs, applied or collect", "type": "string"}}, "type": "object"}}, + }}, + {Name: MeshArtifactStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0075", Delivers: "artifact-store"}, + {Name: MeshCatalog, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0604", + Serves: []Verb{ + {Name: "modules", Description: "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "module", Description: "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module's name", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, + {Name: "provides", Description: "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", Optional: true, + Input: map[string]any{"properties": map[string]any{"provision": map[string]any{"description": "the provision, such as postgres-database", "type": "string"}}, "required": []string{"provision"}, "type": "object"}}, + {Name: "dependents", Description: "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module's name", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, + }}, + {Name: MeshDelivery, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0239", + Serves: []Verb{ + {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", + Input: map[string]any{"properties": map[string]any{"all": map[string]any{"description": "\"true\": the final ones of the last thirty days too", "enum": []string{"true", "false"}, "type": "string"}, "group": map[string]any{"description": "a group's id (its branch name)", "type": "string"}, "repository": map[string]any{"description": "owner/repository", "type": "string"}, "state": map[string]any{"description": "one state, e.g. delivering or held", "type": "string"}}, "type": "object"}}, + {Name: "times", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "a delivery's id, or a group's", "type": "string"}}, "required": []string{"id"}, "type": "object"}}, + {Name: "groups", Description: "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", + Input: map[string]any{"properties": map[string]any{"all": map[string]any{"description": "\"true\": the groups that ended too", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + {Name: "what-if", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", Optional: true, + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "a commit's sha, or the start of one, instead of a pull request", "type": "string"}, "number": map[string]any{"description": "a pull request's number: its head is read", "type": "string"}, "repository": map[string]any{"description": "owner/repository", "type": "string"}}, "required": []string{"repository"}, "type": "object"}}, + {Name: "table", Description: "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "stalled", Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "recheck", Description: "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the delivery's id", "type": "string"}, "why": map[string]any{"description": "why, kept with the transition", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, + {Name: "release", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "stop", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the delivery's id", "type": "string"}, "why": map[string]any{"description": "the condition it answers", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, + {Name: "retire-history", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "settle", Description: "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", Optional: true, + Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"ids": map[string]any{"description": "the held deliveries' ids (owner/repository@commit)", "items": map[string]any{"type": "string"}, "minItems": float64(1), "type": "array"}, "why": map[string]any{"description": "why they may be settled, shown to the operator", "type": "string"}}, "required": []string{"ids", "why"}, "type": "object"}}, + }}, + {Name: MeshPlacement, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0567", + Serves: []Verb{ + {Name: "placement", Description: "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", Optional: true, + Input: map[string]any{"properties": map[string]any{"history": map[string]any{"description": "\"true\": the changes in order, from the stream and the state's revisions", "enum": []string{"true", "false"}, "type": "string"}, "module": map[string]any{"description": "one module: the nodes it is placed on", "type": "string"}, "node": map[string]any{"description": "one node", "type": "string"}, "seats": map[string]any{"description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + {Name: "proposals", 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.", Optional: true, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one proposal's id", "type": "string"}}, "type": "object"}}, + {Name: "propose", Description: "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", Optional: true, + Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"changes": map[string]any{"description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", "items": map[string]any{"required": []string{"op", "node"}, "type": "object"}, "minItems": float64(1), "type": "array"}, "warrant": map[string]any{"description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", "required": []string{"node", "address", "ask", "warrant"}, "type": "object"}, "why": map[string]any{"description": "why, shown to the operator with the proposal", "type": "string"}}, "required": []string{"why", "changes"}, "type": "object"}}, + {Name: "withdraw", Description: "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", Optional: true, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the proposal's id", "type": "string"}, "why": map[string]any{"description": "why, kept in the event", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, + {Name: "again", Description: "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", Optional: true, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the proposal's id", "type": "string"}}, "required": []string{"id"}, "type": "object"}}, + }}, + {Name: MeshDeliveryChecker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0547", + Serves: []Verb{ + {Name: "try", Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", Optional: true, + Input: map[string]any{"properties": map[string]any{"head": map[string]any{"description": "the full commit to try, 40 or 64 hex digits", "type": "string"}, "level": map[string]any{"description": "unit or integration: only this level; both when absent", "type": "string"}, "repository": map[string]any{"description": "/", "type": "string"}, "tests": map[string]any{"description": "a go test -run pattern: only those tests run, and nothing is kept", "type": "string"}, "trunk": map[string]any{"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", "type": "string"}}, "required": []string{"repository", "head"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id", "type": "string"}, "state": map[string]any{"description": "its state", "type": "string"}}, "required": []string{"id"}, "type": "object"}, Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, + {Name: "try-read", Description: "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", Optional: true, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id, try-", "type": "string"}}, "required": []string{"id"}, "type": "object"}, + Output: map[string]any{"description": "the try: its state, its steps and its verdict when it has one", "type": "object"}}, + {Name: "try-end", Description: "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", Optional: true, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id, try-", "type": "string"}, "why": map[string]any{"description": "why it is ended, in words", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"ended": map[string]any{"description": "it is ended", "type": "boolean"}, "id": map[string]any{"description": "the try", "type": "string"}}, "required": []string{"id", "ended"}, "type": "object"}}, + }}, + {Name: MeshConditions, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"condition-raised", "condition-changed", "condition-cleared"}, + Serves: []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.", + Input: map[string]any{"properties": map[string]any{"days": map[string]any{"description": "with history: how many days back (default 7, at most 90)", "type": "string"}, "history": map[string]any{"description": "\"true\": every raising, change, silence and clearing lately, oldest first", "enum": []string{"true", "false"}, "type": "string"}, "key": map[string]any{"description": "a condition's key: that one whole; with history, only its transitions", "type": "string"}, "machine": map[string]any{"description": "only those about this machine", "type": "string"}, "scope": map[string]any{"description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", "type": "string"}, "severity": map[string]any{"description": "only urgent, or only warning", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "the cause in a word (the condition's kind when absent)", "type": "string"}, "for": map[string]any{"description": "how long: 30m, 4h, 2d; at most 7d", "type": "string"}, "key": map[string]any{"description": "the condition's key", "type": "string"}, "why": map[string]any{"description": "why, required and recorded", "type": "string"}}, "required": []string{"key", "for", "why"}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: MeshWatchdogs, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", + Serves: []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: map[string]any{"properties": map[string]any{"row": map[string]any{"description": "one row, e.g. S1", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"machine": map[string]any{"description": "only those on this machine", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"machine": map[string]any{"description": "one machine", "type": "string"}}, "type": "object"}}, + {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.", + Input: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "with deliver or drop: the cause in a word (dead-letter when absent)", "type": "string"}, "consumer": map[string]any{"description": "a consumer's name, or .: only what that one gave up on", "type": "string"}, "deliver": map[string]any{"description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", "type": "string"}, "drop": map[string]any{"description": "a dead letter's id: drop it for good (needs why)", "type": "string"}, "id": map[string]any{"description": "a dead letter's id, as the list gives it: that one whole, with its message", "type": "string"}, "limit": map[string]any{"description": "how many to list, newest first (default 50); only when listing", "type": "string"}, "why": map[string]any{"description": "with deliver or drop: why, required and recorded", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: MeshSelfCheck, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"checked"}, + Serves: []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: map[string]any{"properties": map[string]any{"probe": map[string]any{"description": "one probe's id (D6): its verdict alone, with every finding", "type": "string"}, "run": map[string]any{"description": "\"true\": run every probe now and answer the verdict", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + {Name: "probes", Description: "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: MeshHealers, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"healer-acted"}, + Serves: []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, and the mesh-wide brake.", + Input: map[string]any{"properties": map[string]any{"days": map[string]any{"description": "how many days of acts back (default 7)", "type": "string"}}, "type": "object"}}, + {Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.", + Input: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the condition's key", "type": "string"}, "why": map[string]any{"description": "why now, kept with the act", "type": "string"}}, "required": []string{"key", "why"}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"condition": map[string]any{"description": "the key of the condition the drill is meant to raise, if any (optional)", "type": "string"}, "what": map[string]any{"description": "what was done on purpose, in a line", "type": "string"}, "why": map[string]any{"description": "what the drill tests", "type": "string"}}, "required": []string{"what", "why"}, "type": "object"}}, + }}, + {Name: MeshWants, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0565", Emits: []string{"policy-changed"}, + Serves: []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, each node's own composition and the assigner's findings.", + Input: map[string]any{"properties": map[string]any{"compare": map[string]any{"description": "\"true\": each node's own composition, by its digest, and the assigner's findings", "enum": []string{"true", "false"}, "type": "string"}, "module": map[string]any{"description": "one module", "type": "string"}, "policy": map[string]any{"description": "with module: roll, together, record, or default to take a person's choice back", "type": "string"}, "why": map[string]any{"description": "with policy: why, required and kept", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: MeshHandActs, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0565", Emits: []string{"act-recorded"}, + Serves: []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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "the cause in a word, or a condition's kind", "type": "string"}, "condition": map[string]any{"description": "the key of the condition it addressed, if any (optional)", "type": "string"}, "verb": map[string]any{"description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", "type": "string"}, "what": map[string]any{"description": "what was done, in a line", "type": "string"}, "why": map[string]any{"description": "why it had to be done by hand", "type": "string"}}, "required": []string{"what", "why"}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "only this cause", "type": "string"}, "days": map[string]any{"description": "how many days back (default 14)", "type": "string"}, "warrants": map[string]any{"description": "\"true\": every warrant recorded", "enum": []string{"true", "false"}, "type": "string"}, "who": map[string]any{"description": "only acts whose who contains this", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"ask": map[string]any{"description": "its ask's id", "type": "string"}, "asker": map[string]any{"description": "the module that asked", "type": "string"}}, "required": []string{"asker", "ask"}, "type": "object"}}, + }}, + {Name: MeshEnrolment, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0587", Emits: []string{"enrolment-issued", "node-enrolled", "enrolment-refused", "enrolment-revoked", "enrolment-expired"}, + Serves: []Verb{ + {Name: "issue", Description: "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.", + Input: map[string]any{"properties": map[string]any{"expires": map[string]any{"description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", "type": "string"}, "node": map[string]any{"description": "the new machine's name in the mesh", "type": "string"}, "overlay_key": map[string]any{"description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", "type": "string"}, "why": map[string]any{"description": "why it joins, shown to the operator with the ask", "type": "string"}}, "required": []string{"node", "why", "overlay_key"}, "type": "object"}}, + {Name: "redeem", Description: "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", + Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"node": map[string]any{"description": "the node the token was issued for", "type": "string"}, "overlay_key": map[string]any{"description": "the private network key's public half", "type": "string"}, "profile": map[string]any{"description": "what the machine is, as its record says it", "type": "object"}, "proof": map[string]any{"description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64", "type": "string"}, "public_key": map[string]any{"description": "the identity key's public half, base64", "type": "string"}, "sealing_key": map[string]any{"description": "the sealing key's public half, which the account is sealed to", "type": "string"}, "serving_key": map[string]any{"description": "the serving key's public half, which the certificate binds", "type": "string"}, "tunnel": map[string]any{"description": "a tunnel found on the machine: everything but its private key", "type": "object"}}, "required": []string{"node", "public_key", "sealing_key", "proof"}, "type": "object"}}, + {Name: "confirm", Description: "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "revoke", Description: "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.", + Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "why": map[string]any{"description": "why, recorded with the hand act", "type": "string"}}, "required": []string{"node", "why"}, "type": "object"}}, + {Name: "tokens", Description: "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.", + Input: map[string]any{"properties": map[string]any{"history": map[string]any{"description": "\"true\": the ended enrolments too", "enum": []string{"true", "false"}, "type": "string"}, "node": map[string]any{"description": "one node", "type": "string"}}, "type": "object"}}, + {Name: "again", Description: "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.", + Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node whose ask is raised again", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, + }}, + {Name: MeshArtifactSweep, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"}, + Serves: []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: map[string]any{"properties": map[string]any{"collected": map[string]any{"description": "\"true\": list the collected references too (counted always)", "enum": []string{"true", "false"}, "type": "string"}, "repository": map[string]any{"description": "only this repository's references, as / (the counts stay whole)", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"references": map[string]any{"description": "the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"references"}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": let go of what is eligible (needs why); without it nothing is done", "enum": []string{"true", "false"}, "type": "string"}, "most": map[string]any{"description": "let go of at most this many (500 by default, at most 5000)", "type": "string"}, "why": map[string]any{"description": "with confirm: why, required and recorded", "type": "string"}}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {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: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the machine's name", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, + }}, + {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109; novox/hq ADR 0605", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, + Serves: []Verb{ + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + }}, + {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed"}, + Serves: []Verb{ + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + }}, + {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + Serves: []Verb{ + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, + {Name: "tags", Description: "The tags of one image and the digest each names.", + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}}, "required": []string{"name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}, "tags": map[string]any{"description": "every tag", "items": map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "tag": map[string]any{"description": "the tag", "type": "string"}}, "required": []string{"tag", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "tags"}, "type": "object"}}, + {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, + {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + }}, + {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + Serves: []Verb{ + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, + {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + }}, + {Name: Git, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0111", Delivers: "git"}, + {Name: NodeBuildGo, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, + Serves: []Verb{ + {Name: "current", Description: "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "pause", Description: "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "resume", Description: "Take Go asks again on this node.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: NodeBuildNode, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, + Serves: []Verb{ + {Name: "current", Description: "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "pause", Description: "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "resume", Description: "Take Node asks again on this node.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: NodeBuildImage, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, + Serves: []Verb{ + {Name: "current", Description: "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "pause", Description: "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "resume", Description: "Take image asks again on this node.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: MeshDnsResolver, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0194, ADR 0223", Replicated: true, Delivers: "wildcard-resolution"}, + {Name: NodeHostname, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0199, ADR 0223", + Serves: []Verb{ + {Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"cat /etc/hosts", "getent hosts"}}, + {Name: "add", Description: "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", + Input: map[string]any{"properties": map[string]any{"address": map[string]any{"description": "the IPv4 or IPv6 address", "type": "string"}, "names": map[string]any{"description": "the names for it, separated by spaces", "type": "string"}}, "required": []string{"address", "names"}, "type": "object"}, Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}, + {Name: "remove", Description: "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "a host name, or an address to remove every line of", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"sed -i /etc/hosts"}}, + }, Paths: []Path{{Path: "/etc/hosts", StepsBackFor: ""}, {Path: "/etc/hostname", StepsBackFor: ""}}}, + {Name: NodeIntrusionPrevention, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0121", + Serves: []Verb{ + {Name: "status", Description: "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", + Input: map[string]any{"properties": map[string]any{"jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"fail2ban-client status"}}, + {Name: "banned", Description: "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", + Input: map[string]any{"properties": map[string]any{"jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"fail2ban-client banned"}}, + {Name: "ban", Description: "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", + Input: map[string]any{"properties": map[string]any{"ip": map[string]any{"description": "the address", "type": "string"}, "jail": map[string]any{"description": "the jail to hold it", "type": "string"}}, "required": []string{"ip", "jail"}, "type": "object"}, Replaces: []string{"fail2ban-client set banip"}}, + {Name: "unban", Description: "Let one address go, from one jail or from every jail when none is named.", + Input: map[string]any{"properties": map[string]any{"ip": map[string]any{"description": "the address", "type": "string"}, "jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "required": []string{"ip"}, "type": "object"}, Replaces: []string{"fail2ban-client unban", "fail2ban-client set unbanip"}}, + }}, + {Name: NodePacketFilter, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0121", + Serves: []Verb{ + {Name: "rules", Description: "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", + Input: map[string]any{"properties": map[string]any{"chain": map[string]any{"description": "one chain of that table (optional)", "type": "string"}, "table": map[string]any{"description": "one nftables table, as `family name` (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"nft list ruleset", "iptables -L", "iptables-save"}}, + {Name: "reload", Description: "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"nft -f"}}, + {Name: "remove", Description: "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", + Input: map[string]any{"properties": map[string]any{"where": map[string]any{"description": "the rule set, as `node show` lists it", "type": "string"}}, "required": []string{"where"}, "type": "object"}, Replaces: []string{"nft delete", "iptables -X"}}, + }}, + {Name: NodeServiceManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0177", + Serves: []Verb{ + {Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", + Input: map[string]any{"properties": map[string]any{"pattern": map[string]any{"description": "a glob the unit's name must match (optional)", "type": "string"}, "scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}}, "type": "object"}, Replaces: []string{"systemctl list-units"}}, + {Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl status", "systemctl is-active"}}, + {Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl start"}}, + {Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl stop"}}, + {Name: "restart", Description: "Restart one unit.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl restart"}}, + {Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl enable"}}, + {Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl disable"}}, + {Name: "journal", Description: "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: ].", + Input: map[string]any{"properties": map[string]any{"lines": map[string]any{"description": "how many lines from the end of what matches (default 100, at most 2000)", "type": "string"}, "match": map[string]any{"description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", "type": "string"}, "priority": map[string]any{"description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", "type": "string"}, "scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "since": map[string]any{"description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}, "until": map[string]any{"description": "the window's end, in the same forms (optional; now when absent)", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"journalctl"}}, + {Name: "failed", Description: "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.", Optional: true, + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" or \"user\": only that manager (both when absent)", "type": "string"}}, "type": "object"}, Replaces: []string{"systemctl --failed"}}, + {Name: "reset-failed", Description: "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl reset-failed"}}, + {Name: "wanted-by", Description: "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy", "ls ~/.config/systemd/user/*.wants"}}, + {Name: "unlink-dangling", Description: "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.", + Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}, "why": map[string]any{"description": "the person's reason for removing the links, kept in the record", "type": "string"}}, "required": []string{"unit", "why"}, "type": "object"}, Replaces: []string{"rm ~/.config/systemd/user/*.wants/", "unlink /etc/systemd/system/*.wants/"}}, + }}, + {Name: NodePackageManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0207"}, + {Name: NodeContainerRuntime, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0166, ADR 0207"}, + {Name: NodeEnvironment, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0203"}, + {Name: NodeProvisioner, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0349"}, + {Name: NodeLoginShell, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0204", + Serves: []Verb{ + {Name: "execute", Description: "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.", Optional: true, + Input: map[string]any{"properties": map[string]any{"command": map[string]any{"description": "the command line, as you would type it", "type": "string"}, "timeout_seconds": map[string]any{"description": "give up after this long, at most 25 (default 20)", "type": "string"}}, "required": []string{"command"}, "type": "object"}}, + }}, + {Name: NodePower, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0211"}, + {Name: NodeHotkeys, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0212", + Receives: []Receivable{{Kind: "trigger", Comment: "#"}}}, + {Name: NodeMessageBus, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0215"}, + {Name: NodeBackup, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0214", + Serves: []Verb{ + {Name: "backed-up", Description: "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.", + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"restic snapshots"}}, + {Name: "now", Description: "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.", + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"restic backup"}}, + {Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as .restored-. Swapping it in is a person's act. Lists the restore points when none is named.", + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module", "type": "string"}, "path": map[string]any{"description": "one of the module's directories (all of them when omitted)", "type": "string"}, "snapshot": map[string]any{"description": "the restore point (from `backed-up`; the newest when omitted)", "type": "string"}}, "required": []string{"module"}, "type": "object"}, Replaces: []string{"restic restore"}}, + }, + Receives: []Receivable{{Kind: "backup", Comment: "#", Dirs: true}, {Kind: "data", Comment: "#", Dirs: true}}}, + {Name: ThePrivateNetwork, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0110"}, + {Name: NodeUplink, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0117", + Serves: []Verb{ + {Name: "resolvers", Description: "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "links", Description: "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }, Paths: []Path{{Path: "/etc/resolv.conf", StepsBackFor: "node-resolver"}}}, + {Name: NodeResolver, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0247", + Serves: []Verb{ + {Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}}, + {Name: "route", Description: "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", + Input: map[string]any{"properties": map[string]any{"domains": map[string]any{"description": "the domains to route there, separated by spaces or commas", "type": "string"}, "link": map[string]any{"description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", "type": "string"}, "servers": map[string]any{"description": "the servers' addresses, separated by spaces or commas", "type": "string"}}, "required": []string{"link", "domains", "servers"}, "type": "object"}, Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}}, + {Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", + Input: map[string]any{"properties": map[string]any{"link": map[string]any{"description": "the network link, by name", "type": "string"}}, "required": []string{"link"}, "type": "object"}, Replaces: []string{"resolvectl revert"}}, + }, Paths: []Path{{Path: "/etc/resolv.conf", StepsBackFor: ""}}}, + {Name: NodeNfsServer, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0263", + Serves: []Verb{ + {Name: "exports", Description: "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"exportfs -v", "cat /etc/exports", "zfs get sharenfs"}}, + {Name: "clients", Description: "Which machines have mounted which share now, as the NFS server knows its clients.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"ss -tn sport = :2049", "cat /proc/fs/nfsd/clients/*/info"}}, + {Name: "test", Description: "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.", + Input: map[string]any{"properties": map[string]any{"address": map[string]any{"description": "a node's private address, to ask whether the share is exported to it (optional)", "type": "string"}, "share": map[string]any{"description": "the share, by its name", "type": "string"}}, "required": []string{"share"}, "type": "object"}, Replaces: []string{"showmount -e"}}, + {Name: "reload", Description: "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"exportfs -ra"}}, + {Name: "adopt", Description: "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.", + Input: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": change it (needs why); anything else is a dry run", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "the shared path whose hand-made export is taken over", "type": "string"}, "why": map[string]any{"description": "why, for the record", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"zfs set sharenfs=off"}}, + }}, + {Name: NodeMounts, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0263", + Serves: []Verb{ + {Name: "list", Description: "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"cat /etc/fstab", "findmnt", "systemctl list-units --type=mount"}}, + {Name: "test", Description: "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.", + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"showmount -e", "ping"}}, + {Name: "mount", Description: "Mount one share or occasional source now, rather than at its first access.", + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"mount"}}, + {Name: "unmount", Description: "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.", + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"umount"}}, + {Name: "adopt", Description: "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", + Input: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": change it (needs why); anything else is a dry run", "enum": []string{"true", "false"}, "type": "string"}, "mountpoint": map[string]any{"description": "the mount point whose fstab line is taken over", "type": "string"}, "why": map[string]any{"description": "why, for the record", "type": "string"}}, "required": []string{"mountpoint"}, "type": "object"}, Replaces: []string{"sed -i /etc/fstab"}}, + }}, + {Name: NodeReads, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0310", + Serves: []Verb{ + {Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.", + Input: map[string]any{"properties": map[string]any{"follow": map[string]any{"description": "\"true\": also say where the path leads once every link is followed", "enum": []string{"true", "false"}, "type": "string"}, "paths": map[string]any{"description": "one absolute path, or several separated by line breaks", "type": "string"}}, "required": []string{"paths"}, "type": "object"}, Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}}, + {Name: "readlink", Description: "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.", + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"readlink", "readlink -f", "realpath"}}, + {Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.", + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path to a directory", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"ls", "ls -la"}}, + {Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.", + Input: map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", "type": "string"}, "from": map[string]any{"description": "the first line to answer, counted from 1 (optional)", "type": "string"}, "lines": map[string]any{"description": "how many lines to answer (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"cat", "head", "tail", "less"}}, + {Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.", + Input: map[string]any{"properties": map[string]any{"case": map[string]any{"description": "\"false\": match whatever the case (optional)", "enum": []string{"true", "false"}, "type": "string"}, "depth": map[string]any{"description": "how many directories deep, up to 8 (optional)", "type": "string"}, "matches": map[string]any{"description": "the most matches to answer, up to 500 (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file or a directory", "type": "string"}, "text": map[string]any{"description": "the fixed string to find", "type": "string"}}, "required": []string{"text", "path"}, "type": "object"}, Replaces: []string{"grep", "grep -r", "grep -F"}}, + {Name: "account", Description: "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.", + Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "type": "string"}, "name": map[string]any{"description": "an account's name or number (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"getent passwd", "getent group", "id", "groups"}}, + }}, + {Name: NodeFiles, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0394", + Serves: []Verb{ + {Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"follow": map[string]any{"description": "\"true\": also say where the path leads once every link is followed", "enum": []string{"true", "false"}, "type": "string"}, "paths": map[string]any{"description": "one absolute path, or several separated by line breaks", "type": "string"}}, "required": []string{"paths"}, "type": "object"}, Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}}, + {Name: "readlink", Description: "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"readlink", "readlink -f", "realpath"}}, + {Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path to a directory", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"ls", "ls -la"}}, + {Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", "type": "string"}, "from": map[string]any{"description": "the first line to answer, counted from 1 (optional)", "type": "string"}, "lines": map[string]any{"description": "how many lines to answer (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"cat", "head", "tail", "less"}}, + {Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"case": map[string]any{"description": "\"false\": match whatever the case (optional)", "enum": []string{"true", "false"}, "type": "string"}, "depth": map[string]any{"description": "how many directories deep, up to 8 (optional)", "type": "string"}, "matches": map[string]any{"description": "the most matches to answer, up to 500 (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file or a directory", "type": "string"}, "text": map[string]any{"description": "the fixed string to find", "type": "string"}}, "required": []string{"text", "path"}, "type": "object"}, Replaces: []string{"grep", "grep -r", "grep -F"}}, + {Name: "account", Description: "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "type": "string"}, "name": map[string]any{"description": "an account's name or number (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"getent passwd", "getent group", "id", "groups"}}, + {Name: "write", Description: "Write one text file whole, replacing what it holds; the old content goes to the node's trash. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"content": map[string]any{"description": "the text to write", "type": "string"}, "mode": map[string]any{"description": "the file's mode, such as 0644 (optional)", "enum": []string{"true", "false"}, "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"path", "content", "reason"}, "type": "object"}, Replaces: []string{"tee", ">"}}, + {Name: "copy", Description: "Copy a file or directory to a path on this node. A target on another node (:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"from": map[string]any{"description": "an absolute path", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "to": map[string]any{"description": "an absolute path on this node", "type": "string"}}, "required": []string{"from", "to", "reason"}, "type": "object"}, Replaces: []string{"cp", "cp -a", "ssh cp"}}, + {Name: "move", Description: "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"from": map[string]any{"description": "an absolute path", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "to": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"from", "to", "reason"}, "type": "object"}, Replaces: []string{"mv"}}, + {Name: "delete", Description: "Delete a file or directory into the node's trash, where restore can bring it back. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": delete a directory with what it holds", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "reason"}, "type": "object"}, Replaces: []string{"rm", "rm -r"}}, + {Name: "mkdir", Description: "Make a directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "parents": map[string]any{"description": "\"true\": make the missing parents too", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"path", "reason"}, "type": "object"}, Replaces: []string{"mkdir", "mkdir -p"}}, + {Name: "chmod", Description: "Change the mode of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"mode": map[string]any{"description": "the mode, such as 0644 or u+x", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": also everything under a directory", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "mode", "reason"}, "type": "object"}, Replaces: []string{"chmod"}}, + {Name: "chown", Description: "Change the owner and group of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "enum": []string{"true", "false"}, "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "owner": map[string]any{"description": "an account's name or number", "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": also everything under a directory", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "owner", "reason"}, "type": "object"}, Replaces: []string{"chown", "chgrp"}}, + {Name: "upload", Description: "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + Input: map[string]any{"properties": map[string]any{"bucket": map[string]any{"description": "the bucket the file goes into", "type": "string"}, "key": map[string]any{"description": "the object's key (optional; the file's SHA-256 when not given)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path", "bucket"}, "type": "object"}}, + {Name: "download", Description: "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"bucket": map[string]any{"description": "the bucket", "type": "string"}, "key": map[string]any{"description": "the object's key", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "sha256": map[string]any{"description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", "type": "string"}}, "required": []string{"bucket", "key", "path", "reason"}, "type": "object"}}, + {Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"ls ~/.local/share/Trash"}}, + {Name: "restore", Description: "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "an id from trash", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"id", "reason"}, "type": "object"}, Replaces: []string{"trash-restore"}}, + }}, + {Name: NodeEngine, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused"}, HeldByTheEngine: true, + Serves: []Verb{ + {Name: "hand-over", Description: "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}}, + {Name: "access-act", Description: "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", + Input: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}}, + {Name: "judge-directories", Description: "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "setuid-search", Description: "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", + Input: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}}, + {Name: "root-free", Description: "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "acts", Description: "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}}, + {Name: "packages", 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.", Optional: true, + Input: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + {Name: "composition", Description: "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", Optional: true, + Input: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, + {Name: "compose", Description: "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}}, + {Name: "compose-candidate", Description: "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", Optional: true, + Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"candidate"}, "type": "object"}}, + }}, + {Name: NodeLoginManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "sessions", Description: "The sessions the login manager offers on this machine, and which one the operator account starts by default.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: NodeDisplayServer, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "displays", Description: "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "layout", Description: "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.", + Input: map[string]any{"properties": map[string]any{"action": map[string]any{"description": "list, save or apply", "enum": []string{"list", "save", "apply"}, "type": "string"}, "name": map[string]any{"description": "the profile to save or apply (save and apply only)", "type": "string"}}, "required": []string{"action"}, "type": "object"}}, + }}, + {Name: NodeDisplaySession, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether it is visible or focused.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + {Name: "windows", Description: "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.", + Input: map[string]any{"properties": map[string]any{"workspace": map[string]any{"description": "one workspace (optional)", "type": "string"}}, "type": "object"}}, + }, + Receives: []Receivable{{Kind: "config", Comment: "#"}}}, + {Name: NodeTerminalEmulator, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "open", Description: "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.", + Input: map[string]any{"properties": map[string]any{"command": map[string]any{"description": "what to run in it (optional; the login shell when absent)", "type": "string"}, "directory": map[string]any{"description": "where it starts (optional; the account's home when absent)", "type": "string"}}, "type": "object"}}, + }}, + {Name: NodeLauncher, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "menu", Description: "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", + Input: map[string]any{"properties": map[string]any{"choices": map[string]any{"description": "the lines to choose between, in order", "items": map[string]any{"type": "string"}, "type": "array"}, "prompt": map[string]any{"description": "what the menu asks (optional)", "type": "string"}}, "required": []string{"choices"}, "type": "object"}}, + {Name: "secret", Description: "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.", Optional: true, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module whose own secret is asked for", "type": "string"}, "node": map[string]any{"description": "the machine the module runs on", "type": "string"}, "seal_to": map[string]any{"description": "the asker's public sealing key: the answer is sealed to it", "type": "string"}, "secret": map[string]any{"description": "the own secret's name", "type": "string"}, "timeout_seconds": map[string]any{"description": "give up after this long (optional)", "type": "string"}}, "required": []string{"module", "secret", "node", "seal_to"}, "type": "object"}}, + }}, + {Name: NodeNotifier, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "send", Description: "Show the operator a notification.", + Input: map[string]any{"properties": map[string]any{"body": map[string]any{"description": "its text (optional)", "type": "string"}, "title": map[string]any{"description": "the notification's summary", "type": "string"}, "urgency": map[string]any{"description": "low, normal (the default) or critical", "enum": []string{"low", "normal", "critical"}, "type": "string"}}, "required": []string{"title"}, "type": "object"}}, + {Name: "history", Description: "The notifications shown lately, newest first.", + Input: map[string]any{"properties": map[string]any{"limit": map[string]any{"description": "how many (optional, default 20)", "type": "string"}}, "type": "object"}}, + }}, + {Name: NodeLockScreen, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "lock", Description: "Lock the operator's session now.", + Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + }}, + {Name: NodeClipboard, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Serves: []Verb{ + {Name: "history", Description: "What the clipboard held lately, newest first.", + Input: map[string]any{"properties": map[string]any{"limit": map[string]any{"description": "how many (optional, default 20)", "type": "string"}}, "type": "object"}}, + {Name: "copy", Description: "Put text on the operator's clipboard.", + Input: map[string]any{"properties": map[string]any{"text": map[string]any{"description": "the text", "type": "string"}}, "required": []string{"text"}, "type": "object"}}, + }}, + {Name: NodeBar, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", + Receives: []Receivable{{Kind: "block", Comment: "#", Offered: true, Shape: &Shape{Places: []PlaceField{{Field: "bar", Values: []string{"bottom", "top"}}, {Field: "place", Values: []string{"resources", "status"}}}, Order: "order", Examples: []map[string]any{map[string]any{"bar": "bottom", "place": "status", "shows": "battery"}, map[string]any{"bar": "bottom", "options": map[string]any{"device": "BAT0"}, "place": "status", "shows": "battery"}, map[string]any{"bar": "bottom", "options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}, "place": "status", "shows": "command"}, map[string]any{"bar": "bottom", "options": map[string]any{"state": "power.draw"}, "place": "status", "shows": "state"}, map[string]any{"bar": "bottom", "options": map[string]any{"key": "a-machine", "state": "power.draw"}, "place": "status", "shows": "state"}}, Text: true}}}}, + {Name: NodeCompositor, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208"}, + {Name: NodeSecretService, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208"}, + } +} diff --git a/ts/package.json b/ts/package.json new file mode 100644 index 0000000..28bd3f5 --- /dev/null +++ b/ts/package.json @@ -0,0 +1,19 @@ +{ + "description": "The mesh's seat contracts, generated from their definitions.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/container-image-registry.ts b/ts/src/container-image-registry.ts new file mode 100644 index 0000000..ca92d41 --- /dev/null +++ b/ts/src/container-image-registry.ts @@ -0,0 +1,155 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. + +import type * as registry from "./parts/registry.js"; + +export const seat = "container-image-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", + mirror: "mirror", + tags: "tags", + hold: "hold", + release: "release", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + held: "held", + released: "released", +} as const; + +export const family = { name: "container-image-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.container-image-registry.event.published"; + +export const refusedSubject = "mesh.seat.container-image-registry.event.refused"; + +export const removedSubject = "mesh.seat.container-image-registry.event.removed"; + +export const heldSubject = "mesh.seat.container-image-registry.event.held"; + +export const releasedSubject = "mesh.seat.container-image-registry.event.released"; + +export type Artifact = registry.Artifact; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** the body of held and released */ +export interface Hold { + /** when, RFC 3339 */ + "at"?: string; + /** the caller the holder proved */ + "by"?: string; + /** the digest */ + "digest": string; + /** the module holding the seat */ + "holder": string; + /** the name */ + "name": string; + /** why */ + "reason"?: string; +} + +/** is what mirror takes. */ +export interface MirrorInput { + /** its pinned digest, sha256: */ + "digest": string; + /** the name it is kept under */ + "name": string; + /** the outside image's reference */ + "source": string; +} + +/** is what mirror answers. */ +export interface MirrorAnswer { + /** the digest stored */ + "digest": string; + /** the reference the seat answers */ + "reference": string; +} + +/** is what tags takes. */ +export interface TagsInput { + /** the image */ + "name": string; +} + +export interface TagsAnswerTagsItem { + /** its digest */ + "digest": string; + /** the tag */ + "tag": string; +} + +/** is what tags answers. */ +export interface TagsAnswer { + /** the image */ + "name": string; + /** every tag */ + "tags": TagsAnswerTagsItem[]; +} + +/** is what hold takes. */ +export interface HoldInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** which of the sweep's rules keeps it */ + "reason": string; +} + +/** is what hold answers. */ +export interface HoldAnswer { + /** it is held */ + "held": boolean; + /** when the hold began, RFC 3339 */ + "since"?: string; +} + +/** is what release takes. */ +export interface ReleaseInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** why it is let go */ + "reason": string; +} + +/** is what release answers. */ +export interface ReleaseAnswer { + /** the hold is gone */ + "released": boolean; +} + +/** is the body of the event held. */ +export type HeldEvent = Hold; + +/** is the body of the event released. */ +export type ReleasedEvent = Hold; + +/** is the form of the provision artifact-store. */ +export interface ArtifactStoreForm { + /** the store's address */ + "url": string; +} + diff --git a/ts/src/go-package-registry.ts b/ts/src/go-package-registry.ts new file mode 100644 index 0000000..834ecb3 --- /dev/null +++ b/ts/src/go-package-registry.ts @@ -0,0 +1,50 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. + +import type * as registry from "./parts/registry.js"; + +export const seat = "go-package-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +export const family = { name: "go-package-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.go-package-registry.event.published"; + +export const refusedSubject = "mesh.seat.go-package-registry.event.refused"; + +export const removedSubject = "mesh.seat.go-package-registry.event.removed"; + +export type Artifact = registry.Artifact; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** is the form of the provision go-package-registry. */ +export interface GoPackageRegistryForm { + /** the read side's address */ + "url": string; +} + diff --git a/ts/src/index.ts b/ts/src/index.ts new file mode 100644 index 0000000..18984e2 --- /dev/null +++ b/ts/src/index.ts @@ -0,0 +1,12 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +export * as seats from "./seats.js"; +export * as meshBroker from "./mesh-broker.js"; +export * as meshVault from "./mesh-vault.js"; +export * as meshConfig from "./mesh-config.js"; +export * as meshDeliveryChecker from "./mesh-delivery-checker.js"; +export * as operatorChannel from "./operator-channel.js"; +export * as npmPackageRegistry from "./npm-package-registry.js"; +export * as goPackageRegistry from "./go-package-registry.js"; +export * as containerImageRegistry from "./container-image-registry.js"; +export * as ociArtifactRegistry from "./oci-artifact-registry.js"; diff --git a/ts/src/mesh-broker.ts b/ts/src/mesh-broker.ts new file mode 100644 index 0000000..5f94f67 --- /dev/null +++ b/ts/src/mesh-broker.ts @@ -0,0 +1,261 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-broker", defined whole (seats/mesh-broker/seat.json): The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone. + +export const seat = "mesh-broker"; + +export const verbs = { + list: "list", + show: "show", + check: "check", + orphans: "orphans", + create: "create", + apply: "apply", + recreate: "recreate", + drop: "drop", +} as const; + +export const events = { + created: "created", + refused: "refused", + drifted: "drifted", + driftCleared: "drift-cleared", + full: "full", + fullCleared: "full-cleared", + orphaned: "orphaned", + recreated: "recreated", + dropped: "dropped", +} as const; + +export const family = { name: "mesh-broker.events", maxAgeDays: 30, maxMiB: 256 } as const; + +export const createdSubject = "mesh.seat.mesh-broker.event.created"; + +export const refusedSubject = "mesh.seat.mesh-broker.event.refused"; + +export const driftedSubject = "mesh.seat.mesh-broker.event.drifted"; + +export const driftClearedSubject = "mesh.seat.mesh-broker.event.drift-cleared"; + +export const fullSubject = "mesh.seat.mesh-broker.event.full"; + +export const fullClearedSubject = "mesh.seat.mesh-broker.event.full-cleared"; + +export const orphanedSubject = "mesh.seat.mesh-broker.event.orphaned"; + +export const recreatedSubject = "mesh.seat.mesh-broker.event.recreated"; + +export const droppedSubject = "mesh.seat.mesh-broker.event.dropped"; + +/** one piece of bus state as a manifest asks it */ +export interface Entry { + /** its own configuration, where no template fits */ + "configuration"?: Record; + /** its local name in the module's manifest */ + "local-name": string; + /** the module asking it */ + "module": string; + /** the template's options the module set, within their bounds */ + "options"?: Record; + /** how the bus keeps it */ + "storage-type": "stream" | "key-value" | "object-store" | "ledger"; + /** its template; empty for a configuration of its own */ + "template"?: "state" | "events" | "work-queue" | "log" | "objects" | "ledger" | ""; +} + +export interface Fill { + /** bytes held */ + "bytes": number; + /** its cap in bytes */ + "cap": number; + /** messages, keys or objects held */ + "count": number; +} + +/** one entry as the bus holds it */ +export interface Held { + /** its name on the bus, _ */ + "bus-name": string; + /** its own configuration, where no template fits */ + "configuration"?: Record; + "fill"?: Fill; + /** its local name in the module's manifest */ + "local-name": string; + /** the module asking it */ + "module": string; + /** the template's options the module set, within their bounds */ + "options"?: Record; + /** the settings the bus holds */ + "settings"?: Record; + /** how the bus keeps it */ + "storage-type": "stream" | "key-value" | "object-store" | "ledger"; + /** its template; empty for a configuration of its own */ + "template"?: "state" | "events" | "work-queue" | "log" | "objects" | "ledger" | ""; +} + +export interface Named { + /** the entry's local name */ + "local-name": string; + /** the module */ + "module": string; +} + +/** the body of every event of the seat */ +export interface Said { + /** the settings asked */ + "asked"?: Record; + /** its name on the bus */ + "bus-name": string; + /** its own configuration, where no template fits */ + "configuration"?: Record; + "fill"?: Fill; + /** the settings held */ + "held"?: Record; + /** its local name in the module's manifest */ + "local-name": string; + /** the module asking it */ + "module": string; + /** the template's options the module set, within their bounds */ + "options"?: Record; + /** the stream snapshot taken before a recreate or drop */ + "snapshot"?: string; + /** how the bus keeps it */ + "storage-type"?: "stream" | "key-value" | "object-store" | "ledger"; + /** its template; empty for a configuration of its own */ + "template"?: "state" | "events" | "work-queue" | "log" | "objects" | "ledger" | ""; + /** why: a refusal's cause, a drift's difference, a drop's or recreate's reason */ + "why"?: string; +} + +export interface Warranted { + /** the entry's local name */ + "local-name": string; + /** the module */ + "module": string; + /** the operator's warrant, as the operator channel decided it (ADR 0259 §6) */ + "warrant": string; + /** why, kept in the record */ + "why": string; +} + +/** is what list takes. */ +export interface ListInput { + /** one module (optional) */ + "module"?: string; +} + +/** is what list answers. */ +export interface ListAnswer { + /** every entry */ + "entries": Held[]; +} + +/** is what show takes. */ +export type ShowInput = Named; + +/** is what show answers. */ +export type ShowAnswer = Held; + +/** is what check takes. */ +export interface CheckInput { + /** one module (optional) */ + "module"?: string; +} + +export interface CheckAnswerDifferencesItem { + /** asked */ + "asked"?: Record; + /** the difference only grows: apply may act on it */ + "grows"?: boolean; + /** held */ + "held"?: Record; + /** the entry */ + "local-name": string; + /** the module */ + "module": string; +} + +/** is what check answers. */ +export interface CheckAnswer { + /** every difference */ + "differences": CheckAnswerDifferencesItem[]; +} + +/** is what orphans takes. */ +export interface OrphansInput { +} + +/** is what orphans answers. */ +export interface OrphansAnswer { + /** every orphan */ + "entries": Held[]; +} + +/** is what create takes. */ +export type CreateInput = Entry; + +/** is what create answers. */ +export type CreateAnswer = Held; + +/** is what apply takes. */ +export type ApplyInput = Named; + +/** is what apply answers. */ +export type ApplyAnswer = Held; + +/** is what recreate takes. */ +export type RecreateInput = Warranted; + +/** is what recreate answers. */ +export type RecreateAnswer = Held; + +/** is what drop takes. */ +export type DropInput = Warranted; + +/** is what drop answers. */ +export interface DropAnswer { + /** it is gone */ + "dropped": boolean; + /** the snapshot taken first */ + "snapshot"?: string; +} + +/** is the body of the event created. */ +export type CreatedEvent = Said; + +/** is the body of the event refused. */ +export type RefusedEvent = Said; + +/** is the body of the event drifted. */ +export type DriftedEvent = Said; + +/** is the body of the event drift-cleared. */ +export type DriftClearedEvent = Said; + +/** is the body of the event full. */ +export type FullEvent = Said; + +/** is the body of the event full-cleared. */ +export type FullClearedEvent = Said; + +/** is the body of the event orphaned. */ +export type OrphanedEvent = Said; + +/** is the body of the event recreated. */ +export type RecreatedEvent = Said; + +/** is the body of the event dropped. */ +export type DroppedEvent = Said; + +/** is the form of the provision bus-state. */ +export interface BusStateForm { + /** the entry's name on the bus */ + "bus-name": string; + /** how the bus keeps it */ + "storage-type": string; + /** its subjects, for a stream */ + "subjects"?: string[]; + /** its template */ + "template"?: string; +} + diff --git a/ts/src/mesh-config.ts b/ts/src/mesh-config.ts new file mode 100644 index 0000000..a9d230f --- /dev/null +++ b/ts/src/mesh-config.ts @@ -0,0 +1,119 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-config", defined whole (seats/mesh-config/seat.json): The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2). + +export const seat = "mesh-config"; + +export const verbs = { + config: "config", + renames: "renames", +} as const; + +export const holderEvents = { + configChanged: "config-changed", + renamed: "renamed", + retiring: "retiring", + retired: "retired", +} as const; + +export function configChangedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.config-changed"; +} + +export function renamedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.renamed"; +} + +export function retiringHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.retiring"; +} + +export function retiredHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.retired"; +} + +/** is what config takes. */ +export interface ConfigInput { + /** list (when not given), set, clear or history */ + "action"?: string; + /** who gave the value at the controller's terminal */ + "by"?: string; + /** the config key */ + "key"?: string; + /** for history: at most this many, newest */ + "limit"?: number; + /** a module: its own value, with node */ + "module"?: string; + /** a node */ + "node"?: string; + /** for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list */ + "value"?: unknown; + /** for set and clear: why */ + "why"?: string; +} + +/** is what config answers. */ +export type ConfigAnswer = Record; + +/** is what renames takes. */ +export interface RenamesInput { + /** one module (optional) */ + "module"?: string; +} + +export interface RenamesAnswerRenamesItem { + /** when it moved, RFC 3339 */ + "at"?: string; + /** the old name */ + "from": string; + /** the module */ + "module": string; + /** the nodes still holding the old name open */ + "open"?: string[]; + /** the new name */ + "to": string; +} + +/** is what renames answers. */ +export interface RenamesAnswer { + /** every open rename */ + "renames": RenamesAnswerRenamesItem[]; +} + +/** is the body of the holder's event renamed. */ +export interface RenamedHolderEvent { + /** when, RFC 3339 */ + "at": string; + /** the old name */ + "from": string; + /** the module */ + "module": string; + /** the new name */ + "to": string; +} + +/** is the body of the holder's event retiring. */ +export interface RetiringHolderEvent { + /** the ask's id */ + "ask"?: string; + /** the value's or secret's name */ + "key": string; + "kind": "value" | "secret"; + /** the module */ + "module": string; + /** since when, RFC 3339 */ + "since": string; +} + +/** is the body of the holder's event retired. */ +export interface RetiredHolderEvent { + /** the ask's id */ + "ask"?: string; + /** the module */ + "module": string; + /** the name */ + "name": string; + /** the node */ + "node"?: string; +} + diff --git a/ts/src/mesh-delivery-checker.ts b/ts/src/mesh-delivery-checker.ts new file mode 100644 index 0000000..045181b --- /dev/null +++ b/ts/src/mesh-delivery-checker.ts @@ -0,0 +1,107 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). + +export const seat = "mesh-delivery-checker"; + +export const verbs = { + try: "try", + tryRead: "try-read", + tryEnd: "try-end", +} as const; + +export const holderEvents = { + verdict: "verdict", + tagWanted: "tag-wanted", + previewWritten: "preview.written", +} as const; + +export function verdictHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.verdict"; +} + +export function tagWantedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.tag-wanted"; +} + +export function previewWrittenHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.preview.written"; +} + +/** is what try takes. */ +export interface TryInput { + /** the full commit to try, 40 or 64 hex digits */ + "head": string; + /** unit or integration: only this level; both when absent */ + "level"?: string; + /** / */ + "repository": string; + /** a go test -run pattern: only those tests run, and nothing is kept */ + "tests"?: string; + /** the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent */ + "trunk"?: string; +} + +/** is what try answers. */ +export interface TryAnswer { + /** the try's id */ + "id": string; + /** its state */ + "state"?: string; +} + +/** is what try-read takes. */ +export interface TryReadInput { + /** the try's id, try- */ + "id": string; +} + +/** is what try-read answers. */ +export type TryReadAnswer = Record; + +/** is what try-end takes. */ +export interface TryEndInput { + /** the try's id, try- */ + "id": string; + /** why it is ended, in words */ + "why": string; +} + +/** is what try-end answers. */ +export interface TryEndAnswer { + /** it is ended */ + "ended": boolean; + /** the try */ + "id": string; +} + +/** is the body of the holder's event verdict. */ +export interface VerdictHolderEvent { + /** the commit checked */ + "head": string; + /** the generated files out of step with their seat definitions, each path (design 26) */ + "out-of-step"?: string[]; + /** the pull request's number, for a pull request's check */ + "pull"?: number; + /** owner/repo */ + "repository": string; + /** pass or fail */ + "verdict": string; +} + +/** is the body of the holder's event tag-wanted. */ +export interface TagWantedHolderEvent { + /** its head */ + "head": string; + /** the group member */ + "module": string; + /** the package it declares */ + "package": string; + /** the pull request */ + "pull"?: number; + /** owner/repo */ + "repository": string; + /** the tag wanted */ + "tag"?: string; +} + diff --git a/ts/src/mesh-vault.ts b/ts/src/mesh-vault.ts new file mode 100644 index 0000000..89e9fee --- /dev/null +++ b/ts/src/mesh-vault.ts @@ -0,0 +1,345 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-vault", defined whole (seats/mesh-vault/seat.json): The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2). + +export const seat = "mesh-vault"; + +export const verbs = { + get: "get", + secretAsk: "secret-ask", + issued: "issued", + rotate: "rotate", + moveIn: "move-in", + nodeKey: "node-key", + own: "own", + issueAccount: "issue-account", + sign: "sign", + enrolmentToken: "enrolment-token", + nodeAccount: "node-account", + rotateNode: "rotate-node", + nodeAccountConfirm: "node-account-confirm", + abandon: "abandon", + retire: "retire", +} as const; + +export const holderEvents = { + provisioned: "provisioned", + rotated: "rotated", + secretChanged: "secret-changed", + accountHashed: "account-hashed", + ownReplaced: "own-replaced", + accountIssued: "account-issued", + signed: "signed", + rotationBegun: "rotation-begun", + nodeAccountReady: "node-account-ready", + rotationDone: "rotation-done", + ownRetired: "own-retired", +} as const; + +export function provisionedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.provisioned"; +} + +export function rotatedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.rotated"; +} + +export function secretChangedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.secret-changed"; +} + +export function accountHashedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.account-hashed"; +} + +export function ownReplacedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.own-replaced"; +} + +export function accountIssuedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.account-issued"; +} + +export function signedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.signed"; +} + +export function rotationBegunHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.rotation-begun"; +} + +export function nodeAccountReadyHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.node-account-ready"; +} + +export function rotationDoneHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.rotation-done"; +} + +export function ownRetiredHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.own-retired"; +} + +/** is what get takes. */ +export interface GetInput { + /** the secret's name */ + "name": string; + /** the asker's X25519 public key as SPKI PEM — never a private key */ + "public_key": string; +} + +/** is what get answers. */ +export type GetAnswer = Record; + +/** is what secret-ask takes. */ +export interface SecretAskInput { + /** who asked, as the bus named its caller */ + "asked_by"?: string; + /** the second step: the call the first step answered */ + "call"?: string; + /** the machine whose prompt asks */ + "desk"?: string; + /** the module */ + "module"?: string; + /** the secret's name in the module */ + "name"?: string; + /** the machine the secret is for */ + "node"?: string; + /** instead of sealed: how the prompt ended */ + "outcome"?: string; + /** the second step: the value sealed to the call's key — never the value */ + "sealed"?: string; +} + +/** is what secret-ask answers. */ +export type SecretAskAnswer = Record; + +export interface IssuedInputHoldersItem { + /** the consuming module */ + "module"?: string; + /** the holder's machine */ + "node"?: string; +} + +/** is what issued takes. */ +export interface IssuedInput { + /** the second step: the call the first step answered */ + "call"?: string; + /** the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone */ + "holders"?: IssuedInputHoldersItem[]; + /** the provider's module */ + "module"?: string; + /** the provider's machine */ + "provider"?: string; + /** the provision */ + "provision"?: string; + /** the second step: the value sealed to the call's key — never the value */ + "sealed"?: string; +} + +/** is what issued answers. */ +export type IssuedAnswer = Record; + +/** is what rotate takes. */ +export interface RotateInput { + /** who asked, when the controller carries the act */ + "by"?: string; + /** the module */ + "module"?: string; + /** the secret's name in the module */ + "name"?: string; + /** the machine, for a module's own secret */ + "node"?: string; + /** the provider's machine */ + "provider"?: string; + /** the provision */ + "provision"?: string; +} + +/** is what rotate answers. */ +export type RotateAnswer = Record; + +/** is what move-in takes. */ +export interface MoveInInput { + /** the second step: the call the first step answered */ + "call"?: string; + /** the consumer's machine */ + "consumer"?: string; + /** the consuming module */ + "consumer_module"?: string; + /** the controller's generation for the value */ + "generation"?: string; + /** the credential's local name, when it has one */ + "local"?: string; + /** when it was made (RFC 3339) */ + "made_at"?: string; + /** its module */ + "module"?: string; + /** its name in the module */ + "name"?: string; + /** a module's own secret: its machine */ + "node"?: string; + /** made, accepted or issued, as the controller's store says */ + "origin"?: string; + /** a pair credential: the provider's machine */ + "provider"?: string; + /** the provision */ + "provision"?: string; + /** the second step: a raw X25519 public key the read-back is sealed to */ + "read_back_to"?: string; + /** the second step: the value sealed to the call's key — never the value */ + "sealed"?: string; +} + +/** is what move-in answers. */ +export type MoveInAnswer = Record; + +/** is what node-key takes. */ +export interface NodeKeyInput { + /** the machine's sealing key, a raw X25519 public key in base64 */ + "key"?: string; +} + +/** is what node-key answers. */ +export type NodeKeyAnswer = Record; + +/** is what own takes. */ +export interface OwnInput { + /** the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local} */ + "credentials"?: Record[]; +} + +/** is what own answers. */ +export type OwnAnswer = Record; + +/** is what issue-account takes. */ +export interface IssueAccountInput { + /** the bus user, e.g. ace.mesh-build-go */ + "user": string; +} + +/** is what issue-account answers. */ +export type IssueAccountAnswer = Record; + +/** is what sign takes. */ +export interface SignInput { + /** node-certificate or enrolment */ + "kind": string; + /** the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols */ + "node"?: string; + /** the redeem answer's bytes in base64, for an enrolment */ + "reply"?: string; + /** from mesh-enrolment: the serving key's public half the enrolling node presented */ + "serving_key"?: string; +} + +/** is what sign answers. */ +export type SignAnswer = Record; + +/** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ +export interface EnrolmentTokenInputTunnel { + /** the joining node's tunnel address */ + "address": string; + /** the hub's endpoint, host:port */ + "hub_endpoint": string; + /** the hub's tunnel public key */ + "hub_key": string; + /** the joining machine's own tunnel public key, as given at issue */ + "key": string; + /** the private network's range, a prefix */ + "range": string; +} + +/** is what enrolment-token takes. */ +export interface EnrolmentTokenInput { + /** "true": drop the node's token and its hash */ + "drop"?: "true" | "false"; + /** when the token expires (RFC 3339), as the enrolment's key says */ + "expires"?: string; + /** the node the token is for */ + "node": string; + /** "true": answer the token sealed to the operator key */ + "sealed"?: "true" | "false"; + /** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ + "tunnel"?: EnrolmentTokenInputTunnel; +} + +/** is what enrolment-token answers. */ +export type EnrolmentTokenAnswer = Record; + +/** is what node-account takes. */ +export interface NodeAccountInput { + /** the digest of every public key the node presented (delivery.KeysDigest) */ + "keys_digest"?: string; + /** the joining node */ + "node": string; + /** "true": retire the account minted for this node */ + "retire"?: "true" | "false"; + /** the sealing key's public half the node presented, which the account is sealed to */ + "sealing_key"?: string; +} + +/** is what node-account answers. */ +export type NodeAccountAnswer = Record; + +/** is what rotate-node takes. */ +export interface RotateNodeInput { + /** the id of the hand act recorded for it in mesh-hand-acts */ + "act": string; + /** the node whose account rotates */ + "node": string; + /** why, in a sentence */ + "why": string; +} + +/** is what rotate-node answers. */ +export type RotateNodeAnswer = Record; + +/** is what node-account-confirm takes. */ +export interface NodeAccountConfirmInput { +} + +/** is what node-account-confirm answers. */ +export type NodeAccountConfirmAnswer = Record; + +/** is what abandon takes. */ +export interface AbandonInput { + /** the node whose rotation is abandoned */ + "node": string; +} + +/** is what abandon answers. */ +export type AbandonAnswer = Record; + +/** is what retire takes. */ +export interface RetireInput { + /** the module */ + "module": string; + /** the own secret's name */ + "name": string; + /** the node */ + "node": string; + /** the operator's warrant for this node, module and name */ + "warrant": string; +} + +/** is what retire answers. */ +export interface RetireAnswer { + /** the secret is gone */ + "retired": boolean; +} + +/** is the body of the holder's event own-retired. */ +export interface OwnRetiredHolderEvent { + /** the ask's id */ + "ask"?: string; + /** the caller the vault proved */ + "caller": string; + /** the module */ + "module": string; + /** the secret's name */ + "name": string; + /** the node */ + "node": string; +} + diff --git a/ts/src/npm-package-registry.ts b/ts/src/npm-package-registry.ts new file mode 100644 index 0000000..0834e45 --- /dev/null +++ b/ts/src/npm-package-registry.ts @@ -0,0 +1,50 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. + +import type * as registry from "./parts/registry.js"; + +export const seat = "npm-package-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +export const family = { name: "npm-package-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.npm-package-registry.event.published"; + +export const refusedSubject = "mesh.seat.npm-package-registry.event.refused"; + +export const removedSubject = "mesh.seat.npm-package-registry.event.removed"; + +export type Artifact = registry.Artifact; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** is the form of the provision npm-package-registry. */ +export interface NpmPackageRegistryForm { + /** the read side's address */ + "url": string; +} + diff --git a/ts/src/oci-artifact-registry.ts b/ts/src/oci-artifact-registry.ts new file mode 100644 index 0000000..974cccd --- /dev/null +++ b/ts/src/oci-artifact-registry.ts @@ -0,0 +1,114 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. + +import type * as registry from "./parts/registry.js"; + +export const seat = "oci-artifact-registry"; + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", + hold: "hold", + release: "release", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", + held: "held", + released: "released", +} as const; + +export const family = { name: "oci-artifact-registry.events", maxAgeDays: 7 } as const; + +export const publishedSubject = "mesh.seat.oci-artifact-registry.event.published"; + +export const refusedSubject = "mesh.seat.oci-artifact-registry.event.refused"; + +export const removedSubject = "mesh.seat.oci-artifact-registry.event.removed"; + +export const heldSubject = "mesh.seat.oci-artifact-registry.event.held"; + +export const releasedSubject = "mesh.seat.oci-artifact-registry.event.released"; + +export type Artifact = registry.Artifact; +export type Outcome = registry.Outcome; +export type Part = registry.Part; +export type PublishAnswer = registry.PublishAnswer; +export type PublishInput = registry.PublishInput; +export type PublishPartAnswer = registry.PublishPartAnswer; +export type PublishPartInput = registry.PublishPartInput; +export type PublishedEvent = registry.PublishedEvent; +export type RefusedEvent = registry.RefusedEvent; +export type RemoveAnswer = registry.RemoveAnswer; +export type RemoveInput = registry.RemoveInput; +export type RemovedEvent = registry.RemovedEvent; +export type VersionsAnswer = registry.VersionsAnswer; +export type VersionsInput = registry.VersionsInput; + +/** the body of held and released */ +export interface Hold { + /** when, RFC 3339 */ + "at"?: string; + /** the caller the holder proved */ + "by"?: string; + /** the digest */ + "digest": string; + /** the module holding the seat */ + "holder": string; + /** the name */ + "name": string; + /** why */ + "reason"?: string; +} + +/** is what hold takes. */ +export interface HoldInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** which of the sweep's rules keeps it */ + "reason": string; +} + +/** is what hold answers. */ +export interface HoldAnswer { + /** it is held */ + "held": boolean; + /** when the hold began, RFC 3339 */ + "since"?: string; +} + +/** is what release takes. */ +export interface ReleaseInput { + /** the digest */ + "digest": string; + /** the name */ + "name": string; + /** why it is let go */ + "reason": string; +} + +/** is what release answers. */ +export interface ReleaseAnswer { + /** the hold is gone */ + "released": boolean; +} + +/** is the body of the event held. */ +export type HeldEvent = Hold; + +/** is the body of the event released. */ +export type ReleasedEvent = Hold; + +/** is the form of the provision artifact-store. */ +export interface ArtifactStoreForm { + /** the store's address */ + "url": string; +} + diff --git a/ts/src/operator-channel.ts b/ts/src/operator-channel.ts new file mode 100644 index 0000000..adb1049 --- /dev/null +++ b/ts/src/operator-channel.ts @@ -0,0 +1,282 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "operator-channel", defined whole (seats/operator-channel/seat.json): Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26). + +export const seat = "operator-channel"; + +export const verbs = { + open: "open", + history: "history", + notify: "notify", + tell: "tell", + asks: "asks", +} as const; + +export const accepts = { + ask: "ask", + cancel: "cancel", + condition: "condition", +} as const; + +export const events = { + decided: "decided", +} as const; + +export const family = { name: "operator-channel.decided", maxAgeDays: 30 } as const; + +export function decidedSubject(caller: string): string { + return "mesh.seat.operator-channel.event.decided." + caller; +} + +export function askSubject(caller: string): string { + return "mesh.seat.operator-channel.accept.ask." + caller; +} + +export function cancelSubject(caller: string): string { + return "mesh.seat.operator-channel.accept.cancel." + caller; +} + +export function conditionSubject(caller: string): string { + return "mesh.seat.operator-channel.accept.condition." + caller; +} + +export const holderEvents = { + refused: "refused", + offered: "offered", + closed: "closed", +} as const; + +export function refusedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.refused"; +} + +export function offeredHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.offered"; +} + +export function closedHolderSubject(holder: string): string { + return "mesh.mod." + holder + ".event.closed"; +} + +export interface Ask { + /** what it is about: a newer ask about it replaces the older */ + "about"?: string; + /** its secondary words */ + "details"?: string; + /** when it ends unanswered, RFC 3339 */ + "expires": string; + /** what happened and what it means */ + "explanation": string; + /** a few plain words */ + "headline": string; + /** the asker's own id */ + "id": string; + /** what the asker then does */ + "on-expiry": string; + /** the answers it offers */ + "options": Option[]; + /** urgent */ + "urgent"?: boolean; + /** who may answer: operator */ + "who": string; + /** the explanation with its values whole */ + "whole"?: string; + /** the details whole */ + "whole-details"?: string; +} + +export interface ConditionActionsItem { + "arguments"?: Record; + /** label */ + "label": string; + /** level */ + "level"?: string; + /** machine */ + "machine"?: string; + /** verb */ + "verb": string; +} + +export interface ConditionSubject { + /** id */ + "id": string; + /** machine */ + "machine"?: string; + /** scope */ + "scope": string; +} + +export interface Condition { + /** the answers it offers */ + "actions"?: ConditionActionsItem[]; + /** plain words */ + "explanation"?: string; + /** plain words */ + "headline"?: string; + /** the condition's key */ + "key": string; + /** what it needs */ + "needs"?: string; + /** the line said when it is over */ + "resolved"?: string; + /** urgent or warning */ + "severity"?: string; + /** raised or cleared */ + "state": string; + "subject": ConditionSubject; + /** one line */ + "summary"?: string; + /** the form's version, 1 */ + "v": number; +} + +export interface Option { + /** the digest of exactly what the asker performs when it is chosen */ + "binds"?: string; + /** what it does, in plain words */ + "does": string; + /** the option's id */ + "id": string; + /** the button's label */ + "label": string; + /** the proof its answer needs */ + "level": string; +} + +export interface Person { + /** the name shown */ + "display"?: string; + /** the account */ + "identity": string; + /** the channel kind */ + "kind": string; + /** how the router knew */ + "verified": string; + /** the role */ + "who": string; +} + +export interface Warrant { + /** what it was about */ + "about"?: string; + /** the ask */ + "ask": string; + /** the digest of the ask as the router took it */ + "ask-digest"?: string; + /** its asker */ + "asker": string; + /** when, RFC 3339 */ + "at": string; + "by"?: Person; + /** the module it came through */ + "channel"?: string; + /** its label */ + "label"?: string; + /** its level */ + "level"?: string; + /** the option chosen */ + "option"?: string; + "outcome": "chosen" | "expired" | "cancelled" | "replaced" | "refused"; + /** the proofs present */ + "proofs"?: string[]; + /** why it ended without a choice, or what refused it */ + "words"?: string; +} + +/** is what open takes. */ +export interface OpenInput { +} + +/** is what open answers. */ +export type OpenAnswer = Record; + +/** is what history takes. */ +export interface HistoryInput { + /** at most this many of each (default 50) */ + "limit"?: number; +} + +/** is what history answers. */ +export type HistoryAnswer = Record; + +/** is what notify takes. */ +export interface NotifyInput { + /** it is over */ + "clear"?: boolean; + /** one or two plain sentences */ + "explanation"?: string; + /** plain words */ + "headline"?: string; + /** the message's key */ + "key"?: string; + /** the line said when it is over */ + "resolved"?: string; + "severity"?: "urgent" | "warning"; + /** what it is about */ + "subject"?: string; + /** one line */ + "summary"?: string; +} + +/** is what notify answers. */ +export type NotifyAnswer = Record; + +/** is what tell takes. */ +export interface TellInput { + /** plain words */ + "headline"?: string; + /** one or two plain lines */ + "lines"?: string[]; + /** what it is about */ + "subject"?: string; +} + +/** is what tell answers. */ +export type TellAnswer = Record; + +/** is what asks takes. */ +export interface AsksInput { +} + +/** is what asks answers. */ +export type AsksAnswer = Record; + +/** is the body of ask. */ +export type AskAccept = Ask; + +/** is the body of cancel. */ +export interface CancelAccept { + /** the ask's id */ + "id": string; +} + +/** is the body of condition. */ +export type ConditionAccept = Condition; + +/** is the body of the event decided. */ +export type DecidedEvent = Warrant; + +/** is the body of the holder's event offered. */ +export interface OfferedHolderEvent { + /** its asker */ + "asker": string; + /** the channels it was offered on */ + "channels"?: string[]; + /** its headline */ + "headline"?: string; + /** the ask's id */ + "id": string; +} + +/** is the body of the holder's event closed. */ +export interface ClosedHolderEvent { + /** its asker */ + "asker": string; + "how": "answered" | "expired" | "cancelled" | "replaced" | "unlinked"; + /** the ask's id */ + "id": string; +} + +/** is one value of the record asks. */ +export type AsksRecord = Record; + diff --git a/ts/src/parts/registry.ts b/ts/src/parts/registry.ts new file mode 100644 index 0000000..e13b4d2 --- /dev/null +++ b/ts/src/parts/registry.ts @@ -0,0 +1,173 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 768 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it is gone */ + "removed": boolean; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + diff --git a/ts/src/seats.ts b/ts/src/seats.ts new file mode 100644 index 0000000..c72a5b9 --- /dev/null +++ b/ts/src/seats.ts @@ -0,0 +1,6889 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The mesh's seat list, generated from every seat's definition (design 26). + +export interface Verb { + name: string; + description: string; + input?: Record; + output?: Record; + optional?: boolean; + replaces?: string[]; +} + +export interface Seat { + name: string; + scope: "mesh" | "node"; + grain: "whole" | "seat-list"; + decision: string; + delivers?: string; + replicated?: boolean; + accepts?: string[]; + emits?: string[]; + pulledByKind?: boolean; + yields?: boolean; + heldByTheEngine?: boolean; + serves?: Verb[]; +} + +export const names = { + meshStore: "mesh-store", + meshBroker: "mesh-broker", + meshVault: "mesh-vault", + meshConfig: "mesh-config", + meshProvisioner: "mesh-provisioner", + meshArtifactStore: "mesh-artifact-store", + meshCatalog: "mesh-catalog", + meshDelivery: "mesh-delivery", + meshPlacement: "mesh-placement", + meshDeliveryChecker: "mesh-delivery-checker", + meshConditions: "mesh-conditions", + meshWatchdogs: "mesh-watchdogs", + meshSelfCheck: "mesh-self-check", + meshHealers: "mesh-healers", + meshWants: "mesh-wants", + meshHandActs: "mesh-hand-acts", + meshEnrolment: "mesh-enrolment", + meshArtifactSweep: "mesh-artifact-sweep", + operatorChannel: "operator-channel", + npmPackageRegistry: "npm-package-registry", + goPackageRegistry: "go-package-registry", + containerImageRegistry: "container-image-registry", + ociArtifactRegistry: "oci-artifact-registry", + git: "git", + nodeBuildGo: "node-build-go", + nodeBuildNode: "node-build-node", + nodeBuildImage: "node-build-image", + meshDnsResolver: "mesh-dns-resolver", + nodeHostname: "node-hostname", + nodeIntrusionPrevention: "node-intrusion-prevention", + nodePacketFilter: "node-packet-filter", + nodeServiceManager: "node-service-manager", + nodePackageManager: "node-package-manager", + nodeContainerRuntime: "node-container-runtime", + nodeEnvironment: "node-environment", + nodeProvisioner: "node-provisioner", + nodeLoginShell: "node-login-shell", + nodePower: "node-power", + nodeHotkeys: "node-hotkeys", + nodeMessageBus: "node-message-bus", + nodeBackup: "node-backup", + thePrivateNetwork: "the-private-network", + nodeUplink: "node-uplink", + nodeResolver: "node-resolver", + nodeNfsServer: "node-nfs-server", + nodeMounts: "node-mounts", + nodeReads: "node-reads", + nodeFiles: "node-files", + nodeEngine: "node-engine", + nodeLoginManager: "node-login-manager", + nodeDisplayServer: "node-display-server", + nodeDisplaySession: "node-display-session", + nodeTerminalEmulator: "node-terminal-emulator", + nodeLauncher: "node-launcher", + nodeNotifier: "node-notifier", + nodeLockScreen: "node-lock-screen", + nodeClipboard: "node-clipboard", + nodeBar: "node-bar", + nodeCompositor: "node-compositor", + nodeSecretService: "node-secret-service", +} as const; + +export const list: readonly Seat[] = [ + { + "decision": "novox/hq ADR 0079", + "delivers": "postgres-database", + "grain": "seat-list", + "name": "mesh-store", + "scope": "mesh", + "serves": [ + { + "description": "Every database the store holds, with its on-disk size.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "databases", + "replaces": [ + "psql -l" + ] + }, + { + "description": "One read-only statement against one database the store holds.", + "input": { + "properties": { + "database": { + "description": "the database to query", + "type": "string" + }, + "sql": { + "description": "the read-only statement", + "type": "string" + } + }, + "required": [ + "database", + "sql" + ], + "type": "object" + }, + "name": "query", + "replaces": [ + "psql" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0079; novox/hq ADR 0595", + "delivers": "mesh-bus", + "emits": [ + "created", + "refused", + "drifted", + "drift-cleared", + "full", + "full-cleared", + "orphaned", + "recreated", + "dropped" + ], + "grain": "whole", + "name": "mesh-broker", + "scope": "mesh", + "serves": [ + { + "description": "Every entry the bus module created, its template, its settings and its fill against its cap.", + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "list", + "output": { + "properties": { + "entries": { + "description": "every entry", + "items": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "type": "object" + } + }, + { + "description": "One entry, its template, settings and fill.", + "input": { + "properties": { + "local-name": { + "description": "the entry's local name", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + } + }, + "required": [ + "module", + "local-name" + ], + "type": "object" + }, + "name": "show", + "output": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + } + }, + { + "description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "check", + "output": { + "properties": { + "differences": { + "description": "every difference", + "items": { + "properties": { + "asked": { + "description": "asked", + "type": "object" + }, + "grows": { + "description": "the difference only grows: apply may act on it", + "type": "boolean" + }, + "held": { + "description": "held", + "type": "object" + }, + "local-name": { + "description": "the entry", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + } + }, + "required": [ + "module", + "local-name" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "differences" + ], + "type": "object" + } + }, + { + "description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", + "input": { + "description": "no arguments", + "properties": {}, + "type": "object" + }, + "name": "orphans", + "output": { + "properties": { + "entries": { + "description": "every orphan", + "items": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "entries" + ], + "type": "object" + } + }, + { + "description": "Create one entry now.", + "input": { + "description": "one piece of bus state as a manifest asks it", + "properties": { + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type" + ], + "type": "object" + }, + "name": "create", + "output": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + } + }, + { + "description": "Apply a difference that only grows: a larger cap, a longer age.", + "input": { + "properties": { + "local-name": { + "description": "the entry's local name", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + } + }, + "required": [ + "module", + "local-name" + ], + "type": "object" + }, + "name": "apply", + "output": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + } + }, + { + "description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", + "input": { + "properties": { + "local-name": { + "description": "the entry's local name", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "warrant": { + "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", + "type": "string" + }, + "why": { + "description": "why, kept in the record", + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "warrant", + "why" + ], + "type": "object" + }, + "name": "recreate", + "output": { + "description": "one entry as the bus holds it", + "properties": { + "bus-name": { + "description": "its name on the bus, _", + "type": "string" + }, + "configuration": { + "description": "its own configuration, where no template fits", + "type": "object" + }, + "fill": { + "properties": { + "bytes": { + "description": "bytes held", + "type": "integer" + }, + "cap": { + "description": "its cap in bytes", + "type": "integer" + }, + "count": { + "description": "messages, keys or objects held", + "type": "integer" + } + }, + "required": [ + "count", + "bytes", + "cap" + ], + "type": "object" + }, + "local-name": { + "description": "its local name in the module's manifest", + "type": "string" + }, + "module": { + "description": "the module asking it", + "type": "string" + }, + "options": { + "description": "the template's options the module set, within their bounds", + "type": "object" + }, + "settings": { + "description": "the settings the bus holds", + "type": "object" + }, + "storage-type": { + "description": "how the bus keeps it", + "enum": [ + "stream", + "key-value", + "object-store", + "ledger" + ], + "type": "string" + }, + "template": { + "description": "its template; empty for a configuration of its own", + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger", + "" + ], + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "storage-type", + "bus-name" + ], + "type": "object" + } + }, + { + "description": "Remove one orphan, after a stream snapshot.", + "input": { + "properties": { + "local-name": { + "description": "the entry's local name", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "warrant": { + "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", + "type": "string" + }, + "why": { + "description": "why, kept in the record", + "type": "string" + } + }, + "required": [ + "module", + "local-name", + "warrant", + "why" + ], + "type": "object" + }, + "name": "drop", + "output": { + "properties": { + "dropped": { + "description": "it is gone", + "type": "boolean" + }, + "snapshot": { + "description": "the snapshot taken first", + "type": "string" + } + }, + "required": [ + "dropped" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0161", + "delivers": "secret", + "grain": "whole", + "name": "mesh-vault", + "scope": "mesh", + "serves": [ + { + "description": "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.", + "input": { + "properties": { + "name": { + "description": "the secret's name", + "type": "string" + }, + "public_key": { + "description": "the asker's X25519 public key as SPKI PEM — never a private key", + "type": "string" + } + }, + "required": [ + "name", + "public_key" + ], + "type": "object" + }, + "name": "get", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", + "input": { + "properties": { + "asked_by": { + "description": "who asked, as the bus named its caller", + "type": "string" + }, + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "desk": { + "description": "the machine whose prompt asks", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "name": { + "description": "the secret's name in the module", + "type": "string" + }, + "node": { + "description": "the machine the secret is for", + "type": "string" + }, + "outcome": { + "description": "instead of sealed: how the prompt ended", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "name": "secret-ask", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.", + "input": { + "properties": { + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "holders": { + "description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone", + "items": { + "properties": { + "module": { + "description": "the consuming module", + "type": "string" + }, + "node": { + "description": "the holder's machine", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "module": { + "description": "the provider's module", + "type": "string" + }, + "provider": { + "description": "the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "name": "issued", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.", + "input": { + "properties": { + "by": { + "description": "who asked, when the controller carries the act", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "name": { + "description": "the secret's name in the module", + "type": "string" + }, + "node": { + "description": "the machine, for a module's own secret", + "type": "string" + }, + "provider": { + "description": "the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + } + }, + "type": "object" + }, + "name": "rotate", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.", + "input": { + "properties": { + "call": { + "description": "the second step: the call the first step answered", + "type": "string" + }, + "consumer": { + "description": "the consumer's machine", + "type": "string" + }, + "consumer_module": { + "description": "the consuming module", + "type": "string" + }, + "generation": { + "description": "the controller's generation for the value", + "type": "string" + }, + "local": { + "description": "the credential's local name, when it has one", + "type": "string" + }, + "made_at": { + "description": "when it was made (RFC 3339)", + "type": "string" + }, + "module": { + "description": "its module", + "type": "string" + }, + "name": { + "description": "its name in the module", + "type": "string" + }, + "node": { + "description": "a module's own secret: its machine", + "type": "string" + }, + "origin": { + "description": "made, accepted or issued, as the controller's store says", + "type": "string" + }, + "provider": { + "description": "a pair credential: the provider's machine", + "type": "string" + }, + "provision": { + "description": "the provision", + "type": "string" + }, + "read_back_to": { + "description": "the second step: a raw X25519 public key the read-back is sealed to", + "type": "string" + }, + "sealed": { + "description": "the second step: the value sealed to the call's key — never the value", + "type": "string" + } + }, + "type": "object" + }, + "name": "move-in", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.", + "input": { + "properties": { + "key": { + "description": "the machine's sealing key, a raw X25519 public key in base64", + "type": "string" + } + }, + "type": "object" + }, + "name": "node-key", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.", + "input": { + "properties": { + "credentials": { + "description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}", + "items": { + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "name": "own", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", + "input": { + "properties": { + "user": { + "description": "the bus user, e.g. ace.mesh-build-go", + "type": "string" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "name": "issue-account", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", + "input": { + "properties": { + "kind": { + "description": "node-certificate or enrolment", + "type": "string" + }, + "node": { + "description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", + "type": "string" + }, + "reply": { + "description": "the redeem answer's bytes in base64, for an enrolment", + "type": "string" + }, + "serving_key": { + "description": "from mesh-enrolment: the serving key's public half the enrolling node presented", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, + "name": "sign", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", + "input": { + "properties": { + "drop": { + "description": "\"true\": drop the node's token and its hash", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "expires": { + "description": "when the token expires (RFC 3339), as the enrolment's key says", + "type": "string" + }, + "node": { + "description": "the node the token is for", + "type": "string" + }, + "sealed": { + "description": "\"true\": answer the token sealed to the operator key", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "tunnel": { + "additionalProperties": false, + "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", + "properties": { + "address": { + "description": "the joining node's tunnel address", + "type": "string" + }, + "hub_endpoint": { + "description": "the hub's endpoint, host:port", + "type": "string" + }, + "hub_key": { + "description": "the hub's tunnel public key", + "type": "string" + }, + "key": { + "description": "the joining machine's own tunnel public key, as given at issue", + "type": "string" + }, + "range": { + "description": "the private network's range, a prefix", + "type": "string" + } + }, + "required": [ + "address", + "hub_endpoint", + "hub_key", + "key", + "range" + ], + "type": "object" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "enrolment-token", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", + "input": { + "properties": { + "keys_digest": { + "description": "the digest of every public key the node presented (delivery.KeysDigest)", + "type": "string" + }, + "node": { + "description": "the joining node", + "type": "string" + }, + "retire": { + "description": "\"true\": retire the account minted for this node", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "sealing_key": { + "description": "the sealing key's public half the node presented, which the account is sealed to", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "node-account", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", + "input": { + "properties": { + "act": { + "description": "the id of the hand act recorded for it in mesh-hand-acts", + "type": "string" + }, + "node": { + "description": "the node whose account rotates", + "type": "string" + }, + "why": { + "description": "why, in a sentence", + "type": "string" + } + }, + "required": [ + "node", + "why", + "act" + ], + "type": "object" + }, + "name": "rotate-node", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "node-account-confirm", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.", + "input": { + "properties": { + "node": { + "description": "the node whose rotation is abandoned", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "abandon", + "optional": true, + "output": { + "description": "the holder's answer", + "type": "object" + } + }, + { + "description": "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", + "input": { + "properties": { + "module": { + "description": "the module", + "type": "string" + }, + "name": { + "description": "the own secret's name", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "warrant": { + "description": "the operator's warrant for this node, module and name", + "type": "string" + } + }, + "required": [ + "node", + "module", + "name", + "warrant" + ], + "type": "object" + }, + "name": "retire", + "optional": true, + "output": { + "properties": { + "retired": { + "description": "the secret is gone", + "type": "boolean" + } + }, + "required": [ + "retired" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0348, ADR 0565", + "grain": "whole", + "name": "mesh-config", + "scope": "mesh", + "serves": [ + { + "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.", + "input": { + "properties": { + "action": { + "description": "list (when not given), set, clear or history", + "type": "string" + }, + "by": { + "description": "who gave the value at the controller's terminal", + "type": "string" + }, + "key": { + "description": "the config key", + "type": "string" + }, + "limit": { + "description": "for history: at most this many, newest", + "type": "number" + }, + "module": { + "description": "a module: its own value, with node", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "value": { + "description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list" + }, + "why": { + "description": "for set and clear: why", + "type": "string" + } + }, + "type": "object" + }, + "name": "config", + "optional": true, + "output": { + "description": "the values asked, or the one set or cleared, or its history", + "type": "object" + } + }, + { + "description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "renames", + "optional": true, + "output": { + "properties": { + "renames": { + "description": "every open rename", + "items": { + "properties": { + "at": { + "description": "when it moved, RFC 3339", + "type": "string" + }, + "from": { + "description": "the old name", + "type": "string" + }, + "module": { + "description": "the module", + "type": "string" + }, + "open": { + "description": "the nodes still holding the old name open", + "items": { + "type": "string" + }, + "type": "array" + }, + "to": { + "description": "the new name", + "type": "string" + } + }, + "required": [ + "module", + "from", + "to" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "renames" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0349", + "grain": "seat-list", + "name": "mesh-provisioner", + "scope": "mesh", + "serves": [ + { + "description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", + "input": { + "properties": { + "collection": { + "description": "for collect: a collection", + "type": "string" + }, + "module": { + "description": "a module: for collect, a contributor", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "what": { + "description": "registry (when not given), needs, applied or collect", + "type": "string" + } + }, + "type": "object" + }, + "name": "provisioning", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0075", + "delivers": "artifact-store", + "grain": "seat-list", + "name": "mesh-artifact-store", + "scope": "mesh" + }, + { + "decision": "novox/hq ADR 0604", + "grain": "seat-list", + "name": "mesh-catalog", + "scope": "mesh", + "serves": [ + { + "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "modules", + "optional": true + }, + { + "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "name": "module", + "optional": true + }, + { + "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", + "input": { + "properties": { + "provision": { + "description": "the provision, such as postgres-database", + "type": "string" + } + }, + "required": [ + "provision" + ], + "type": "object" + }, + "name": "provides", + "optional": true + }, + { + "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", + "input": { + "properties": { + "module": { + "description": "the module's name", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "name": "dependents", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0239", + "grain": "seat-list", + "name": "mesh-delivery", + "scope": "mesh", + "serves": [ + { + "description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", + "input": { + "properties": { + "all": { + "description": "\"true\": the final ones of the last thirty days too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "group": { + "description": "a group's id (its branch name)", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + }, + "state": { + "description": "one state, e.g. delivering or held", + "type": "string" + } + }, + "type": "object" + }, + "name": "deliveries" + }, + { + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", + "input": { + "properties": {}, + "type": "object" + }, + "name": "times", + "optional": true + }, + { + "description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", + "input": { + "properties": { + "id": { + "description": "a delivery's id, or a group's", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "name": "show" + }, + { + "description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", + "input": { + "properties": { + "all": { + "description": "\"true\": the groups that ended too", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "name": "groups" + }, + { + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", + "input": { + "properties": {}, + "type": "object" + }, + "name": "what-if", + "optional": true + }, + { + "description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", + "input": { + "properties": { + "commit": { + "description": "a commit's sha, or the start of one, instead of a pull request", + "type": "string" + }, + "number": { + "description": "a pull request's number: its head is read", + "type": "string" + }, + "repository": { + "description": "owner/repository", + "type": "string" + } + }, + "required": [ + "repository" + ], + "type": "object" + }, + "name": "checks", + "optional": true + }, + { + "description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "table" + }, + { + "description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "stalled" + }, + { + "description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "why, kept with the transition", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "name": "recheck" + }, + { + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", + "input": { + "properties": {}, + "type": "object" + }, + "name": "release", + "optional": true + }, + { + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", + "input": { + "properties": {}, + "type": "object" + }, + "name": "stop", + "optional": true + }, + { + "description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", + "input": { + "properties": { + "id": { + "description": "the delivery's id", + "type": "string" + }, + "why": { + "description": "the condition it answers", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "name": "close" + }, + { + "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", + "input": { + "properties": {}, + "type": "object" + }, + "name": "retire-history", + "optional": true + }, + { + "description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", + "input": { + "additionalProperties": false, + "properties": { + "ids": { + "description": "the held deliveries' ids (owner/repository@commit)", + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "why": { + "description": "why they may be settled, shown to the operator", + "type": "string" + } + }, + "required": [ + "ids", + "why" + ], + "type": "object" + }, + "name": "settle", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0567", + "grain": "seat-list", + "name": "mesh-placement", + "scope": "mesh", + "serves": [ + { + "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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": { + "properties": { + "history": { + "description": "\"true\": the changes in order, from the stream and the state's revisions", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module: the nodes it is placed on", + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + }, + "seats": { + "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "name": "placement", + "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.", + "input": { + "properties": { + "id": { + "description": "one proposal's id", + "type": "string" + } + }, + "type": "object" + }, + "name": "proposals", + "optional": true + }, + { + "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", + "input": { + "additionalProperties": false, + "properties": { + "changes": { + "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", + "items": { + "required": [ + "op", + "node" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "warrant": { + "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", + "required": [ + "node", + "address", + "ask", + "warrant" + ], + "type": "object" + }, + "why": { + "description": "why, shown to the operator with the proposal", + "type": "string" + } + }, + "required": [ + "why", + "changes" + ], + "type": "object" + }, + "name": "propose", + "optional": true + }, + { + "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + }, + "why": { + "description": "why, kept in the event", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "name": "withdraw", + "optional": true + }, + { + "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "name": "again", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0547", + "grain": "whole", + "name": "mesh-delivery-checker", + "scope": "mesh", + "serves": [ + { + "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", + "input": { + "properties": { + "head": { + "description": "the full commit to try, 40 or 64 hex digits", + "type": "string" + }, + "level": { + "description": "unit or integration: only this level; both when absent", + "type": "string" + }, + "repository": { + "description": "/", + "type": "string" + }, + "tests": { + "description": "a go test -run pattern: only those tests run, and nothing is kept", + "type": "string" + }, + "trunk": { + "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", + "type": "string" + } + }, + "required": [ + "repository", + "head" + ], + "type": "object" + }, + "name": "try", + "optional": true, + "output": { + "properties": { + "id": { + "description": "the try's id", + "type": "string" + }, + "state": { + "description": "its state", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "replaces": [ + "sh merge-check.sh", + "docker run postgres" + ] + }, + { + "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "name": "try-read", + "optional": true, + "output": { + "description": "the try: its state, its steps and its verdict when it has one", + "type": "object" + } + }, + { + "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", + "input": { + "properties": { + "id": { + "description": "the try's id, try-", + "type": "string" + }, + "why": { + "description": "why it is ended, in words", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "name": "try-end", + "optional": true, + "output": { + "properties": { + "ended": { + "description": "it is ended", + "type": "boolean" + }, + "id": { + "description": "the try", + "type": "string" + } + }, + "required": [ + "id", + "ended" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0564", + "emits": [ + "condition-raised", + "condition-changed", + "condition-cleared" + ], + "grain": "seat-list", + "name": "mesh-conditions", + "scope": "mesh", + "serves": [ + { + "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.", + "input": { + "properties": { + "days": { + "description": "with history: how many days back (default 7, at most 90)", + "type": "string" + }, + "history": { + "description": "\"true\": every raising, change, silence and clearing lately, oldest first", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "key": { + "description": "a condition's key: that one whole; with history, only its transitions", + "type": "string" + }, + "machine": { + "description": "only those about this machine", + "type": "string" + }, + "scope": { + "description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", + "type": "string" + }, + "severity": { + "description": "only urgent, or only warning", + "type": "string" + } + }, + "type": "object" + }, + "name": "conditions" + }, + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word (the condition's kind when absent)", + "type": "string" + }, + "for": { + "description": "how long: 30m, 4h, 2d; at most 7d", + "type": "string" + }, + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why, required and recorded", + "type": "string" + } + }, + "required": [ + "key", + "for", + "why" + ], + "type": "object" + }, + "name": "silence" + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "name": "resync" + } + ] + }, + { + "decision": "novox/hq ADR 0564", + "grain": "seat-list", + "name": "mesh-watchdogs", + "scope": "mesh", + "serves": [ + { + "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": { + "properties": { + "row": { + "description": "one row, e.g. S1", + "type": "string" + } + }, + "type": "object" + }, + "name": "signals" + }, + { + "description": "Every want, build or step the watchman says stopped moving past its bound, with since when and the words of its finding.", + "input": { + "properties": { + "machine": { + "description": "only those on this machine", + "type": "string" + } + }, + "type": "object" + }, + "name": "stuck" + }, + { + "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": { + "properties": { + "machine": { + "description": "one machine", + "type": "string" + } + }, + "type": "object" + }, + "name": "presence" + }, + { + "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.", + "input": { + "properties": { + "cause": { + "description": "with deliver or drop: the cause in a word (dead-letter when absent)", + "type": "string" + }, + "consumer": { + "description": "a consumer's name, or .: only what that one gave up on", + "type": "string" + }, + "deliver": { + "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", + "type": "string" + }, + "drop": { + "description": "a dead letter's id: drop it for good (needs why)", + "type": "string" + }, + "id": { + "description": "a dead letter's id, as the list gives it: that one whole, with its message", + "type": "string" + }, + "limit": { + "description": "how many to list, newest first (default 50); only when listing", + "type": "string" + }, + "why": { + "description": "with deliver or drop: why, required and recorded", + "type": "string" + } + }, + "type": "object" + }, + "name": "dead-letters" + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "name": "resync" + } + ] + }, + { + "decision": "novox/hq ADR 0564", + "emits": [ + "checked" + ], + "grain": "seat-list", + "name": "mesh-self-check", + "scope": "mesh", + "serves": [ + { + "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": { + "properties": { + "probe": { + "description": "one probe's id (D6): its verdict alone, with every finding", + "type": "string" + }, + "run": { + "description": "\"true\": run every probe now and answer the verdict", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "name": "doctor" + }, + { + "description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "probes" + } + ] + }, + { + "decision": "novox/hq ADR 0564", + "emits": [ + "healer-acted" + ], + "grain": "seat-list", + "name": "mesh-healers", + "scope": "mesh", + "serves": [ + { + "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, and the mesh-wide brake.", + "input": { + "properties": { + "days": { + "description": "how many days of acts back (default 7)", + "type": "string" + } + }, + "type": "object" + }, + "name": "healers" + }, + { + "description": "Take one healer's act for one open condition now, within its budget and the brake.", + "input": { + "properties": { + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why now, kept with the act", + "type": "string" + } + }, + "required": [ + "key", + "why" + ], + "type": "object" + }, + "name": "heal" + }, + { + "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": { + "properties": { + "condition": { + "description": "the key of the condition the drill is meant to raise, if any (optional)", + "type": "string" + }, + "what": { + "description": "what was done on purpose, in a line", + "type": "string" + }, + "why": { + "description": "what the drill tests", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + }, + "name": "drill" + } + ] + }, + { + "decision": "novox/hq ADR 0565", + "emits": [ + "policy-changed" + ], + "grain": "seat-list", + "name": "mesh-wants", + "scope": "mesh", + "serves": [ + { + "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, each node's own composition and the assigner's findings.", + "input": { + "properties": { + "compare": { + "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module", + "type": "string" + }, + "policy": { + "description": "with module: roll, together, record, or default to take a person's choice back", + "type": "string" + }, + "why": { + "description": "with policy: why, required and kept", + "type": "string" + } + }, + "type": "object" + }, + "name": "upgrade" + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "name": "bus" + } + ] + }, + { + "decision": "novox/hq ADR 0565", + "emits": [ + "act-recorded" + ], + "grain": "seat-list", + "name": "mesh-hand-acts", + "scope": "mesh", + "serves": [ + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word, or a condition's kind", + "type": "string" + }, + "condition": { + "description": "the key of the condition it addressed, if any (optional)", + "type": "string" + }, + "verb": { + "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", + "type": "string" + }, + "what": { + "description": "what was done, in a line", + "type": "string" + }, + "why": { + "description": "why it had to be done by hand", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + }, + "name": "hand-act" + }, + { + "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": { + "properties": { + "cause": { + "description": "only this cause", + "type": "string" + }, + "days": { + "description": "how many days back (default 14)", + "type": "string" + }, + "warrants": { + "description": "\"true\": every warrant recorded", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "who": { + "description": "only acts whose who contains this", + "type": "string" + } + }, + "type": "object" + }, + "name": "hand-acts" + }, + { + "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": { + "properties": { + "ask": { + "description": "its ask's id", + "type": "string" + }, + "asker": { + "description": "the module that asked", + "type": "string" + } + }, + "required": [ + "asker", + "ask" + ], + "type": "object" + }, + "name": "warranted" + } + ] + }, + { + "decision": "novox/hq ADR 0587", + "emits": [ + "enrolment-issued", + "node-enrolled", + "enrolment-refused", + "enrolment-revoked", + "enrolment-expired" + ], + "grain": "seat-list", + "name": "mesh-enrolment", + "scope": "mesh", + "serves": [ + { + "description": "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.", + "input": { + "properties": { + "expires": { + "description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", + "type": "string" + }, + "node": { + "description": "the new machine's name in the mesh", + "type": "string" + }, + "overlay_key": { + "description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", + "type": "string" + }, + "why": { + "description": "why it joins, shown to the operator with the ask", + "type": "string" + } + }, + "required": [ + "node", + "why", + "overlay_key" + ], + "type": "object" + }, + "name": "issue" + }, + { + "description": "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", + "input": { + "additionalProperties": false, + "properties": { + "node": { + "description": "the node the token was issued for", + "type": "string" + }, + "overlay_key": { + "description": "the private network key's public half", + "type": "string" + }, + "profile": { + "description": "what the machine is, as its record says it", + "type": "object" + }, + "proof": { + "description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64", + "type": "string" + }, + "public_key": { + "description": "the identity key's public half, base64", + "type": "string" + }, + "sealing_key": { + "description": "the sealing key's public half, which the account is sealed to", + "type": "string" + }, + "serving_key": { + "description": "the serving key's public half, which the certificate binds", + "type": "string" + }, + "tunnel": { + "description": "a tunnel found on the machine: everything but its private key", + "type": "object" + } + }, + "required": [ + "node", + "public_key", + "sealing_key", + "proof" + ], + "type": "object" + }, + "name": "redeem" + }, + { + "description": "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "confirm" + }, + { + "description": "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.", + "input": { + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "why": { + "description": "why, recorded with the hand act", + "type": "string" + } + }, + "required": [ + "node", + "why" + ], + "type": "object" + }, + "name": "revoke" + }, + { + "description": "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.", + "input": { + "properties": { + "history": { + "description": "\"true\": the ended enrolments too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + } + }, + "type": "object" + }, + "name": "tokens" + }, + { + "description": "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.", + "input": { + "properties": { + "node": { + "description": "the node whose ask is raised again", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "again" + } + ] + }, + { + "decision": "novox/hq ADR 0571", + "emits": [ + "artifacts-gone" + ], + "grain": "seat-list", + "name": "mesh-artifact-sweep", + "scope": "mesh", + "serves": [ + { + "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": { + "properties": { + "collected": { + "description": "\"true\": list the collected references too (counted always)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "repository": { + "description": "only this repository's references, as / (the counts stay whole)", + "type": "string" + } + }, + "type": "object" + }, + "name": "artifacts" + }, + { + "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": { + "properties": { + "references": { + "description": "the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "references" + ], + "type": "object" + }, + "name": "held" + }, + { + "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": { + "properties": { + "confirm": { + "description": "\"true\": let go of what is eligible (needs why); without it nothing is done", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "most": { + "description": "let go of at most this many (500 by default, at most 5000)", + "type": "string" + }, + "why": { + "description": "with confirm: why, required and recorded", + "type": "string" + } + }, + "type": "object" + }, + "name": "collect" + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "name": "mirrors" + }, + { + "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": { + "properties": { + "node": { + "description": "the machine's name", + "type": "string" + } + }, + "required": [ + "node" + ], + "type": "object" + }, + "name": "images" + } + ] + }, + { + "decision": "novox/hq ADR 0109; novox/hq ADR 0605", + "delivers": "npm-package-registry", + "emits": [ + "published", + "refused", + "removed" + ], + "grain": "whole", + "name": "npm-package-registry", + "scope": "mesh", + "serves": [ + { + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "input": { + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "parts": { + "description": "every part's digest and size; none when the digest is already held", + "items": { + "description": "one part of a transfer", + "properties": { + "digest": { + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", + "type": "string" + }, + "size": { + "description": "its size in bytes", + "type": "integer" + } + }, + "required": [ + "digest", + "size" + ], + "type": "object" + }, + "type": "array" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "transfer": { + "description": "the transfer's name, the caller's own; the same name opens the same transfer again", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ], + "type": "object" + }, + "name": "publish", + "output": { + "properties": { + "held": { + "description": "the digest is held already: nothing to send; the outcome follows as an event", + "type": "boolean" + }, + "lacks": { + "description": "the digests of the parts the holder lacks, to send by publish-part", + "items": { + "type": "string" + }, + "type": "array" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer" + ], + "type": "object" + } + }, + { + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "input": { + "properties": { + "data": { + "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "type": "string" + }, + "last": { + "description": "this chunk is the part's last", + "type": "boolean" + }, + "part": { + "description": "the part's digest", + "type": "string" + }, + "sequence": { + "description": "the chunk's place in its part, from 0", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ], + "type": "object" + }, + "name": "publish-part", + "output": { + "properties": { + "complete": { + "description": "the part is whole and its bytes match its digest", + "type": "boolean" + }, + "part": { + "description": "the part", + "type": "string" + }, + "received": { + "description": "the chunks of the part held so far", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part" + ], + "type": "object" + } + }, + { + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "input": { + "properties": { + "kind": { + "description": "narrowed to one kind (optional)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "versions", + "output": { + "properties": { + "name": { + "description": "the name", + "type": "string" + }, + "versions": { + "description": "newest first", + "items": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "name", + "versions" + ], + "type": "object" + } + }, + { + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "input": { + "properties": { + "digest": { + "description": "the digest (for a bundle, archive or copied base)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it goes", + "enum": [ + "tag-gone", + "retention" + ], + "type": "string" + }, + "tag": { + "description": "the tag (for a tagged version)", + "type": "string" + }, + "version": { + "description": "the version", + "type": "string" + } + }, + "required": [ + "name", + "reason" + ], + "type": "object" + }, + "name": "remove", + "output": { + "properties": { + "artifact": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "removed": { + "description": "it is gone", + "type": "boolean" + } + }, + "required": [ + "removed" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0605", + "delivers": "go-package-registry", + "emits": [ + "published", + "refused", + "removed" + ], + "grain": "whole", + "name": "go-package-registry", + "scope": "mesh", + "serves": [ + { + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "input": { + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "parts": { + "description": "every part's digest and size; none when the digest is already held", + "items": { + "description": "one part of a transfer", + "properties": { + "digest": { + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", + "type": "string" + }, + "size": { + "description": "its size in bytes", + "type": "integer" + } + }, + "required": [ + "digest", + "size" + ], + "type": "object" + }, + "type": "array" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "transfer": { + "description": "the transfer's name, the caller's own; the same name opens the same transfer again", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ], + "type": "object" + }, + "name": "publish", + "output": { + "properties": { + "held": { + "description": "the digest is held already: nothing to send; the outcome follows as an event", + "type": "boolean" + }, + "lacks": { + "description": "the digests of the parts the holder lacks, to send by publish-part", + "items": { + "type": "string" + }, + "type": "array" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer" + ], + "type": "object" + } + }, + { + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "input": { + "properties": { + "data": { + "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "type": "string" + }, + "last": { + "description": "this chunk is the part's last", + "type": "boolean" + }, + "part": { + "description": "the part's digest", + "type": "string" + }, + "sequence": { + "description": "the chunk's place in its part, from 0", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ], + "type": "object" + }, + "name": "publish-part", + "output": { + "properties": { + "complete": { + "description": "the part is whole and its bytes match its digest", + "type": "boolean" + }, + "part": { + "description": "the part", + "type": "string" + }, + "received": { + "description": "the chunks of the part held so far", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part" + ], + "type": "object" + } + }, + { + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "input": { + "properties": { + "kind": { + "description": "narrowed to one kind (optional)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "versions", + "output": { + "properties": { + "name": { + "description": "the name", + "type": "string" + }, + "versions": { + "description": "newest first", + "items": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "name", + "versions" + ], + "type": "object" + } + }, + { + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "input": { + "properties": { + "digest": { + "description": "the digest (for a bundle, archive or copied base)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it goes", + "enum": [ + "tag-gone", + "retention" + ], + "type": "string" + }, + "tag": { + "description": "the tag (for a tagged version)", + "type": "string" + }, + "version": { + "description": "the version", + "type": "string" + } + }, + "required": [ + "name", + "reason" + ], + "type": "object" + }, + "name": "remove", + "output": { + "properties": { + "artifact": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "removed": { + "description": "it is gone", + "type": "boolean" + } + }, + "required": [ + "removed" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0605", + "delivers": "artifact-store", + "emits": [ + "published", + "refused", + "removed", + "held", + "released" + ], + "grain": "whole", + "name": "container-image-registry", + "scope": "mesh", + "serves": [ + { + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "input": { + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "parts": { + "description": "every part's digest and size; none when the digest is already held", + "items": { + "description": "one part of a transfer", + "properties": { + "digest": { + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", + "type": "string" + }, + "size": { + "description": "its size in bytes", + "type": "integer" + } + }, + "required": [ + "digest", + "size" + ], + "type": "object" + }, + "type": "array" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "transfer": { + "description": "the transfer's name, the caller's own; the same name opens the same transfer again", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ], + "type": "object" + }, + "name": "publish", + "output": { + "properties": { + "held": { + "description": "the digest is held already: nothing to send; the outcome follows as an event", + "type": "boolean" + }, + "lacks": { + "description": "the digests of the parts the holder lacks, to send by publish-part", + "items": { + "type": "string" + }, + "type": "array" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer" + ], + "type": "object" + } + }, + { + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "input": { + "properties": { + "data": { + "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "type": "string" + }, + "last": { + "description": "this chunk is the part's last", + "type": "boolean" + }, + "part": { + "description": "the part's digest", + "type": "string" + }, + "sequence": { + "description": "the chunk's place in its part, from 0", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ], + "type": "object" + }, + "name": "publish-part", + "output": { + "properties": { + "complete": { + "description": "the part is whole and its bytes match its digest", + "type": "boolean" + }, + "part": { + "description": "the part", + "type": "string" + }, + "received": { + "description": "the chunks of the part held so far", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part" + ], + "type": "object" + } + }, + { + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "input": { + "properties": { + "kind": { + "description": "narrowed to one kind (optional)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "versions", + "output": { + "properties": { + "name": { + "description": "the name", + "type": "string" + }, + "versions": { + "description": "newest first", + "items": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "name", + "versions" + ], + "type": "object" + } + }, + { + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "input": { + "properties": { + "digest": { + "description": "the digest (for a bundle, archive or copied base)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it goes", + "enum": [ + "tag-gone", + "retention" + ], + "type": "string" + }, + "tag": { + "description": "the tag (for a tagged version)", + "type": "string" + }, + "version": { + "description": "the version", + "type": "string" + } + }, + "required": [ + "name", + "reason" + ], + "type": "object" + }, + "name": "remove", + "output": { + "properties": { + "artifact": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "removed": { + "description": "it is gone", + "type": "boolean" + } + }, + "required": [ + "removed" + ], + "type": "object" + } + }, + { + "description": "Copy an outside image into the store by its pinned digest; emits published or refused.", + "input": { + "properties": { + "digest": { + "description": "its pinned digest, sha256:", + "type": "string" + }, + "name": { + "description": "the name it is kept under", + "type": "string" + }, + "source": { + "description": "the outside image's reference", + "type": "string" + } + }, + "required": [ + "source", + "digest", + "name" + ], + "type": "object" + }, + "name": "mirror", + "output": { + "properties": { + "digest": { + "description": "the digest stored", + "type": "string" + }, + "reference": { + "description": "the reference the seat answers", + "type": "string" + } + }, + "required": [ + "reference", + "digest" + ], + "type": "object" + } + }, + { + "description": "The tags of one image and the digest each names.", + "input": { + "properties": { + "name": { + "description": "the image", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "tags", + "output": { + "properties": { + "name": { + "description": "the image", + "type": "string" + }, + "tags": { + "description": "every tag", + "items": { + "properties": { + "digest": { + "description": "its digest", + "type": "string" + }, + "tag": { + "description": "the tag", + "type": "string" + } + }, + "required": [ + "tag", + "digest" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "name", + "tags" + ], + "type": "object" + } + }, + { + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "input": { + "properties": { + "digest": { + "description": "the digest", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "which of the sweep's rules keeps it", + "type": "string" + } + }, + "required": [ + "name", + "digest", + "reason" + ], + "type": "object" + }, + "name": "hold", + "output": { + "properties": { + "held": { + "description": "it is held", + "type": "boolean" + }, + "since": { + "description": "when the hold began, RFC 3339", + "type": "string" + } + }, + "required": [ + "held" + ], + "type": "object" + } + }, + { + "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "input": { + "properties": { + "digest": { + "description": "the digest", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it is let go", + "type": "string" + } + }, + "required": [ + "name", + "digest", + "reason" + ], + "type": "object" + }, + "name": "release", + "output": { + "properties": { + "released": { + "description": "the hold is gone", + "type": "boolean" + } + }, + "required": [ + "released" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0605", + "delivers": "artifact-store", + "emits": [ + "published", + "refused", + "removed", + "held", + "released" + ], + "grain": "whole", + "name": "oci-artifact-registry", + "scope": "mesh", + "serves": [ + { + "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + "input": { + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "parts": { + "description": "every part's digest and size; none when the digest is already held", + "items": { + "description": "one part of a transfer", + "properties": { + "digest": { + "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", + "type": "string" + }, + "size": { + "description": "its size in bytes", + "type": "integer" + } + }, + "required": [ + "digest", + "size" + ], + "type": "object" + }, + "type": "array" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "transfer": { + "description": "the transfer's name, the caller's own; the same name opens the same transfer again", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "transfer", + "name", + "kind", + "digest" + ], + "type": "object" + }, + "name": "publish", + "output": { + "properties": { + "held": { + "description": "the digest is held already: nothing to send; the outcome follows as an event", + "type": "boolean" + }, + "lacks": { + "description": "the digests of the parts the holder lacks, to send by publish-part", + "items": { + "type": "string" + }, + "type": "array" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer" + ], + "type": "object" + } + }, + { + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "input": { + "properties": { + "data": { + "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "type": "string" + }, + "last": { + "description": "this chunk is the part's last", + "type": "boolean" + }, + "part": { + "description": "the part's digest", + "type": "string" + }, + "sequence": { + "description": "the chunk's place in its part, from 0", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part", + "sequence", + "last", + "data" + ], + "type": "object" + }, + "name": "publish-part", + "output": { + "properties": { + "complete": { + "description": "the part is whole and its bytes match its digest", + "type": "boolean" + }, + "part": { + "description": "the part", + "type": "string" + }, + "received": { + "description": "the chunks of the part held so far", + "type": "integer" + }, + "transfer": { + "description": "the transfer", + "type": "string" + } + }, + "required": [ + "transfer", + "part" + ], + "type": "object" + } + }, + { + "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + "input": { + "properties": { + "kind": { + "description": "narrowed to one kind (optional)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "versions", + "output": { + "properties": { + "name": { + "description": "the name", + "type": "string" + }, + "versions": { + "description": "newest first", + "items": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "name", + "versions" + ], + "type": "object" + } + }, + { + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "input": { + "properties": { + "digest": { + "description": "the digest (for a bundle, archive or copied base)", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it goes", + "enum": [ + "tag-gone", + "retention" + ], + "type": "string" + }, + "tag": { + "description": "the tag (for a tagged version)", + "type": "string" + }, + "version": { + "description": "the version", + "type": "string" + } + }, + "required": [ + "name", + "reason" + ], + "type": "object" + }, + "name": "remove", + "output": { + "properties": { + "artifact": { + "description": "one published thing, as every registry event and answer names it", + "properties": { + "commit": { + "description": "the source commit it was built from", + "type": "string" + }, + "digest": { + "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", + "type": "string" + }, + "fingerprint": { + "description": "the build's fingerprint", + "type": "string" + }, + "kind": { + "description": "what it is", + "enum": [ + "package", + "image", + "bundle", + "archive", + "upstream" + ], + "type": "string" + }, + "name": { + "description": "the package's, image's, bundle's or archive's name", + "type": "string" + }, + "tag": { + "description": "the tag it carries, for a tagged version", + "type": "string" + }, + "version": { + "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", + "type": "string" + } + }, + "required": [ + "name", + "kind", + "digest" + ], + "type": "object" + }, + "removed": { + "description": "it is gone", + "type": "boolean" + } + }, + "required": [ + "removed" + ], + "type": "object" + } + }, + { + "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + "input": { + "properties": { + "digest": { + "description": "the digest", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "which of the sweep's rules keeps it", + "type": "string" + } + }, + "required": [ + "name", + "digest", + "reason" + ], + "type": "object" + }, + "name": "hold", + "output": { + "properties": { + "held": { + "description": "it is held", + "type": "boolean" + }, + "since": { + "description": "when the hold began, RFC 3339", + "type": "string" + } + }, + "required": [ + "held" + ], + "type": "object" + } + }, + { + "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "input": { + "properties": { + "digest": { + "description": "the digest", + "type": "string" + }, + "name": { + "description": "the name", + "type": "string" + }, + "reason": { + "description": "why it is let go", + "type": "string" + } + }, + "required": [ + "name", + "digest", + "reason" + ], + "type": "object" + }, + "name": "release", + "output": { + "properties": { + "released": { + "description": "the hold is gone", + "type": "boolean" + } + }, + "required": [ + "released" + ], + "type": "object" + } + } + ] + }, + { + "decision": "novox/hq ADR 0111", + "delivers": "git", + "grain": "seat-list", + "name": "git", + "scope": "mesh" + }, + { + "accepts": [ + "delivery", + "check", + "recheck" + ], + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "grain": "seat-list", + "name": "node-build-go", + "pulledByKind": true, + "scope": "node", + "serves": [ + { + "description": "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "current" + }, + { + "description": "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "pause" + }, + { + "description": "Take Go asks again on this node.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "resume" + } + ], + "yields": true + }, + { + "accepts": [ + "delivery", + "check", + "recheck" + ], + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "grain": "seat-list", + "name": "node-build-node", + "pulledByKind": true, + "scope": "node", + "serves": [ + { + "description": "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "current" + }, + { + "description": "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "pause" + }, + { + "description": "Take Node asks again on this node.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "resume" + } + ], + "yields": true + }, + { + "accepts": [ + "delivery", + "check", + "recheck" + ], + "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", + "grain": "seat-list", + "name": "node-build-image", + "pulledByKind": true, + "scope": "node", + "serves": [ + { + "description": "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "current" + }, + { + "description": "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "pause" + }, + { + "description": "Take image asks again on this node.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "resume" + } + ], + "yields": true + }, + { + "decision": "novox/hq ADR 0194, ADR 0223", + "delivers": "wildcard-resolution", + "grain": "seat-list", + "name": "mesh-dns-resolver", + "replicated": true, + "scope": "mesh" + }, + { + "decision": "novox/hq ADR 0199, ADR 0223", + "grain": "seat-list", + "name": "node-hostname", + "scope": "node", + "serves": [ + { + "description": "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "entries", + "replaces": [ + "cat /etc/hosts", + "getent hosts" + ] + }, + { + "description": "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", + "input": { + "properties": { + "address": { + "description": "the IPv4 or IPv6 address", + "type": "string" + }, + "names": { + "description": "the names for it, separated by spaces", + "type": "string" + } + }, + "required": [ + "address", + "names" + ], + "type": "object" + }, + "name": "add", + "replaces": [ + "edit /etc/hosts", + "HOSTALIASES" + ] + }, + { + "description": "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", + "input": { + "properties": { + "name": { + "description": "a host name, or an address to remove every line of", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "remove", + "replaces": [ + "sed -i /etc/hosts" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0121", + "grain": "seat-list", + "name": "node-intrusion-prevention", + "scope": "node", + "serves": [ + { + "description": "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", + "input": { + "properties": { + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "status", + "replaces": [ + "fail2ban-client status" + ] + }, + { + "description": "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", + "input": { + "properties": { + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "banned", + "replaces": [ + "fail2ban-client banned" + ] + }, + { + "description": "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", + "input": { + "properties": { + "ip": { + "description": "the address", + "type": "string" + }, + "jail": { + "description": "the jail to hold it", + "type": "string" + } + }, + "required": [ + "ip", + "jail" + ], + "type": "object" + }, + "name": "ban", + "replaces": [ + "fail2ban-client set banip" + ] + }, + { + "description": "Let one address go, from one jail or from every jail when none is named.", + "input": { + "properties": { + "ip": { + "description": "the address", + "type": "string" + }, + "jail": { + "description": "one jail (optional)", + "type": "string" + } + }, + "required": [ + "ip" + ], + "type": "object" + }, + "name": "unban", + "replaces": [ + "fail2ban-client unban", + "fail2ban-client set unbanip" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0121", + "grain": "seat-list", + "name": "node-packet-filter", + "scope": "node", + "serves": [ + { + "description": "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", + "input": { + "properties": { + "chain": { + "description": "one chain of that table (optional)", + "type": "string" + }, + "table": { + "description": "one nftables table, as `family name` (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "rules", + "replaces": [ + "nft list ruleset", + "iptables -L", + "iptables-save" + ] + }, + { + "description": "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "reload", + "replaces": [ + "nft -f" + ] + }, + { + "description": "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", + "input": { + "properties": { + "where": { + "description": "the rule set, as `node show` lists it", + "type": "string" + } + }, + "required": [ + "where" + ], + "type": "object" + }, + "name": "remove", + "replaces": [ + "nft delete", + "iptables -X" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0177", + "grain": "seat-list", + "name": "node-service-manager", + "scope": "node", + "serves": [ + { + "description": "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", + "input": { + "properties": { + "pattern": { + "description": "a glob the unit's name must match (optional)", + "type": "string" + }, + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + } + }, + "type": "object" + }, + "name": "units", + "replaces": [ + "systemctl list-units" + ] + }, + { + "description": "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "status", + "replaces": [ + "systemctl status", + "systemctl is-active" + ] + }, + { + "description": "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "start", + "replaces": [ + "systemctl start" + ] + }, + { + "description": "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "stop", + "replaces": [ + "systemctl stop" + ] + }, + { + "description": "Restart one unit.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "restart", + "replaces": [ + "systemctl restart" + ] + }, + { + "description": "Make one unit start at boot (or at the account's login, in user scope).", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "enable", + "replaces": [ + "systemctl enable" + ] + }, + { + "description": "Stop one unit starting at boot (or at login, in user scope).", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "disable", + "replaces": [ + "systemctl disable" + ] + }, + { + "description": "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: ].", + "input": { + "properties": { + "lines": { + "description": "how many lines from the end of what matches (default 100, at most 2000)", + "type": "string" + }, + "match": { + "description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", + "type": "string" + }, + "priority": { + "description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", + "type": "string" + }, + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "since": { + "description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + }, + "until": { + "description": "the window's end, in the same forms (optional; now when absent)", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "journal", + "replaces": [ + "journalctl" + ] + }, + { + "description": "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.", + "input": { + "properties": { + "scope": { + "description": "\"system\" or \"user\": only that manager (both when absent)", + "type": "string" + } + }, + "type": "object" + }, + "name": "failed", + "optional": true, + "replaces": [ + "systemctl --failed" + ] + }, + { + "description": "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "reset-failed", + "replaces": [ + "systemctl reset-failed" + ] + }, + { + "description": "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + } + }, + "required": [ + "unit" + ], + "type": "object" + }, + "name": "wanted-by", + "replaces": [ + "systemctl list-dependencies --reverse", + "systemctl show -p WantedBy", + "ls ~/.config/systemd/user/*.wants" + ] + }, + { + "description": "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.", + "input": { + "properties": { + "scope": { + "description": "\"system\" (the default) or \"user\": the operator account's own manager", + "type": "string" + }, + "unit": { + "description": "the unit's name, as the service manager knows it", + "type": "string" + }, + "why": { + "description": "the person's reason for removing the links, kept in the record", + "type": "string" + } + }, + "required": [ + "unit", + "why" + ], + "type": "object" + }, + "name": "unlink-dangling", + "replaces": [ + "rm ~/.config/systemd/user/*.wants/", + "unlink /etc/systemd/system/*.wants/" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0207", + "grain": "seat-list", + "name": "node-package-manager", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0166, ADR 0207", + "grain": "seat-list", + "name": "node-container-runtime", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0203", + "grain": "seat-list", + "name": "node-environment", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0349", + "grain": "seat-list", + "name": "node-provisioner", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0204", + "grain": "seat-list", + "name": "node-login-shell", + "scope": "node", + "serves": [ + { + "description": "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.", + "input": { + "properties": { + "command": { + "description": "the command line, as you would type it", + "type": "string" + }, + "timeout_seconds": { + "description": "give up after this long, at most 25 (default 20)", + "type": "string" + } + }, + "required": [ + "command" + ], + "type": "object" + }, + "name": "execute", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0211", + "grain": "seat-list", + "name": "node-power", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0212", + "grain": "seat-list", + "name": "node-hotkeys", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0215", + "grain": "seat-list", + "name": "node-message-bus", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0214", + "grain": "seat-list", + "name": "node-backup", + "scope": "node", + "serves": [ + { + "description": "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.", + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "backed-up", + "replaces": [ + "restic snapshots" + ] + }, + { + "description": "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.", + "input": { + "properties": { + "module": { + "description": "one module (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "now", + "replaces": [ + "restic backup" + ] + }, + { + "description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as .restored-. Swapping it in is a person's act. Lists the restore points when none is named.", + "input": { + "properties": { + "module": { + "description": "the module", + "type": "string" + }, + "path": { + "description": "one of the module's directories (all of them when omitted)", + "type": "string" + }, + "snapshot": { + "description": "the restore point (from `backed-up`; the newest when omitted)", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "name": "restore", + "replaces": [ + "restic restore" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0110", + "grain": "seat-list", + "name": "the-private-network", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0117", + "grain": "seat-list", + "name": "node-uplink", + "scope": "node", + "serves": [ + { + "description": "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "resolvers", + "optional": true + }, + { + "description": "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "links", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0247", + "grain": "seat-list", + "name": "node-resolver", + "scope": "node", + "serves": [ + { + "description": "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "routes", + "replaces": [ + "resolvectl status", + "resolvectl dns", + "resolvectl domain" + ] + }, + { + "description": "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", + "input": { + "properties": { + "domains": { + "description": "the domains to route there, separated by spaces or commas", + "type": "string" + }, + "link": { + "description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", + "type": "string" + }, + "servers": { + "description": "the servers' addresses, separated by spaces or commas", + "type": "string" + } + }, + "required": [ + "link", + "domains", + "servers" + ], + "type": "object" + }, + "name": "route", + "replaces": [ + "resolvectl dns", + "resolvectl domain", + "resolvectl default-route" + ] + }, + { + "description": "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", + "input": { + "properties": { + "link": { + "description": "the network link, by name", + "type": "string" + } + }, + "required": [ + "link" + ], + "type": "object" + }, + "name": "unroute", + "replaces": [ + "resolvectl revert" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0263", + "grain": "seat-list", + "name": "node-nfs-server", + "scope": "node", + "serves": [ + { + "description": "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "exports", + "replaces": [ + "exportfs -v", + "cat /etc/exports", + "zfs get sharenfs" + ] + }, + { + "description": "Which machines have mounted which share now, as the NFS server knows its clients.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "clients", + "replaces": [ + "ss -tn sport = :2049", + "cat /proc/fs/nfsd/clients/*/info" + ] + }, + { + "description": "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.", + "input": { + "properties": { + "address": { + "description": "a node's private address, to ask whether the share is exported to it (optional)", + "type": "string" + }, + "share": { + "description": "the share, by its name", + "type": "string" + } + }, + "required": [ + "share" + ], + "type": "object" + }, + "name": "test", + "replaces": [ + "showmount -e" + ] + }, + { + "description": "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "reload", + "replaces": [ + "exportfs -ra" + ] + }, + { + "description": "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.", + "input": { + "properties": { + "confirm": { + "description": "\"true\": change it (needs why); anything else is a dry run", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "the shared path whose hand-made export is taken over", + "type": "string" + }, + "why": { + "description": "why, for the record", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "adopt", + "replaces": [ + "zfs set sharenfs=off" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0263", + "grain": "seat-list", + "name": "node-mounts", + "scope": "node", + "serves": [ + { + "description": "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "list", + "replaces": [ + "cat /etc/fstab", + "findmnt", + "systemctl list-units --type=mount" + ] + }, + { + "description": "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.", + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "test", + "replaces": [ + "showmount -e", + "ping" + ] + }, + { + "description": "Mount one share or occasional source now, rather than at its first access.", + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "mount", + "replaces": [ + "mount" + ] + }, + { + "description": "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.", + "input": { + "properties": { + "name": { + "description": "the share or source, by its name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "unmount", + "replaces": [ + "umount" + ] + }, + { + "description": "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", + "input": { + "properties": { + "confirm": { + "description": "\"true\": change it (needs why); anything else is a dry run", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "mountpoint": { + "description": "the mount point whose fstab line is taken over", + "type": "string" + }, + "why": { + "description": "why, for the record", + "type": "string" + } + }, + "required": [ + "mountpoint" + ], + "type": "object" + }, + "name": "adopt", + "replaces": [ + "sed -i /etc/fstab" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0310", + "grain": "seat-list", + "name": "node-reads", + "scope": "node", + "serves": [ + { + "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.", + "input": { + "properties": { + "follow": { + "description": "\"true\": also say where the path leads once every link is followed", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "paths": { + "description": "one absolute path, or several separated by line breaks", + "type": "string" + } + }, + "required": [ + "paths" + ], + "type": "object" + }, + "name": "stat", + "replaces": [ + "stat", + "ls -ld", + "namei -l", + "findmnt -T" + ] + }, + { + "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.", + "input": { + "properties": { + "path": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "readlink", + "replaces": [ + "readlink", + "readlink -f", + "realpath" + ] + }, + { + "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.", + "input": { + "properties": { + "path": { + "description": "an absolute path to a directory", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "list", + "replaces": [ + "ls", + "ls -la" + ] + }, + { + "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.", + "input": { + "properties": { + "bytes": { + "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", + "type": "string" + }, + "from": { + "description": "the first line to answer, counted from 1 (optional)", + "type": "string" + }, + "lines": { + "description": "how many lines to answer (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "read", + "replaces": [ + "cat", + "head", + "tail", + "less" + ] + }, + { + "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.", + "input": { + "properties": { + "case": { + "description": "\"false\": match whatever the case (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "depth": { + "description": "how many directories deep, up to 8 (optional)", + "type": "string" + }, + "matches": { + "description": "the most matches to answer, up to 500 (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file or a directory", + "type": "string" + }, + "text": { + "description": "the fixed string to find", + "type": "string" + } + }, + "required": [ + "text", + "path" + ], + "type": "object" + }, + "name": "search", + "replaces": [ + "grep", + "grep -r", + "grep -F" + ] + }, + { + "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.", + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "type": "string" + }, + "name": { + "description": "an account's name or number (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "account", + "replaces": [ + "getent passwd", + "getent group", + "id", + "groups" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0394", + "grain": "seat-list", + "name": "node-files", + "scope": "node", + "serves": [ + { + "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "follow": { + "description": "\"true\": also say where the path leads once every link is followed", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "paths": { + "description": "one absolute path, or several separated by line breaks", + "type": "string" + } + }, + "required": [ + "paths" + ], + "type": "object" + }, + "name": "stat", + "replaces": [ + "stat", + "ls -ld", + "namei -l", + "findmnt -T" + ] + }, + { + "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "path": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "readlink", + "replaces": [ + "readlink", + "readlink -f", + "realpath" + ] + }, + { + "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "path": { + "description": "an absolute path to a directory", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "list", + "replaces": [ + "ls", + "ls -la" + ] + }, + { + "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "bytes": { + "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", + "type": "string" + }, + "from": { + "description": "the first line to answer, counted from 1 (optional)", + "type": "string" + }, + "lines": { + "description": "how many lines to answer (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "read", + "replaces": [ + "cat", + "head", + "tail", + "less" + ] + }, + { + "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "case": { + "description": "\"false\": match whatever the case (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "depth": { + "description": "how many directories deep, up to 8 (optional)", + "type": "string" + }, + "matches": { + "description": "the most matches to answer, up to 500 (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file or a directory", + "type": "string" + }, + "text": { + "description": "the fixed string to find", + "type": "string" + } + }, + "required": [ + "text", + "path" + ], + "type": "object" + }, + "name": "search", + "replaces": [ + "grep", + "grep -r", + "grep -F" + ] + }, + { + "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "type": "string" + }, + "name": { + "description": "an account's name or number (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "account", + "replaces": [ + "getent passwd", + "getent group", + "id", + "groups" + ] + }, + { + "description": "Write one text file whole, replacing what it holds; the old content goes to the node's trash. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "content": { + "description": "the text to write", + "type": "string" + }, + "mode": { + "description": "the file's mode, such as 0644 (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "path", + "content", + "reason" + ], + "type": "object" + }, + "name": "write", + "replaces": [ + "tee", + ">" + ] + }, + { + "description": "Copy a file or directory to a path on this node. A target on another node (:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "from": { + "description": "an absolute path", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "to": { + "description": "an absolute path on this node", + "type": "string" + } + }, + "required": [ + "from", + "to", + "reason" + ], + "type": "object" + }, + "name": "copy", + "replaces": [ + "cp", + "cp -a", + "ssh cp" + ] + }, + { + "description": "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "from": { + "description": "an absolute path", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "to": { + "description": "an absolute path", + "type": "string" + } + }, + "required": [ + "from", + "to", + "reason" + ], + "type": "object" + }, + "name": "move", + "replaces": [ + "mv" + ] + }, + { + "description": "Delete a file or directory into the node's trash, where restore can bring it back. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": delete a directory with what it holds", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "reason" + ], + "type": "object" + }, + "name": "delete", + "replaces": [ + "rm", + "rm -r" + ] + }, + { + "description": "Make a directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "parents": { + "description": "\"true\": make the missing parents too", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "path", + "reason" + ], + "type": "object" + }, + "name": "mkdir", + "replaces": [ + "mkdir", + "mkdir -p" + ] + }, + { + "description": "Change the mode of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "mode": { + "description": "the mode, such as 0644 or u+x", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": also everything under a directory", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "mode", + "reason" + ], + "type": "object" + }, + "name": "chmod", + "replaces": [ + "chmod" + ] + }, + { + "description": "Change the owner and group of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "group": { + "description": "a group's name or number (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "owner": { + "description": "an account's name or number", + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "recursive": { + "description": "\"true\": also everything under a directory", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "path", + "owner", + "reason" + ], + "type": "object" + }, + "name": "chown", + "replaces": [ + "chown", + "chgrp" + ] + }, + { + "description": "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", + "input": { + "properties": { + "bucket": { + "description": "the bucket the file goes into", + "type": "string" + }, + "key": { + "description": "the object's key (optional; the file's SHA-256 when not given)", + "type": "string" + }, + "path": { + "description": "an absolute path to a file", + "type": "string" + } + }, + "required": [ + "path", + "bucket" + ], + "type": "object" + }, + "name": "upload" + }, + { + "description": "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "bucket": { + "description": "the bucket", + "type": "string" + }, + "key": { + "description": "the object's key", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "type": "string" + }, + "path": { + "description": "an absolute path", + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + }, + "sha256": { + "description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", + "type": "string" + } + }, + "required": [ + "bucket", + "key", + "path", + "reason" + ], + "type": "object" + }, + "name": "download" + }, + { + "description": "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "trash", + "replaces": [ + "ls ~/.local/share/Trash" + ] + }, + { + "description": "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", + "input": { + "properties": { + "id": { + "description": "an id from trash", + "type": "string" + }, + "override": { + "description": "a reason, to change a path a module owns anyway (optional)", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "reason": { + "description": "why the file is changed, for the audit trail", + "type": "string" + } + }, + "required": [ + "id", + "reason" + ], + "type": "object" + }, + "name": "restore", + "replaces": [ + "trash-restore" + ] + } + ] + }, + { + "decision": "novox/hq ADR 0563", + "emits": [ + "asked", + "done", + "refused" + ], + "grain": "seat-list", + "heldByTheEngine": true, + "name": "node-engine", + "scope": "node", + "serves": [ + { + "description": "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", + "input": { + "properties": { + "path": { + "description": "the directory's absolute path, as the engine states it", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + }, + "name": "hand-over" + }, + { + "description": "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", + "input": { + "properties": { + "access": { + "description": "the access, library:", + "type": "string" + }, + "act": { + "description": "the act: take", + "type": "string" + }, + "why": { + "description": "why it is asked, in words the operator reads", + "type": "string" + } + }, + "required": [ + "act", + "access" + ], + "type": "object" + }, + "name": "access-act" + }, + { + "description": "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "judge-directories" + }, + { + "description": "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", + "input": { + "properties": { + "why": { + "description": "why it is asked, in words", + "type": "string" + } + }, + "type": "object" + }, + "name": "setuid-search" + }, + { + "description": "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "root-free" + }, + { + "description": "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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": { + "properties": { + "id": { + "description": "one act, by its id", + "type": "string" + }, + "limit": { + "description": "at most this many, newest", + "type": "string" + } + }, + "type": "object" + }, + "name": "acts" + }, + { + "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": { + "properties": { + "write": { + "description": "\"true\": write this machine's key again from what the engine composed", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "name": "packages", + "optional": true + }, + { + "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", + "input": { + "properties": { + "diff": { + "description": "\"true\": list the resources that differ from what runs", + "type": "string" + }, + "digest": { + "description": "a composition's digest (sha256:…), to read that revision rather than the latest", + "type": "string" + }, + "module": { + "description": "the module, as placed on this machine; node for the node's own", + "type": "string" + } + }, + "required": [ + "module" + ], + "type": "object" + }, + "name": "composition", + "optional": true + }, + { + "description": "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", + "input": { + "properties": { + "module": { + "description": "a module placed on this machine whose apply is also tried again (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "compose", + "optional": true + }, + { + "description": "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", + "input": { + "properties": { + "candidate": { + "description": "the key of the indexer's candidate, .", + "type": "string" + }, + "input": { + "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "required": [ + "candidate" + ], + "type": "object" + }, + "name": "compose-candidate", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-login-manager", + "scope": "node", + "serves": [ + { + "description": "The sessions the login manager offers on this machine, and which one the operator account starts by default.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "sessions" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-display-server", + "scope": "node", + "serves": [ + { + "description": "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "displays" + }, + { + "description": "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.", + "input": { + "properties": { + "action": { + "description": "list, save or apply", + "enum": [ + "list", + "save", + "apply" + ], + "type": "string" + }, + "name": { + "description": "the profile to save or apply (save and apply only)", + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "name": "layout" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-display-session", + "scope": "node", + "serves": [ + { + "description": "Reload the session's configuration in place, keeping its windows.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "reload" + }, + { + "description": "The session's workspaces: each one's name, output, and whether it is visible or focused.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "workspaces" + }, + { + "description": "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.", + "input": { + "properties": { + "workspace": { + "description": "one workspace (optional)", + "type": "string" + } + }, + "type": "object" + }, + "name": "windows" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-terminal-emulator", + "scope": "node", + "serves": [ + { + "description": "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.", + "input": { + "properties": { + "command": { + "description": "what to run in it (optional; the login shell when absent)", + "type": "string" + }, + "directory": { + "description": "where it starts (optional; the account's home when absent)", + "type": "string" + } + }, + "type": "object" + }, + "name": "open" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-launcher", + "scope": "node", + "serves": [ + { + "description": "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", + "input": { + "properties": { + "choices": { + "description": "the lines to choose between, in order", + "items": { + "type": "string" + }, + "type": "array" + }, + "prompt": { + "description": "what the menu asks (optional)", + "type": "string" + } + }, + "required": [ + "choices" + ], + "type": "object" + }, + "name": "menu" + }, + { + "description": "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.", + "input": { + "properties": { + "module": { + "description": "the module whose own secret is asked for", + "type": "string" + }, + "node": { + "description": "the machine the module runs on", + "type": "string" + }, + "seal_to": { + "description": "the asker's public sealing key: the answer is sealed to it", + "type": "string" + }, + "secret": { + "description": "the own secret's name", + "type": "string" + }, + "timeout_seconds": { + "description": "give up after this long (optional)", + "type": "string" + } + }, + "required": [ + "module", + "secret", + "node", + "seal_to" + ], + "type": "object" + }, + "name": "secret", + "optional": true + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-notifier", + "scope": "node", + "serves": [ + { + "description": "Show the operator a notification.", + "input": { + "properties": { + "body": { + "description": "its text (optional)", + "type": "string" + }, + "title": { + "description": "the notification's summary", + "type": "string" + }, + "urgency": { + "description": "low, normal (the default) or critical", + "enum": [ + "low", + "normal", + "critical" + ], + "type": "string" + } + }, + "required": [ + "title" + ], + "type": "object" + }, + "name": "send" + }, + { + "description": "The notifications shown lately, newest first.", + "input": { + "properties": { + "limit": { + "description": "how many (optional, default 20)", + "type": "string" + } + }, + "type": "object" + }, + "name": "history" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-lock-screen", + "scope": "node", + "serves": [ + { + "description": "Lock the operator's session now.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "lock" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-clipboard", + "scope": "node", + "serves": [ + { + "description": "What the clipboard held lately, newest first.", + "input": { + "properties": { + "limit": { + "description": "how many (optional, default 20)", + "type": "string" + } + }, + "type": "object" + }, + "name": "history" + }, + { + "description": "Put text on the operator's clipboard.", + "input": { + "properties": { + "text": { + "description": "the text", + "type": "string" + } + }, + "required": [ + "text" + ], + "type": "object" + }, + "name": "copy" + } + ] + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-bar", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-compositor", + "scope": "node" + }, + { + "decision": "novox/hq ADR 0208", + "grain": "seat-list", + "name": "node-secret-service", + "scope": "node" + } +]; diff --git a/ts/tsconfig.json b/ts/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} -- 2.54.0 From f0a2feb7787ac02583d5b8c13db91d8633a6231f Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:54:38 +0100 Subject: [PATCH 09/94] SD-M3: each whole seat's package lists its verbs and events Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/containerimageregistry/contract.go | 10 ++++++++++ go/seats/gopackageregistry/contract.go | 10 ++++++++++ go/seats/meshbroker/contract.go | 10 ++++++++++ go/seats/meshconfig/contract.go | 5 +++++ go/seats/meshdeliverychecker/contract.go | 5 +++++ go/seats/meshvault/contract.go | 5 +++++ go/seats/npmpackageregistry/contract.go | 10 ++++++++++ go/seats/ociartifactregistry/contract.go | 10 ++++++++++ go/seats/operatorchannel/contract.go | 10 ++++++++++ 9 files changed, 75 insertions(+) diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index d14c33e..c18b82c 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -29,6 +29,16 @@ const ( EventReleased = "released" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} +} + // Family is where the seat's events are kept. const ( Family = "container-image-registry.events" diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 39f58cf..5b3fb87 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -23,6 +23,16 @@ const ( EventRemoved = "removed" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved} +} + // Family is where the seat's events are kept. const ( Family = "go-package-registry.events" diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 46907d3..2a95c36 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -29,6 +29,16 @@ const ( EventDropped = "dropped" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbList, VerbShow, VerbCheck, VerbOrphans, VerbCreate, VerbApply, VerbRecreate, VerbDrop} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventCreated, EventRefused, EventDrifted, EventDriftCleared, EventFull, EventFullCleared, EventOrphaned, EventRecreated, EventDropped} +} + // Family is where the seat's events are kept. const ( Family = "mesh-broker.events" diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go index ff82d22..207bf63 100644 --- a/go/seats/meshconfig/contract.go +++ b/go/seats/meshconfig/contract.go @@ -11,6 +11,11 @@ const ( VerbRenames = "renames" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbConfig, VerbRenames} +} + // The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). const ( HolderEventConfigChanged = "config-changed" diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go index da03e1a..6f881a7 100644 --- a/go/seats/meshdeliverychecker/contract.go +++ b/go/seats/meshdeliverychecker/contract.go @@ -12,6 +12,11 @@ const ( VerbTryEnd = "try-end" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbTry, VerbTryRead, VerbTryEnd} +} + // The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). const ( HolderEventVerdict = "verdict" diff --git a/go/seats/meshvault/contract.go b/go/seats/meshvault/contract.go index b0231b3..bf04ecc 100644 --- a/go/seats/meshvault/contract.go +++ b/go/seats/meshvault/contract.go @@ -24,6 +24,11 @@ const ( VerbRetire = "retire" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbGet, VerbSecretAsk, VerbIssued, VerbRotate, VerbMoveIn, VerbNodeKey, VerbOwn, VerbIssueAccount, VerbSign, VerbEnrolmentToken, VerbNodeAccount, VerbRotateNode, VerbNodeAccountConfirm, VerbAbandon, VerbRetire} +} + // The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). const ( HolderEventProvisioned = "provisioned" diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 690946c..5489417 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -23,6 +23,16 @@ const ( EventRemoved = "removed" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved} +} + // Family is where the seat's events are kept. const ( Family = "npm-package-registry.events" diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index e602f28..9042e7e 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -27,6 +27,16 @@ const ( EventReleased = "released" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} +} + // Family is where the seat's events are kept. const ( Family = "oci-artifact-registry.events" diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go index 49fdc1e..86a4786 100644 --- a/go/seats/operatorchannel/contract.go +++ b/go/seats/operatorchannel/contract.go @@ -24,6 +24,16 @@ const ( EventDecided = "decided" ) +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbOpen, VerbHistory, VerbNotify, VerbTell, VerbAsks} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventDecided} +} + // Family is where the seat's events are kept. const ( Family = "operator-channel.decided" -- 2.54.0 From a6e3b3bb4f369ef7f0febfde4b1b222f7c2d4eaf Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 11:55:32 +0100 Subject: [PATCH 10/94] SD-M2a, SD-M2b: keep the record that made npm-package-registry and mesh-broker seats as their decision Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/seats.go | 4 ++-- seats/mesh-broker/seat.json | 2 +- seats/npm-package-registry/seat.json | 2 +- ts/src/seats.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go/seats/seats.go b/go/seats/seats.go index fb55e4e..d3d6151 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -142,7 +142,7 @@ func List() []Seat { {Name: "query", Description: "One read-only statement against one database the store holds.", Input: map[string]any{"properties": map[string]any{"database": map[string]any{"description": "the database to query", "type": "string"}, "sql": map[string]any{"description": "the read-only statement", "type": "string"}}, "required": []string{"database", "sql"}, "type": "object"}, Replaces: []string{"psql"}}, }}, - {Name: MeshBroker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0079; novox/hq ADR 0595", Delivers: "mesh-bus", Emits: []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"}, + {Name: MeshBroker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0079", Delivers: "mesh-bus", Emits: []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"}, Serves: []Verb{ {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, @@ -381,7 +381,7 @@ func List() []Seat { {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: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the machine's name", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, }}, - {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109; novox/hq ADR 0605", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, + {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index 59df817..7587e3e 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -3,7 +3,7 @@ "seat": "mesh-broker", "scope": "mesh", "grain": "whole", - "decision": "novox/hq ADR 0079; novox/hq ADR 0595", + "decision": "novox/hq ADR 0079", "delivers": "mesh-bus", "description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.", "verbs": [ diff --git a/seats/npm-package-registry/seat.json b/seats/npm-package-registry/seat.json index 3330edd..61f77c3 100644 --- a/seats/npm-package-registry/seat.json +++ b/seats/npm-package-registry/seat.json @@ -3,7 +3,7 @@ "seat": "npm-package-registry", "scope": "mesh", "grain": "whole", - "decision": "novox/hq ADR 0109; novox/hq ADR 0605", + "decision": "novox/hq ADR 0109", "description": "The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.", "delivers": "npm-package-registry", "includes": [ diff --git a/ts/src/seats.ts b/ts/src/seats.ts index c72a5b9..bf9c0dd 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -135,7 +135,7 @@ export const list: readonly Seat[] = [ ] }, { - "decision": "novox/hq ADR 0079; novox/hq ADR 0595", + "decision": "novox/hq ADR 0079", "delivers": "mesh-bus", "emits": [ "created", @@ -2864,7 +2864,7 @@ export const list: readonly Seat[] = [ ] }, { - "decision": "novox/hq ADR 0109; novox/hq ADR 0605", + "decision": "novox/hq ADR 0109", "delivers": "npm-package-registry", "emits": [ "published", -- 2.54.0 From 7f0b0f3b42c7e93713e46c4833d3dbdeab869bbc Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:01:18 +0100 Subject: [PATCH 11/94] SD-M2c: retire takes ask The ask's id beside the warrant (the hand act's id), as own-retired carries it (A4's request). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshvault/contract.go | 4 +++- go/seats/meshvault/contract_test.go | 2 +- go/seats/seats.go | 2 +- seats/mesh-vault/fixtures/verbs.json | 3 ++- seats/mesh-vault/seat.json | 9 +++++++-- ts/src/mesh-vault.ts | 4 +++- ts/src/seats.ts | 9 +++++++-- 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/go/seats/meshvault/contract.go b/go/seats/meshvault/contract.go index bf04ecc..e5beead 100644 --- a/go/seats/meshvault/contract.go +++ b/go/seats/meshvault/contract.go @@ -329,13 +329,15 @@ type AbandonAnswer = map[string]any // RetireInput is what retire takes. type RetireInput struct { + // the id of the operator's ask the warrant answers + Ask string `json:"ask"` // the module Module string `json:"module"` // the own secret's name Name string `json:"name"` // the node Node string `json:"node"` - // the operator's warrant for this node, module and name + // the operator's warrant for this node, module and name: the hand act's id Warrant string `json:"warrant"` } diff --git a/go/seats/meshvault/contract_test.go b/go/seats/meshvault/contract_test.go index 56dbe3b..63f54dc 100644 --- a/go/seats/meshvault/contract_test.go +++ b/go/seats/meshvault/contract_test.go @@ -39,7 +39,7 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-done$")}, {name: "own-retired", subject: "mesh.mod.mesh-vault.event.own-retired", given: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v OwnRetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-retired$")}, - {name: "retire on a warrant", subject: "", given: "{\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", + {name: "retire on a warrant", subject: "", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", decode: func(b []byte) (any, error) { var v RetireInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { diff --git a/go/seats/seats.go b/go/seats/seats.go index d3d6151..371a5fd 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -214,7 +214,7 @@ func List() []Seat { Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node whose rotation is abandoned", "type": "string"}}, "required": []string{"node"}, "type": "object"}, Output: map[string]any{"description": "the holder's answer", "type": "object"}}, {Name: "retire", Description: "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the own secret's name", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant for this node, module and name", "type": "string"}}, "required": []string{"node", "module", "name", "warrant"}, "type": "object"}, + Input: map[string]any{"properties": map[string]any{"ask": map[string]any{"description": "the id of the operator's ask the warrant answers", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the own secret's name", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant for this node, module and name: the hand act's id", "type": "string"}}, "required": []string{"node", "module", "name", "warrant", "ask"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"retired": map[string]any{"description": "the secret is gone", "type": "boolean"}}, "required": []string{"retired"}, "type": "object"}}, }}, {Name: MeshConfig, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0348, ADR 0565", diff --git a/seats/mesh-vault/fixtures/verbs.json b/seats/mesh-vault/fixtures/verbs.json index 4f2608e..a031109 100644 --- a/seats/mesh-vault/fixtures/verbs.json +++ b/seats/mesh-vault/fixtures/verbs.json @@ -6,7 +6,8 @@ "node": "ace", "module": "messenger", "name": "token", - "warrant": "w-1" + "warrant": "w-1", + "ask": "a1" } } ] diff --git a/seats/mesh-vault/seat.json b/seats/mesh-vault/seat.json index a6a753d..4cb5d2c 100644 --- a/seats/mesh-vault/seat.json +++ b/seats/mesh-vault/seat.json @@ -529,14 +529,19 @@ }, "warrant": { "type": "string", - "description": "the operator's warrant for this node, module and name" + "description": "the operator's warrant for this node, module and name: the hand act's id" + }, + "ask": { + "type": "string", + "description": "the id of the operator's ask the warrant answers" } }, "required": [ "node", "module", "name", - "warrant" + "warrant", + "ask" ] }, "answer": { diff --git a/ts/src/mesh-vault.ts b/ts/src/mesh-vault.ts index 89e9fee..7c51701 100644 --- a/ts/src/mesh-vault.ts +++ b/ts/src/mesh-vault.ts @@ -313,13 +313,15 @@ export type AbandonAnswer = Record; /** is what retire takes. */ export interface RetireInput { + /** the id of the operator's ask the warrant answers */ + "ask": string; /** the module */ "module": string; /** the own secret's name */ "name": string; /** the node */ "node": string; - /** the operator's warrant for this node, module and name */ + /** the operator's warrant for this node, module and name: the hand act's id */ "warrant": string; } diff --git a/ts/src/seats.ts b/ts/src/seats.ts index bf9c0dd..eec8f18 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -1426,6 +1426,10 @@ export const list: readonly Seat[] = [ "description": "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", "input": { "properties": { + "ask": { + "description": "the id of the operator's ask the warrant answers", + "type": "string" + }, "module": { "description": "the module", "type": "string" @@ -1439,7 +1443,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "warrant": { - "description": "the operator's warrant for this node, module and name", + "description": "the operator's warrant for this node, module and name: the hand act's id", "type": "string" } }, @@ -1447,7 +1451,8 @@ export const list: readonly Seat[] = [ "node", "module", "name", - "warrant" + "warrant", + "ask" ], "type": "object" }, -- 2.54.0 From 5b4239e6d955a1e170b78752c7dbde7140d03234 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:19:52 +0100 Subject: [PATCH 12/94] SD-M3: the node-engine seat's read-only verb pull-waits (design 51, A7's ENG-9) Optional until every engine serves it; answers each failed pull waiting on a store's holder. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/seats.go | 3 ++ seats/node-engine/seat.json | 72 +++++++++++++++++++++++++++++++++++-- ts/src/seats.ts | 66 ++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/go/seats/seats.go b/go/seats/seats.go index 371a5fd..5e4e996 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -684,6 +684,9 @@ func List() []Seat { Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}}, {Name: "compose-candidate", Description: "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", Optional: true, Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"candidate"}, "type": "object"}}, + {Name: "pull-waits", Description: "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"waits": map[string]any{"description": "every failed pull waiting on a holder", "items": map[string]any{"properties": map[string]any{"holder": map[string]any{"description": "that seat's holder, whose settled the retry waits on", "type": "string"}, "image": map[string]any{"description": "the image whose pull failed", "type": "string"}, "module": map[string]any{"description": "the module whose apply failed", "type": "string"}, "pull-began": map[string]any{"description": "when the failed pull began, RFC 3339", "type": "string"}, "retry-last": map[string]any{"description": "when it was last tried again, RFC 3339", "type": "string"}, "retry-trigger": map[string]any{"description": "what started that retry: settled, or the engine's start", "type": "string"}, "seat": map[string]any{"description": "the seat the image came through", "type": "string"}, "settled-count": map[string]any{"description": "the settled events of the holder heard since the engine started", "type": "integer"}, "settled-last": map[string]any{"description": "when the last was heard, RFC 3339", "type": "string"}}, "required": []string{"module", "image"}, "type": "object"}, "type": "array"}}, "required": []string{"waits"}, "type": "object"}}, }}, {Name: NodeLoginManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", Serves: []Verb{ diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 20ee202..21b288c 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -32,7 +32,7 @@ "input": { "properties": { "access": { - "description": "the access, library:\u003cname\u003e", + "description": "the access, library:", "type": "string" }, "act": { @@ -117,7 +117,7 @@ }, { "name": "composition", - "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed-\u003cnode\u003e (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", + "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", "optional": true, "input": { "properties": { @@ -161,7 +161,7 @@ "input": { "properties": { "candidate": { - "description": "the key of the indexer's candidate, \u003cgroup\u003e.\u003cheads hash\u003e", + "description": "the key of the indexer's candidate, .", "type": "string" }, "input": { @@ -178,6 +178,72 @@ ], "type": "object" } + }, + { + "name": "pull-waits", + "optional": true, + "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "required": [ + "waits" + ], + "properties": { + "waits": { + "type": "array", + "description": "every failed pull waiting on a holder", + "items": { + "type": "object", + "required": [ + "module", + "image" + ], + "properties": { + "module": { + "type": "string", + "description": "the module whose apply failed" + }, + "image": { + "type": "string", + "description": "the image whose pull failed" + }, + "seat": { + "type": "string", + "description": "the seat the image came through" + }, + "holder": { + "type": "string", + "description": "that seat's holder, whose settled the retry waits on" + }, + "pull-began": { + "type": "string", + "description": "when the failed pull began, RFC 3339" + }, + "settled-count": { + "type": "integer", + "description": "the settled events of the holder heard since the engine started" + }, + "settled-last": { + "type": "string", + "description": "when the last was heard, RFC 3339" + }, + "retry-last": { + "type": "string", + "description": "when it was last tried again, RFC 3339" + }, + "retry-trigger": { + "type": "string", + "description": "what started that retry: settled, or the engine's start" + } + } + } + } + } + } } ], "events": [ diff --git a/ts/src/seats.ts b/ts/src/seats.ts index eec8f18..3cbdb10 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -6581,6 +6581,72 @@ export const list: readonly Seat[] = [ }, "name": "compose-candidate", "optional": true + }, + { + "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "pull-waits", + "optional": true, + "output": { + "properties": { + "waits": { + "description": "every failed pull waiting on a holder", + "items": { + "properties": { + "holder": { + "description": "that seat's holder, whose settled the retry waits on", + "type": "string" + }, + "image": { + "description": "the image whose pull failed", + "type": "string" + }, + "module": { + "description": "the module whose apply failed", + "type": "string" + }, + "pull-began": { + "description": "when the failed pull began, RFC 3339", + "type": "string" + }, + "retry-last": { + "description": "when it was last tried again, RFC 3339", + "type": "string" + }, + "retry-trigger": { + "description": "what started that retry: settled, or the engine's start", + "type": "string" + }, + "seat": { + "description": "the seat the image came through", + "type": "string" + }, + "settled-count": { + "description": "the settled events of the holder heard since the engine started", + "type": "integer" + }, + "settled-last": { + "description": "when the last was heard, RFC 3339", + "type": "string" + } + }, + "required": [ + "module", + "image" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "waits" + ], + "type": "object" + } } ] }, -- 2.54.0 From 88972faf087319edb90bbe153de2db3f9ff46a12 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:26:44 +0100 Subject: [PATCH 13/94] SD-M2a: a publish-part chunk is at most 760 KiB, so its message stays under the bus's 1 MiB with its fields Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/parts/registry/contract.go | 2 +- go/seats/seats.go | 16 ++++++++-------- parts/registry.json | 4 ++-- ts/src/parts/registry.ts | 2 +- ts/src/seats.ts | 16 ++++++++-------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go/parts/registry/contract.go b/go/parts/registry/contract.go index 81d4e3b..e155910 100644 --- a/go/parts/registry/contract.go +++ b/go/parts/registry/contract.go @@ -102,7 +102,7 @@ type PublishAnswer struct { // PublishPartInput is what publish-part takes. type PublishPartInput struct { - // the chunk's bytes, base64 (at most 768 KiB before encoding) + // the chunk's bytes, base64 (at most 760 KiB before encoding) Data string `json:"data"` // this chunk is the part's last Last bool `json:"last"` diff --git a/go/seats/seats.go b/go/seats/seats.go index 5e4e996..daa7022 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -386,8 +386,8 @@ func List() []Seat { {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, @@ -401,8 +401,8 @@ func List() []Seat { {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, @@ -416,8 +416,8 @@ func List() []Seat { {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, @@ -443,8 +443,8 @@ func List() []Seat { {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, diff --git a/parts/registry.json b/parts/registry.json index af29f85..ccfe514 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -90,7 +90,7 @@ }, { "name": "publish-part", - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", "callers": "the build modules, and only the proved caller that opened the transfer", "input": { "type": "object", @@ -113,7 +113,7 @@ }, "data": { "type": "string", - "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)" + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)" } }, "required": [ diff --git a/ts/src/parts/registry.ts b/ts/src/parts/registry.ts index e13b4d2..71cc661 100644 --- a/ts/src/parts/registry.ts +++ b/ts/src/parts/registry.ts @@ -101,7 +101,7 @@ export interface PublishAnswer { /** is what publish-part takes. */ export interface PublishPartInput { - /** the chunk's bytes, base64 (at most 768 KiB before encoding) */ + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ "data": string; /** this chunk is the part's last */ "last": boolean; diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 3cbdb10..c44b230 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -2980,11 +2980,11 @@ export const list: readonly Seat[] = [ } }, { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", "input": { "properties": { "data": { - "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string" }, "last": { @@ -3332,11 +3332,11 @@ export const list: readonly Seat[] = [ } }, { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", "input": { "properties": { "data": { - "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string" }, "last": { @@ -3686,11 +3686,11 @@ export const list: readonly Seat[] = [ } }, { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", "input": { "properties": { "data": { - "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string" }, "last": { @@ -4213,11 +4213,11 @@ export const list: readonly Seat[] = [ } }, { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 768 KiB of data, base64.", + "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", "input": { "properties": { "data": { - "description": "the chunk's bytes, base64 (at most 768 KiB before encoding)", + "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string" }, "last": { -- 2.54.0 From 52b05d50cbb8d852e5f00cd044346046b63b1a8a Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:27:55 +0100 Subject: [PATCH 14/94] SD-M4: mesh-contracts declares its Go and TypeScript packages, series 0.1, generated from its definitions (design 26) Each package carries the contracts marker generated-from naming the module's root, where seats/, parts/ and format/ are. No merge-check.sh: the checker runs every repository's tests by Go's convention (ADR 0529). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- module.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 module.json diff --git a/module.json b/module.json new file mode 100644 index 0000000..b67dc32 --- /dev/null +++ b/module.json @@ -0,0 +1,12 @@ +{ + "module": "mesh-contracts", + "version": "1", + "slug": "contracts", + "build": { + "artifacts": [ + { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, + { "name": "lib", "kind": "package", "language": "typescript", "paths": ["ts"], "series": "0.1", "generated-from": "." } + ] + }, + "resources": [] +} -- 2.54.0 From e9c97f9e9fa2385f9d14a891774b1d9ada1a1a10 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:33:59 +0100 Subject: [PATCH 15/94] =?UTF-8?q?SD-M2a,=20SD-M2b:=20the=20registry=20and?= =?UTF-8?q?=20broker=20verbs=20are=20optional=20until=20their=20holders=20?= =?UTF-8?q?serve=20them=20(design=2033=20=C2=A77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deciding parts built with this seat list judge the running nats and gitea before their new builds are delivered; a required verb they do not serve yet would refuse them as the seats' holders (mesh-host's composition and the grants part's last-holder rule, read in their suites). The mark goes once every holder serves the verbs. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/seats.go | 60 ++++++++++++------------ parts/registry.json | 12 +++-- seats/container-image-registry/seat.json | 12 +++-- seats/mesh-broker/seat.json | 24 ++++++---- seats/oci-artifact-registry/seat.json | 6 ++- ts/src/seats.ts | 30 ++++++++++++ 6 files changed, 96 insertions(+), 48 deletions(-) diff --git a/go/seats/seats.go b/go/seats/seats.go index daa7022..eebcaef 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -144,28 +144,28 @@ func List() []Seat { }}, {Name: MeshBroker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0079", Delivers: "mesh-bus", Emits: []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"}, Serves: []Verb{ - {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", + {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", Optional: true, Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every entry", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, - {Name: "show", Description: "One entry, its template, settings and fill.", + {Name: "show", Description: "One entry, its template, settings and fill.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "check", Description: "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", + {Name: "check", Description: "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", Optional: true, Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"differences": map[string]any{"description": "every difference", "items": map[string]any{"properties": map[string]any{"asked": map[string]any{"description": "asked", "type": "object"}, "grows": map[string]any{"description": "the difference only grows: apply may act on it", "type": "boolean"}, "held": map[string]any{"description": "held", "type": "object"}, "local-name": map[string]any{"description": "the entry", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, "type": "array"}}, "required": []string{"differences"}, "type": "object"}}, - {Name: "orphans", Description: "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", + {Name: "orphans", Description: "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", Optional: true, Input: map[string]any{"description": "no arguments", "properties": map[string]any{}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every orphan", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, - {Name: "create", Description: "Create one entry now.", + {Name: "create", Description: "Create one entry now.", Optional: true, Input: map[string]any{"description": "one piece of bus state as a manifest asks it", "properties": map[string]any{"configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type"}, "type": "object"}, Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "apply", Description: "Apply a difference that only grows: a larger cap, a longer age.", + {Name: "apply", Description: "Apply a difference that only grows: a larger cap, a longer age.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "recreate", Description: "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", + {Name: "recreate", Description: "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "drop", Description: "Remove one orphan, after a stream snapshot.", + {Name: "drop", Description: "Remove one orphan, after a stream snapshot.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"dropped": map[string]any{"description": "it is gone", "type": "boolean"}, "snapshot": map[string]any{"description": "the snapshot taken first", "type": "string"}}, "required": []string{"dropped"}, "type": "object"}}, }}, @@ -383,79 +383,79 @@ func List() []Seat { }}, {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, }}, {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed"}, Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, }}, {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, - {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", + {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, - {Name: "tags", Description: "The tags of one image and the digest each names.", + {Name: "tags", Description: "The tags of one image and the digest each names.", Optional: true, Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}, "tags": map[string]any{"description": "every tag", "items": map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "tag": map[string]any{"description": "the tag", "type": "string"}}, "required": []string{"tag", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "tags"}, "type": "object"}}, - {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", + {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", + {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", + {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, - {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", + {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, diff --git a/parts/registry.json b/parts/registry.json index ccfe514..d636cc2 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -86,7 +86,8 @@ "required": [ "transfer" ] - } + }, + "optional": true }, { "name": "publish-part", @@ -148,7 +149,8 @@ "transfer", "part" ] - } + }, + "optional": true }, { "name": "versions", @@ -189,7 +191,8 @@ "name", "versions" ] - } + }, + "optional": true }, { "name": "remove", @@ -242,7 +245,8 @@ "required": [ "removed" ] - } + }, + "optional": true } ], "events": [ diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json index c289aad..651ad69 100644 --- a/seats/container-image-registry/seat.json +++ b/seats/container-image-registry/seat.json @@ -52,7 +52,8 @@ "reference", "digest" ] - } + }, + "optional": true }, { "name": "tags", @@ -103,7 +104,8 @@ "name", "tags" ] - } + }, + "optional": true }, { "name": "hold", @@ -146,7 +148,8 @@ "required": [ "held" ] - } + }, + "optional": true }, { "name": "release", @@ -185,7 +188,8 @@ "required": [ "released" ] - } + }, + "optional": true } ], "events": [ diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index 7587e3e..1aa166f 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -34,7 +34,8 @@ "required": [ "entries" ] - } + }, + "optional": true }, { "name": "show", @@ -45,7 +46,8 @@ }, "answer": { "$ref": "#/defs/Held" - } + }, + "optional": true }, { "name": "check", @@ -100,7 +102,8 @@ "required": [ "differences" ] - } + }, + "optional": true }, { "name": "orphans", @@ -125,7 +128,8 @@ "required": [ "entries" ] - } + }, + "optional": true }, { "name": "create", @@ -136,7 +140,8 @@ }, "answer": { "$ref": "#/defs/Held" - } + }, + "optional": true }, { "name": "apply", @@ -147,7 +152,8 @@ }, "answer": { "$ref": "#/defs/Held" - } + }, + "optional": true }, { "name": "recreate", @@ -158,7 +164,8 @@ }, "answer": { "$ref": "#/defs/Held" - } + }, + "optional": true }, { "name": "drop", @@ -182,7 +189,8 @@ "required": [ "dropped" ] - } + }, + "optional": true } ], "events": [ diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json index 52921d3..c826e5c 100644 --- a/seats/oci-artifact-registry/seat.json +++ b/seats/oci-artifact-registry/seat.json @@ -51,7 +51,8 @@ "required": [ "held" ] - } + }, + "optional": true }, { "name": "release", @@ -90,7 +91,8 @@ "required": [ "released" ] - } + }, + "optional": true } ], "events": [ diff --git a/ts/src/seats.ts b/ts/src/seats.ts index c44b230..5eb57ff 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -164,6 +164,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "list", + "optional": true, "output": { "properties": { "entries": { @@ -278,6 +279,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "show", + "optional": true, "output": { "description": "one entry as the bus holds it", "properties": { @@ -372,6 +374,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "check", + "optional": true, "output": { "properties": { "differences": { @@ -422,6 +425,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "orphans", + "optional": true, "output": { "properties": { "entries": { @@ -569,6 +573,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "create", + "optional": true, "output": { "description": "one entry as the bus holds it", "properties": { @@ -671,6 +676,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "apply", + "optional": true, "output": { "description": "one entry as the bus holds it", "properties": { @@ -783,6 +789,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "recreate", + "optional": true, "output": { "description": "one entry as the bus holds it", "properties": { @@ -895,6 +902,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "drop", + "optional": true, "output": { "properties": { "dropped": { @@ -2955,6 +2963,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish", + "optional": true, "output": { "properties": { "held": { @@ -3014,6 +3023,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish-part", + "optional": true, "output": { "properties": { "complete": { @@ -3059,6 +3069,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "versions", + "optional": true, "output": { "properties": { "name": { @@ -3159,6 +3170,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "remove", + "optional": true, "output": { "properties": { "artifact": { @@ -3307,6 +3319,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish", + "optional": true, "output": { "properties": { "held": { @@ -3366,6 +3379,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish-part", + "optional": true, "output": { "properties": { "complete": { @@ -3411,6 +3425,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "versions", + "optional": true, "output": { "properties": { "name": { @@ -3511,6 +3526,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "remove", + "optional": true, "output": { "properties": { "artifact": { @@ -3661,6 +3677,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish", + "optional": true, "output": { "properties": { "held": { @@ -3720,6 +3737,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish-part", + "optional": true, "output": { "properties": { "complete": { @@ -3765,6 +3783,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "versions", + "optional": true, "output": { "properties": { "name": { @@ -3865,6 +3884,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "remove", + "optional": true, "output": { "properties": { "artifact": { @@ -3949,6 +3969,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "mirror", + "optional": true, "output": { "properties": { "digest": { @@ -3982,6 +4003,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "tags", + "optional": true, "output": { "properties": { "name": { @@ -4042,6 +4064,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "hold", + "optional": true, "output": { "properties": { "held": { @@ -4084,6 +4107,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "release", + "optional": true, "output": { "properties": { "released": { @@ -4188,6 +4212,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish", + "optional": true, "output": { "properties": { "held": { @@ -4247,6 +4272,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "publish-part", + "optional": true, "output": { "properties": { "complete": { @@ -4292,6 +4318,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "versions", + "optional": true, "output": { "properties": { "name": { @@ -4392,6 +4419,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "remove", + "optional": true, "output": { "properties": { "artifact": { @@ -4476,6 +4504,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "hold", + "optional": true, "output": { "properties": { "held": { @@ -4518,6 +4547,7 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "release", + "optional": true, "output": { "properties": { "released": { -- 2.54.0 From f3a9cd17d6653ba91f6fe7fd19a2e293ae1848c6 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:35:30 +0100 Subject: [PATCH 16/94] SD-M2b: mesh-broker's events carry the binding and the verdict's revision (the lead's ruling, 12:4x) The binding a module receives is one named type, Binding, the provision's form and each event's binding. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshbroker/contract.go | 28 +++++++++------ seats/mesh-broker/seat.json | 64 +++++++++++++++++++-------------- ts/src/mesh-broker.ts | 26 ++++++++------ 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 2a95c36..374d7de 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -73,6 +73,18 @@ const SubjectRecreated = "mesh.seat.mesh-broker.event.recreated" // SubjectDropped is where dropped is published. const SubjectDropped = "mesh.seat.mesh-broker.event.dropped" +// Binding is the binding a module receives in its membership for one entry (ADR 0595 §1, §7). +type Binding struct { + // the entry's name on the bus + BusName string `json:"bus-name"` + // how the bus keeps it + StorageType string `json:"storage-type"` + // its subjects, for a stream + Subjects []string `json:"subjects,omitempty"` + // its template + Template string `json:"template,omitempty"` +} + // Entry is one piece of bus state as a manifest asks it. type Entry struct { // its own configuration, where no template fits @@ -131,7 +143,8 @@ type Named struct { // Said is the body of every event of the seat. type Said struct { // the settings asked - Asked map[string]any `json:"asked,omitempty"` + Asked map[string]any `json:"asked,omitempty"` + Binding *Binding `json:"binding,omitempty"` // its name on the bus BusName string `json:"bus-name"` // its own configuration, where no template fits @@ -145,6 +158,8 @@ type Said struct { Module string `json:"module"` // the template's options the module set, within their bounds Options map[string]any `json:"options,omitempty"` + // the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears + Revision int64 `json:"revision,omitempty"` // the stream snapshot taken before a recreate or drop Snapshot string `json:"snapshot,omitempty"` // how the bus keeps it @@ -278,13 +293,4 @@ type RecreatedEvent = Said type DroppedEvent = Said // BusStateForm is the form of the provision bus-state. -type BusStateForm struct { - // the entry's name on the bus - BusName string `json:"bus-name"` - // how the bus keeps it - StorageType string `json:"storage-type"` - // its subjects, for a stream - Subjects []string `json:"subjects,omitempty"` - // its template - Template string `json:"template,omitempty"` -} +type BusStateForm = Binding diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index 1aa166f..d551eb7 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -258,33 +258,7 @@ { "name": "bus-state", "form": { - "type": "object", - "properties": { - "bus-name": { - "type": "string", - "description": "the entry's name on the bus" - }, - "storage-type": { - "type": "string", - "description": "how the bus keeps it" - }, - "template": { - "type": "string", - "description": "its template" - }, - "subjects": { - "type": "array", - "items": { - "type": "string" - }, - "description": "its subjects, for a stream" - } - }, - "required": [ - "bus-name", - "storage-type" - ], - "description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)" + "$ref": "#/defs/Binding" } } ], @@ -487,6 +461,13 @@ "snapshot": { "type": "string", "description": "the stream snapshot taken before a recreate or drop" + }, + "binding": { + "$ref": "#/defs/Binding" + }, + "revision": { + "type": "integer", + "description": "the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears" } }, "required": [ @@ -539,6 +520,35 @@ "warrant", "why" ] + }, + "Binding": { + "type": "object", + "properties": { + "bus-name": { + "type": "string", + "description": "the entry's name on the bus" + }, + "storage-type": { + "type": "string", + "description": "how the bus keeps it" + }, + "template": { + "type": "string", + "description": "its template" + }, + "subjects": { + "type": "array", + "items": { + "type": "string" + }, + "description": "its subjects, for a stream" + } + }, + "required": [ + "bus-name", + "storage-type" + ], + "description": "the binding a module receives in its membership for one entry (ADR 0595 §1, §7)" } } } diff --git a/ts/src/mesh-broker.ts b/ts/src/mesh-broker.ts index 5f94f67..01c8c50 100644 --- a/ts/src/mesh-broker.ts +++ b/ts/src/mesh-broker.ts @@ -47,6 +47,18 @@ export const recreatedSubject = "mesh.seat.mesh-broker.event.recreated"; export const droppedSubject = "mesh.seat.mesh-broker.event.dropped"; +/** the binding a module receives in its membership for one entry (ADR 0595 §1, §7) */ +export interface Binding { + /** the entry's name on the bus */ + "bus-name": string; + /** how the bus keeps it */ + "storage-type": string; + /** its subjects, for a stream */ + "subjects"?: string[]; + /** its template */ + "template"?: string; +} + /** one piece of bus state as a manifest asks it */ export interface Entry { /** its own configuration, where no template fits */ @@ -104,6 +116,7 @@ export interface Named { export interface Said { /** the settings asked */ "asked"?: Record; + "binding"?: Binding; /** its name on the bus */ "bus-name": string; /** its own configuration, where no template fits */ @@ -117,6 +130,8 @@ export interface Said { "module": string; /** the template's options the module set, within their bounds */ "options"?: Record; + /** the indexer's revision of the verdict the bus module acted on (ADR 0622 §2), so a reader orders what it hears */ + "revision"?: number; /** the stream snapshot taken before a recreate or drop */ "snapshot"?: string; /** how the bus keeps it */ @@ -248,14 +263,5 @@ export type RecreatedEvent = Said; export type DroppedEvent = Said; /** is the form of the provision bus-state. */ -export interface BusStateForm { - /** the entry's name on the bus */ - "bus-name": string; - /** how the bus keeps it */ - "storage-type": string; - /** its subjects, for a stream */ - "subjects"?: string[]; - /** its template */ - "template"?: string; -} +export type BusStateForm = Binding; -- 2.54.0 From 7498d689e2bc33424ba0a1af9522461c64ff9010 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:36:10 +0100 Subject: [PATCH 17/94] The node-engine seat whole: every verb's answer, its events' bodies, and ADR 0618's manifest-reads and manifest-reads-changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat's area changes in this milestone, so it moves whole into its contracts (ADR 0613 §3): the eleven verbs it serves get their answers as the engine writes them, asked, done and refused their body, and the seat serves the optional verb manifest-reads (ADR 0618 §3) and says manifest-reads-changed (§4). Each event is published per node, the node's name its last token (by-caller), and kept with the seat's events for seven days. compose-candidate's input is a boolean, as the engine and the checker read it. Fixtures for every event, compose-candidate's input and three answers. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- go/seats/nodeengine/contract.go | 554 ++++++++++++++++ go/seats/nodeengine/contract_test.go | 74 +++ go/seats/seats.go | 35 +- seats/node-engine/fixtures/events.json | 69 ++ seats/node-engine/fixtures/verbs.json | 76 +++ seats/node-engine/seat.json | 883 ++++++++++++++++++++++++- ts/src/index.ts | 1 + ts/src/node-engine.ts | 539 +++++++++++++++ ts/src/seats.ts | 867 +++++++++++++++++++++++- 9 files changed, 3058 insertions(+), 40 deletions(-) create mode 100644 go/seats/nodeengine/contract.go create mode 100644 go/seats/nodeengine/contract_test.go create mode 100644 seats/node-engine/fixtures/events.json create mode 100644 seats/node-engine/fixtures/verbs.json create mode 100644 ts/src/node-engine.ts diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go new file mode 100644 index 0000000..efc6d4d --- /dev/null +++ b/go/seats/nodeengine/contract.go @@ -0,0 +1,554 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package nodeengine is the contract of the seat "node-engine", defined whole (seats/node-engine/seat.json): The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token. +package nodeengine + +// Seat is the seat's name. +const Seat = "node-engine" + +const ( + VerbHandOver = "hand-over" + VerbAccessAct = "access-act" + VerbJudgeDirectories = "judge-directories" + VerbSetuidSearch = "setuid-search" + VerbRootFree = "root-free" + VerbActs = "acts" + VerbPackages = "packages" + VerbComposition = "composition" + VerbCompose = "compose" + VerbComposeCandidate = "compose-candidate" + VerbPullWaits = "pull-waits" + VerbManifestReads = "manifest-reads" +) + +const ( + EventAsked = "asked" + EventDone = "done" + EventRefused = "refused" + EventManifestReadsChanged = "manifest-reads-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbPullWaits, VerbManifestReads} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventAsked, EventDone, EventRefused, EventManifestReadsChanged} +} + +// Family is where the seat's events are kept. +const ( + Family = "node-engine.events" + FamilyMaxAgeDays = 7 +) + +// SubjectAsked is where asked is published for one caller. +func SubjectAsked(caller string) string { return "mesh.seat.node-engine.event.asked." + caller } + +// SubjectDone is where done is published for one caller. +func SubjectDone(caller string) string { return "mesh.seat.node-engine.event.done." + caller } + +// SubjectRefused is where refused is published for one caller. +func SubjectRefused(caller string) string { return "mesh.seat.node-engine.event.refused." + caller } + +// SubjectManifestReadsChanged is where manifest-reads-changed is published for one caller. +func SubjectManifestReadsChanged(caller string) string { + return "mesh.seat.node-engine.event.manifest-reads-changed." + caller +} + +// Act is one act asked through the seat, as acts reads it (ADR 0563 §9). +type Act struct { + // the act: hand-over, access-act, judge-directories or setuid-search + Act string `json:"act"` + // the operator's ask of a hand-over or an access act + Ask string `json:"ask,omitempty"` + // when it was asked, RFC 3339 + At string `json:"at"` + // the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4) + Auto bool `json:"auto,omitempty"` + // who answered, as the operator's warrant names them + By string `json:"by,omitempty"` + // the caller as the runtime proved it + Caller string `json:"caller"` + // what the engine read on disk when it judged + Disk string `json:"disk,omitempty"` + // when it ended, RFC 3339 + Ended string `json:"ended,omitempty"` + // when the ask ends unanswered, RFC 3339 + Expires string `json:"expires,omitempty"` + // the act's id + ID string `json:"id"` + // the directory the act changes + Path string `json:"path,omitempty"` + // the warrant's proofs + Proofs []string `json:"proofs,omitempty"` + // what the engine said of it: its outcome or why it was refused + Said string `json:"said,omitempty"` + // the owner, group and mode it sets, in words + Sets string `json:"sets,omitempty"` + // where it stands + State string `json:"state"` + // the path of a hand-over, or the access of an access act + Subject string `json:"subject,omitempty"` + // the channel the warrant came on + Via string `json:"via,omitempty"` + // why it was asked, in words + Why string `json:"why,omitempty"` +} + +// ActSaid is what the seat says of an act: the act, the path or access, the caller as the runtime proved it and the ask (ADR 0563 §1). +type ActSaid struct { + // the act + Act string `json:"act"` + // the operator's ask + Ask string `json:"ask,omitempty"` + // the caller as the runtime proved it + Caller string `json:"caller"` + // the node whose engine says it + Node string `json:"node"` + // what the engine said of it + Said string `json:"said,omitempty"` + // where the act stands + State string `json:"state"` + // the path or the access + Subject string `json:"subject,omitempty"` +} + +// Acts is the acts kept. +type Acts struct { + // the acts, newest first + Acts []Act `json:"acts"` + // the node + Node string `json:"node"` +} + +// Asked is the answer of an act the operator is asked. +type Asked struct { + // the ask's id: acts shows where it stands + Asked string `json:"asked"` + // the node + Node string `json:"node"` + // what happens next, in words + Says string `json:"says"` +} + +// ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). +type ManifestReadsChanged struct { + // when it says it, RFC 3339 + At string `json:"at"` + // what its engine states it reads now + ManifestReads []string `json:"manifest-reads,omitempty"` + // the node + Node string `json:"node"` + // the layout's version + V int64 `json:"v"` + // started: the engine started or what it states changed; returned: its node returned after being away + Why string `json:"why"` +} + +// Module is one module's composition with the candidate. +type Module struct { + // the candidate changes it from the one composed without it + Changed bool `json:"changed,omitempty"` + // the composition's digest when composed + Digest string `json:"digest,omitempty"` + // the fingerprint it was composed at + Fp string `json:"fp,omitempty"` + // the module, node for the node's own composition + Module string `json:"module"` + // composed (digest set), refused by the candidate, or held without it too (words set) + State string `json:"state"` + // why, for a refused or held one + Words string `json:"words,omitempty"` +} + +// Revision is one revision of a composition key. +type Revision struct { + // its digest + Digest string `json:"digest,omitempty"` + // the composition's key + Key string `json:"key"` + // the latest revision + Latest bool `json:"latest,omitempty"` + // why it cannot be read or trusted + Refused string `json:"refused,omitempty"` + // how many resources it names + Resources int64 `json:"resources,omitempty"` + // its revision + Revision int64 `json:"revision,omitempty"` +} + +// RootVerdict is the engine's root-free verdict (ADR 0563 §6). +type RootVerdict struct { + // the machine is root-free + Free bool `json:"free"` + // when the engine judged it, RFC 3339 + Judged string `json:"judged"` + // the machine + Machine string `json:"machine"` + // why, in words + Why string `json:"why"` +} + +// Said is the answer of a read-only judgement. +type Said struct { + // the node + Node string `json:"node"` + // the engine's judgement or search, one line per directory or program + Said string `json:"said"` +} + +// HandOverInput is what hand-over takes. +type HandOverInput struct { + // the directory's absolute path, as the engine states it + Path string `json:"path"` + // why it is asked, in words the operator reads + Why string `json:"why,omitempty"` +} + +// HandOverAnswer is what hand-over answers. +type HandOverAnswer = Asked + +// AccessActInput is what access-act takes. +type AccessActInput struct { + // the access, library: + Access string `json:"access"` + // the act: take + Act string `json:"act"` + // why it is asked, in words the operator reads + Why string `json:"why,omitempty"` +} + +// AccessActAnswer is what access-act answers. +type AccessActAnswer = Asked + +// JudgeDirectoriesInput is what judge-directories takes. +type JudgeDirectoriesInput struct { +} + +// JudgeDirectoriesAnswer is what judge-directories answers. +type JudgeDirectoriesAnswer = Said + +// SetuidSearchInput is what setuid-search takes. +type SetuidSearchInput struct { + // why it is asked, in words + Why string `json:"why,omitempty"` +} + +// SetuidSearchAnswer is what setuid-search answers. +type SetuidSearchAnswer = Said + +// RootFreeInput is what root-free takes. +type RootFreeInput struct { +} + +// RootFreeAnswer is what root-free answers. +type RootFreeAnswer struct { + // this machine's verdict + Machines []RootVerdict `json:"machines"` +} + +// ActsInput is what acts takes. +type ActsInput struct { + // one act, by its id + ID string `json:"id,omitempty"` + // at most this many, newest + Limit string `json:"limit,omitempty"` +} + +// ActsAnswer is what acts answers. +type ActsAnswer = Acts + +// PackagesInput is what packages takes. +type PackagesInput struct { + // "true": write this machine's key again from what the engine composed + Write string `json:"write,omitempty"` +} + +// PackagesAnswerKeyPackagesItem is a type of the contract. +type PackagesAnswerKeyPackagesItem struct { + // declared absent + Absent bool `json:"absent"` + // the modules that declare it + Modules []string `json:"modules"` + // the package + Package string `json:"package"` +} + +// PackagesAnswerKey is the node's key as the bus holds it; absent when the engine wrote none. +type PackagesAnswerKey struct { + // the node + Node string `json:"node"` + // its packages sorted by name, absent after present + Packages []PackagesAnswerKeyPackagesItem `json:"packages"` +} + +// PackagesAnswer is what packages answers. +type PackagesAnswer struct { + // the node's key as the bus holds it; absent when the engine wrote none + Key *PackagesAnswerKey `json:"key,omitempty"` + // why the key could not be read + KeyError string `json:"key_error,omitempty"` + // the node + Node string `json:"node"` + // a note, such as that no key is written yet + Note string `json:"note,omitempty"` + // where the key is read: the engine's state packages + Source string `json:"source"` + // with write: the key was written again (false: it already held what the engine composed) + Wrote bool `json:"wrote,omitempty"` +} + +// CompositionInput is what composition takes. +type CompositionInput struct { + // "true": list the resources that differ from what runs + Diff string `json:"diff,omitempty"` + // a composition's digest (sha256:…), to read that revision rather than the latest + Digest string `json:"digest,omitempty"` + // the module, as placed on this machine; node for the node's own + Module string `json:"module"` +} + +// CompositionAnswerAttempt is the module's last apply that did not end running; absent when none. +type CompositionAnswerAttempt struct { + // the want's revision the hold took + AnsweredRevision int64 `json:"answered_revision,omitempty"` + // the digest tried + Composed string `json:"composed,omitempty"` + // the composition's revision it was tried at + CompositionRevision int64 `json:"composition_revision"` + // the fingerprint tried + Fp string `json:"fp"` + // held for a failed backup + Held bool `json:"held,omitempty"` + // it failed because the bus did not answer for a moment + Transient bool `json:"transient,omitempty"` + // the want's revision it was tried at + WantRevision int64 `json:"want_revision"` +} + +// CompositionAnswerHave is what the module's have key says now; absent when it says nothing. +type CompositionAnswerHave struct { + // digest + Composed string `json:"composed,omitempty"` + // detail + Detail string `json:"detail,omitempty"` + // fingerprint + Fp string `json:"fp,omitempty"` + // judgement + Judgement string `json:"judgement,omitempty"` + // running, failed, held or waiting-yes + Phase string `json:"phase"` + // the digest that waits for a yes + Waits string `json:"waits,omitempty"` +} + +// CompositionAnswerJudged is the move from what runs to the composition read. +type CompositionAnswerJudged struct { + // the judgement + Judgement string `json:"judgement"` + // the agent would wait for a person's yes before applying it + WaitsForYes bool `json:"waits_for_yes"` + // its words + Words string `json:"words"` +} + +// CompositionAnswerOwn is of the node's own composition, its own fields beside what runs names. +type CompositionAnswerOwn struct { + // the revision's adoption; absent for a converged node + Adoption map[string]any `json:"adoption,omitempty"` + // groups + Groups int64 `json:"groups,omitempty"` + // each module the node's composer held in its last pass, with why + Held map[string]string `json:"held,omitempty"` + // identities the revision names + Identities int64 `json:"identities,omitempty"` + // libraries + Libraries int64 `json:"libraries,omitempty"` + // the account the provisioner's client runs as, as the revision names it + ProvisionerAccount string `json:"provisioner_account"` + // as what runs names it + RunsProvisionerAccount string `json:"runs_provisioner_account,omitempty"` +} + +// CompositionAnswerRuns is what runs of the module; absent when nothing runs. +type CompositionAnswerRuns struct { + // digest + Composed string `json:"composed,omitempty"` + // fingerprint + Fp string `json:"fp,omitempty"` + // applied, have or kept + From string `json:"from"` + // phase + Phase string `json:"phase,omitempty"` + // how many resources run + Resources int64 `json:"resources"` +} + +// CompositionAnswerWant is the module's want as the agent holds it; absent when it holds none. +type CompositionAnswerWant struct { + // the operator's answer to a failed backup, again or skip + Backup string `json:"backup,omitempty"` + // the composition's digest it names + Composed string `json:"composed,omitempty"` + // its build's fingerprint + Fp string `json:"fp"` + // the module is removed + Removed bool `json:"removed,omitempty"` + // the revision of the want's key read last + Revision int64 `json:"revision"` + // the node composes the module itself (ADR 0547 §2) + Self bool `json:"self,omitempty"` + // its words + Words string `json:"words,omitempty"` + // the digest a person said yes to + Yes string `json:"yes,omitempty"` +} + +// CompositionAnswer is what composition answers. +type CompositionAnswer struct { + // the module's last apply that did not end running; absent when none + Attempt *CompositionAnswerAttempt `json:"attempt,omitempty"` + Composition Revision `json:"composition"` + // every resource that differs from what runs, when asked + Diff []map[string]any `json:"diff,omitempty"` + // what the module's have key says now; absent when it says nothing + Have *CompositionAnswerHave `json:"have,omitempty"` + // the move from what runs to the composition read + Judged CompositionAnswerJudged `json:"judged"` + // the module + Module string `json:"module"` + // the node + Node string `json:"node"` + // of the node's own composition, its own fields beside what runs names + Own *CompositionAnswerOwn `json:"own,omitempty"` + // the revisions of its key the bucket keeps, oldest first + Revisions []Revision `json:"revisions,omitempty"` + // what runs of the module; absent when nothing runs + Runs *CompositionAnswerRuns `json:"runs,omitempty"` + // the agent has looked at the module once since it started + Started bool `json:"started"` + // the module's want as the agent holds it; absent when it holds none + Want *CompositionAnswerWant `json:"want,omitempty"` +} + +// ComposeInput is what compose takes. +type ComposeInput struct { + // a module placed on this machine whose apply is also tried again (optional) + Module string `json:"module,omitempty"` +} + +// ComposeAnswerPass is how the pass went. +type ComposeAnswerPass struct { + // when the node composes again on its own, RFC 3339 + Again string `json:"again,omitempty"` + // when the pass ended, RFC 3339 + At string `json:"at"` + // the answer is the last pass, made moments ago + Earlier bool `json:"earlier,omitempty"` + // how many compositions the pass handed the agent that differ from before + Handed int64 `json:"handed"` + // each module refused alone, with why + Held map[string]string `json:"held,omitempty"` + // why the node composed none of its modules + Refused string `json:"refused,omitempty"` +} + +// ComposeAnswer is what compose answers. +type ComposeAnswer struct { + // how the pass went + Pass ComposeAnswerPass `json:"pass"` + // the module whose apply was tried again + Retried string `json:"retried,omitempty"` +} + +// ComposeCandidateInput is what compose-candidate takes. +type ComposeCandidateInput struct { + // the key of the indexer's candidate, . + Candidate string `json:"candidate"` + // also answer the composition input the candidate was composed from, for the sandbox path + Input bool `json:"input,omitempty"` +} + +// ComposeCandidateAnswer is what compose-candidate answers. +type ComposeCandidateAnswer struct { + // the candidate's key + Candidate string `json:"candidate"` + // when asked: the composition input it composed from + Input map[string]any `json:"input,omitempty"` + // every module placed on the node, sorted by name + Modules []Module `json:"modules"` + // the node + Node string `json:"node"` + Own Module `json:"own"` + // why the node could not compose with the candidate at all + Refused string `json:"refused,omitempty"` + // how long the engine took, in milliseconds + Took int64 `json:"took"` + // the layout's version + V int64 `json:"v"` + // the node's verdict (ADR 0593 §7, §9) + Verdict string `json:"verdict"` +} + +// PullWaitsInput is what pull-waits takes. +type PullWaitsInput struct { +} + +// PullWaitsAnswerWaitsItem is a type of the contract. +type PullWaitsAnswerWaitsItem struct { + // that seat's holder, whose settled the retry waits on + Holder string `json:"holder,omitempty"` + // the image whose pull failed + Image string `json:"image"` + // the module whose apply failed + Module string `json:"module"` + // when the failed pull began, RFC 3339 + PullBegan string `json:"pull-began,omitempty"` + // when it was last tried again, RFC 3339 + RetryLast string `json:"retry-last,omitempty"` + // what started that retry: settled, or the engine's start + RetryTrigger string `json:"retry-trigger,omitempty"` + // the seat the image came through + Seat string `json:"seat,omitempty"` + // the settled events of the holder heard since the engine started + SettledCount int64 `json:"settled-count,omitempty"` + // when the last was heard, RFC 3339 + SettledLast string `json:"settled-last,omitempty"` +} + +// PullWaitsAnswer is what pull-waits answers. +type PullWaitsAnswer struct { + // every failed pull waiting on a holder + Waits []PullWaitsAnswerWaitsItem `json:"waits"` +} + +// ManifestReadsInput is what manifest-reads takes. +type ManifestReadsInput struct { +} + +// ManifestReadsAnswer is what manifest-reads answers. +type ManifestReadsAnswer struct { + // the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline + ManifestReads []string `json:"manifest-reads,omitempty"` + // the node + Node string `json:"node"` + // when the engine last stated them, RFC 3339 + StatedAt string `json:"stated-at"` + // the layout's version + V int64 `json:"v"` +} + +// AskedEvent is the body of the event asked. +type AskedEvent = ActSaid + +// DoneEvent is the body of the event done. +type DoneEvent = ActSaid + +// RefusedEvent is the body of the event refused. +type RefusedEvent = ActSaid + +// ManifestReadsChangedEvent is the body of the event manifest-reads-changed. +type ManifestReadsChangedEvent = ManifestReadsChanged diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go new file mode 100644 index 0000000..b1d7720 --- /dev/null +++ b/go/seats/nodeengine/contract_test.go @@ -0,0 +1,74 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package nodeengine + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/node-engine/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "asked on the node's subject", subject: "mesh.seat.node-engine.event.asked.ace", given: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"state\":\"asked\",\"subject\":\"/srv/media\"}", wire: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"state\":\"asked\",\"subject\":\"/srv/media\"}", + decode: func(b []byte) (any, error) { var v AskedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.asked\\.[A-Za-z0-9_-]+$")}, + {name: "done on the node's subject", subject: "mesh.seat.node-engine.event.done.ace", given: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"handed over: owner media, group media, mode 2775\",\"state\":\"done\",\"subject\":\"/srv/media\"}", wire: "{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"handed over: owner media, group media, mode 2775\",\"state\":\"done\",\"subject\":\"/srv/media\"}", + decode: func(b []byte) (any, error) { var v DoneEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.done\\.[A-Za-z0-9_-]+$")}, + {name: "refused on the node's subject", subject: "mesh.seat.node-engine.event.refused.ace", given: "{\"act\":\"access-act\",\"ask\":\"access-act-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"the operator said no\",\"state\":\"refused\",\"subject\":\"library:music\"}", wire: "{\"act\":\"access-act\",\"ask\":\"access-act-1\",\"caller\":\"mesh-tools\",\"node\":\"ace\",\"said\":\"the operator said no\",\"state\":\"refused\",\"subject\":\"library:music\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.refused\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads-changed when the engine started", subject: "mesh.seat.node-engine.event.manifest-reads-changed.ace", given: "{\"at\":\"2026-11-04T10:00:00Z\",\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"v\":1,\"why\":\"started\"}", wire: "{\"at\":\"2026-11-04T10:00:00Z\",\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"v\":1,\"why\":\"started\"}", + decode: func(b []byte) (any, error) { + var v ManifestReadsChangedEvent + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads-changed when the node returned, from an engine that states none", subject: "mesh.seat.node-engine.event.manifest-reads-changed.g14", given: "{\"at\":\"2026-11-04T10:05:00Z\",\"node\":\"g14\",\"v\":1,\"why\":\"returned\"}", wire: "{\"at\":\"2026-11-04T10:05:00Z\",\"node\":\"g14\",\"v\":1,\"why\":\"returned\"}", + decode: func(b []byte) (any, error) { + var v ManifestReadsChangedEvent + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "compose-candidate asks the candidate and, as a boolean, its composition input", subject: "", given: "{\"candidate\":\"mesh-host.0123abcd\",\"input\":true}", wire: "{\"candidate\":\"mesh-host.0123abcd\",\"input\":true}", + decode: func(b []byte) (any, error) { var v ComposeCandidateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "compose-candidate answers per module and the node's own", subject: "", given: "{\"candidate\":\"mesh-host.0123abcd\",\"modules\":[{\"changed\":true,\"digest\":\"sha256:aa\",\"fp\":\"fp-1\",\"module\":\"plex\",\"state\":\"composed\"}],\"node\":\"ace\",\"own\":{\"digest\":\"sha256:bb\",\"module\":\"node\",\"state\":\"composed\"},\"took\":1200,\"v\":1,\"verdict\":\"composed\"}", wire: "{\"candidate\":\"mesh-host.0123abcd\",\"modules\":[{\"changed\":true,\"digest\":\"sha256:aa\",\"fp\":\"fp-1\",\"module\":\"plex\",\"state\":\"composed\"}],\"node\":\"ace\",\"own\":{\"digest\":\"sha256:bb\",\"module\":\"node\",\"state\":\"composed\"},\"took\":1200,\"v\":1,\"verdict\":\"composed\"}", + decode: func(b []byte) (any, error) { var v ComposeCandidateAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "acts answers each act kept", subject: "", given: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", wire: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v ActsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/go/seats/seats.go b/go/seats/seats.go index eebcaef..b389720 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -662,31 +662,44 @@ func List() []Seat { {Name: "restore", Description: "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "an id from trash", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"id", "reason"}, "type": "object"}, Replaces: []string{"trash-restore"}}, }}, - {Name: NodeEngine, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused"}, HeldByTheEngine: true, + {Name: NodeEngine, Scope: "node", Grain: "whole", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused", "manifest-reads-changed"}, HeldByTheEngine: true, Serves: []Verb{ {Name: "hand-over", Description: "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}, + Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, {Name: "access-act", Description: "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", - Input: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}, + Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, {Name: "judge-directories", Description: "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, {Name: "setuid-search", Description: "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", - Input: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, {Name: "root-free", Description: "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"machines": map[string]any{"description": "this machine's verdict", "items": map[string]any{"description": "the engine's root-free verdict (ADR 0563 §6)", "properties": map[string]any{"free": map[string]any{"description": "the machine is root-free", "type": "boolean"}, "judged": map[string]any{"description": "when the engine judged it, RFC 3339", "type": "string"}, "machine": map[string]any{"description": "the machine", "type": "string"}, "why": map[string]any{"description": "why, in words", "type": "string"}}, "required": []string{"machine", "free", "why", "judged"}, "type": "object"}, "type": "array"}}, "required": []string{"machines"}, "type": "object"}}, {Name: "acts", Description: "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}, + Output: map[string]any{"description": "the acts kept", "properties": map[string]any{"acts": map[string]any{"description": "the acts, newest first", "items": map[string]any{"description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", "properties": map[string]any{"act": map[string]any{"description": "the act: hand-over, access-act, judge-directories or setuid-search", "type": "string"}, "ask": map[string]any{"description": "the operator's ask of a hand-over or an access act", "type": "string"}, "at": map[string]any{"description": "when it was asked, RFC 3339", "type": "string"}, "auto": map[string]any{"description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", "type": "boolean"}, "by": map[string]any{"description": "who answered, as the operator's warrant names them", "type": "string"}, "caller": map[string]any{"description": "the caller as the runtime proved it", "type": "string"}, "disk": map[string]any{"description": "what the engine read on disk when it judged", "type": "string"}, "ended": map[string]any{"description": "when it ended, RFC 3339", "type": "string"}, "expires": map[string]any{"description": "when the ask ends unanswered, RFC 3339", "type": "string"}, "id": map[string]any{"description": "the act's id", "type": "string"}, "path": map[string]any{"description": "the directory the act changes", "type": "string"}, "proofs": map[string]any{"description": "the warrant's proofs", "items": map[string]any{"type": "string"}, "type": "array"}, "said": map[string]any{"description": "what the engine said of it: its outcome or why it was refused", "type": "string"}, "sets": map[string]any{"description": "the owner, group and mode it sets, in words", "type": "string"}, "state": map[string]any{"description": "where it stands", "enum": []string{"asked", "done", "refused", "answered"}, "type": "string"}, "subject": map[string]any{"description": "the path of a hand-over, or the access of an access act", "type": "string"}, "via": map[string]any{"description": "the channel the warrant came on", "type": "string"}, "why": map[string]any{"description": "why it was asked, in words", "type": "string"}}, "required": []string{"id", "act", "caller", "at", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}}, "required": []string{"node", "acts"}, "type": "object"}}, {Name: "packages", 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.", Optional: true, - Input: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the node's key as the bus holds it; absent when the engine wrote none", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "packages": map[string]any{"description": "its packages sorted by name, absent after present", "items": map[string]any{"properties": map[string]any{"absent": map[string]any{"description": "declared absent", "type": "boolean"}, "modules": map[string]any{"description": "the modules that declare it", "items": map[string]any{"type": "string"}, "type": "array"}, "package": map[string]any{"description": "the package", "type": "string"}}, "required": []string{"package", "modules", "absent"}, "type": "object"}, "type": "array"}}, "required": []string{"node", "packages"}, "type": "object"}, "key_error": map[string]any{"description": "why the key could not be read", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "note": map[string]any{"description": "a note, such as that no key is written yet", "type": "string"}, "source": map[string]any{"description": "where the key is read: the engine's state packages", "type": "string"}, "wrote": map[string]any{"description": "with write: the key was written again (false: it already held what the engine composed)", "type": "boolean"}}, "required": []string{"node", "source"}, "type": "object"}}, {Name: "composition", Description: "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", Optional: true, - Input: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"attempt": map[string]any{"description": "the module's last apply that did not end running; absent when none", "properties": map[string]any{"answered_revision": map[string]any{"description": "the want's revision the hold took", "type": "integer"}, "composed": map[string]any{"description": "the digest tried", "type": "string"}, "composition_revision": map[string]any{"description": "the composition's revision it was tried at", "type": "integer"}, "fp": map[string]any{"description": "the fingerprint tried", "type": "string"}, "held": map[string]any{"description": "held for a failed backup", "type": "boolean"}, "transient": map[string]any{"description": "it failed because the bus did not answer for a moment", "type": "boolean"}, "want_revision": map[string]any{"description": "the want's revision it was tried at", "type": "integer"}}, "required": []string{"fp", "want_revision", "composition_revision"}, "type": "object"}, "composition": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "diff": map[string]any{"description": "every resource that differs from what runs, when asked", "items": map[string]any{"description": "one resource added, removed or changed, as the engine's judge says it", "type": "object"}, "type": "array"}, "have": map[string]any{"description": "what the module's have key says now; absent when it says nothing", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "detail": map[string]any{"description": "detail", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "judgement": map[string]any{"description": "judgement", "type": "string"}, "phase": map[string]any{"description": "running, failed, held or waiting-yes", "type": "string"}, "waits": map[string]any{"description": "the digest that waits for a yes", "type": "string"}}, "required": []string{"phase"}, "type": "object"}, "judged": map[string]any{"description": "the move from what runs to the composition read", "properties": map[string]any{"judgement": map[string]any{"description": "the judgement", "type": "string"}, "waits_for_yes": map[string]any{"description": "the agent would wait for a person's yes before applying it", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}}, "required": []string{"judgement", "words", "waits_for_yes"}, "type": "object"}, "module": map[string]any{"description": "the module", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "of the node's own composition, its own fields beside what runs names", "properties": map[string]any{"adoption": map[string]any{"description": "the revision's adoption; absent for a converged node", "type": "object"}, "groups": map[string]any{"description": "groups", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module the node's composer held in its last pass, with why", "type": "object"}, "identities": map[string]any{"description": "identities the revision names", "type": "integer"}, "libraries": map[string]any{"description": "libraries", "type": "integer"}, "provisioner_account": map[string]any{"description": "the account the provisioner's client runs as, as the revision names it", "type": "string"}, "runs_provisioner_account": map[string]any{"description": "as what runs names it", "type": "string"}}, "required": []string{"provisioner_account"}, "type": "object"}, "revisions": map[string]any{"description": "the revisions of its key the bucket keeps, oldest first", "items": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "type": "array"}, "runs": map[string]any{"description": "what runs of the module; absent when nothing runs", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "from": map[string]any{"description": "applied, have or kept", "type": "string"}, "phase": map[string]any{"description": "phase", "type": "string"}, "resources": map[string]any{"description": "how many resources run", "type": "integer"}}, "required": []string{"from", "resources"}, "type": "object"}, "started": map[string]any{"description": "the agent has looked at the module once since it started", "type": "boolean"}, "want": map[string]any{"description": "the module's want as the agent holds it; absent when it holds none", "properties": map[string]any{"backup": map[string]any{"description": "the operator's answer to a failed backup, again or skip", "type": "string"}, "composed": map[string]any{"description": "the composition's digest it names", "type": "string"}, "fp": map[string]any{"description": "its build's fingerprint", "type": "string"}, "removed": map[string]any{"description": "the module is removed", "type": "boolean"}, "revision": map[string]any{"description": "the revision of the want's key read last", "type": "integer"}, "self": map[string]any{"description": "the node composes the module itself (ADR 0547 §2)", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}, "yes": map[string]any{"description": "the digest a person said yes to", "type": "string"}}, "required": []string{"fp", "revision"}, "type": "object"}}, "required": []string{"node", "module", "started", "composition", "judged"}, "type": "object"}}, {Name: "compose", Description: "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"pass": map[string]any{"description": "how the pass went", "properties": map[string]any{"again": map[string]any{"description": "when the node composes again on its own, RFC 3339", "type": "string"}, "at": map[string]any{"description": "when the pass ended, RFC 3339", "type": "string"}, "earlier": map[string]any{"description": "the answer is the last pass, made moments ago", "type": "boolean"}, "handed": map[string]any{"description": "how many compositions the pass handed the agent that differ from before", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module refused alone, with why", "type": "object"}, "refused": map[string]any{"description": "why the node composed none of its modules", "type": "string"}}, "required": []string{"at", "handed"}, "type": "object"}, "retried": map[string]any{"description": "the module whose apply was tried again", "type": "string"}}, "required": []string{"pass"}, "type": "object"}}, {Name: "compose-candidate", Description: "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", Optional: true, - Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"candidate"}, "type": "object"}}, + Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "also answer the composition input the candidate was composed from, for the sandbox path", "type": "boolean"}}, "required": []string{"candidate"}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the candidate's key", "type": "string"}, "input": map[string]any{"description": "when asked: the composition input it composed from", "type": "object"}, "modules": map[string]any{"description": "every module placed on the node, sorted by name", "items": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "refused": map[string]any{"description": "why the node could not compose with the candidate at all", "type": "string"}, "took": map[string]any{"description": "how long the engine took, in milliseconds", "type": "integer"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}, "verdict": map[string]any{"description": "the node's verdict (ADR 0593 §7, §9)", "enum": []string{"composed", "refused", "unready", "not-judged"}, "type": "string"}}, "required": []string{"v", "node", "candidate", "verdict", "modules", "own", "took"}, "type": "object"}}, {Name: "pull-waits", Description: "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", Optional: true, Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"waits": map[string]any{"description": "every failed pull waiting on a holder", "items": map[string]any{"properties": map[string]any{"holder": map[string]any{"description": "that seat's holder, whose settled the retry waits on", "type": "string"}, "image": map[string]any{"description": "the image whose pull failed", "type": "string"}, "module": map[string]any{"description": "the module whose apply failed", "type": "string"}, "pull-began": map[string]any{"description": "when the failed pull began, RFC 3339", "type": "string"}, "retry-last": map[string]any{"description": "when it was last tried again, RFC 3339", "type": "string"}, "retry-trigger": map[string]any{"description": "what started that retry: settled, or the engine's start", "type": "string"}, "seat": map[string]any{"description": "the seat the image came through", "type": "string"}, "settled-count": map[string]any{"description": "the settled events of the holder heard since the engine started", "type": "integer"}, "settled-last": map[string]any{"description": "when the last was heard, RFC 3339", "type": "string"}}, "required": []string{"module", "image"}, "type": "object"}, "type": "array"}}, "required": []string{"waits"}, "type": "object"}}, + {Name: "manifest-reads", Description: "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", Optional: true, + Input: map[string]any{"properties": map[string]any{}, "type": "object"}, + Output: map[string]any{"description": "what this machine's engine reads of a manifest (ADR 0618 §3)", "properties": map[string]any{"manifest-reads": map[string]any{"description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline", "items": map[string]any{"minLength": float64(1), "type": "string"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "stated-at": map[string]any{"description": "when the engine last stated them, RFC 3339", "type": "string"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}}, "required": []string{"v", "node", "stated-at"}, "type": "object"}}, }}, {Name: NodeLoginManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", Serves: []Verb{ diff --git a/seats/node-engine/fixtures/events.json b/seats/node-engine/fixtures/events.json new file mode 100644 index 0000000..eb2c8d7 --- /dev/null +++ b/seats/node-engine/fixtures/events.json @@ -0,0 +1,69 @@ +[ + { + "name": "asked on the node's subject", + "of": "event:asked", + "subject": "mesh.seat.node-engine.event.asked.ace", + "given": { + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "ask": "hand-over-1", + "state": "asked", + "node": "ace" + } + }, + { + "name": "done on the node's subject", + "of": "event:done", + "subject": "mesh.seat.node-engine.event.done.ace", + "given": { + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "ask": "hand-over-1", + "state": "done", + "said": "handed over: owner media, group media, mode 2775", + "node": "ace" + } + }, + { + "name": "refused on the node's subject", + "of": "event:refused", + "subject": "mesh.seat.node-engine.event.refused.ace", + "given": { + "act": "access-act", + "subject": "library:music", + "caller": "mesh-tools", + "ask": "access-act-1", + "state": "refused", + "said": "the operator said no", + "node": "ace" + } + }, + { + "name": "manifest-reads-changed when the engine started", + "of": "event:manifest-reads-changed", + "subject": "mesh.seat.node-engine.event.manifest-reads-changed.ace", + "given": { + "v": 1, + "node": "ace", + "manifest-reads": [ + "form:seat-reference", + "word:series" + ], + "at": "2026-11-04T10:00:00Z", + "why": "started" + } + }, + { + "name": "manifest-reads-changed when the node returned, from an engine that states none", + "of": "event:manifest-reads-changed", + "subject": "mesh.seat.node-engine.event.manifest-reads-changed.g14", + "given": { + "v": 1, + "node": "g14", + "at": "2026-11-04T10:05:00Z", + "why": "returned" + } + } +] diff --git a/seats/node-engine/fixtures/verbs.json b/seats/node-engine/fixtures/verbs.json new file mode 100644 index 0000000..40fff31 --- /dev/null +++ b/seats/node-engine/fixtures/verbs.json @@ -0,0 +1,76 @@ +[ + { + "name": "manifest-reads answers what the engine reads and when it stated it", + "of": "verb:manifest-reads.answer", + "given": { + "v": 1, + "node": "ace", + "manifest-reads": [ + "form:seat-reference", + "word:series" + ], + "stated-at": "2026-11-04T10:00:00Z" + } + }, + { + "name": "manifest-reads of an engine built before it said them is the baseline", + "of": "verb:manifest-reads.answer", + "given": { + "v": 1, + "node": "g14", + "stated-at": "2026-11-04T10:05:00Z" + } + }, + { + "name": "compose-candidate asks the candidate and, as a boolean, its composition input", + "of": "verb:compose-candidate.input", + "given": { + "candidate": "mesh-host.0123abcd", + "input": true + } + }, + { + "name": "compose-candidate answers per module and the node's own", + "of": "verb:compose-candidate.answer", + "given": { + "v": 1, + "node": "ace", + "candidate": "mesh-host.0123abcd", + "verdict": "composed", + "modules": [ + { + "module": "plex", + "fp": "fp-1", + "state": "composed", + "digest": "sha256:aa", + "changed": true + } + ], + "own": { + "module": "node", + "state": "composed", + "digest": "sha256:bb" + }, + "took": 1200 + } + }, + { + "name": "acts answers each act kept", + "of": "verb:acts.answer", + "given": { + "node": "ace", + "acts": [ + { + "id": "hand-over-1", + "act": "hand-over", + "subject": "/srv/media", + "caller": "mesh-tools", + "at": "2026-11-04T09:00:00Z", + "state": "asked", + "ask": "hand-over-1", + "expires": "2026-11-05T09:00:00Z" + } + ] + } + } +] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 21b288c..229e220 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -2,8 +2,9 @@ "format": 1, "seat": "node-engine", "scope": "node", - "grain": "seat-list", + "grain": "whole", "decision": "novox/hq ADR 0563", + "description": "The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token.", "held-by-the-engine": true, "verbs": [ { @@ -24,6 +25,9 @@ "path" ], "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" } }, { @@ -49,6 +53,9 @@ "access" ], "type": "object" + }, + "answer": { + "$ref": "#/defs/Asked" } }, { @@ -57,6 +64,9 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" } }, { @@ -70,6 +80,9 @@ } }, "type": "object" + }, + "answer": { + "$ref": "#/defs/Said" } }, { @@ -78,6 +91,21 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "machines": { + "type": "array", + "items": { + "$ref": "#/defs/RootVerdict" + }, + "description": "this machine's verdict" + } + }, + "required": [ + "machines" + ] } }, { @@ -95,6 +123,9 @@ } }, "type": "object" + }, + "answer": { + "$ref": "#/defs/Acts" } }, { @@ -113,6 +144,78 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "source": { + "type": "string", + "description": "where the key is read: the engine's state packages" + }, + "key": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string", + "description": "the package" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the modules that declare it" + }, + "absent": { + "type": "boolean", + "description": "declared absent" + } + }, + "required": [ + "package", + "modules", + "absent" + ] + }, + "description": "its packages sorted by name, absent after present" + } + }, + "required": [ + "node", + "packages" + ], + "description": "the node's key as the bus holds it; absent when the engine wrote none" + }, + "key_error": { + "type": "string", + "description": "why the key could not be read" + }, + "wrote": { + "type": "boolean", + "description": "with write: the key was written again (false: it already held what the engine composed)" + }, + "note": { + "type": "string", + "description": "a note, such as that no key is written yet" + } + }, + "required": [ + "node", + "source" + ] } }, { @@ -138,6 +241,255 @@ "module" ], "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "module": { + "type": "string", + "description": "the module" + }, + "want": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "its build's fingerprint" + }, + "composed": { + "type": "string", + "description": "the composition's digest it names" + }, + "yes": { + "type": "string", + "description": "the digest a person said yes to" + }, + "removed": { + "type": "boolean", + "description": "the module is removed" + }, + "self": { + "type": "boolean", + "description": "the node composes the module itself (ADR 0547 §2)" + }, + "backup": { + "type": "string", + "description": "the operator's answer to a failed backup, again or skip" + }, + "words": { + "type": "string", + "description": "its words" + }, + "revision": { + "type": "integer", + "description": "the revision of the want's key read last" + } + }, + "required": [ + "fp", + "revision" + ], + "description": "the module's want as the agent holds it; absent when it holds none" + }, + "attempt": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "the fingerprint tried" + }, + "composed": { + "type": "string", + "description": "the digest tried" + }, + "want_revision": { + "type": "integer", + "description": "the want's revision it was tried at" + }, + "composition_revision": { + "type": "integer", + "description": "the composition's revision it was tried at" + }, + "held": { + "type": "boolean", + "description": "held for a failed backup" + }, + "answered_revision": { + "type": "integer", + "description": "the want's revision the hold took" + }, + "transient": { + "type": "boolean", + "description": "it failed because the bus did not answer for a moment" + } + }, + "required": [ + "fp", + "want_revision", + "composition_revision" + ], + "description": "the module's last apply that did not end running; absent when none" + }, + "started": { + "type": "boolean", + "description": "the agent has looked at the module once since it started" + }, + "have": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "fingerprint" + }, + "composed": { + "type": "string", + "description": "digest" + }, + "phase": { + "type": "string", + "description": "running, failed, held or waiting-yes" + }, + "detail": { + "type": "string", + "description": "detail" + }, + "judgement": { + "type": "string", + "description": "judgement" + }, + "waits": { + "type": "string", + "description": "the digest that waits for a yes" + } + }, + "required": [ + "phase" + ], + "description": "what the module's have key says now; absent when it says nothing" + }, + "composition": { + "$ref": "#/defs/Revision" + }, + "revisions": { + "type": "array", + "items": { + "$ref": "#/defs/Revision" + }, + "description": "the revisions of its key the bucket keeps, oldest first" + }, + "runs": { + "type": "object", + "properties": { + "fp": { + "type": "string", + "description": "fingerprint" + }, + "composed": { + "type": "string", + "description": "digest" + }, + "phase": { + "type": "string", + "description": "phase" + }, + "from": { + "type": "string", + "description": "applied, have or kept" + }, + "resources": { + "type": "integer", + "description": "how many resources run" + } + }, + "required": [ + "from", + "resources" + ], + "description": "what runs of the module; absent when nothing runs" + }, + "judged": { + "type": "object", + "properties": { + "judgement": { + "type": "string", + "description": "the judgement" + }, + "words": { + "type": "string", + "description": "its words" + }, + "waits_for_yes": { + "type": "boolean", + "description": "the agent would wait for a person's yes before applying it" + } + }, + "required": [ + "judgement", + "words", + "waits_for_yes" + ], + "description": "the move from what runs to the composition read" + }, + "diff": { + "type": "array", + "items": { + "type": "object", + "description": "one resource added, removed or changed, as the engine's judge says it" + }, + "description": "every resource that differs from what runs, when asked" + }, + "own": { + "type": "object", + "properties": { + "provisioner_account": { + "type": "string", + "description": "the account the provisioner's client runs as, as the revision names it" + }, + "runs_provisioner_account": { + "type": "string", + "description": "as what runs names it" + }, + "adoption": { + "type": "object", + "description": "the revision's adoption; absent for a converged node" + }, + "identities": { + "type": "integer", + "description": "identities the revision names" + }, + "groups": { + "type": "integer", + "description": "groups" + }, + "libraries": { + "type": "integer", + "description": "libraries" + }, + "held": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "each module the node's composer held in its last pass, with why" + } + }, + "required": [ + "provisioner_account" + ], + "description": "of the node's own composition, its own fields beside what runs names" + } + }, + "required": [ + "node", + "module", + "started", + "composition", + "judged" + ] } }, { @@ -152,6 +504,55 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "retried": { + "type": "string", + "description": "the module whose apply was tried again" + }, + "pass": { + "type": "object", + "properties": { + "at": { + "type": "string", + "description": "when the pass ended, RFC 3339" + }, + "refused": { + "type": "string", + "description": "why the node composed none of its modules" + }, + "held": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "each module refused alone, with why" + }, + "handed": { + "type": "integer", + "description": "how many compositions the pass handed the agent that differ from before" + }, + "again": { + "type": "string", + "description": "when the node composes again on its own, RFC 3339" + }, + "earlier": { + "type": "boolean", + "description": "the answer is the last pass, made moments ago" + } + }, + "required": [ + "at", + "handed" + ], + "description": "how the pass went" + } + }, + "required": [ + "pass" + ] } }, { @@ -165,18 +566,72 @@ "type": "string" }, "input": { - "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", - "enum": [ - "true", - "false" - ], - "type": "string" + "description": "also answer the composition input the candidate was composed from, for the sandbox path", + "type": "boolean" } }, "required": [ "candidate" ], "type": "object" + }, + "answer": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "candidate": { + "type": "string", + "description": "the candidate's key" + }, + "verdict": { + "type": "string", + "enum": [ + "composed", + "refused", + "unready", + "not-judged" + ], + "description": "the node's verdict (ADR 0593 §7, §9)" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/defs/Module" + }, + "description": "every module placed on the node, sorted by name" + }, + "own": { + "$ref": "#/defs/Module" + }, + "refused": { + "type": "string", + "description": "why the node could not compose with the candidate at all" + }, + "took": { + "type": "integer", + "description": "how long the engine took, in milliseconds" + }, + "input": { + "type": "object", + "description": "when asked: the composition input it composed from" + } + }, + "required": [ + "v", + "node", + "candidate", + "verdict", + "modules", + "own", + "took" + ] } }, { @@ -244,17 +699,423 @@ } } } + }, + { + "name": "manifest-reads", + "optional": true, + "description": "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "manifest-reads": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline" + }, + "stated-at": { + "type": "string", + "description": "when the engine last stated them, RFC 3339" + } + }, + "required": [ + "v", + "node", + "stated-at" + ], + "description": "what this machine's engine reads of a manifest (ADR 0618 §3)" + } } ], "events": [ { - "name": "asked" + "name": "asked", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } }, { - "name": "done" + "name": "done", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } }, { - "name": "refused" + "name": "refused", + "by-caller": true, + "body": { + "$ref": "#/defs/ActSaid" + } + }, + { + "name": "manifest-reads-changed", + "by-caller": true, + "body": { + "$ref": "#/defs/ManifestReadsChanged" + } } - ] + ], + "family": { + "name": "node-engine.events", + "max-age-days": 7 + }, + "defs": { + "Act": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "the act's id" + }, + "act": { + "type": "string", + "description": "the act: hand-over, access-act, judge-directories or setuid-search" + }, + "subject": { + "type": "string", + "description": "the path of a hand-over, or the access of an access act" + }, + "caller": { + "type": "string", + "description": "the caller as the runtime proved it" + }, + "why": { + "type": "string", + "description": "why it was asked, in words" + }, + "at": { + "type": "string", + "description": "when it was asked, RFC 3339" + }, + "state": { + "type": "string", + "enum": [ + "asked", + "done", + "refused", + "answered" + ], + "description": "where it stands" + }, + "said": { + "type": "string", + "description": "what the engine said of it: its outcome or why it was refused" + }, + "ended": { + "type": "string", + "description": "when it ended, RFC 3339" + }, + "ask": { + "type": "string", + "description": "the operator's ask of a hand-over or an access act" + }, + "sets": { + "type": "string", + "description": "the owner, group and mode it sets, in words" + }, + "disk": { + "type": "string", + "description": "what the engine read on disk when it judged" + }, + "path": { + "type": "string", + "description": "the directory the act changes" + }, + "expires": { + "type": "string", + "description": "when the ask ends unanswered, RFC 3339" + }, + "by": { + "type": "string", + "description": "who answered, as the operator's warrant names them" + }, + "via": { + "type": "string", + "description": "the channel the warrant came on" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the warrant's proofs" + }, + "auto": { + "type": "boolean", + "description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)" + } + }, + "required": [ + "id", + "act", + "caller", + "at", + "state" + ], + "description": "one act asked through the seat, as acts reads it (ADR 0563 §9)" + }, + "ActSaid": { + "type": "object", + "properties": { + "act": { + "type": "string", + "description": "the act" + }, + "subject": { + "type": "string", + "description": "the path or the access" + }, + "caller": { + "type": "string", + "description": "the caller as the runtime proved it" + }, + "ask": { + "type": "string", + "description": "the operator's ask" + }, + "state": { + "type": "string", + "enum": [ + "asked", + "done", + "refused" + ], + "description": "where the act stands" + }, + "said": { + "type": "string", + "description": "what the engine said of it" + }, + "node": { + "type": "string", + "description": "the node whose engine says it" + } + }, + "required": [ + "act", + "caller", + "state", + "node" + ], + "description": "what the seat says of an act: the act, the path or access, the caller as the runtime proved it and the ask (ADR 0563 §1)" + }, + "Acts": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "acts": { + "type": "array", + "items": { + "$ref": "#/defs/Act" + }, + "description": "the acts, newest first" + } + }, + "required": [ + "node", + "acts" + ], + "description": "the acts kept" + }, + "Asked": { + "type": "object", + "properties": { + "asked": { + "type": "string", + "description": "the ask's id: acts shows where it stands" + }, + "node": { + "type": "string", + "description": "the node" + }, + "says": { + "type": "string", + "description": "what happens next, in words" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "description": "the answer of an act the operator is asked" + }, + "ManifestReadsChanged": { + "type": "object", + "properties": { + "v": { + "type": "integer", + "description": "the layout's version" + }, + "node": { + "type": "string", + "description": "the node" + }, + "manifest-reads": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "what its engine states it reads now" + }, + "at": { + "type": "string", + "description": "when it says it, RFC 3339" + }, + "why": { + "type": "string", + "enum": [ + "started", + "returned" + ], + "description": "started: the engine started or what it states changed; returned: its node returned after being away" + } + }, + "required": [ + "v", + "node", + "at", + "why" + ], + "description": "what a node-engine says when what it reads changed or its node returned (ADR 0618 §4)" + }, + "Module": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module, node for the node's own composition" + }, + "fp": { + "type": "string", + "description": "the fingerprint it was composed at" + }, + "state": { + "type": "string", + "enum": [ + "composed", + "refused", + "held" + ], + "description": "composed (digest set), refused by the candidate, or held without it too (words set)" + }, + "digest": { + "type": "string", + "description": "the composition's digest when composed" + }, + "changed": { + "type": "boolean", + "description": "the candidate changes it from the one composed without it" + }, + "words": { + "type": "string", + "description": "why, for a refused or held one" + } + }, + "required": [ + "module", + "state" + ], + "description": "one module's composition with the candidate" + }, + "Revision": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "the composition's key" + }, + "revision": { + "type": "integer", + "description": "its revision" + }, + "digest": { + "type": "string", + "description": "its digest" + }, + "resources": { + "type": "integer", + "description": "how many resources it names" + }, + "latest": { + "type": "boolean", + "description": "the latest revision" + }, + "refused": { + "type": "string", + "description": "why it cannot be read or trusted" + } + }, + "required": [ + "key" + ], + "description": "one revision of a composition key" + }, + "RootVerdict": { + "type": "object", + "properties": { + "machine": { + "type": "string", + "description": "the machine" + }, + "free": { + "type": "boolean", + "description": "the machine is root-free" + }, + "why": { + "type": "string", + "description": "why, in words" + }, + "judged": { + "type": "string", + "description": "when the engine judged it, RFC 3339" + } + }, + "required": [ + "machine", + "free", + "why", + "judged" + ], + "description": "the engine's root-free verdict (ADR 0563 §6)" + }, + "Said": { + "type": "object", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "said": { + "type": "string", + "description": "the engine's judgement or search, one line per directory or program" + } + }, + "required": [ + "node", + "said" + ], + "description": "the answer of a read-only judgement" + } + } } diff --git a/ts/src/index.ts b/ts/src/index.ts index 18984e2..d7d9552 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -10,3 +10,4 @@ export * as npmPackageRegistry from "./npm-package-registry.js"; export * as goPackageRegistry from "./go-package-registry.js"; export * as containerImageRegistry from "./container-image-registry.js"; export * as ociArtifactRegistry from "./oci-artifact-registry.js"; +export * as nodeEngine from "./node-engine.js"; diff --git a/ts/src/node-engine.ts b/ts/src/node-engine.ts new file mode 100644 index 0000000..8c0f71e --- /dev/null +++ b/ts/src/node-engine.ts @@ -0,0 +1,539 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "node-engine", defined whole (seats/node-engine/seat.json): The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token. + +export const seat = "node-engine"; + +export const verbs = { + handOver: "hand-over", + accessAct: "access-act", + judgeDirectories: "judge-directories", + setuidSearch: "setuid-search", + rootFree: "root-free", + acts: "acts", + packages: "packages", + composition: "composition", + compose: "compose", + composeCandidate: "compose-candidate", + pullWaits: "pull-waits", + manifestReads: "manifest-reads", +} as const; + +export const events = { + asked: "asked", + done: "done", + refused: "refused", + manifestReadsChanged: "manifest-reads-changed", +} as const; + +export const family = { name: "node-engine.events", maxAgeDays: 7 } as const; + +export function askedSubject(caller: string): string { + return "mesh.seat.node-engine.event.asked." + caller; +} + +export function doneSubject(caller: string): string { + return "mesh.seat.node-engine.event.done." + caller; +} + +export function refusedSubject(caller: string): string { + return "mesh.seat.node-engine.event.refused." + caller; +} + +export function manifestReadsChangedSubject(caller: string): string { + return "mesh.seat.node-engine.event.manifest-reads-changed." + caller; +} + +/** one act asked through the seat, as acts reads it (ADR 0563 §9) */ +export interface Act { + /** the act: hand-over, access-act, judge-directories or setuid-search */ + "act": string; + /** the operator's ask of a hand-over or an access act */ + "ask"?: string; + /** when it was asked, RFC 3339 */ + "at": string; + /** the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4) */ + "auto"?: boolean; + /** who answered, as the operator's warrant names them */ + "by"?: string; + /** the caller as the runtime proved it */ + "caller": string; + /** what the engine read on disk when it judged */ + "disk"?: string; + /** when it ended, RFC 3339 */ + "ended"?: string; + /** when the ask ends unanswered, RFC 3339 */ + "expires"?: string; + /** the act's id */ + "id": string; + /** the directory the act changes */ + "path"?: string; + /** the warrant's proofs */ + "proofs"?: string[]; + /** what the engine said of it: its outcome or why it was refused */ + "said"?: string; + /** the owner, group and mode it sets, in words */ + "sets"?: string; + /** where it stands */ + "state": "asked" | "done" | "refused" | "answered"; + /** the path of a hand-over, or the access of an access act */ + "subject"?: string; + /** the channel the warrant came on */ + "via"?: string; + /** why it was asked, in words */ + "why"?: string; +} + +/** what the seat says of an act: the act, the path or access, the caller as the runtime proved it and the ask (ADR 0563 §1) */ +export interface ActSaid { + /** the act */ + "act": string; + /** the operator's ask */ + "ask"?: string; + /** the caller as the runtime proved it */ + "caller": string; + /** the node whose engine says it */ + "node": string; + /** what the engine said of it */ + "said"?: string; + /** where the act stands */ + "state": "asked" | "done" | "refused"; + /** the path or the access */ + "subject"?: string; +} + +/** the acts kept */ +export interface Acts { + /** the acts, newest first */ + "acts": Act[]; + /** the node */ + "node": string; +} + +/** the answer of an act the operator is asked */ +export interface Asked { + /** the ask's id: acts shows where it stands */ + "asked": string; + /** the node */ + "node": string; + /** what happens next, in words */ + "says": string; +} + +/** what a node-engine says when what it reads changed or its node returned (ADR 0618 §4) */ +export interface ManifestReadsChanged { + /** when it says it, RFC 3339 */ + "at": string; + /** what its engine states it reads now */ + "manifest-reads"?: string[]; + /** the node */ + "node": string; + /** the layout's version */ + "v": number; + /** started: the engine started or what it states changed; returned: its node returned after being away */ + "why": "started" | "returned"; +} + +/** one module's composition with the candidate */ +export interface Module { + /** the candidate changes it from the one composed without it */ + "changed"?: boolean; + /** the composition's digest when composed */ + "digest"?: string; + /** the fingerprint it was composed at */ + "fp"?: string; + /** the module, node for the node's own composition */ + "module": string; + /** composed (digest set), refused by the candidate, or held without it too (words set) */ + "state": "composed" | "refused" | "held"; + /** why, for a refused or held one */ + "words"?: string; +} + +/** one revision of a composition key */ +export interface Revision { + /** its digest */ + "digest"?: string; + /** the composition's key */ + "key": string; + /** the latest revision */ + "latest"?: boolean; + /** why it cannot be read or trusted */ + "refused"?: string; + /** how many resources it names */ + "resources"?: number; + /** its revision */ + "revision"?: number; +} + +/** the engine's root-free verdict (ADR 0563 §6) */ +export interface RootVerdict { + /** the machine is root-free */ + "free": boolean; + /** when the engine judged it, RFC 3339 */ + "judged": string; + /** the machine */ + "machine": string; + /** why, in words */ + "why": string; +} + +/** the answer of a read-only judgement */ +export interface Said { + /** the node */ + "node": string; + /** the engine's judgement or search, one line per directory or program */ + "said": string; +} + +/** is what hand-over takes. */ +export interface HandOverInput { + /** the directory's absolute path, as the engine states it */ + "path": string; + /** why it is asked, in words the operator reads */ + "why"?: string; +} + +/** is what hand-over answers. */ +export type HandOverAnswer = Asked; + +/** is what access-act takes. */ +export interface AccessActInput { + /** the access, library: */ + "access": string; + /** the act: take */ + "act": string; + /** why it is asked, in words the operator reads */ + "why"?: string; +} + +/** is what access-act answers. */ +export type AccessActAnswer = Asked; + +/** is what judge-directories takes. */ +export interface JudgeDirectoriesInput { +} + +/** is what judge-directories answers. */ +export type JudgeDirectoriesAnswer = Said; + +/** is what setuid-search takes. */ +export interface SetuidSearchInput { + /** why it is asked, in words */ + "why"?: string; +} + +/** is what setuid-search answers. */ +export type SetuidSearchAnswer = Said; + +/** is what root-free takes. */ +export interface RootFreeInput { +} + +/** is what root-free answers. */ +export interface RootFreeAnswer { + /** this machine's verdict */ + "machines": RootVerdict[]; +} + +/** is what acts takes. */ +export interface ActsInput { + /** one act, by its id */ + "id"?: string; + /** at most this many, newest */ + "limit"?: string; +} + +/** is what acts answers. */ +export type ActsAnswer = Acts; + +/** is what packages takes. */ +export interface PackagesInput { + /** "true": write this machine's key again from what the engine composed */ + "write"?: "true" | "false"; +} + +export interface PackagesAnswerKeyPackagesItem { + /** declared absent */ + "absent": boolean; + /** the modules that declare it */ + "modules": string[]; + /** the package */ + "package": string; +} + +/** the node's key as the bus holds it; absent when the engine wrote none */ +export interface PackagesAnswerKey { + /** the node */ + "node": string; + /** its packages sorted by name, absent after present */ + "packages": PackagesAnswerKeyPackagesItem[]; +} + +/** is what packages answers. */ +export interface PackagesAnswer { + /** the node's key as the bus holds it; absent when the engine wrote none */ + "key"?: PackagesAnswerKey; + /** why the key could not be read */ + "key_error"?: string; + /** the node */ + "node": string; + /** a note, such as that no key is written yet */ + "note"?: string; + /** where the key is read: the engine's state packages */ + "source": string; + /** with write: the key was written again (false: it already held what the engine composed) */ + "wrote"?: boolean; +} + +/** is what composition takes. */ +export interface CompositionInput { + /** "true": list the resources that differ from what runs */ + "diff"?: string; + /** a composition's digest (sha256:…), to read that revision rather than the latest */ + "digest"?: string; + /** the module, as placed on this machine; node for the node's own */ + "module": string; +} + +/** the module's last apply that did not end running; absent when none */ +export interface CompositionAnswerAttempt { + /** the want's revision the hold took */ + "answered_revision"?: number; + /** the digest tried */ + "composed"?: string; + /** the composition's revision it was tried at */ + "composition_revision": number; + /** the fingerprint tried */ + "fp": string; + /** held for a failed backup */ + "held"?: boolean; + /** it failed because the bus did not answer for a moment */ + "transient"?: boolean; + /** the want's revision it was tried at */ + "want_revision": number; +} + +/** what the module's have key says now; absent when it says nothing */ +export interface CompositionAnswerHave { + /** digest */ + "composed"?: string; + /** detail */ + "detail"?: string; + /** fingerprint */ + "fp"?: string; + /** judgement */ + "judgement"?: string; + /** running, failed, held or waiting-yes */ + "phase": string; + /** the digest that waits for a yes */ + "waits"?: string; +} + +/** the move from what runs to the composition read */ +export interface CompositionAnswerJudged { + /** the judgement */ + "judgement": string; + /** the agent would wait for a person's yes before applying it */ + "waits_for_yes": boolean; + /** its words */ + "words": string; +} + +/** of the node's own composition, its own fields beside what runs names */ +export interface CompositionAnswerOwn { + /** the revision's adoption; absent for a converged node */ + "adoption"?: Record; + /** groups */ + "groups"?: number; + /** each module the node's composer held in its last pass, with why */ + "held"?: Record; + /** identities the revision names */ + "identities"?: number; + /** libraries */ + "libraries"?: number; + /** the account the provisioner's client runs as, as the revision names it */ + "provisioner_account": string; + /** as what runs names it */ + "runs_provisioner_account"?: string; +} + +/** what runs of the module; absent when nothing runs */ +export interface CompositionAnswerRuns { + /** digest */ + "composed"?: string; + /** fingerprint */ + "fp"?: string; + /** applied, have or kept */ + "from": string; + /** phase */ + "phase"?: string; + /** how many resources run */ + "resources": number; +} + +/** the module's want as the agent holds it; absent when it holds none */ +export interface CompositionAnswerWant { + /** the operator's answer to a failed backup, again or skip */ + "backup"?: string; + /** the composition's digest it names */ + "composed"?: string; + /** its build's fingerprint */ + "fp": string; + /** the module is removed */ + "removed"?: boolean; + /** the revision of the want's key read last */ + "revision": number; + /** the node composes the module itself (ADR 0547 §2) */ + "self"?: boolean; + /** its words */ + "words"?: string; + /** the digest a person said yes to */ + "yes"?: string; +} + +/** is what composition answers. */ +export interface CompositionAnswer { + /** the module's last apply that did not end running; absent when none */ + "attempt"?: CompositionAnswerAttempt; + "composition": Revision; + /** every resource that differs from what runs, when asked */ + "diff"?: Record[]; + /** what the module's have key says now; absent when it says nothing */ + "have"?: CompositionAnswerHave; + /** the move from what runs to the composition read */ + "judged": CompositionAnswerJudged; + /** the module */ + "module": string; + /** the node */ + "node": string; + /** of the node's own composition, its own fields beside what runs names */ + "own"?: CompositionAnswerOwn; + /** the revisions of its key the bucket keeps, oldest first */ + "revisions"?: Revision[]; + /** what runs of the module; absent when nothing runs */ + "runs"?: CompositionAnswerRuns; + /** the agent has looked at the module once since it started */ + "started": boolean; + /** the module's want as the agent holds it; absent when it holds none */ + "want"?: CompositionAnswerWant; +} + +/** is what compose takes. */ +export interface ComposeInput { + /** a module placed on this machine whose apply is also tried again (optional) */ + "module"?: string; +} + +/** how the pass went */ +export interface ComposeAnswerPass { + /** when the node composes again on its own, RFC 3339 */ + "again"?: string; + /** when the pass ended, RFC 3339 */ + "at": string; + /** the answer is the last pass, made moments ago */ + "earlier"?: boolean; + /** how many compositions the pass handed the agent that differ from before */ + "handed": number; + /** each module refused alone, with why */ + "held"?: Record; + /** why the node composed none of its modules */ + "refused"?: string; +} + +/** is what compose answers. */ +export interface ComposeAnswer { + /** how the pass went */ + "pass": ComposeAnswerPass; + /** the module whose apply was tried again */ + "retried"?: string; +} + +/** is what compose-candidate takes. */ +export interface ComposeCandidateInput { + /** the key of the indexer's candidate, . */ + "candidate": string; + /** also answer the composition input the candidate was composed from, for the sandbox path */ + "input"?: boolean; +} + +/** is what compose-candidate answers. */ +export interface ComposeCandidateAnswer { + /** the candidate's key */ + "candidate": string; + /** when asked: the composition input it composed from */ + "input"?: Record; + /** every module placed on the node, sorted by name */ + "modules": Module[]; + /** the node */ + "node": string; + "own": Module; + /** why the node could not compose with the candidate at all */ + "refused"?: string; + /** how long the engine took, in milliseconds */ + "took": number; + /** the layout's version */ + "v": number; + /** the node's verdict (ADR 0593 §7, §9) */ + "verdict": "composed" | "refused" | "unready" | "not-judged"; +} + +/** is what pull-waits takes. */ +export interface PullWaitsInput { +} + +export interface PullWaitsAnswerWaitsItem { + /** that seat's holder, whose settled the retry waits on */ + "holder"?: string; + /** the image whose pull failed */ + "image": string; + /** the module whose apply failed */ + "module": string; + /** when the failed pull began, RFC 3339 */ + "pull-began"?: string; + /** when it was last tried again, RFC 3339 */ + "retry-last"?: string; + /** what started that retry: settled, or the engine's start */ + "retry-trigger"?: string; + /** the seat the image came through */ + "seat"?: string; + /** the settled events of the holder heard since the engine started */ + "settled-count"?: number; + /** when the last was heard, RFC 3339 */ + "settled-last"?: string; +} + +/** is what pull-waits answers. */ +export interface PullWaitsAnswer { + /** every failed pull waiting on a holder */ + "waits": PullWaitsAnswerWaitsItem[]; +} + +/** is what manifest-reads takes. */ +export interface ManifestReadsInput { +} + +/** is what manifest-reads answers. */ +export interface ManifestReadsAnswer { + /** the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline */ + "manifest-reads"?: string[]; + /** the node */ + "node": string; + /** when the engine last stated them, RFC 3339 */ + "stated-at": string; + /** the layout's version */ + "v": number; +} + +/** is the body of the event asked. */ +export type AskedEvent = ActSaid; + +/** is the body of the event done. */ +export type DoneEvent = ActSaid; + +/** is the body of the event refused. */ +export type RefusedEvent = ActSaid; + +/** is the body of the event manifest-reads-changed. */ +export type ManifestReadsChangedEvent = ManifestReadsChanged; + diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 5eb57ff..6ab54ab 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -6432,9 +6432,10 @@ export const list: readonly Seat[] = [ "emits": [ "asked", "done", - "refused" + "refused", + "manifest-reads-changed" ], - "grain": "seat-list", + "grain": "whole", "heldByTheEngine": true, "name": "node-engine", "scope": "node", @@ -6457,7 +6458,30 @@ export const list: readonly Seat[] = [ ], "type": "object" }, - "name": "hand-over" + "name": "hand-over", + "output": { + "description": "the answer of an act the operator is asked", + "properties": { + "asked": { + "description": "the ask's id: acts shows where it stands", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "says": { + "description": "what happens next, in words", + "type": "string" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "type": "object" + } }, { "description": "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", @@ -6482,7 +6506,30 @@ export const list: readonly Seat[] = [ ], "type": "object" }, - "name": "access-act" + "name": "access-act", + "output": { + "description": "the answer of an act the operator is asked", + "properties": { + "asked": { + "description": "the ask's id: acts shows where it stands", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "says": { + "description": "what happens next, in words", + "type": "string" + } + }, + "required": [ + "asked", + "node", + "says" + ], + "type": "object" + } }, { "description": "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", @@ -6490,7 +6537,25 @@ export const list: readonly Seat[] = [ "properties": {}, "type": "object" }, - "name": "judge-directories" + "name": "judge-directories", + "output": { + "description": "the answer of a read-only judgement", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "said": { + "description": "the engine's judgement or search, one line per directory or program", + "type": "string" + } + }, + "required": [ + "node", + "said" + ], + "type": "object" + } }, { "description": "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", @@ -6503,7 +6568,25 @@ export const list: readonly Seat[] = [ }, "type": "object" }, - "name": "setuid-search" + "name": "setuid-search", + "output": { + "description": "the answer of a read-only judgement", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "said": { + "description": "the engine's judgement or search, one line per directory or program", + "type": "string" + } + }, + "required": [ + "node", + "said" + ], + "type": "object" + } }, { "description": "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", @@ -6511,7 +6594,47 @@ export const list: readonly Seat[] = [ "properties": {}, "type": "object" }, - "name": "root-free" + "name": "root-free", + "output": { + "properties": { + "machines": { + "description": "this machine's verdict", + "items": { + "description": "the engine's root-free verdict (ADR 0563 §6)", + "properties": { + "free": { + "description": "the machine is root-free", + "type": "boolean" + }, + "judged": { + "description": "when the engine judged it, RFC 3339", + "type": "string" + }, + "machine": { + "description": "the machine", + "type": "string" + }, + "why": { + "description": "why, in words", + "type": "string" + } + }, + "required": [ + "machine", + "free", + "why", + "judged" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "machines" + ], + "type": "object" + } }, { "description": "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): each hand-over, access act and setuid search, open, done or refused, with who asked, the ask and the operator's warrant. Read-only.", @@ -6528,7 +6651,119 @@ export const list: readonly Seat[] = [ }, "type": "object" }, - "name": "acts" + "name": "acts", + "output": { + "description": "the acts kept", + "properties": { + "acts": { + "description": "the acts, newest first", + "items": { + "description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", + "properties": { + "act": { + "description": "the act: hand-over, access-act, judge-directories or setuid-search", + "type": "string" + }, + "ask": { + "description": "the operator's ask of a hand-over or an access act", + "type": "string" + }, + "at": { + "description": "when it was asked, RFC 3339", + "type": "string" + }, + "auto": { + "description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", + "type": "boolean" + }, + "by": { + "description": "who answered, as the operator's warrant names them", + "type": "string" + }, + "caller": { + "description": "the caller as the runtime proved it", + "type": "string" + }, + "disk": { + "description": "what the engine read on disk when it judged", + "type": "string" + }, + "ended": { + "description": "when it ended, RFC 3339", + "type": "string" + }, + "expires": { + "description": "when the ask ends unanswered, RFC 3339", + "type": "string" + }, + "id": { + "description": "the act's id", + "type": "string" + }, + "path": { + "description": "the directory the act changes", + "type": "string" + }, + "proofs": { + "description": "the warrant's proofs", + "items": { + "type": "string" + }, + "type": "array" + }, + "said": { + "description": "what the engine said of it: its outcome or why it was refused", + "type": "string" + }, + "sets": { + "description": "the owner, group and mode it sets, in words", + "type": "string" + }, + "state": { + "description": "where it stands", + "enum": [ + "asked", + "done", + "refused", + "answered" + ], + "type": "string" + }, + "subject": { + "description": "the path of a hand-over, or the access of an access act", + "type": "string" + }, + "via": { + "description": "the channel the warrant came on", + "type": "string" + }, + "why": { + "description": "why it was asked, in words", + "type": "string" + } + }, + "required": [ + "id", + "act", + "caller", + "at", + "state" + ], + "type": "object" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + } + }, + "required": [ + "node", + "acts" + ], + "type": "object" + } }, { "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.", @@ -6546,7 +6781,79 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "packages", - "optional": true + "optional": true, + "output": { + "properties": { + "key": { + "description": "the node's key as the bus holds it; absent when the engine wrote none", + "properties": { + "node": { + "description": "the node", + "type": "string" + }, + "packages": { + "description": "its packages sorted by name, absent after present", + "items": { + "properties": { + "absent": { + "description": "declared absent", + "type": "boolean" + }, + "modules": { + "description": "the modules that declare it", + "items": { + "type": "string" + }, + "type": "array" + }, + "package": { + "description": "the package", + "type": "string" + } + }, + "required": [ + "package", + "modules", + "absent" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "node", + "packages" + ], + "type": "object" + }, + "key_error": { + "description": "why the key could not be read", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "note": { + "description": "a note, such as that no key is written yet", + "type": "string" + }, + "source": { + "description": "where the key is read: the engine's state packages", + "type": "string" + }, + "wrote": { + "description": "with write: the key was written again (false: it already held what the engine composed)", + "type": "boolean" + } + }, + "required": [ + "node", + "source" + ], + "type": "object" + } }, { "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", @@ -6571,7 +6878,316 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "composition", - "optional": true + "optional": true, + "output": { + "properties": { + "attempt": { + "description": "the module's last apply that did not end running; absent when none", + "properties": { + "answered_revision": { + "description": "the want's revision the hold took", + "type": "integer" + }, + "composed": { + "description": "the digest tried", + "type": "string" + }, + "composition_revision": { + "description": "the composition's revision it was tried at", + "type": "integer" + }, + "fp": { + "description": "the fingerprint tried", + "type": "string" + }, + "held": { + "description": "held for a failed backup", + "type": "boolean" + }, + "transient": { + "description": "it failed because the bus did not answer for a moment", + "type": "boolean" + }, + "want_revision": { + "description": "the want's revision it was tried at", + "type": "integer" + } + }, + "required": [ + "fp", + "want_revision", + "composition_revision" + ], + "type": "object" + }, + "composition": { + "description": "one revision of a composition key", + "properties": { + "digest": { + "description": "its digest", + "type": "string" + }, + "key": { + "description": "the composition's key", + "type": "string" + }, + "latest": { + "description": "the latest revision", + "type": "boolean" + }, + "refused": { + "description": "why it cannot be read or trusted", + "type": "string" + }, + "resources": { + "description": "how many resources it names", + "type": "integer" + }, + "revision": { + "description": "its revision", + "type": "integer" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "diff": { + "description": "every resource that differs from what runs, when asked", + "items": { + "description": "one resource added, removed or changed, as the engine's judge says it", + "type": "object" + }, + "type": "array" + }, + "have": { + "description": "what the module's have key says now; absent when it says nothing", + "properties": { + "composed": { + "description": "digest", + "type": "string" + }, + "detail": { + "description": "detail", + "type": "string" + }, + "fp": { + "description": "fingerprint", + "type": "string" + }, + "judgement": { + "description": "judgement", + "type": "string" + }, + "phase": { + "description": "running, failed, held or waiting-yes", + "type": "string" + }, + "waits": { + "description": "the digest that waits for a yes", + "type": "string" + } + }, + "required": [ + "phase" + ], + "type": "object" + }, + "judged": { + "description": "the move from what runs to the composition read", + "properties": { + "judgement": { + "description": "the judgement", + "type": "string" + }, + "waits_for_yes": { + "description": "the agent would wait for a person's yes before applying it", + "type": "boolean" + }, + "words": { + "description": "its words", + "type": "string" + } + }, + "required": [ + "judgement", + "words", + "waits_for_yes" + ], + "type": "object" + }, + "module": { + "description": "the module", + "type": "string" + }, + "node": { + "description": "the node", + "type": "string" + }, + "own": { + "description": "of the node's own composition, its own fields beside what runs names", + "properties": { + "adoption": { + "description": "the revision's adoption; absent for a converged node", + "type": "object" + }, + "groups": { + "description": "groups", + "type": "integer" + }, + "held": { + "additionalProperties": { + "type": "string" + }, + "description": "each module the node's composer held in its last pass, with why", + "type": "object" + }, + "identities": { + "description": "identities the revision names", + "type": "integer" + }, + "libraries": { + "description": "libraries", + "type": "integer" + }, + "provisioner_account": { + "description": "the account the provisioner's client runs as, as the revision names it", + "type": "string" + }, + "runs_provisioner_account": { + "description": "as what runs names it", + "type": "string" + } + }, + "required": [ + "provisioner_account" + ], + "type": "object" + }, + "revisions": { + "description": "the revisions of its key the bucket keeps, oldest first", + "items": { + "description": "one revision of a composition key", + "properties": { + "digest": { + "description": "its digest", + "type": "string" + }, + "key": { + "description": "the composition's key", + "type": "string" + }, + "latest": { + "description": "the latest revision", + "type": "boolean" + }, + "refused": { + "description": "why it cannot be read or trusted", + "type": "string" + }, + "resources": { + "description": "how many resources it names", + "type": "integer" + }, + "revision": { + "description": "its revision", + "type": "integer" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "type": "array" + }, + "runs": { + "description": "what runs of the module; absent when nothing runs", + "properties": { + "composed": { + "description": "digest", + "type": "string" + }, + "fp": { + "description": "fingerprint", + "type": "string" + }, + "from": { + "description": "applied, have or kept", + "type": "string" + }, + "phase": { + "description": "phase", + "type": "string" + }, + "resources": { + "description": "how many resources run", + "type": "integer" + } + }, + "required": [ + "from", + "resources" + ], + "type": "object" + }, + "started": { + "description": "the agent has looked at the module once since it started", + "type": "boolean" + }, + "want": { + "description": "the module's want as the agent holds it; absent when it holds none", + "properties": { + "backup": { + "description": "the operator's answer to a failed backup, again or skip", + "type": "string" + }, + "composed": { + "description": "the composition's digest it names", + "type": "string" + }, + "fp": { + "description": "its build's fingerprint", + "type": "string" + }, + "removed": { + "description": "the module is removed", + "type": "boolean" + }, + "revision": { + "description": "the revision of the want's key read last", + "type": "integer" + }, + "self": { + "description": "the node composes the module itself (ADR 0547 §2)", + "type": "boolean" + }, + "words": { + "description": "its words", + "type": "string" + }, + "yes": { + "description": "the digest a person said yes to", + "type": "string" + } + }, + "required": [ + "fp", + "revision" + ], + "type": "object" + } + }, + "required": [ + "node", + "module", + "started", + "composition", + "judged" + ], + "type": "object" + } }, { "description": "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", @@ -6585,7 +7201,56 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "compose", - "optional": true + "optional": true, + "output": { + "properties": { + "pass": { + "description": "how the pass went", + "properties": { + "again": { + "description": "when the node composes again on its own, RFC 3339", + "type": "string" + }, + "at": { + "description": "when the pass ended, RFC 3339", + "type": "string" + }, + "earlier": { + "description": "the answer is the last pass, made moments ago", + "type": "boolean" + }, + "handed": { + "description": "how many compositions the pass handed the agent that differ from before", + "type": "integer" + }, + "held": { + "additionalProperties": { + "type": "string" + }, + "description": "each module refused alone, with why", + "type": "object" + }, + "refused": { + "description": "why the node composed none of its modules", + "type": "string" + } + }, + "required": [ + "at", + "handed" + ], + "type": "object" + }, + "retried": { + "description": "the module whose apply was tried again", + "type": "string" + } + }, + "required": [ + "pass" + ], + "type": "object" + } }, { "description": "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", @@ -6596,12 +7261,8 @@ export const list: readonly Seat[] = [ "type": "string" }, "input": { - "description": "\"true\": also answer the composition input the candidate was composed from, for the sandbox path", - "enum": [ - "true", - "false" - ], - "type": "string" + "description": "also answer the composition input the candidate was composed from, for the sandbox path", + "type": "boolean" } }, "required": [ @@ -6610,7 +7271,137 @@ export const list: readonly Seat[] = [ "type": "object" }, "name": "compose-candidate", - "optional": true + "optional": true, + "output": { + "properties": { + "candidate": { + "description": "the candidate's key", + "type": "string" + }, + "input": { + "description": "when asked: the composition input it composed from", + "type": "object" + }, + "modules": { + "description": "every module placed on the node, sorted by name", + "items": { + "description": "one module's composition with the candidate", + "properties": { + "changed": { + "description": "the candidate changes it from the one composed without it", + "type": "boolean" + }, + "digest": { + "description": "the composition's digest when composed", + "type": "string" + }, + "fp": { + "description": "the fingerprint it was composed at", + "type": "string" + }, + "module": { + "description": "the module, node for the node's own composition", + "type": "string" + }, + "state": { + "description": "composed (digest set), refused by the candidate, or held without it too (words set)", + "enum": [ + "composed", + "refused", + "held" + ], + "type": "string" + }, + "words": { + "description": "why, for a refused or held one", + "type": "string" + } + }, + "required": [ + "module", + "state" + ], + "type": "object" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + }, + "own": { + "description": "one module's composition with the candidate", + "properties": { + "changed": { + "description": "the candidate changes it from the one composed without it", + "type": "boolean" + }, + "digest": { + "description": "the composition's digest when composed", + "type": "string" + }, + "fp": { + "description": "the fingerprint it was composed at", + "type": "string" + }, + "module": { + "description": "the module, node for the node's own composition", + "type": "string" + }, + "state": { + "description": "composed (digest set), refused by the candidate, or held without it too (words set)", + "enum": [ + "composed", + "refused", + "held" + ], + "type": "string" + }, + "words": { + "description": "why, for a refused or held one", + "type": "string" + } + }, + "required": [ + "module", + "state" + ], + "type": "object" + }, + "refused": { + "description": "why the node could not compose with the candidate at all", + "type": "string" + }, + "took": { + "description": "how long the engine took, in milliseconds", + "type": "integer" + }, + "v": { + "description": "the layout's version", + "type": "integer" + }, + "verdict": { + "description": "the node's verdict (ADR 0593 §7, §9)", + "enum": [ + "composed", + "refused", + "unready", + "not-judged" + ], + "type": "string" + } + }, + "required": [ + "v", + "node", + "candidate", + "verdict", + "modules", + "own", + "took" + ], + "type": "object" + } }, { "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", @@ -6677,6 +7468,46 @@ export const list: readonly Seat[] = [ ], "type": "object" } + }, + { + "description": "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", + "input": { + "properties": {}, + "type": "object" + }, + "name": "manifest-reads", + "optional": true, + "output": { + "description": "what this machine's engine reads of a manifest (ADR 0618 §3)", + "properties": { + "manifest-reads": { + "description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline", + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "node": { + "description": "the node", + "type": "string" + }, + "stated-at": { + "description": "when the engine last stated them, RFC 3339", + "type": "string" + }, + "v": { + "description": "the layout's version", + "type": "integer" + } + }, + "required": [ + "v", + "node", + "stated-at" + ], + "type": "object" + } } ] }, -- 2.54.0 From f31de828e92e70a3afb7c4d3dece3d90dc9c8741 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:42:16 +0100 Subject: [PATCH 18/94] SD-M2b: mesh-broker's events on mesh.seat.mesh-broker.said. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lead's ruling of 12:5x (design 25 § 12.3): mesh-broker's events ride the bus module's own stream, 30 days, on mesh.seat.mesh-broker.said., outside the shared events stream's mesh.seat.*.event.>, so the bus never refuses the two as overlapping. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- format/seat-definition.schema.json | 16 +++++++++++- go/seats/meshbroker/contract.go | 21 ++++++++------- go/seats/meshbroker/contract_test.go | 36 +++++++++++++------------- seats/mesh-broker/fixtures/events.json | 18 ++++++------- seats/mesh-broker/seat.json | 3 ++- ts/src/mesh-broker.ts | 20 +++++++------- 6 files changed, 66 insertions(+), 48 deletions(-) diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index d78ab51..b27097f 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -83,7 +83,7 @@ "items": { "$ref": "#/$defs/message" }, - "description": "published on mesh.seat..event." + "description": "published on mesh.seat..., the token event unless the family names its own" }, "family": { "type": "object", @@ -103,6 +103,20 @@ "max-mib": { "type": "integer", "minimum": 1 + }, + "token": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$", + "not": { + "enum": [ + "event", + "accept", + "tool", + "call", + "proof" + ] + }, + "description": "the subject's word after the seat's name, event when absent: a family kept in a stream of its own names another, outside the shared events stream's mesh.seat.*.event.>" } } }, diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 374d7de..c188609 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -46,32 +46,35 @@ const ( FamilyMaxMiB = 256 ) +// FamilySubjects is what the family's own stream takes, outside the shared events stream's subjects. +const FamilySubjects = "mesh.seat.mesh-broker.said.>" + // SubjectCreated is where created is published. -const SubjectCreated = "mesh.seat.mesh-broker.event.created" +const SubjectCreated = "mesh.seat.mesh-broker.said.created" // SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.mesh-broker.event.refused" +const SubjectRefused = "mesh.seat.mesh-broker.said.refused" // SubjectDrifted is where drifted is published. -const SubjectDrifted = "mesh.seat.mesh-broker.event.drifted" +const SubjectDrifted = "mesh.seat.mesh-broker.said.drifted" // SubjectDriftCleared is where drift-cleared is published. -const SubjectDriftCleared = "mesh.seat.mesh-broker.event.drift-cleared" +const SubjectDriftCleared = "mesh.seat.mesh-broker.said.drift-cleared" // SubjectFull is where full is published. -const SubjectFull = "mesh.seat.mesh-broker.event.full" +const SubjectFull = "mesh.seat.mesh-broker.said.full" // SubjectFullCleared is where full-cleared is published. -const SubjectFullCleared = "mesh.seat.mesh-broker.event.full-cleared" +const SubjectFullCleared = "mesh.seat.mesh-broker.said.full-cleared" // SubjectOrphaned is where orphaned is published. -const SubjectOrphaned = "mesh.seat.mesh-broker.event.orphaned" +const SubjectOrphaned = "mesh.seat.mesh-broker.said.orphaned" // SubjectRecreated is where recreated is published. -const SubjectRecreated = "mesh.seat.mesh-broker.event.recreated" +const SubjectRecreated = "mesh.seat.mesh-broker.said.recreated" // SubjectDropped is where dropped is published. -const SubjectDropped = "mesh.seat.mesh-broker.event.dropped" +const SubjectDropped = "mesh.seat.mesh-broker.said.dropped" // Binding is the binding a module receives in its membership for one entry (ADR 0595 §1, §7). type Binding struct { diff --git a/go/seats/meshbroker/contract_test.go b/go/seats/meshbroker/contract_test.go index 03c1f9c..49861f6 100644 --- a/go/seats/meshbroker/contract_test.go +++ b/go/seats/meshbroker/contract_test.go @@ -17,24 +17,24 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "created on the seat's subject", subject: "mesh.seat.mesh-broker.event.created", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v CreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.created$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.mesh-broker.event.refused", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.refused$")}, - {name: "drifted on the seat's subject", subject: "mesh.seat.mesh-broker.event.drifted", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v DriftedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drifted$")}, - {name: "drift-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.drift-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v DriftClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drift-cleared$")}, - {name: "full on the seat's subject", subject: "mesh.seat.mesh-broker.event.full", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v FullEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full$")}, - {name: "full-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.full-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v FullClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full-cleared$")}, - {name: "orphaned on the seat's subject", subject: "mesh.seat.mesh-broker.event.orphaned", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v OrphanedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.orphaned$")}, - {name: "recreated on the seat's subject", subject: "mesh.seat.mesh-broker.event.recreated", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.recreated$")}, - {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.event.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.dropped$")}, + {name: "created on the seat's subject", subject: "mesh.seat.mesh-broker.said.created", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v CreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.created$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.mesh-broker.said.refused", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.refused$")}, + {name: "drifted on the seat's subject", subject: "mesh.seat.mesh-broker.said.drifted", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DriftedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.drifted$")}, + {name: "drift-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.said.drift-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v DriftClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.drift-cleared$")}, + {name: "full on the seat's subject", subject: "mesh.seat.mesh-broker.said.full", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.full$")}, + {name: "full-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.said.full-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.full-cleared$")}, + {name: "orphaned on the seat's subject", subject: "mesh.seat.mesh-broker.said.orphaned", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v OrphanedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.orphaned$")}, + {name: "recreated on the seat's subject", subject: "mesh.seat.mesh-broker.said.recreated", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.recreated$")}, + {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.said.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.dropped$")}, {name: "create one entry", subject: "", given: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", decode: func(b []byte) (any, error) { var v CreateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "drop on the operator's warrant", subject: "", given: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", wire: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", diff --git a/seats/mesh-broker/fixtures/events.json b/seats/mesh-broker/fixtures/events.json index 374448d..c6167bd 100644 --- a/seats/mesh-broker/fixtures/events.json +++ b/seats/mesh-broker/fixtures/events.json @@ -2,7 +2,7 @@ { "name": "created on the seat's subject", "of": "event:created", - "subject": "mesh.seat.mesh-broker.event.created", + "subject": "mesh.seat.mesh-broker.said.created", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -25,7 +25,7 @@ { "name": "refused on the seat's subject", "of": "event:refused", - "subject": "mesh.seat.mesh-broker.event.refused", + "subject": "mesh.seat.mesh-broker.said.refused", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -49,7 +49,7 @@ { "name": "drifted on the seat's subject", "of": "event:drifted", - "subject": "mesh.seat.mesh-broker.event.drifted", + "subject": "mesh.seat.mesh-broker.said.drifted", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -73,7 +73,7 @@ { "name": "drift-cleared on the seat's subject", "of": "event:drift-cleared", - "subject": "mesh.seat.mesh-broker.event.drift-cleared", + "subject": "mesh.seat.mesh-broker.said.drift-cleared", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -96,7 +96,7 @@ { "name": "full on the seat's subject", "of": "event:full", - "subject": "mesh.seat.mesh-broker.event.full", + "subject": "mesh.seat.mesh-broker.said.full", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -119,7 +119,7 @@ { "name": "full-cleared on the seat's subject", "of": "event:full-cleared", - "subject": "mesh.seat.mesh-broker.event.full-cleared", + "subject": "mesh.seat.mesh-broker.said.full-cleared", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -142,7 +142,7 @@ { "name": "orphaned on the seat's subject", "of": "event:orphaned", - "subject": "mesh.seat.mesh-broker.event.orphaned", + "subject": "mesh.seat.mesh-broker.said.orphaned", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -165,7 +165,7 @@ { "name": "recreated on the seat's subject", "of": "event:recreated", - "subject": "mesh.seat.mesh-broker.event.recreated", + "subject": "mesh.seat.mesh-broker.said.recreated", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -189,7 +189,7 @@ { "name": "dropped on the seat's subject", "of": "event:dropped", - "subject": "mesh.seat.mesh-broker.event.dropped", + "subject": "mesh.seat.mesh-broker.said.dropped", "given": { "module": "mesh-knowledge", "local-name": "ledger", diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index d551eb7..167b3d7 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -252,7 +252,8 @@ "family": { "name": "mesh-broker.events", "max-age-days": 30, - "max-mib": 256 + "max-mib": 256, + "token": "said" }, "provisions": [ { diff --git a/ts/src/mesh-broker.ts b/ts/src/mesh-broker.ts index 01c8c50..bb3d52c 100644 --- a/ts/src/mesh-broker.ts +++ b/ts/src/mesh-broker.ts @@ -27,25 +27,25 @@ export const events = { dropped: "dropped", } as const; -export const family = { name: "mesh-broker.events", maxAgeDays: 30, maxMiB: 256 } as const; +export const family = { name: "mesh-broker.events", maxAgeDays: 30, maxMiB: 256, subjects: "mesh.seat.mesh-broker.said.>" } as const; -export const createdSubject = "mesh.seat.mesh-broker.event.created"; +export const createdSubject = "mesh.seat.mesh-broker.said.created"; -export const refusedSubject = "mesh.seat.mesh-broker.event.refused"; +export const refusedSubject = "mesh.seat.mesh-broker.said.refused"; -export const driftedSubject = "mesh.seat.mesh-broker.event.drifted"; +export const driftedSubject = "mesh.seat.mesh-broker.said.drifted"; -export const driftClearedSubject = "mesh.seat.mesh-broker.event.drift-cleared"; +export const driftClearedSubject = "mesh.seat.mesh-broker.said.drift-cleared"; -export const fullSubject = "mesh.seat.mesh-broker.event.full"; +export const fullSubject = "mesh.seat.mesh-broker.said.full"; -export const fullClearedSubject = "mesh.seat.mesh-broker.event.full-cleared"; +export const fullClearedSubject = "mesh.seat.mesh-broker.said.full-cleared"; -export const orphanedSubject = "mesh.seat.mesh-broker.event.orphaned"; +export const orphanedSubject = "mesh.seat.mesh-broker.said.orphaned"; -export const recreatedSubject = "mesh.seat.mesh-broker.event.recreated"; +export const recreatedSubject = "mesh.seat.mesh-broker.said.recreated"; -export const droppedSubject = "mesh.seat.mesh-broker.event.dropped"; +export const droppedSubject = "mesh.seat.mesh-broker.said.dropped"; /** the binding a module receives in its membership for one entry (ADR 0595 §1, §7) */ export interface Binding { -- 2.54.0 From 87412fd307864370c3ed9730d2ad13b301ef9a60 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:42:35 +0100 Subject: [PATCH 19/94] SD-M2d: operator-channel's decided kept 7 days, on the shared events stream The lead's ruling of 12:5x: no record fixes 30 days; decided rides the shared events stream, 7 days, as on the bus today. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/operatorchannel/contract.go | 2 +- seats/operator-channel/seat.json | 2 +- ts/src/operator-channel.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go index 86a4786..7e96e3e 100644 --- a/go/seats/operatorchannel/contract.go +++ b/go/seats/operatorchannel/contract.go @@ -37,7 +37,7 @@ func Events() []string { // Family is where the seat's events are kept. const ( Family = "operator-channel.decided" - FamilyMaxAgeDays = 30 + FamilyMaxAgeDays = 7 ) // SubjectDecided is where decided is published for one caller. diff --git a/seats/operator-channel/seat.json b/seats/operator-channel/seat.json index 26d0e95..12fb625 100644 --- a/seats/operator-channel/seat.json +++ b/seats/operator-channel/seat.json @@ -170,7 +170,7 @@ ], "family": { "name": "operator-channel.decided", - "max-age-days": 30 + "max-age-days": 7 }, "records": [ { diff --git a/ts/src/operator-channel.ts b/ts/src/operator-channel.ts index adb1049..e8ad2ba 100644 --- a/ts/src/operator-channel.ts +++ b/ts/src/operator-channel.ts @@ -22,7 +22,7 @@ export const events = { decided: "decided", } as const; -export const family = { name: "operator-channel.decided", maxAgeDays: 30 } as const; +export const family = { name: "operator-channel.decided", maxAgeDays: 7 } as const; export function decidedSubject(caller: string): string { return "mesh.seat.operator-channel.event.decided." + caller; -- 2.54.0 From c62ce1c309d7f8cd64722b17ea285081bd6e527e Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:48:57 +0100 Subject: [PATCH 20/94] ADR 0625: each seat defined whole is a Go module and a package artifact of its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0625 §1-§3 (accepted 12:45): the seat set stays the root Go module's package; each seat defined whole is a Go module of its own, since the fingerprint counts a Go module's go.mod and go.sum, so a change of one seat moves only its importers. A seat's module imports nothing: the registry part's types are written into each registry seat whole, so no seat module requires a sibling's version. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/parts/registry/contract.go | 173 ----------------- go/seats/containerimageregistry/contract.go | 200 +++++++++++++++----- go/seats/containerimageregistry/go.mod | 3 + go/seats/gopackageregistry/contract.go | 174 +++++++++++++---- go/seats/gopackageregistry/go.mod | 3 + go/seats/meshbroker/go.mod | 3 + go/seats/meshconfig/go.mod | 3 + go/seats/meshdeliverychecker/go.mod | 3 + go/seats/meshvault/go.mod | 3 + go/seats/npmpackageregistry/contract.go | 174 +++++++++++++---- go/seats/npmpackageregistry/go.mod | 3 + go/seats/ociartifactregistry/contract.go | 200 +++++++++++++++----- go/seats/ociartifactregistry/go.mod | 3 + go/seats/operatorchannel/go.mod | 3 + module.json | 9 + 15 files changed, 630 insertions(+), 327 deletions(-) delete mode 100644 go/parts/registry/contract.go create mode 100644 go/seats/containerimageregistry/go.mod create mode 100644 go/seats/gopackageregistry/go.mod create mode 100644 go/seats/meshbroker/go.mod create mode 100644 go/seats/meshconfig/go.mod create mode 100644 go/seats/meshdeliverychecker/go.mod create mode 100644 go/seats/meshvault/go.mod create mode 100644 go/seats/npmpackageregistry/go.mod create mode 100644 go/seats/ociartifactregistry/go.mod create mode 100644 go/seats/operatorchannel/go.mod diff --git a/go/parts/registry/contract.go b/go/parts/registry/contract.go deleted file mode 100644 index e155910..0000000 --- a/go/parts/registry/contract.go +++ /dev/null @@ -1,173 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package registry is the part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. -package registry - -const ( - VerbPublish = "publish" - VerbPublishPart = "publish-part" - VerbVersions = "versions" - VerbRemove = "remove" -) - -const ( - EventPublished = "published" - EventRefused = "refused" - EventRemoved = "removed" -) - -// Artifact is one published thing, as every registry event and answer names it. -type Artifact struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name - Name string `json:"name"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// Outcome is the body of published, refused and removed. -type Outcome struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // the module holding the seat that said it - Holder string `json:"holder"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name - Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) - Reason string `json:"reason,omitempty"` - // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) - Reference string `json:"reference,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer it ends, when it ended one - Transfer string `json:"transfer,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// Part is one part of a transfer. -type Part struct { - // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob - Digest string `json:"digest"` - // its size in bytes - Size int64 `json:"size"` -} - -// PublishInput is what publish takes. -type PublishInput struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name - Name string `json:"name"` - // every part's digest and size; none when the digest is already held - Parts []Part `json:"parts,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer's name, the caller's own; the same name opens the same transfer again - Transfer string `json:"transfer"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// PublishAnswer is what publish answers. -type PublishAnswer struct { - // the digest is held already: nothing to send; the outcome follows as an event - Held bool `json:"held,omitempty"` - // the digests of the parts the holder lacks, to send by publish-part - Lacks []string `json:"lacks,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartInput is what publish-part takes. -type PublishPartInput struct { - // the chunk's bytes, base64 (at most 760 KiB before encoding) - Data string `json:"data"` - // this chunk is the part's last - Last bool `json:"last"` - // the part's digest - Part string `json:"part"` - // the chunk's place in its part, from 0 - Sequence int64 `json:"sequence"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartAnswer is what publish-part answers. -type PublishPartAnswer struct { - // the part is whole and its bytes match its digest - Complete bool `json:"complete,omitempty"` - // the part - Part string `json:"part"` - // the chunks of the part held so far - Received int64 `json:"received,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// VersionsInput is what versions takes. -type VersionsInput struct { - // narrowed to one kind (optional) - Kind string `json:"kind,omitempty"` - // the name - Name string `json:"name"` -} - -// VersionsAnswer is what versions answers. -type VersionsAnswer struct { - // the name - Name string `json:"name"` - // newest first - Versions []Artifact `json:"versions"` -} - -// RemoveInput is what remove takes. -type RemoveInput struct { - // the digest (for a bundle, archive or copied base) - Digest string `json:"digest,omitempty"` - // the name - Name string `json:"name"` - // why it goes - Reason string `json:"reason"` - // the tag (for a tagged version) - Tag string `json:"tag,omitempty"` - // the version - Version string `json:"version,omitempty"` -} - -// RemoveAnswer is what remove answers. -type RemoveAnswer struct { - Artifact *Artifact `json:"artifact,omitempty"` - // it is gone - Removed bool `json:"removed"` -} - -// PublishedEvent is the body of the event published. -type PublishedEvent = Outcome - -// RefusedEvent is the body of the event refused. -type RefusedEvent = Outcome - -// RemovedEvent is the body of the event removed. -type RemovedEvent = Outcome diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index c18b82c..4037d21 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -3,10 +3,6 @@ // Package containerimageregistry is the contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. package containerimageregistry -import ( - "git.novox.be/novox/mesh-contracts/go/parts/registry" -) - // Seat is the seat's name. const Seat = "container-image-registry" @@ -60,47 +56,23 @@ const SubjectHeld = "mesh.seat.container-image-registry.event.held" // SubjectReleased is where released is published. const SubjectReleased = "mesh.seat.container-image-registry.event.released" -// Artifact is the part registry's. -type Artifact = registry.Artifact - -// Outcome is the part registry's. -type Outcome = registry.Outcome - -// Part is the part registry's. -type Part = registry.Part - -// PublishAnswer is the part registry's. -type PublishAnswer = registry.PublishAnswer - -// PublishInput is the part registry's. -type PublishInput = registry.PublishInput - -// PublishPartAnswer is the part registry's. -type PublishPartAnswer = registry.PublishPartAnswer - -// PublishPartInput is the part registry's. -type PublishPartInput = registry.PublishPartInput - -// PublishedEvent is the part registry's. -type PublishedEvent = registry.PublishedEvent - -// RefusedEvent is the part registry's. -type RefusedEvent = registry.RefusedEvent - -// RemoveAnswer is the part registry's. -type RemoveAnswer = registry.RemoveAnswer - -// RemoveInput is the part registry's. -type RemoveInput = registry.RemoveInput - -// RemovedEvent is the part registry's. -type RemovedEvent = registry.RemovedEvent - -// VersionsAnswer is the part registry's. -type VersionsAnswer = registry.VersionsAnswer - -// VersionsInput is the part registry's. -type VersionsInput = registry.VersionsInput +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} // Hold is the body of held and released. type Hold struct { @@ -118,6 +90,135 @@ type Hold struct { Reason string `json:"reason,omitempty"` } +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it is gone + Removed bool `json:"removed"` +} + // MirrorInput is what mirror takes. type MirrorInput struct { // its pinned digest, sha256: @@ -192,6 +293,15 @@ type ReleaseAnswer struct { Released bool `json:"released"` } +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/containerimageregistry/go.mod b/go/seats/containerimageregistry/go.mod new file mode 100644 index 0000000..3ee2145 --- /dev/null +++ b/go/seats/containerimageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry + +go 1.26.0 diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 5b3fb87..d63ad9b 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -3,10 +3,6 @@ // Package gopackageregistry is the contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. package gopackageregistry -import ( - "git.novox.be/novox/mesh-contracts/go/parts/registry" -) - // Seat is the seat's name. const Seat = "go-package-registry" @@ -48,47 +44,161 @@ const SubjectRefused = "mesh.seat.go-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" -// Artifact is the part registry's. -type Artifact = registry.Artifact +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// Outcome is the part registry's. -type Outcome = registry.Outcome +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// Part is the part registry's. -type Part = registry.Part +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} -// PublishAnswer is the part registry's. -type PublishAnswer = registry.PublishAnswer +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// PublishInput is the part registry's. -type PublishInput = registry.PublishInput +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishPartAnswer is the part registry's. -type PublishPartAnswer = registry.PublishPartAnswer +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishPartInput is the part registry's. -type PublishPartInput = registry.PublishPartInput +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishedEvent is the part registry's. -type PublishedEvent = registry.PublishedEvent +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} -// RefusedEvent is the part registry's. -type RefusedEvent = registry.RefusedEvent +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} -// RemoveAnswer is the part registry's. -type RemoveAnswer = registry.RemoveAnswer +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} -// RemoveInput is the part registry's. -type RemoveInput = registry.RemoveInput +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it is gone + Removed bool `json:"removed"` +} -// RemovedEvent is the part registry's. -type RemovedEvent = registry.RemovedEvent +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome -// VersionsAnswer is the part registry's. -type VersionsAnswer = registry.VersionsAnswer +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome -// VersionsInput is the part registry's. -type VersionsInput = registry.VersionsInput +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome // GoPackageRegistryForm is the form of the provision go-package-registry. type GoPackageRegistryForm struct { diff --git a/go/seats/gopackageregistry/go.mod b/go/seats/gopackageregistry/go.mod new file mode 100644 index 0000000..da0340e --- /dev/null +++ b/go/seats/gopackageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry + +go 1.26.0 diff --git a/go/seats/meshbroker/go.mod b/go/seats/meshbroker/go.mod new file mode 100644 index 0000000..b84250f --- /dev/null +++ b/go/seats/meshbroker/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/meshbroker + +go 1.26.0 diff --git a/go/seats/meshconfig/go.mod b/go/seats/meshconfig/go.mod new file mode 100644 index 0000000..7f014f1 --- /dev/null +++ b/go/seats/meshconfig/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/meshconfig + +go 1.26.0 diff --git a/go/seats/meshdeliverychecker/go.mod b/go/seats/meshdeliverychecker/go.mod new file mode 100644 index 0000000..c0318ae --- /dev/null +++ b/go/seats/meshdeliverychecker/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker + +go 1.26.0 diff --git a/go/seats/meshvault/go.mod b/go/seats/meshvault/go.mod new file mode 100644 index 0000000..10f7a40 --- /dev/null +++ b/go/seats/meshvault/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/meshvault + +go 1.26.0 diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 5489417..0f60980 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -3,10 +3,6 @@ // Package npmpackageregistry is the contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. package npmpackageregistry -import ( - "git.novox.be/novox/mesh-contracts/go/parts/registry" -) - // Seat is the seat's name. const Seat = "npm-package-registry" @@ -48,47 +44,161 @@ const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" -// Artifact is the part registry's. -type Artifact = registry.Artifact +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// Outcome is the part registry's. -type Outcome = registry.Outcome +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// Part is the part registry's. -type Part = registry.Part +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} -// PublishAnswer is the part registry's. -type PublishAnswer = registry.PublishAnswer +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} -// PublishInput is the part registry's. -type PublishInput = registry.PublishInput +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishPartAnswer is the part registry's. -type PublishPartAnswer = registry.PublishPartAnswer +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishPartInput is the part registry's. -type PublishPartInput = registry.PublishPartInput +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} -// PublishedEvent is the part registry's. -type PublishedEvent = registry.PublishedEvent +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} -// RefusedEvent is the part registry's. -type RefusedEvent = registry.RefusedEvent +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} -// RemoveAnswer is the part registry's. -type RemoveAnswer = registry.RemoveAnswer +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} -// RemoveInput is the part registry's. -type RemoveInput = registry.RemoveInput +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it is gone + Removed bool `json:"removed"` +} -// RemovedEvent is the part registry's. -type RemovedEvent = registry.RemovedEvent +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome -// VersionsAnswer is the part registry's. -type VersionsAnswer = registry.VersionsAnswer +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome -// VersionsInput is the part registry's. -type VersionsInput = registry.VersionsInput +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome // NpmPackageRegistryForm is the form of the provision npm-package-registry. type NpmPackageRegistryForm struct { diff --git a/go/seats/npmpackageregistry/go.mod b/go/seats/npmpackageregistry/go.mod new file mode 100644 index 0000000..208b96c --- /dev/null +++ b/go/seats/npmpackageregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry + +go 1.26.0 diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index 9042e7e..be4ab7d 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -3,10 +3,6 @@ // Package ociartifactregistry is the contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. package ociartifactregistry -import ( - "git.novox.be/novox/mesh-contracts/go/parts/registry" -) - // Seat is the seat's name. const Seat = "oci-artifact-registry" @@ -58,47 +54,23 @@ const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" // SubjectReleased is where released is published. const SubjectReleased = "mesh.seat.oci-artifact-registry.event.released" -// Artifact is the part registry's. -type Artifact = registry.Artifact - -// Outcome is the part registry's. -type Outcome = registry.Outcome - -// Part is the part registry's. -type Part = registry.Part - -// PublishAnswer is the part registry's. -type PublishAnswer = registry.PublishAnswer - -// PublishInput is the part registry's. -type PublishInput = registry.PublishInput - -// PublishPartAnswer is the part registry's. -type PublishPartAnswer = registry.PublishPartAnswer - -// PublishPartInput is the part registry's. -type PublishPartInput = registry.PublishPartInput - -// PublishedEvent is the part registry's. -type PublishedEvent = registry.PublishedEvent - -// RefusedEvent is the part registry's. -type RefusedEvent = registry.RefusedEvent - -// RemoveAnswer is the part registry's. -type RemoveAnswer = registry.RemoveAnswer - -// RemoveInput is the part registry's. -type RemoveInput = registry.RemoveInput - -// RemovedEvent is the part registry's. -type RemovedEvent = registry.RemovedEvent - -// VersionsAnswer is the part registry's. -type VersionsAnswer = registry.VersionsAnswer - -// VersionsInput is the part registry's. -type VersionsInput = registry.VersionsInput +// Artifact is one published thing, as every registry event and answer names it. +type Artifact struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} // Hold is the body of held and released. type Hold struct { @@ -116,6 +88,135 @@ type Hold struct { Reason string `json:"reason,omitempty"` } +// Outcome is the body of published, refused and removed. +type Outcome struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // the module holding the seat that said it + Holder string `json:"holder"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + Reason string `json:"reason,omitempty"` + // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) + Reference string `json:"reference,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer it ends, when it ended one + Transfer string `json:"transfer,omitempty"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// Part is one part of a transfer. +type Part struct { + // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob + Digest string `json:"digest"` + // its size in bytes + Size int64 `json:"size"` +} + +// PublishInput is what publish takes. +type PublishInput struct { + // the source commit it was built from + Commit string `json:"commit,omitempty"` + // its digest, sha256: (a Go package's h1: hash beside it in the version's record) + Digest string `json:"digest"` + // the build's fingerprint + Fingerprint string `json:"fingerprint,omitempty"` + // what it is + Kind string `json:"kind"` + // the package's, image's, bundle's or archive's name + Name string `json:"name"` + // every part's digest and size; none when the digest is already held + Parts []Part `json:"parts,omitempty"` + // the tag it carries, for a tagged version + Tag string `json:"tag,omitempty"` + // the transfer's name, the caller's own; the same name opens the same transfer again + Transfer string `json:"transfer"` + // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone + Version string `json:"version,omitempty"` +} + +// PublishAnswer is what publish answers. +type PublishAnswer struct { + // the digest is held already: nothing to send; the outcome follows as an event + Held bool `json:"held,omitempty"` + // the digests of the parts the holder lacks, to send by publish-part + Lacks []string `json:"lacks,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartInput is what publish-part takes. +type PublishPartInput struct { + // the chunk's bytes, base64 (at most 760 KiB before encoding) + Data string `json:"data"` + // this chunk is the part's last + Last bool `json:"last"` + // the part's digest + Part string `json:"part"` + // the chunk's place in its part, from 0 + Sequence int64 `json:"sequence"` + // the transfer + Transfer string `json:"transfer"` +} + +// PublishPartAnswer is what publish-part answers. +type PublishPartAnswer struct { + // the part is whole and its bytes match its digest + Complete bool `json:"complete,omitempty"` + // the part + Part string `json:"part"` + // the chunks of the part held so far + Received int64 `json:"received,omitempty"` + // the transfer + Transfer string `json:"transfer"` +} + +// VersionsInput is what versions takes. +type VersionsInput struct { + // narrowed to one kind (optional) + Kind string `json:"kind,omitempty"` + // the name + Name string `json:"name"` +} + +// VersionsAnswer is what versions answers. +type VersionsAnswer struct { + // the name + Name string `json:"name"` + // newest first + Versions []Artifact `json:"versions"` +} + +// RemoveInput is what remove takes. +type RemoveInput struct { + // the digest (for a bundle, archive or copied base) + Digest string `json:"digest,omitempty"` + // the name + Name string `json:"name"` + // why it goes + Reason string `json:"reason"` + // the tag (for a tagged version) + Tag string `json:"tag,omitempty"` + // the version + Version string `json:"version,omitempty"` +} + +// RemoveAnswer is what remove answers. +type RemoveAnswer struct { + Artifact *Artifact `json:"artifact,omitempty"` + // it is gone + Removed bool `json:"removed"` +} + // HoldInput is what hold takes. type HoldInput struct { // the digest @@ -150,6 +251,15 @@ type ReleaseAnswer struct { Released bool `json:"released"` } +// PublishedEvent is the body of the event published. +type PublishedEvent = Outcome + +// RefusedEvent is the body of the event refused. +type RefusedEvent = Outcome + +// RemovedEvent is the body of the event removed. +type RemovedEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/ociartifactregistry/go.mod b/go/seats/ociartifactregistry/go.mod new file mode 100644 index 0000000..993f3bc --- /dev/null +++ b/go/seats/ociartifactregistry/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry + +go 1.26.0 diff --git a/go/seats/operatorchannel/go.mod b/go/seats/operatorchannel/go.mod new file mode 100644 index 0000000..894ee5e --- /dev/null +++ b/go/seats/operatorchannel/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/operatorchannel + +go 1.26.0 diff --git a/module.json b/module.json index b67dc32..118627c 100644 --- a/module.json +++ b/module.json @@ -5,6 +5,15 @@ "build": { "artifacts": [ { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, + { "name": "go-containerimageregistry", "kind": "package", "language": "go", "paths": ["go/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "go-gopackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "go-meshbroker", "kind": "package", "language": "go", "paths": ["go/seats/meshbroker"], "series": "0.1", "generated-from": "." }, + { "name": "go-meshconfig", "kind": "package", "language": "go", "paths": ["go/seats/meshconfig"], "series": "0.1", "generated-from": "." }, + { "name": "go-meshdeliverychecker", "kind": "package", "language": "go", "paths": ["go/seats/meshdeliverychecker"], "series": "0.1", "generated-from": "." }, + { "name": "go-meshvault", "kind": "package", "language": "go", "paths": ["go/seats/meshvault"], "series": "0.1", "generated-from": "." }, + { "name": "go-npmpackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/npmpackageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "go-ociartifactregistry", "kind": "package", "language": "go", "paths": ["go/seats/ociartifactregistry"], "series": "0.1", "generated-from": "." }, + { "name": "go-operatorchannel", "kind": "package", "language": "go", "paths": ["go/seats/operatorchannel"], "series": "0.1", "generated-from": "." }, { "name": "lib", "kind": "package", "language": "typescript", "paths": ["ts"], "series": "0.1", "generated-from": "." } ] }, -- 2.54.0 From 96b7eeeaf768446dfbd9865dccc84d7a89134d32 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:53:19 +0100 Subject: [PATCH 21/94] SD-M2b: mesh-broker's events on the shared events stream, 7 days The lead's ruling of 13:0x replaces 12:5x's said-subject: mesh-broker's events follow every seat, on mesh.seat.mesh-broker.event. in the shared events stream, 7 days; the bus module keeps a longer history on a stream of its own module. So every seat family is that stream filtered to the seat's subjects: a definition says 7 days and no cap of its own, and the reader refuses anything else. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- format/seat-definition.schema.json | 24 ++------------ go/seats/containerimageregistry/contract.go | 2 +- go/seats/gopackageregistry/contract.go | 2 +- go/seats/meshbroker/contract.go | 26 +++++++-------- go/seats/meshbroker/contract_test.go | 36 ++++++++++----------- go/seats/npmpackageregistry/contract.go | 2 +- go/seats/ociartifactregistry/contract.go | 2 +- go/seats/operatorchannel/contract.go | 2 +- seats/mesh-broker/fixtures/events.json | 18 +++++------ seats/mesh-broker/seat.json | 4 +-- ts/src/mesh-broker.ts | 20 ++++++------ 11 files changed, 57 insertions(+), 81 deletions(-) diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index b27097f..2c64151 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -83,7 +83,7 @@ "items": { "$ref": "#/$defs/message" }, - "description": "published on mesh.seat..., the token event unless the family names its own" + "description": "published on mesh.seat..event." }, "family": { "type": "object", @@ -97,26 +97,8 @@ "type": "string" }, "max-age-days": { - "type": "integer", - "minimum": 1 - }, - "max-mib": { - "type": "integer", - "minimum": 1 - }, - "token": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$", - "not": { - "enum": [ - "event", - "accept", - "tool", - "call", - "proof" - ] - }, - "description": "the subject's word after the seat's name, event when absent: a family kept in a stream of its own names another, outside the shared events stream's mesh.seat.*.event.>" + "const": 7, + "description": "the shared events stream's age: a seat's events ride it, filtered to the seat's subjects" } } }, diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index 4037d21..8d53626 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -35,7 +35,7 @@ func Events() []string { return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "container-image-registry.events" FamilyMaxAgeDays = 7 diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index d63ad9b..9b6d148 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -29,7 +29,7 @@ func Events() []string { return []string{EventPublished, EventRefused, EventRemoved} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "go-package-registry.events" FamilyMaxAgeDays = 7 diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index c188609..63e9044 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -39,42 +39,38 @@ func Events() []string { return []string{EventCreated, EventRefused, EventDrifted, EventDriftCleared, EventFull, EventFullCleared, EventOrphaned, EventRecreated, EventDropped} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "mesh-broker.events" - FamilyMaxAgeDays = 30 - FamilyMaxMiB = 256 + FamilyMaxAgeDays = 7 ) -// FamilySubjects is what the family's own stream takes, outside the shared events stream's subjects. -const FamilySubjects = "mesh.seat.mesh-broker.said.>" - // SubjectCreated is where created is published. -const SubjectCreated = "mesh.seat.mesh-broker.said.created" +const SubjectCreated = "mesh.seat.mesh-broker.event.created" // SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.mesh-broker.said.refused" +const SubjectRefused = "mesh.seat.mesh-broker.event.refused" // SubjectDrifted is where drifted is published. -const SubjectDrifted = "mesh.seat.mesh-broker.said.drifted" +const SubjectDrifted = "mesh.seat.mesh-broker.event.drifted" // SubjectDriftCleared is where drift-cleared is published. -const SubjectDriftCleared = "mesh.seat.mesh-broker.said.drift-cleared" +const SubjectDriftCleared = "mesh.seat.mesh-broker.event.drift-cleared" // SubjectFull is where full is published. -const SubjectFull = "mesh.seat.mesh-broker.said.full" +const SubjectFull = "mesh.seat.mesh-broker.event.full" // SubjectFullCleared is where full-cleared is published. -const SubjectFullCleared = "mesh.seat.mesh-broker.said.full-cleared" +const SubjectFullCleared = "mesh.seat.mesh-broker.event.full-cleared" // SubjectOrphaned is where orphaned is published. -const SubjectOrphaned = "mesh.seat.mesh-broker.said.orphaned" +const SubjectOrphaned = "mesh.seat.mesh-broker.event.orphaned" // SubjectRecreated is where recreated is published. -const SubjectRecreated = "mesh.seat.mesh-broker.said.recreated" +const SubjectRecreated = "mesh.seat.mesh-broker.event.recreated" // SubjectDropped is where dropped is published. -const SubjectDropped = "mesh.seat.mesh-broker.said.dropped" +const SubjectDropped = "mesh.seat.mesh-broker.event.dropped" // Binding is the binding a module receives in its membership for one entry (ADR 0595 §1, §7). type Binding struct { diff --git a/go/seats/meshbroker/contract_test.go b/go/seats/meshbroker/contract_test.go index 49861f6..03c1f9c 100644 --- a/go/seats/meshbroker/contract_test.go +++ b/go/seats/meshbroker/contract_test.go @@ -17,24 +17,24 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "created on the seat's subject", subject: "mesh.seat.mesh-broker.said.created", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v CreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.created$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.mesh-broker.said.refused", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.refused$")}, - {name: "drifted on the seat's subject", subject: "mesh.seat.mesh-broker.said.drifted", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v DriftedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.drifted$")}, - {name: "drift-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.said.drift-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v DriftClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.drift-cleared$")}, - {name: "full on the seat's subject", subject: "mesh.seat.mesh-broker.said.full", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v FullEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.full$")}, - {name: "full-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.said.full-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v FullClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.full-cleared$")}, - {name: "orphaned on the seat's subject", subject: "mesh.seat.mesh-broker.said.orphaned", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", - decode: func(b []byte) (any, error) { var v OrphanedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.orphaned$")}, - {name: "recreated on the seat's subject", subject: "mesh.seat.mesh-broker.said.recreated", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.recreated$")}, - {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.said.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", - decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.said\\.dropped$")}, + {name: "created on the seat's subject", subject: "mesh.seat.mesh-broker.event.created", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v CreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.created$")}, + {name: "refused on the seat's subject", subject: "mesh.seat.mesh-broker.event.refused", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.refused$")}, + {name: "drifted on the seat's subject", subject: "mesh.seat.mesh-broker.event.drifted", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DriftedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drifted$")}, + {name: "drift-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.drift-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v DriftClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.drift-cleared$")}, + {name: "full on the seat's subject", subject: "mesh.seat.mesh-broker.event.full", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full$")}, + {name: "full-cleared on the seat's subject", subject: "mesh.seat.mesh-broker.event.full-cleared", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v FullClearedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.full-cleared$")}, + {name: "orphaned on the seat's subject", subject: "mesh.seat.mesh-broker.event.orphaned", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\"}", + decode: func(b []byte) (any, error) { var v OrphanedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.orphaned$")}, + {name: "recreated on the seat's subject", subject: "mesh.seat.mesh-broker.event.recreated", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.recreated$")}, + {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.event.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", + decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.dropped$")}, {name: "create one entry", subject: "", given: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", decode: func(b []byte) (any, error) { var v CreateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "drop on the operator's warrant", subject: "", given: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", wire: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 0f60980..8758751 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -29,7 +29,7 @@ func Events() []string { return []string{EventPublished, EventRefused, EventRemoved} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "npm-package-registry.events" FamilyMaxAgeDays = 7 diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index be4ab7d..1eb950d 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -33,7 +33,7 @@ func Events() []string { return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "oci-artifact-registry.events" FamilyMaxAgeDays = 7 diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go index 7e96e3e..104afe6 100644 --- a/go/seats/operatorchannel/contract.go +++ b/go/seats/operatorchannel/contract.go @@ -34,7 +34,7 @@ func Events() []string { return []string{EventDecided} } -// Family is where the seat's events are kept. +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. const ( Family = "operator-channel.decided" FamilyMaxAgeDays = 7 diff --git a/seats/mesh-broker/fixtures/events.json b/seats/mesh-broker/fixtures/events.json index c6167bd..374448d 100644 --- a/seats/mesh-broker/fixtures/events.json +++ b/seats/mesh-broker/fixtures/events.json @@ -2,7 +2,7 @@ { "name": "created on the seat's subject", "of": "event:created", - "subject": "mesh.seat.mesh-broker.said.created", + "subject": "mesh.seat.mesh-broker.event.created", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -25,7 +25,7 @@ { "name": "refused on the seat's subject", "of": "event:refused", - "subject": "mesh.seat.mesh-broker.said.refused", + "subject": "mesh.seat.mesh-broker.event.refused", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -49,7 +49,7 @@ { "name": "drifted on the seat's subject", "of": "event:drifted", - "subject": "mesh.seat.mesh-broker.said.drifted", + "subject": "mesh.seat.mesh-broker.event.drifted", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -73,7 +73,7 @@ { "name": "drift-cleared on the seat's subject", "of": "event:drift-cleared", - "subject": "mesh.seat.mesh-broker.said.drift-cleared", + "subject": "mesh.seat.mesh-broker.event.drift-cleared", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -96,7 +96,7 @@ { "name": "full on the seat's subject", "of": "event:full", - "subject": "mesh.seat.mesh-broker.said.full", + "subject": "mesh.seat.mesh-broker.event.full", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -119,7 +119,7 @@ { "name": "full-cleared on the seat's subject", "of": "event:full-cleared", - "subject": "mesh.seat.mesh-broker.said.full-cleared", + "subject": "mesh.seat.mesh-broker.event.full-cleared", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -142,7 +142,7 @@ { "name": "orphaned on the seat's subject", "of": "event:orphaned", - "subject": "mesh.seat.mesh-broker.said.orphaned", + "subject": "mesh.seat.mesh-broker.event.orphaned", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -165,7 +165,7 @@ { "name": "recreated on the seat's subject", "of": "event:recreated", - "subject": "mesh.seat.mesh-broker.said.recreated", + "subject": "mesh.seat.mesh-broker.event.recreated", "given": { "module": "mesh-knowledge", "local-name": "ledger", @@ -189,7 +189,7 @@ { "name": "dropped on the seat's subject", "of": "event:dropped", - "subject": "mesh.seat.mesh-broker.said.dropped", + "subject": "mesh.seat.mesh-broker.event.dropped", "given": { "module": "mesh-knowledge", "local-name": "ledger", diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index 167b3d7..4cea764 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -251,9 +251,7 @@ ], "family": { "name": "mesh-broker.events", - "max-age-days": 30, - "max-mib": 256, - "token": "said" + "max-age-days": 7 }, "provisions": [ { diff --git a/ts/src/mesh-broker.ts b/ts/src/mesh-broker.ts index bb3d52c..b43ba38 100644 --- a/ts/src/mesh-broker.ts +++ b/ts/src/mesh-broker.ts @@ -27,25 +27,25 @@ export const events = { dropped: "dropped", } as const; -export const family = { name: "mesh-broker.events", maxAgeDays: 30, maxMiB: 256, subjects: "mesh.seat.mesh-broker.said.>" } as const; +export const family = { name: "mesh-broker.events", maxAgeDays: 7 } as const; -export const createdSubject = "mesh.seat.mesh-broker.said.created"; +export const createdSubject = "mesh.seat.mesh-broker.event.created"; -export const refusedSubject = "mesh.seat.mesh-broker.said.refused"; +export const refusedSubject = "mesh.seat.mesh-broker.event.refused"; -export const driftedSubject = "mesh.seat.mesh-broker.said.drifted"; +export const driftedSubject = "mesh.seat.mesh-broker.event.drifted"; -export const driftClearedSubject = "mesh.seat.mesh-broker.said.drift-cleared"; +export const driftClearedSubject = "mesh.seat.mesh-broker.event.drift-cleared"; -export const fullSubject = "mesh.seat.mesh-broker.said.full"; +export const fullSubject = "mesh.seat.mesh-broker.event.full"; -export const fullClearedSubject = "mesh.seat.mesh-broker.said.full-cleared"; +export const fullClearedSubject = "mesh.seat.mesh-broker.event.full-cleared"; -export const orphanedSubject = "mesh.seat.mesh-broker.said.orphaned"; +export const orphanedSubject = "mesh.seat.mesh-broker.event.orphaned"; -export const recreatedSubject = "mesh.seat.mesh-broker.said.recreated"; +export const recreatedSubject = "mesh.seat.mesh-broker.event.recreated"; -export const droppedSubject = "mesh.seat.mesh-broker.said.dropped"; +export const droppedSubject = "mesh.seat.mesh-broker.event.dropped"; /** the binding a module receives in its membership for one entry (ADR 0595 §1, §7) */ export interface Binding { -- 2.54.0 From f32ea4ba752cdc8b564c842e613e1e5caa878a47 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:53:49 +0100 Subject: [PATCH 22/94] SD-M2d: the checker's tag-wanted names the pull request's branch The lead's ruling of 13:0x: the builder removes the tag when the branch ends, so it needs the branch. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshdeliverychecker/contract.go | 2 ++ go/seats/meshdeliverychecker/contract_test.go | 2 +- seats/mesh-delivery-checker/fixtures/events.json | 1 + seats/mesh-delivery-checker/seat.json | 5 +++++ ts/src/mesh-delivery-checker.ts | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go index 6f881a7..af921c3 100644 --- a/go/seats/meshdeliverychecker/contract.go +++ b/go/seats/meshdeliverychecker/contract.go @@ -98,6 +98,8 @@ type VerdictHolderEvent struct { // TagWantedHolderEvent is the body of the holder's event tag-wanted. type TagWantedHolderEvent struct { + // the pull request's branch: its tag is removed when the branch ends + Branch string `json:"branch"` // its head Head string `json:"head"` // the group member diff --git a/go/seats/meshdeliverychecker/contract_test.go b/go/seats/meshdeliverychecker/contract_test.go index 0e1514f..b3e334a 100644 --- a/go/seats/meshdeliverychecker/contract_test.go +++ b/go/seats/meshdeliverychecker/contract_test.go @@ -19,7 +19,7 @@ func TestFixtures(t *testing.T) { }{ {name: "verdict on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.verdict$")}, - {name: "tag-wanted on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.tag-wanted", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", + {name: "tag-wanted on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, {name: "preview.written on the checker's stream", subject: "mesh.mod.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, diff --git a/seats/mesh-delivery-checker/fixtures/events.json b/seats/mesh-delivery-checker/fixtures/events.json index 46ef4d8..cc8ae18 100644 --- a/seats/mesh-delivery-checker/fixtures/events.json +++ b/seats/mesh-delivery-checker/fixtures/events.json @@ -21,6 +21,7 @@ "repository": "novox/mesh-sdk", "pull": 170, "head": "cccccccccccccccccccccccccccccccccccccccc", + "branch": "feat/registry", "module": "mesh-sdk", "package": "go", "tag": "registry-1966" diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json index e671f2e..fb80b49 100644 --- a/seats/mesh-delivery-checker/seat.json +++ b/seats/mesh-delivery-checker/seat.json @@ -179,6 +179,10 @@ "type": "string", "description": "its head" }, + "branch": { + "type": "string", + "description": "the pull request's branch: its tag is removed when the branch ends" + }, "module": { "type": "string", "description": "the group member" @@ -195,6 +199,7 @@ "required": [ "repository", "head", + "branch", "module", "package" ] diff --git a/ts/src/mesh-delivery-checker.ts b/ts/src/mesh-delivery-checker.ts index 045181b..4bbc94b 100644 --- a/ts/src/mesh-delivery-checker.ts +++ b/ts/src/mesh-delivery-checker.ts @@ -91,6 +91,8 @@ export interface VerdictHolderEvent { /** is the body of the holder's event tag-wanted. */ export interface TagWantedHolderEvent { + /** the pull request's branch: its tag is removed when the branch ends */ + "branch": string; /** its head */ "head": string; /** the group member */ -- 2.54.0 From a1ef06b20e9b28ac00ab33c98f3690de37e60b53 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:08:59 +0100 Subject: [PATCH 23/94] =?UTF-8?q?ADR=200627=20=C2=A71:=20the=20four=20regi?= =?UTF-8?q?stry=20seats'=20clients=20publish=20a=20file=20in=20parts=20ove?= =?UTF-8?q?r=20their=20own=20seat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each client binds mesh-sdk's seat-neutral parts sender to its own seat's publish and publish-part and types, so no client uses another seat's; each is a Go module and a package artifact of its own (ADR 0625 §3). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../containerimageregistryclient/client.go | 63 +++++++++++++++++++ .../client_test.go | 57 +++++++++++++++++ .../containerimageregistryclient/go.mod | 8 +++ go/clients/gopackageregistryclient/client.go | 63 +++++++++++++++++++ .../gopackageregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/gopackageregistryclient/go.mod | 8 +++ go/clients/npmpackageregistryclient/client.go | 63 +++++++++++++++++++ .../npmpackageregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/npmpackageregistryclient/go.mod | 8 +++ .../ociartifactregistryclient/client.go | 63 +++++++++++++++++++ .../ociartifactregistryclient/client_test.go | 57 +++++++++++++++++ go/clients/ociartifactregistryclient/go.mod | 8 +++ module.json | 4 ++ 13 files changed, 516 insertions(+) create mode 100644 go/clients/containerimageregistryclient/client.go create mode 100644 go/clients/containerimageregistryclient/client_test.go create mode 100644 go/clients/containerimageregistryclient/go.mod create mode 100644 go/clients/gopackageregistryclient/client.go create mode 100644 go/clients/gopackageregistryclient/client_test.go create mode 100644 go/clients/gopackageregistryclient/go.mod create mode 100644 go/clients/npmpackageregistryclient/client.go create mode 100644 go/clients/npmpackageregistryclient/client_test.go create mode 100644 go/clients/npmpackageregistryclient/go.mod create mode 100644 go/clients/ociartifactregistryclient/client.go create mode 100644 go/clients/ociartifactregistryclient/client_test.go create mode 100644 go/clients/ociartifactregistryclient/go.mod diff --git a/go/clients/containerimageregistryclient/client.go b/go/clients/containerimageregistryclient/client.go new file mode 100644 index 0000000..4add3df --- /dev/null +++ b/go/clients/containerimageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package containerimageregistryclient is the container-image-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package containerimageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/containerimageregistryclient/client_test.go b/go/clients/containerimageregistryclient/client_test.go new file mode 100644 index 0000000..8ef827a --- /dev/null +++ b/go/clients/containerimageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package containerimageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:container-image-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:container-image-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod new file mode 100644 index 0000000..a58584d --- /dev/null +++ b/go/clients/containerimageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/gopackageregistryclient/client.go b/go/clients/gopackageregistryclient/client.go new file mode 100644 index 0000000..e339065 --- /dev/null +++ b/go/clients/gopackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package gopackageregistryclient is the go-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package gopackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/gopackageregistryclient/client_test.go b/go/clients/gopackageregistryclient/client_test.go new file mode 100644 index 0000000..3180a68 --- /dev/null +++ b/go/clients/gopackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package gopackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:go-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:go-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod new file mode 100644 index 0000000..59bef72 --- /dev/null +++ b/go/clients/gopackageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/npmpackageregistryclient/client.go b/go/clients/npmpackageregistryclient/client.go new file mode 100644 index 0000000..f72a8cf --- /dev/null +++ b/go/clients/npmpackageregistryclient/client.go @@ -0,0 +1,63 @@ +// Package npmpackageregistryclient is the npm-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package npmpackageregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/npmpackageregistryclient/client_test.go b/go/clients/npmpackageregistryclient/client_test.go new file mode 100644 index 0000000..4433025 --- /dev/null +++ b/go/clients/npmpackageregistryclient/client_test.go @@ -0,0 +1,57 @@ +package npmpackageregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:npm-package-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:npm-package-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod new file mode 100644 index 0000000..a7815fe --- /dev/null +++ b/go/clients/npmpackageregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/go/clients/ociartifactregistryclient/client.go b/go/clients/ociartifactregistryclient/client.go new file mode 100644 index 0000000..3f9a894 --- /dev/null +++ b/go/clients/ociartifactregistryclient/client.go @@ -0,0 +1,63 @@ +// Package ociartifactregistryclient is the oci-artifact-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 +// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, +// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a +// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is +// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the +// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. +package ociartifactregistryclient + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + + seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. +type Call func(key string, body any) (json.RawMessage, error) + +// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were +// sent, and the chunks they took. +type Sent struct { + Transfer string + parts.Sent +} + +// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in +// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes +// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. +// Its outcome, published or refused, is the holder's event, which the caller waits for. +func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { + open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { + in.Parts = make([]seat.Part, 0, len(ps)) + for _, p := range ps { + in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) + } + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) + if err != nil { + return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) + } + var a seat.PublishAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) + } + return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil + } + chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { + raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, + Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) + if err != nil { + return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) + } + var a seat.PublishPartAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) + } + return nil + } + sent, err := parts.Send(ctx, open, chunk, ps) + return Sent{Transfer: in.Transfer, Sent: sent}, err +} diff --git a/go/clients/ociartifactregistryclient/client_test.go b/go/clients/ociartifactregistryclient/client_test.go new file mode 100644 index 0000000..0f64e6f --- /dev/null +++ b/go/clients/ociartifactregistryclient/client_test.go @@ -0,0 +1,57 @@ +package ociartifactregistryclient + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "io" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-sdk/go/parts" +) + +// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the +// holder lacks travels by publish-part, its bytes whole, the last chunk marked. +func TestPublishTalksToItsOwnSeat(t *testing.T) { + held, lacked := []byte("held"), []byte("lacked") + part := func(b []byte) parts.Part { + sum := sha256.Sum256(b) + return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), + Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} + } + ps := []parts.Part{part(held), part(lacked)} + var asked []string + var got []byte + call := func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + raw, _ := json.Marshal(body) + switch key { + case "seat:oci-artifact-registry.publish": + var in seat.PublishInput + _ = json.Unmarshal(raw, &in) + if len(in.Parts) != 2 { + t.Errorf("publish names %d parts, want 2", len(in.Parts)) + } + return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) + case "seat:oci-artifact-registry.publish-part": + var in seat.PublishPartInput + _ = json.Unmarshal(raw, &in) + data, _ := base64.StdEncoding.DecodeString(in.Data) + if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { + t.Errorf("publish-part %+v", in) + } + got = append(got, data...) + return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) + } + t.Fatalf("asked %s", key) + return nil, nil + } + sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) + if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { + t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) + } +} diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod new file mode 100644 index 0000000..a08e021 --- /dev/null +++ b/go/clients/ociartifactregistryclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0 + git.novox.be/novox/mesh-sdk/go v0.0.0 +) diff --git a/module.json b/module.json index 118627c..5fecc68 100644 --- a/module.json +++ b/module.json @@ -5,6 +5,10 @@ "build": { "artifacts": [ { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, + { "name": "go-containerimageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/containerimageregistryclient"], "series": "0.1" }, + { "name": "go-gopackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/gopackageregistryclient"], "series": "0.1" }, + { "name": "go-npmpackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/npmpackageregistryclient"], "series": "0.1" }, + { "name": "go-ociartifactregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/ociartifactregistryclient"], "series": "0.1" }, { "name": "go-containerimageregistry", "kind": "package", "language": "go", "paths": ["go/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-gopackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-meshbroker", "kind": "package", "language": "go", "paths": ["go/seats/meshbroker"], "series": "0.1", "generated-from": "." }, -- 2.54.0 From b5db9466c22462676969d0a5e319df493334e2d2 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:13:43 +0100 Subject: [PATCH 24/94] =?UTF-8?q?ADR=200627=20=C2=A71:=20operator-channel'?= =?UTF-8?q?s=20client=20holds=20the=20asking=20and=20the=20warrant's=20che?= =?UTF-8?q?cks=20over=20the=20contract's=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-sdk's asks logic (an ask's check and digest, the act's digest, a warrant's For and Says, the condition's encoding) and its asker client move here, over operatorchannel's generated Ask, Option, Warrant and Condition; the digests are those asks gave (a row holds two of them), so a router and an asker of either build agree. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../containerimageregistryclient/go.mod | 5 - go/clients/gopackageregistryclient/go.mod | 5 - go/clients/npmpackageregistryclient/go.mod | 5 - go/clients/ociartifactregistryclient/go.mod | 5 - go/clients/operatorchannelclient/ask.go | 374 ++++++++++++++++++ go/clients/operatorchannelclient/ask_test.go | 368 +++++++++++++++++ go/clients/operatorchannelclient/client.go | 197 +++++++++ .../operatorchannelclient/client_test.go | 139 +++++++ go/clients/operatorchannelclient/condition.go | 117 ++++++ .../operatorchannelclient/condition_test.go | 145 +++++++ go/clients/operatorchannelclient/go.mod | 3 + module.json | 1 + 12 files changed, 1344 insertions(+), 20 deletions(-) create mode 100644 go/clients/operatorchannelclient/ask.go create mode 100644 go/clients/operatorchannelclient/ask_test.go create mode 100644 go/clients/operatorchannelclient/client.go create mode 100644 go/clients/operatorchannelclient/client_test.go create mode 100644 go/clients/operatorchannelclient/condition.go create mode 100644 go/clients/operatorchannelclient/condition_test.go create mode 100644 go/clients/operatorchannelclient/go.mod diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod index a58584d..ea5bb60 100644 --- a/go/clients/containerimageregistryclient/go.mod +++ b/go/clients/containerimageregistryclient/go.mod @@ -1,8 +1,3 @@ module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0 - git.novox.be/novox/mesh-sdk/go v0.0.0 -) diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod index 59bef72..7678fd3 100644 --- a/go/clients/gopackageregistryclient/go.mod +++ b/go/clients/gopackageregistryclient/go.mod @@ -1,8 +1,3 @@ module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0 - git.novox.be/novox/mesh-sdk/go v0.0.0 -) diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod index a7815fe..94d8c30 100644 --- a/go/clients/npmpackageregistryclient/go.mod +++ b/go/clients/npmpackageregistryclient/go.mod @@ -1,8 +1,3 @@ module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0 - git.novox.be/novox/mesh-sdk/go v0.0.0 -) diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod index a08e021..a37d128 100644 --- a/go/clients/ociartifactregistryclient/go.mod +++ b/go/clients/ociartifactregistryclient/go.mod @@ -1,8 +1,3 @@ module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0 - git.novox.be/novox/mesh-sdk/go v0.0.0 -) diff --git a/go/clients/operatorchannelclient/ask.go b/go/clients/operatorchannelclient/ask.go new file mode 100644 index 0000000..a886bb4 --- /dev/null +++ b/go/clients/operatorchannelclient/ask.go @@ -0,0 +1,374 @@ +// Package operatorchannelclient is the operator-channel seat's client (novox/hq ADR 0627 §1; ADR 0259): the logic +// every asker and the router share over the seat's contract. An asker checks its ask (Check, Glance), binds what each +// option performs (ActDigest), publishes it under its own name and acts on the warrant it hears only when the +// warrant answers that ask, as the person was shown it (Digest, For, Performs). A module says a condition to the +// operator by the seat's condition accept (EncodeCondition). The asker's keeping of its asks and warrants is Client. +// The shapes are the seat's contract's (operatorchannel); this package holds no type of its own for them. +package operatorchannelclient + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" +) + +// The levels an option is answered at: how much proof its answer needs (novox/hq ADR 0234 §8, the glossary's +// assurance level). +const ( + // Acknowledge performs only what any granted principal may already do: silencing, details. No proof. + Acknowledge = "acknowledge" + // Approve needs one proof: a verified sender (a linked account, linked an hour or more) or a code. + Approve = "approve" + // Destroy needs two proofs, one of them a code. + Destroy = "destroy" +) + +// Rank orders the levels; an unknown level ranks above every known one, so it is never taken as less. +func Rank(level string) int { + switch level { + case Acknowledge: + return 0 + case Approve: + return 1 + case Destroy: + return 2 + } + return 3 +} + +// Operator is the one role an ask may be answered by today. +const Operator = "operator" + +// How an ask ended (a warrant's outcome). +const ( + OutcomeChosen = "chosen" // a person chose an option: a warrant + OutcomeExpired = "expired" // nobody answered in time + OutcomeCancelled = "cancelled" // its asker took it back + OutcomeReplaced = "replaced" // a newer ask about the same thing replaced it + OutcomeRefused = "refused" // it was never shown: Words says why +) + +// An Act is what an option binds: named fields, each a string — the verb, the machine, the level, and each +// argument under a name of its own ("arg.delivery"). Flat on purpose: its digest is over these names and +// values alone, never over how a language happens to encode a struct. +type Act map[string]string + +// ActDigest is the digest an asker puts in an option's Binds: SHA-256 over the act's canonical encoding +// (canonical), written "sha256:". The same names and values give the same digest in any language, whatever +// order they were set in; a field renamed, added or emptied gives another. +func ActDigest(act Act) (string, error) { + if len(act) == 0 { + return "", fmt.Errorf("the act cannot be digested: it names nothing") + } + keys := make([]string, 0, len(act)) + for k := range act { + if k == "" { + return "", fmt.Errorf("the act cannot be digested: a field has no name") + } + keys = append(keys, k) + } + sort.Strings(keys) + var b strings.Builder + b.WriteString("novox.act.v1\n") + for _, k := range keys { + canonical(&b, k) + canonical(&b, act[k]) + } + sum := sha256.Sum256([]byte(b.String())) + return "sha256:" + hex.EncodeToString(sum[:]), nil +} + +// canonical writes one value as its length in bytes, a colon, the bytes and a newline: no value can be read +// as another's end or start, so two different sequences of values never encode the same. +func canonical(b *strings.Builder, v string) { + b.WriteString(strconv.Itoa(len(v))) + b.WriteByte(':') + b.WriteString(v) + b.WriteByte('\n') +} + +// Time reads an RFC 3339 time of the contract (an ask's expiry, a warrant's time); empty is the zero time. +func Time(s string) (time.Time, error) { + if s == "" { + return time.Time{}, nil + } + return time.Parse(time.RFC3339Nano, s) +} + +// FormatTime is a time as the contract carries it: UTC, RFC 3339 to the nanosecond; the zero time is empty. +func FormatTime(t time.Time) string { + if t.IsZero() { + return "" + } + return t.Format(time.RFC3339Nano) +} + +// Digest is the digest of the ask exactly as its asker published it: its id, words, options with what each +// binds, who answers, and its expiry. The router puts it in the warrant (AskDigest), and an asker acts only on a +// warrant whose digest is that of the ask it keeps — so a warrant answers one ask, as the person was shown it, and +// nothing published under the same id before or after. +// +// It is over the ask's named fields in a fixed order, each written canonically, and the expiry as UTC RFC 3339 to +// the nanosecond — never over a language's encoding of the struct, so a field added to the ask later changes no +// digest until it is added here, on purpose. Whole and Details (novox/hq issue 383) follow the options only when the +// ask gives either: an ask without them digests as it did before they existed, so a router and an asker of +// different builds still agree on every such ask. An expiry that is no RFC 3339 time is digested as written. +func Digest(a seat.Ask) string { + expires := a.Expires + if t, err := Time(a.Expires); err == nil { + expires = t.UTC().Format(time.RFC3339Nano) + } + var b strings.Builder + b.WriteString("novox.ask.v1\n") + for _, v := range []string{a.ID, a.Headline, a.Explanation, a.Who, expires, a.OnExpiry, a.About, + strconv.FormatBool(a.Urgent), strconv.Itoa(len(a.Options))} { + canonical(&b, v) + } + for _, o := range a.Options { + for _, v := range []string{o.ID, o.Label, o.Does, o.Level, o.Binds} { + canonical(&b, v) + } + } + // Whole and Details are written whenever either or WholeDetails is given, and WholeDetails after them whenever + // it is given (novox/hq ADR 0347): the fields after the options are then always one of three fixed shapes — + // none, the two, or the three — and the count of values the sequence holds tells them apart, so no two asks + // encode the same. An ask with none of them digests as before they existed, and one without WholeDetails as + // before it existed. + if a.Whole != "" || a.Details != "" || a.WholeDetails != "" { + canonical(&b, a.Whole) + canonical(&b, a.Details) + } + if a.WholeDetails != "" { + canonical(&b, a.WholeDetails) + } + sum := sha256.Sum256([]byte(b.String())) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary +// of two or three short sentences on its first line — what changes, why, what it costs or risks — then at +// most three points, a line each, only where they add something. Whole is held to the same. Who asked, when, +// the fingerprint and how to read it whole are Details, never the explanation. +const ( + SummaryLength = 300 + MostPoints = 3 + PointLength = 160 +) + +// Glance says why an explanation does not read at a glance, or nil: its first line, the summary, longer than +// SummaryLength; more than MostPoints lines after it; a point longer than PointLength. Blank lines are not +// counted. An asker holds its asks to it in its own tests; the router says, without refusing, an ask that +// does not hold to it. +func Glance(explanation string) error { + var lines []string + for _, l := range strings.Split(explanation, "\n") { + if strings.TrimSpace(l) != "" { + lines = append(lines, strings.TrimSpace(l)) + } + } + var problems []string + switch { + case len(lines) == 0: + problems = append(problems, "it has no summary") + case len([]rune(lines[0])) > SummaryLength: + problems = append(problems, fmt.Sprintf("its summary is %d characters, more than %d", len([]rune(lines[0])), SummaryLength)) + } + if len(lines) > 1+MostPoints { + problems = append(problems, fmt.Sprintf("it has %d points after its summary, more than %d", len(lines)-1, MostPoints)) + } + for i, l := range lines { + if i > 0 && len([]rune(l)) > PointLength { + problems = append(problems, fmt.Sprintf("its point %d is %d characters, more than %d", i, len([]rune(l)), PointLength)) + } + } + if len(problems) > 0 { + return errors.New("the explanation does not read at a glance: " + strings.Join(problems, "; ")) + } + return nil +} + +// The bounds of an ask (novox/hq ADR 0234 §8, ADR 0259 §4). +const ( + MostOptions = 4 + MostOpen = 3 + ApproveLasts = 24 * time.Hour + DestroyLasts = 10 * time.Minute + HeadlineLength = 60 + LabelLength = 24 +) + +var usableID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`) + +// UsableID says whether a name can be an ask's or an option's id: a key and a subject token both. +func UsableID(id string) bool { return usableID.MatchString(id) } + +// Highest is the highest level among the ask's options. +func Highest(a seat.Ask) string { + high := Acknowledge + for _, o := range a.Options { + if Rank(o.Level) > Rank(high) { + high = o.Level + } + } + return high +} + +// OptionOf is the ask's option of this id, or false. +func OptionOf(a seat.Ask, id string) (seat.Option, bool) { + for _, o := range a.Options { + if o.ID == id { + return o, true + } + } + return seat.Option{}, false +} + +// Check is what an ask is held to before anything is shown: every refusal, in words its asker can act on. +func Check(a seat.Ask, now time.Time) error { + var problems []string + say := func(format string, args ...any) { problems = append(problems, fmt.Sprintf(format, args...)) } + if !UsableID(a.ID) { + say("its id %q is not letters, digits, - and _, at most 64", a.ID) + } + if strings.TrimSpace(a.Headline) == "" || len([]rune(a.Headline)) > HeadlineLength { + say("its headline is empty or longer than %d characters", HeadlineLength) + } + if strings.TrimSpace(a.Explanation) == "" { + say("it explains nothing") + } + if a.Who != Operator { + say("it is answered by %q, and only the operator answers today", a.Who) + } + if len(a.Options) == 0 || len(a.Options) > MostOptions { + say("it offers %d options, and an ask offers one to %d", len(a.Options), MostOptions) + } + seen := map[string]bool{} + for _, o := range a.Options { + switch { + case !UsableID(o.ID): + say("an option's id %q is not letters, digits, - and _", o.ID) + case seen[o.ID]: + say("the option %s is offered twice", o.ID) + } + seen[o.ID] = true + if strings.TrimSpace(o.Label) == "" || len([]rune(o.Label)) > LabelLength { + say("the option %s's label is empty or longer than %d characters", o.ID, LabelLength) + } + if strings.TrimSpace(o.Does) == "" { + say("the option %s does not say what it does", o.ID) + } + if Rank(o.Level) > Rank(Destroy) { + say("the option %s has the level %q, which is none of acknowledge, approve, destroy", o.ID, o.Level) + } + if o.Level != Acknowledge && !strings.HasPrefix(o.Binds, "sha256:") { + say("the option %s authorises and does not bind what it performs (its binds is not an ActDigest)", o.ID) + } + } + expires, err := Time(a.Expires) + if err != nil { + say("its expiry %q is not an RFC 3339 time", a.Expires) + } + if !expires.After(now) { + say("it expires before it is asked") + } + switch Highest(a) { + case Approve: + if expires.After(now.Add(ApproveLasts)) { + say("an ask that approves lasts at most %s", ApproveLasts) + } + case Destroy: + if expires.After(now.Add(DestroyLasts)) { + say("an ask that destroys lasts at most %s", DestroyLasts) + } + } + if Highest(a) != Acknowledge && strings.TrimSpace(a.OnExpiry) == "" { + say("it does not say what happens when nobody answers, and an ask that authorises never defaults") + } + if a.About != "" && strings.ContainsAny(a.About, " \n") { + say("what it is about is a key, without spaces") + } + if len(problems) > 0 { + return errors.New("the ask is refused: " + strings.Join(problems, "; ")) + } + return nil +} + +// Says is the warrant in the words an asker records with its act: "the operator, via telegram (user id +// verified), chose Release". +func Says(w seat.Warrant) string { + if w.Outcome != OutcomeChosen || w.By == nil { + return fmt.Sprintf("no person chose: the ask %s %s", w.Ask, w.Outcome) + } + via := w.By.Kind + if w.By.Verified != "" { + via += " (" + w.By.Verified + ")" + } + return fmt.Sprintf("the %s, via %s, chose %s", w.By.Who, via, w.Label) +} + +// For checks a warrant against the ask its asker made: the same asker and ask, the same ask's digest (so the +// same words, options and binds), a choice, an option the ask offered, at that option's level, before the +// ask expired. An asker acts on nothing else, and then performs only what the option's Binds names. +func For(w seat.Warrant, asker string, a seat.Ask) (seat.Option, error) { + if w.Asker != asker || w.Ask != a.ID { + return seat.Option{}, fmt.Errorf("the warrant is for %s's ask %s, not %s's %s", w.Asker, w.Ask, asker, a.ID) + } + if w.Outcome != OutcomeChosen || w.By == nil || w.By.Who != a.Who { + return seat.Option{}, fmt.Errorf("the ask %s ended %s; no person chose", a.ID, w.Outcome) + } + if d := Digest(a); w.AskDigest != d { + return seat.Option{}, fmt.Errorf("the warrant answers an ask whose digest is %q, and the ask %s kept here is %s: "+ + "it was not the ask the person was shown", w.AskDigest, a.ID, d) + } + o, offered := OptionOf(a, w.Option) + if !offered { + return seat.Option{}, fmt.Errorf("the ask %s offered no option %s", a.ID, w.Option) + } + if w.Level != o.Level { + return seat.Option{}, fmt.Errorf("the option %s is %s, and the warrant says %s", o.ID, o.Level, w.Level) + } + // A choice made after the ask expired is no answer to it, whatever the router said. An ask that says no + // expiry, or a warrant that says no time, is no answer either: neither can be shown to be in time. + expires, err := Time(a.Expires) + if err != nil || expires.IsZero() { + return seat.Option{}, fmt.Errorf("the ask %s says no expiry, so no answer to it can be in time", a.ID) + } + at, err := Time(w.At) + if err != nil || at.IsZero() { + return seat.Option{}, fmt.Errorf("the warrant for the ask %s says no time it was given, so it cannot be shown to be in time", a.ID) + } + if at.After(expires) { + return seat.Option{}, fmt.Errorf("the warrant was given at %s, after the ask %s expired at %s", + at.UTC().Format(time.RFC3339), a.ID, expires.UTC().Format(time.RFC3339)) + } + return o, nil +} + +// Performs checks that the act an asker is about to perform is the one the chosen option bound when it +// asked: the act's digest equals the option's Binds. An acknowledge option that bound nothing passes. +func Performs(o seat.Option, act Act) error { + if o.Binds == "" && o.Level == Acknowledge { + return nil + } + d, err := ActDigest(act) + if err != nil { + return err + } + if d != o.Binds { + return fmt.Errorf("the option %s bound %s, and the act about to be performed is %s: nothing is done", o.ID, o.Binds, d) + } + return nil +} + +// EngineAsker is the asker name a node's engine asks the operator under (novox/hq ADR 0563 §3, which changes ADR +// 0259 §2 to admit a node's engine as an asker): `node-engine-`, the last token of the by-caller subjects +// granted to that node's engine account alone, so no engine acts on another node's warrant. +func EngineAsker(node string) string { return "node-engine-" + node } diff --git a/go/clients/operatorchannelclient/ask_test.go b/go/clients/operatorchannelclient/ask_test.go new file mode 100644 index 0000000..1a68435 --- /dev/null +++ b/go/clients/operatorchannelclient/ask_test.go @@ -0,0 +1,368 @@ +package operatorchannelclient + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "reflect" + "strings" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" +) + +var now = time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC) + +func held() seat.Ask { + return seat.Ask{ID: "d-1", Headline: "openrazer delivery held", Explanation: "Needs you: release it, or stop it.", + Who: Operator, Expires: FormatTime(now.Add(time.Hour)), OnExpiry: "the delivery stays held", About: "delivery.openrazer", + Options: []seat.Option{ + {ID: "release", Label: "Release", Does: "the delivery goes on", Level: Approve, Binds: bind("release", "openrazer")}, + {ID: "stop", Label: "Stop", Does: "the delivery ends", Level: Approve, Binds: bind("stop", "openrazer")}, + {ID: "silence", Label: "Silence for a week", Does: "nothing more is said for a week", Level: Acknowledge}, + }} +} + +func bind(verb, what string) string { + d, err := ActDigest(Act{"verb": verb, "delivery": what}) + if err != nil { + panic(err) + } + return d +} + +func TestASoundAskPasses(t *testing.T) { + if err := Check(held(), now); err != nil { + t.Fatal(err) + } +} + +func TestAnAskIsRefusedInWordsForEachFault(t *testing.T) { + for want, change := range map[string]func(*seat.Ask){ + "is not letters": func(a *seat.Ask) { a.ID = "a.b" }, + "headline is empty": func(a *seat.Ask) { a.Headline = "" }, + "explains nothing": func(a *seat.Ask) { a.Explanation = " " }, + "only the operator answers": func(a *seat.Ask) { a.Who = "agent" }, + "offers 0 options": func(a *seat.Ask) { a.Options = nil }, + "offers 5 options": func(a *seat.Ask) { a.Options = append(a.Options, a.Options[0], a.Options[1]) }, + "is offered twice": func(a *seat.Ask) { a.Options[1].ID = "release" }, + "does not say what it does": func(a *seat.Ask) { a.Options[0].Does = "" }, + "none of acknowledge": func(a *seat.Ask) { a.Options[0].Level = "admin" }, + "expires before it is asked": func(a *seat.Ask) { a.Expires = FormatTime(now) }, + "approves lasts at most": func(a *seat.Ask) { a.Expires = FormatTime(now.Add(25 * time.Hour)) }, + "destroys lasts at most": func(a *seat.Ask) { a.Options[0].Level = Destroy }, + "never defaults": func(a *seat.Ask) { a.OnExpiry = "" }, + "is a key, without spaces": func(a *seat.Ask) { a.About = "a b" }, + "does not bind what it": func(a *seat.Ask) { a.Options[0].Binds = "" }, + } { + a := held() + change(&a) + if err := Check(a, now); err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("want a refusal saying %q, got %v", want, err) + } + } + ack := held() + ack.Options = ack.Options[2:] + ack.OnExpiry = "" + ack.Expires = FormatTime(now.Add(7 * 24 * time.Hour)) + if err := Check(ack, now); err != nil { + t.Errorf("an ask that only acknowledges needs no expiry words and may last a week: %v", err) + } +} + +func TestAnAskerActsOnlyOnAWarrantForItsOwnAskAndOption(t *testing.T) { + w := seat.Warrant{Ask: "d-1", Asker: "an-asker", Outcome: OutcomeChosen, Option: "release", Label: "Release", + Level: Approve, By: &seat.Person{Who: Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}, + AskDigest: Digest(held()), At: FormatTime(now.Add(time.Minute))} + if o, err := For(w, "an-asker", held()); err != nil || o.ID != "release" { + t.Fatalf("a sound warrant was refused: %v", err) + } + if got := Says(w); got != "the operator, via telegram (user id verified), chose Release" { + t.Errorf("the warrant says %q", got) + } + for why, change := range map[string]func(*seat.Warrant){ + "another asker": func(w *seat.Warrant) { w.Asker = "mesh-controller" }, + "another ask": func(w *seat.Warrant) { w.Ask = "d-2" }, + "no choice": func(w *seat.Warrant) { w.Outcome = OutcomeExpired }, + "nobody": func(w *seat.Warrant) { w.By = nil }, + "not offered": func(w *seat.Warrant) { w.Option = "delete" }, + "a lower level": func(w *seat.Warrant) { w.Level = Acknowledge }, + "another role": func(w *seat.Warrant) { w.By = &seat.Person{Who: "agent"} }, + "no digest": func(w *seat.Warrant) { w.AskDigest = "" }, + "no time given": func(w *seat.Warrant) { w.At = "" }, + "after expiry": func(w *seat.Warrant) { w.At = FormatTime(now.Add(2 * time.Hour)) }, + "another ask's digest": func(w *seat.Warrant) { + other := held() + other.Options[0].Binds = bind("release", "another") + w.AskDigest = Digest(other) + }, + } { + c := w + by := *w.By + c.By = &by + change(&c) + if _, err := For(c, "an-asker", held()); err == nil { + t.Errorf("a warrant with %s was acted on", why) + } + } +} + +// A warrant answers one ask as it was shown, and authorises the act its option bound and no other +// (novox/hq ADR 0259 §6: the warrant is bound to the exact ask and its arguments). +func TestAWarrantAuthorisesOnlyTheActItsOptionBound(t *testing.T) { + a := held() + // The digest survives the trip through the bus: what the router decodes and digests is what was sent. + raw, err := json.Marshal(a) + if err != nil { + t.Fatal(err) + } + var decoded seat.Ask + if err := json.Unmarshal(raw, &decoded); err != nil { + t.Fatal(err) + } + if Digest(decoded) != Digest(a) { + t.Fatalf("the ask's digest changed on the way: %s, %s", Digest(decoded), Digest(a)) + } + local := a + local.Expires = inZone(a.Expires, time.FixedZone("CEST", 2*3600)) + if Digest(local) != Digest(a) { + t.Error("the same instant in another zone digests differently") + } + // Changing anything the person was shown, or what an option binds, changes the digest. + for what, change := range map[string]func(*seat.Ask){ + "the headline": func(a *seat.Ask) { a.Headline += "!" }, + "the explanation": func(a *seat.Ask) { a.Explanation += " " }, + "a label": func(a *seat.Ask) { a.Options[0].Label = "Go" }, + "what it binds": func(a *seat.Ask) { a.Options[0].Binds = bind("release", "another") }, + "the expiry": func(a *seat.Ask) { a.Expires = shift(a.Expires, time.Second) }, + "what it is about": func(a *seat.Ask) { a.About = "delivery.other" }, + } { + c := held() + change(&c) + if Digest(c) == Digest(a) { + t.Errorf("changing %s leaves the digest", what) + } + } + o, _ := OptionOf(a, "release") + if err := Performs(o, Act{"verb": "release", "delivery": "openrazer"}); err != nil { + t.Errorf("the bound act was refused: %v", err) + } + if err := Performs(o, Act{"verb": "release", "delivery": "another"}); err == nil { + t.Error("an act the option did not bind was let through") + } + ack, _ := OptionOf(a, "silence") + if err := Performs(ack, Act{"anything": "x"}); err != nil { + t.Errorf("an acknowledgement that bound nothing: %v", err) + } +} + +// The digests are over named fields in a canonical encoding (the review of 2026-10-09): not over Go's +// encoding of a struct, so a field added to Ask or Option, a JSON tag renamed, or a map built in another +// order changes nothing it should not — and every field that is shown or bound is in it. +func TestDigestsAreCanonicalOverNamedFields(t *testing.T) { + // The digests are those mesh-sdk's asks gave before the move to the contract's types (ADR 0627 §1): a router and + // an asker of either build agree on every ask. + gold := held() + gold.Whole, gold.Details, gold.WholeDetails = "w", "d", "wd" + if Digest(held()) != "sha256:088e7e69e1783b78c1b7e8310e0a6d0504a88a37982b293bc1e667da4565bb0f" || + Digest(gold) != "sha256:997af29a80663c5eb7d9dd38fc029c4a15fd726220b25e3c7cdc54d5a60bbbae" { + t.Errorf("the digests changed with the move: %s, %s", Digest(held()), Digest(gold)) + } + a1, _ := ActDigest(Act{"verb": "release", "arg.delivery": "hq"}) + b := Act{} + b["arg.delivery"] = "hq" + b["verb"] = "release" + a2, _ := ActDigest(b) + if a1 != a2 { + t.Error("the order an act was built in changed its digest") + } + // A value cannot be read as two: "a"+"b=c" is not "a=b"+"c". + x, _ := ActDigest(Act{"a": "b\n1:c"}) + y, _ := ActDigest(Act{"a": "b", "c": ""}) + if x == y { + t.Error("two different acts encode the same") + } + if _, err := ActDigest(Act{}); err == nil { + t.Error("an empty act was digested") + } + // The encoding, by hand: any language reproduces it. + sum := sha256.Sum256([]byte("novox.act.v1\n4:verb\n7:release\n")) + if got, _ := ActDigest(Act{"verb": "release"}); got != "sha256:"+hex.EncodeToString(sum[:]) { + t.Errorf("the act's encoding is not the one written down: %s", got) + } + + // Every exported field of Ask and Option is in the digest: changing any one changes it. + base := held() + mutations := map[string]func(*seat.Ask){ + "ID": func(a *seat.Ask) { a.ID = "other" }, + "Headline": func(a *seat.Ask) { a.Headline += "!" }, + "Explanation": func(a *seat.Ask) { a.Explanation += "!" }, + "Who": func(a *seat.Ask) { a.Who = "someone" }, + "Expires": func(a *seat.Ask) { a.Expires = shift(a.Expires, time.Nanosecond) }, + "OnExpiry": func(a *seat.Ask) { a.OnExpiry += "!" }, + "About": func(a *seat.Ask) { a.About += "!" }, + "Urgent": func(a *seat.Ask) { a.Urgent = !a.Urgent }, + "Options": func(a *seat.Ask) { a.Options = a.Options[:1] }, + "Option.ID": func(a *seat.Ask) { a.Options = append([]seat.Option(nil), a.Options...); a.Options[0].ID = "x" }, + "Option.Label": func(a *seat.Ask) { + a.Options = append([]seat.Option(nil), a.Options...) + a.Options[0].Label += "!" + }, + "Option.Does": func(a *seat.Ask) { a.Options = append([]seat.Option(nil), a.Options...); a.Options[0].Does += "!" }, + "Option.Level": func(a *seat.Ask) { + a.Options = append([]seat.Option(nil), a.Options...) + a.Options[0].Level = Destroy + }, + "Option.Binds": func(a *seat.Ask) { + a.Options = append([]seat.Option(nil), a.Options...) + a.Options[0].Binds = "sha256:00" + }, + } + for name, mutate := range mutations { + c := base + mutate(&c) + if Digest(c) == Digest(base) { + t.Errorf("changing %s left the digest the same", name) + } + } + // Whole and Details (issue 383) are in the digest once given, and change it; an ask without either digests + // exactly as before they existed — the encoding by hand, so no build can drift from it unseen. + plain := seat.Ask{ID: "p", Headline: "h", Explanation: "e", Who: Operator, Expires: FormatTime(now), OnExpiry: "o", About: "a", + Options: []seat.Option{{ID: "ok", Label: "OK", Does: "d", Level: Acknowledge}}} + byHand := sha256.Sum256([]byte("novox.ask.v1\n1:p\n1:h\n1:e\n8:operator\n20:2026-10-08T14:00:00Z\n1:o\n1:a\n5:false\n1:1\n" + + "2:ok\n2:OK\n1:d\n11:acknowledge\n0:\n")) + if Digest(plain) != "sha256:"+hex.EncodeToString(byHand[:]) { + t.Errorf("an ask without Whole and Details does not digest as it did before them: %s", Digest(plain)) + } + withWhole, withDetails := plain, plain + withWhole.Whole = "e, whole" + withDetails.Details = "fingerprint 1234" + if Digest(withWhole) == Digest(plain) || Digest(withDetails) == Digest(plain) || Digest(withWhole) == Digest(withDetails) { + t.Error("Whole or Details left the digest the same") + } + both := withWhole + both.Details = "fingerprint 1234" + changed := both + changed.Whole += "!" + if Digest(changed) == Digest(both) { + t.Error("changing Whole beside Details left the digest the same") + } + // A field added to Ask or Option without being added to Digest fails here. + if n := reflect.TypeOf(seat.Ask{}).NumField(); n != 12 { + t.Errorf("Ask has %d fields, and Digest covers 12: add the new one to Digest and to this test", n) + } + if n := reflect.TypeOf(seat.Option{}).NumField(); n != 5 { + t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n) + } + // The same instant in another zone is the same ask. + z := base + z.Expires = inZone(z.Expires, time.FixedZone("x", 3600)) + if Digest(z) != Digest(base) { + t.Error("the expiry's zone changed the digest") + } +} + +// An ask that says no expiry is answered by no warrant (the confirmation review of 2026-10-09): nothing could +// show the answer was in time. +func TestAWarrantForAnAskWithNoExpiryIsRefused(t *testing.T) { + a := held() + a.Expires = "" + w := seat.Warrant{Ask: "d-1", Asker: "an-asker", Outcome: OutcomeChosen, Option: "release", Label: "Release", + Level: Approve, By: &seat.Person{Who: Operator, Kind: "telegram", Identity: "42"}, AskDigest: Digest(a), + At: FormatTime(now.Add(time.Minute))} + if _, err := For(w, "an-asker", a); err == nil { + t.Error("a warrant for an ask with no expiry was taken") + } +} + +// An explanation that reads at a glance passes; a wall of text says each way it is one (novox/hq issue 639). +func TestAnExplanationReadsAtAGlance(t *testing.T) { + if err := Glance("Your agents' memory follows you to every machine. A file changed on two keeps both.\n\n• Only the memory folders are carried\n• A conflict reaches you as a warning"); err != nil { + t.Fatalf("a summary and two points: %v", err) + } + var wall strings.Builder + wall.WriteString("ADR 0343 was proposed by fix-367 and approved by the knowledge base's own review.") + for i := 1; i <= 13; i++ { + wall.WriteString("\n" + strings.Repeat("A clause lead. ", 3)) + } + err := Glance(wall.String()) + if err == nil || !strings.Contains(err.Error(), "13 points") { + t.Fatalf("thirteen clause lines pass as a glance: %v", err) + } + if err := Glance(strings.Repeat("word ", 70)); err == nil || !strings.Contains(err.Error(), "summary is 349") { + t.Fatalf("a long summary passes: %v", err) + } + if err := Glance("short.\n" + strings.Repeat("p", PointLength+1)); err == nil || !strings.Contains(err.Error(), "point 1") { + t.Fatalf("a long point passes: %v", err) + } + if err := Glance(" \n"); err == nil { + t.Fatal("an empty explanation passes") + } +} + +// WholeDetails is digested when given, and an ask without it digests as before it existed: a router and an asker +// of different builds still agree on every such ask. +func TestWholeDetailsAreDigestedOnlyWhenGiven(t *testing.T) { + a := seat.Ask{ID: "a", Headline: "h", Explanation: "e", Details: "d"} + before := Digest(a) + a.WholeDetails = "+ shares: media=/storage/media" + if Digest(a) == before { + t.Fatal("the whole details are not in the digest") + } + b := a + b.WholeDetails = "+ shares: media=/storage/other" + if Digest(a) == Digest(b) { + t.Fatal("two whole details digest the same") + } + a.WholeDetails = "" + if Digest(a) != before { + t.Fatal("an ask without whole details digests otherwise than before") + } +} + +// No two asks encode the same (the review of mesh-sdk #25): whole details given beside nothing else are not read as +// a Whole and Details that happen to spell the same values, nor as any other split of the three fields. +func TestWholeDetailsCollideWithNoOtherAsk(t *testing.T) { + base := seat.Ask{ID: "a", Headline: "h", Explanation: "e"} + shapes := []seat.Ask{} + for _, f := range []struct{ whole, details, wholeDetails string }{ + {"", "", "+ x: 1"}, + {"whole-details", "+ x: 1", ""}, + {"", "+ x: 1", ""}, + {"+ x: 1", "", ""}, + {"", "", "whole-details"}, + {"", "+ x: 1", "+ x: 1"}, + {"+ x: 1", "+ x: 1", ""}, + {"", "", ""}, + } { + a := base + a.Whole, a.Details, a.WholeDetails = f.whole, f.details, f.wholeDetails + shapes = append(shapes, a) + } + seen := map[string]int{} + for i, a := range shapes { + if j, ok := seen[Digest(a)]; ok { + t.Errorf("asks %d and %d digest the same: %+v / %+v", j, i, shapes[j], a) + } + seen[Digest(a)] = i + } +} + +// shift is an RFC 3339 time of the contract moved by d. +func shift(s string, d time.Duration) string { + t, err := Time(s) + if err != nil { + panic(err) + } + return t.Add(d).Format(time.RFC3339Nano) +} + +// inZone is an RFC 3339 time of the contract written in another zone: the same instant. +func inZone(s string, z *time.Location) string { + t, err := Time(s) + if err != nil { + panic(err) + } + return t.In(z).Format(time.RFC3339Nano) +} diff --git a/go/clients/operatorchannelclient/client.go b/go/clients/operatorchannelclient/client.go new file mode 100644 index 0000000..f67025a --- /dev/null +++ b/go/clients/operatorchannelclient/client.go @@ -0,0 +1,197 @@ +package operatorchannelclient + +// Client is a module's asking of a person (novox/hq ADR 0259 §10): it publishes an ask under the module's own name, +// cancels it, reads its state from the router's record, and hands the module each warrant once — only one for an ask +// it made, for an option it offered, at that option's level, before it expired. The module then acts with its own +// grants. +// +// It keeps what it asked, and which warrants it acted on, in a state of the module's own (declared in its manifest, +// by default `asked`): the ask is written before it is published, and a warrant is marked acted on by compare-and-set +// before the module's handler runs, so a redelivery, or a second instance, acts once. + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// Client asks as one module. +type Client struct { + // Module is the asking module's name: the one the bus grants its asks under. + Module string + // State is the module's own state the asks are kept in. + State Store + // Now is the clock; nil is time.Now. + Now func() time.Time + // Publish, Record and Subscribe reach the bus; nil is the stdio SDK's, through the module's runtime. + Publish func(subject string, body any, id string) error + Record func(key string) (*stdio.StateEntry, error) + Subscribe func(pattern string, handle func(stdio.Envelope) error) error +} + +// Store is where the asks are kept: stdio.State(name) is one. +type Store interface { + Get(key string) (*stdio.StateEntry, error) + Create(key string, value any) (uint64, error) + Update(key string, value any, revision uint64) (uint64, error) +} + +// New is a client for the module the runtime serves, keeping its asks in the state named. +func New(state string) *Client { + return &Client{Module: os.Getenv("MESH_MODULE"), State: stdio.State(state)} +} + +func (c *Client) now() time.Time { + if c.Now != nil { + return c.Now() + } + return time.Now() +} + +// Kept is one ask as the module keeps it. +type Kept struct { + Ask seat.Ask `json:"ask"` + Asked time.Time `json:"asked"` + Acted bool `json:"acted,omitempty"` + Ended string `json:"ended,omitempty"` + Answer *seat.Warrant `json:"answer,omitempty"` +} + +func key(id string) string { return "ask." + id } + +// Ask checks an ask, keeps it, and publishes it under the module's own name. An ask with an id already kept +// is refused: a new question is a new id. +func (c *Client) Ask(a seat.Ask) error { + if err := Check(a, c.now()); err != nil { + return err + } + if _, err := c.State.Create(key(a.ID), Kept{Ask: a, Asked: c.now()}); err != nil { + if errors.Is(err, stdio.ErrChanged) { + return fmt.Errorf("an ask %s was already made: a new question is a new id", a.ID) + } + return err + } + return c.publish(seat.AcceptSubjectAsk(c.Module), a, a.ID) +} + +// Cancel takes an ask back: the router ends it and edits every copy. +func (c *Client) Cancel(id string) error { + return c.publish(seat.AcceptSubjectCancel(c.Module), map[string]string{"id": id}, "cancel."+id) +} + +func (c *Client) publish(subject string, body any, id string) error { + if c.Publish != nil { + return c.Publish(subject, body, id) + } + return stdio.SeatPublish(subject, body, id) +} + +// Where is an ask's state as the router keeps it: its state and its warrant, if any — read under the +// module's own name and no other's. +func (c *Client) Where(id string) (state string, w *seat.Warrant, err error) { + read := c.Record + if read == nil { + read = func(k string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", k) } + } + e, err := read(id) + if err != nil || e == nil { + return "", nil, err + } + var rec struct { + State string `json:"state"` + Warrant *seat.Warrant `json:"warrant"` + } + if err := json.Unmarshal(e.Value, &rec); err != nil { + return "", nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err) + } + return rec.State, rec.Warrant, nil +} + +// Handled is what came of a warrant: acted on, refused (and why), or already done. +type Handled string + +const ( + Acted Handled = "acted" + Refused Handled = "refused" + Done Handled = "already acted on" + Recorded Handled = "ended without a choice" +) + +// Take hands act a warrant once, after holding it to the ask it answers. act performs the option with the +// module's own grants; its error is said and the warrant stays acted on (a person's decision is not +// performed twice; the module says the failure in its own record). +func (c *Client) Take(w seat.Warrant, act func(seat.Option, seat.Warrant) error) (Handled, error) { + if w.Asker != c.Module { + return Refused, fmt.Errorf("a warrant for %s reached %s", w.Asker, c.Module) + } + e, err := c.State.Get(key(w.Ask)) + if err != nil { + return "", err + } + if e == nil { + return Refused, fmt.Errorf("%s holds no ask %s", c.Module, w.Ask) + } + var k Kept + if err := json.Unmarshal(e.Value, &k); err != nil { + return "", err + } + if k.Acted || k.Ended != "" { + return Done, nil + } + if w.Outcome != OutcomeChosen { + k.Ended = w.Outcome + k.Answer = &w + if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil && !errors.Is(err, stdio.ErrChanged) { + return "", err + } + return Recorded, nil + } + o, err := For(w, c.Module, k.Ask) + if err != nil { + return Refused, err + } + // Marked before acting: of two deliveries, or two instances, only the one whose write stands acts. + k.Acted, k.Ended, k.Answer = true, OutcomeChosen, &w + if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil { + if errors.Is(err, stdio.ErrChanged) { + return Done, nil + } + return "", err + } + return Acted, act(o, w) +} + +// Listen hands every warrant for this module to Take, with act. It asks the runtime for the module's +// events matching the router's `decided` subject for this module. +func (c *Client) Listen(act func(seat.Option, seat.Warrant) error, said func(Handled, seat.Warrant, error)) error { + subscribe := c.Subscribe + if subscribe == nil { + subscribe = stdio.Subscribe + } + return subscribe(seat.Seat+"."+seat.EventDecided+"."+c.Module, func(e stdio.Envelope) error { + if !strings.HasPrefix(e.Key, seat.Seat+"."+seat.EventDecided+".") { + return nil + } + var w seat.Warrant + if err := json.Unmarshal(e.Body, &w); err != nil { + if said != nil { + said(Refused, w, fmt.Errorf("a warrant could not be read: %w", err)) + } + return nil + } + h, err := c.Take(w, act) + if h == "" && err != nil { + return err // the state could not be reached: offered again + } + if said != nil { + said(h, w, err) + } + return nil + }) +} diff --git a/go/clients/operatorchannelclient/client_test.go b/go/clients/operatorchannelclient/client_test.go new file mode 100644 index 0000000..0b16073 --- /dev/null +++ b/go/clients/operatorchannelclient/client_test.go @@ -0,0 +1,139 @@ +package operatorchannelclient + +import ( + "encoding/json" + "strings" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +type mem struct { + vals map[string]json.RawMessage + revs map[string]uint64 + n uint64 +} + +func (m *mem) Get(k string) (*stdio.StateEntry, error) { + v, ok := m.vals[k] + if !ok { + return nil, nil + } + return &stdio.StateEntry{Key: k, Value: v, Revision: m.revs[k]}, nil +} +func (m *mem) Create(k string, v any) (uint64, error) { + if _, ok := m.vals[k]; ok { + return 0, stdio.ErrChanged + } + return m.put(k, v) +} +func (m *mem) Update(k string, v any, rev uint64) (uint64, error) { + if rev == 0 || m.revs[k] != rev { + return 0, stdio.ErrChanged + } + return m.put(k, v) +} +func (m *mem) put(k string, v any) (uint64, error) { + raw, _ := json.Marshal(v) + m.n++ + m.vals[k], m.revs[k] = raw, m.n + return m.n, nil +} + +func client() (*Client, map[string][]string) { + sent := map[string][]string{} + c := &Client{Module: "an-asker", State: &mem{vals: map[string]json.RawMessage{}, revs: map[string]uint64{}}, + Now: func() time.Time { return now }, + Publish: func(subject string, body any, id string) error { + sent[subject] = append(sent[subject], id) + return nil + }} + return c, sent +} + +// releaseHQ is the act the ask's one option binds. +var releaseHQ = map[string]string{"verb": "release", "delivery": "hq"} + +func askedHeld() seat.Ask { + binds, _ := ActDigest(releaseHQ) + return seat.Ask{ID: "d-1", Headline: "Delivery of hq held", Explanation: "Needs you: release it, or stop it.", + Who: Operator, Expires: FormatTime(now.Add(time.Hour)), OnExpiry: "the delivery stays held", + Options: []seat.Option{{ID: "release", Label: "Release", Does: "it goes on", Level: Approve, Binds: binds}}} +} + +func askedWarrant() seat.Warrant { + return seat.Warrant{Ask: "d-1", Asker: "an-asker", Outcome: OutcomeChosen, Option: "release", + Label: "Release", Level: Approve, At: FormatTime(now.Add(time.Minute)), AskDigest: Digest(askedHeld()), + By: &seat.Person{Who: Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}} +} + +func TestAnAskIsKeptThenPublishedUnderTheModulesOwnName(t *testing.T) { + c, sent := client() + if err := c.Ask(askedHeld()); err != nil { + t.Fatal(err) + } + if got := sent[seat.AcceptSubjectAsk("an-asker")]; len(got) != 1 || got[0] != "d-1" { + t.Fatalf("%v", sent) + } + if err := c.Ask(askedHeld()); err == nil || !strings.Contains(err.Error(), "already made") { + t.Errorf("the same id was asked twice: %v", err) + } + bad := askedHeld() + bad.Who = "agent" + if err := c.Ask(bad); err == nil { + t.Error("an ask the contract refuses was published") + } +} + +func TestAWarrantIsActedOnOnceAndOnlyForItsOwnAsk(t *testing.T) { + c, _ := client() + _ = c.Ask(askedHeld()) + acted := 0 + act := func(o seat.Option, _ seat.Warrant) error { + if err := Performs(o, releaseHQ); err != nil { + t.Errorf("the bound act: %v", err) + } + acted++ + return nil + } + if h, err := c.Take(askedWarrant(), act); h != Acted || err != nil || acted != 1 { + t.Fatalf("%s %v %d", h, err, acted) + } + if h, _ := c.Take(askedWarrant(), act); h != Done || acted != 1 { + t.Errorf("a redelivery was acted on: %s", h) + } + for why, change := range map[string]func(*seat.Warrant){ + "another asker": func(w *seat.Warrant) { w.Asker = "mesh-controller" }, + "an ask not made": func(w *seat.Warrant) { w.Ask = "d-9" }, + "another level": func(w *seat.Warrant) { w.Level = Acknowledge }, + "an option not offered": func(w *seat.Warrant) { w.Option = "delete" }, + "after its expiry": func(w *seat.Warrant) { w.At = FormatTime(now.Add(2 * time.Hour)) }, + "another ask's digest": func(w *seat.Warrant) { w.AskDigest = "sha256:00" }, + } { + c2, _ := client() + _ = c2.Ask(askedHeld()) + w := askedWarrant() + change(&w) + if h, _ := c2.Take(w, act); h == Acted { + t.Errorf("a warrant from %s was acted on", why) + } + } + if acted != 1 { + t.Errorf("acted %d times", acted) + } +} + +func TestAnAskEndedWithoutAChoiceIsRecordedAndNothingDone(t *testing.T) { + c, _ := client() + _ = c.Ask(askedHeld()) + w := askedWarrant() + w.Outcome, w.Option, w.By = OutcomeExpired, "", nil + if h, err := c.Take(w, func(seat.Option, seat.Warrant) error { t.Fatal("acted"); return nil }); h != Recorded || err != nil { + t.Fatalf("%s %v", h, err) + } + if h, _ := c.Take(askedWarrant(), func(seat.Option, seat.Warrant) error { t.Fatal("acted after it ended"); return nil }); h != Done { + t.Errorf("%s", h) + } +} diff --git a/go/clients/operatorchannelclient/condition.go b/go/clients/operatorchannelclient/condition.go new file mode 100644 index 0000000..af9485b --- /dev/null +++ b/go/clients/operatorchannelclient/condition.go @@ -0,0 +1,117 @@ +package operatorchannelclient + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" +) + +// ConditionSubject is where a module says a condition to the operator: the seat's by-caller condition accept +// (novox/hq ADR 0437 §9). The router reads the verb at the fifth part and the caller at the sixth, so the publisher is +// one token, the module's own name; it is refused when empty or when it holds a dot, a wildcard or whitespace. +func ConditionSubject(publisher string) (string, error) { + if publisher == "" { + return "", errors.New("operator-channel: the condition's publisher is empty") + } + if strings.ContainsAny(publisher, ".*> \t\r\n") || strings.ContainsFunc(publisher, func(r rune) bool { return r <= ' ' }) { + return "", fmt.Errorf("operator-channel: the condition's publisher %q is not one token: it holds a dot, a wildcard or whitespace", publisher) + } + return seat.AcceptSubjectCondition(publisher), nil +} + +// ConditionVersion is the version of the condition value this package writes and the only one it reads. +const ConditionVersion = 1 + +// ErrUnknownConditionVersion is what DecodeCondition returns for a value whose version is missing or not +// ConditionVersion. Test it with errors.Is. +var ErrUnknownConditionVersion = errors.New("operator-channel: unknown condition version") + +// The words a condition's explanation opens with (novox/hq ADR 0253 §1). +const ( + NothingToDo = "Nothing for you to do." + NeedsYou = "Needs you: " +) + +// The severities and states of a condition. +const ( + SeverityUrgent = "urgent" + SeverityWarning = "warning" + StateRaised = "raised" + StateCleared = "cleared" +) + +func checkCondition(c seat.Condition) error { + if c.Key == "" { + return errors.New("operator-channel: a condition has no key") + } + if c.Subject.Scope == "" || c.Subject.ID == "" { + return errors.New("operator-channel: a condition's subject needs its scope and id") + } + switch c.State { + case StateCleared: + return nil + case StateRaised: + default: + return fmt.Errorf("operator-channel: the condition's state %q is not raised or cleared", c.State) + } + switch c.Severity { + case SeverityUrgent, SeverityWarning: + default: + return fmt.Errorf("operator-channel: the condition's severity %q is not urgent or warning", c.Severity) + } + if c.Headline == "" { + return errors.New("operator-channel: a raised condition has no headline") + } + if n := len([]rune(c.Headline)); n > HeadlineLength { + return fmt.Errorf("operator-channel: the condition's headline is %d characters, more than %d", n, HeadlineLength) + } + verdict := NothingToDo + if c.Needs != "" { + verdict = NeedsYou + c.Needs + } + if !strings.HasPrefix(c.Explanation, verdict) { + return fmt.Errorf("operator-channel: the condition's explanation must open with %q", verdict) + } + return nil +} + +// EncodeCondition is c as JSON at ConditionVersion; it refuses what ADR 0253 and ADR 0437 §9 refuse. +func EncodeCondition(c seat.Condition) ([]byte, error) { + if err := checkCondition(c); err != nil { + return nil, err + } + c.V = ConditionVersion + return json.Marshal(c) +} + +// DecodeCondition reads a condition; it refuses an unknown version (ErrUnknownConditionVersion) and what +// EncodeCondition refuses. +func DecodeCondition(data []byte) (seat.Condition, error) { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return seat.Condition{}, fmt.Errorf("operator-channel: the condition is not JSON: %w", err) + } + if probe.V == nil { + return seat.Condition{}, fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownConditionVersion, ConditionVersion) + } + if *probe.V != ConditionVersion { + return seat.Condition{}, fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownConditionVersion, *probe.V, ConditionVersion) + } + var c seat.Condition + if err := json.Unmarshal(data, &c); err != nil { + return seat.Condition{}, fmt.Errorf("operator-channel: the condition does not fit its shape: %w", err) + } + if err := checkCondition(c); err != nil { + return seat.Condition{}, err + } + return c, nil +} + +// SameCondition says whether two conditions are the same value. +func SameCondition(a, b seat.Condition) bool { return reflect.DeepEqual(a, b) } diff --git a/go/clients/operatorchannelclient/condition_test.go b/go/clients/operatorchannelclient/condition_test.go new file mode 100644 index 0000000..c9f32d1 --- /dev/null +++ b/go/clients/operatorchannelclient/condition_test.go @@ -0,0 +1,145 @@ +package operatorchannelclient + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" +) + +func TestConditionSubject(t *testing.T) { // 1202/AC1 + s, err := ConditionSubject("an-asker-teller") + if err != nil || s != "mesh.seat.operator-channel.accept.condition.an-asker-teller" { + t.Fatalf("subject = %q, %v", s, err) + } + // The router splits an accept subject in six parts and reads the verb at 4 and the caller at 5. + parts := strings.Split(s, ".") + if len(parts) != 6 || parts[4] != "condition" || parts[5] != "an-asker-teller" { + t.Errorf("subject %q does not keep the router's shape: %v", s, parts) + } + for _, bad := range []string{"", "a.b", "a*", "a>", "a b", "a\tb", "a\nb"} { + if _, err := ConditionSubject(bad); err == nil { + t.Errorf("publisher %q accepted", bad) + } + } +} + +func raisedCondition() seat.Condition { + return seat.Condition{ + Key: "delivery.stuck.mesh-mail", Severity: "warning", + Subject: seat.ConditionSubject{Scope: "module", ID: "mesh-mail", Machine: "ace"}, State: "raised", + Summary: "plan 12 stopped", Headline: "Delivery of mesh-mail is stuck", + Needs: "approve the merge.", Explanation: "Needs you: approve the merge. The plan waits.", + Resolved: "The delivery of mesh-mail moved on.", + Actions: []seat.ConditionActionsItem{{Label: "Approve", Verb: "mesh-controller.assign", Machine: "ace", + Arguments: map[string]string{"why": ""}, Level: Approve}}, + } +} + +func clearedCondition() seat.Condition { + return seat.Condition{Key: "delivery.stuck.mesh-mail", Subject: seat.ConditionSubject{Scope: "module", ID: "mesh-mail"}, State: "cleared"} +} + +func TestConditionRoundTrip(t *testing.T) { // 1202/AC2 + for _, c := range []seat.Condition{raisedCondition(), clearedCondition()} { + data, err := EncodeCondition(c) + if err != nil { + t.Fatalf("%s: %v", c.State, err) + } + var probe map[string]any + if err := json.Unmarshal(data, &probe); err != nil || probe["v"] != float64(1) { + t.Fatalf("%s: %s does not carry v:1", c.State, data) + } + c.V = ConditionVersion + got, err := DecodeCondition(data) + if err != nil || !SameCondition(got, c) { + t.Errorf("%s round trip: %+v %v", c.State, got, err) + } + } + data, _ := EncodeCondition(raisedCondition()) + var probe map[string]any + _ = json.Unmarshal(data, &probe) + for _, name := range []string{"key", "severity", "subject", "summary", "headline", "needs", "explanation", "resolved", "actions", "state"} { + if _, ok := probe[name]; !ok { + t.Errorf("the JSON has no %q: %s", name, data) + } + } + subject := probe["subject"].(map[string]any) + for _, name := range []string{"scope", "id", "machine"} { + if _, ok := subject[name]; !ok { + t.Errorf("the subject has no %q: %s", name, data) + } + } + action := probe["actions"].([]any)[0].(map[string]any) + for _, name := range []string{"label", "verb", "machine", "arguments", "level"} { + if _, ok := action[name]; !ok { + t.Errorf("the action has no %q: %s", name, data) + } + } + for _, in := range []string{`{"key":"k"}`, `{"v":2,"key":"k"}`} { + if _, err := DecodeCondition([]byte(in)); !errors.Is(err, ErrUnknownConditionVersion) { + t.Errorf("decode of %s = %v, want ErrUnknownConditionVersion", in, err) + } + } +} + +func TestConditionVerdict(t *testing.T) { // 1202/AC3 + c := raisedCondition() + c.Needs, c.Explanation = "", "The plan waits." + _, err := EncodeCondition(c) + if err == nil || !strings.Contains(err.Error(), "Nothing for you to do.") { + t.Errorf("empty needs without its verdict: %v", err) + } + c.Explanation = "Nothing for you to do. The plan waits." + if _, err := EncodeCondition(c); err != nil { + t.Errorf("empty needs with its verdict refused: %v", err) + } + c = raisedCondition() + c.Explanation = "Nothing for you to do. The plan waits." + _, err = EncodeCondition(c) + if err == nil || !strings.Contains(err.Error(), "Needs you: approve the merge.") { + t.Errorf("needs set with the wrong verdict: %v", err) + } + c.Explanation = "Needs you: something else. The plan waits." + if _, err = EncodeCondition(c); err == nil { + t.Error("needs set with other needs in the verdict accepted") + } +} + +func TestConditionRefused(t *testing.T) { // 1202/AC4 + mutate := func(f func(*seat.Condition)) error { + c := raisedCondition() + f(&c) + _, err := EncodeCondition(c) + return err + } + for name, f := range map[string]func(*seat.Condition){ + "severity": func(c *seat.Condition) { c.Severity = "critical" }, + "state": func(c *seat.Condition) { c.State = "open" }, + "key": func(c *seat.Condition) { c.Key = "" }, + "headline": func(c *seat.Condition) { c.Headline = "" }, + "long headline": func(c *seat.Condition) { c.Headline = strings.Repeat("h", HeadlineLength+1) }, + } { + if err := mutate(f); err == nil { + t.Errorf("a condition with a bad %s accepted", name) + } + } + if err := mutate(func(c *seat.Condition) { c.Headline = strings.Repeat("h", HeadlineLength) }); err != nil { + t.Errorf("a headline of the greatest length refused: %v", err) + } + if _, err := EncodeCondition(clearedCondition()); err != nil { + t.Errorf("a cleared condition with only key, subject and state refused: %v", err) + } + c := clearedCondition() + c.Key = "" + if _, err := EncodeCondition(c); err == nil { + t.Error("a cleared condition without its key accepted") + } + c = clearedCondition() + c.State = "gone" + if _, err := EncodeCondition(c); err == nil { + t.Error("a condition with an unknown state accepted") + } +} diff --git a/go/clients/operatorchannelclient/go.mod b/go/clients/operatorchannelclient/go.mod new file mode 100644 index 0000000..d7217d2 --- /dev/null +++ b/go/clients/operatorchannelclient/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient + +go 1.26.0 diff --git a/module.json b/module.json index 5fecc68..fd20a00 100644 --- a/module.json +++ b/module.json @@ -9,6 +9,7 @@ { "name": "go-gopackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/gopackageregistryclient"], "series": "0.1" }, { "name": "go-npmpackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/npmpackageregistryclient"], "series": "0.1" }, { "name": "go-ociartifactregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/ociartifactregistryclient"], "series": "0.1" }, + { "name": "go-operatorchannelclient", "kind": "package", "language": "go", "paths": ["go/clients/operatorchannelclient"], "series": "0.1" }, { "name": "go-containerimageregistry", "kind": "package", "language": "go", "paths": ["go/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-gopackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-meshbroker", "kind": "package", "language": "go", "paths": ["go/seats/meshbroker"], "series": "0.1", "generated-from": "." }, -- 2.54.0 From 323e6a6be20affde4faec7c36d3e34ed03448ff6 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:15:18 +0100 Subject: [PATCH 25/94] SD-M2a: mesh-broker's Held says the entry's state and why, so show says why an entry is in its state Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshbroker/contract.go | 4 ++ go/seats/seats.go | 12 ++--- seats/mesh-broker/seat.json | 16 ++++++ ts/src/mesh-broker.ts | 4 ++ ts/src/seats.ts | 96 +++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 63e9044..1dcdb23 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -125,10 +125,14 @@ type Held struct { Options map[string]any `json:"options,omitempty"` // the settings the bus holds Settings map[string]any `json:"settings,omitempty"` + // the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it) + State string `json:"state,omitempty"` // how the bus keeps it StorageType string `json:"storage-type"` // its template; empty for a configuration of its own Template string `json:"template,omitempty"` + // why it is in its state, in plain words; empty when held as asked + Why string `json:"why,omitempty"` } // Named is a type of the contract. diff --git a/go/seats/seats.go b/go/seats/seats.go index eebcaef..fb51082 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -146,25 +146,25 @@ func List() []Seat { Serves: []Verb{ {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", Optional: true, Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every entry", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every entry", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, {Name: "show", Description: "One entry, its template, settings and fill.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, {Name: "check", Description: "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", Optional: true, Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"differences": map[string]any{"description": "every difference", "items": map[string]any{"properties": map[string]any{"asked": map[string]any{"description": "asked", "type": "object"}, "grows": map[string]any{"description": "the difference only grows: apply may act on it", "type": "boolean"}, "held": map[string]any{"description": "held", "type": "object"}, "local-name": map[string]any{"description": "the entry", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, "type": "array"}}, "required": []string{"differences"}, "type": "object"}}, {Name: "orphans", Description: "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", Optional: true, Input: map[string]any{"description": "no arguments", "properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every orphan", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every orphan", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, {Name: "create", Description: "Create one entry now.", Optional: true, Input: map[string]any{"description": "one piece of bus state as a manifest asks it", "properties": map[string]any{"configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, {Name: "apply", Description: "Apply a difference that only grows: a larger cap, a longer age.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, {Name: "recreate", Description: "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, + Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, {Name: "drop", Description: "Remove one orphan, after a stream snapshot.", Optional: true, Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"dropped": map[string]any{"description": "it is gone", "type": "boolean"}, "snapshot": map[string]any{"description": "the snapshot taken first", "type": "string"}}, "required": []string{"dropped"}, "type": "object"}}, diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index 4cea764..f0ebf4a 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -386,6 +386,22 @@ }, "fill": { "$ref": "#/defs/Fill" + }, + "state": { + "type": "string", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)" + }, + "why": { + "type": "string", + "description": "why it is in its state, in plain words; empty when held as asked" } }, "required": [ diff --git a/ts/src/mesh-broker.ts b/ts/src/mesh-broker.ts index b43ba38..a2b85a2 100644 --- a/ts/src/mesh-broker.ts +++ b/ts/src/mesh-broker.ts @@ -99,10 +99,14 @@ export interface Held { "options"?: Record; /** the settings the bus holds */ "settings"?: Record; + /** the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it) */ + "state"?: "held" | "refused" | "waiting" | "drifted" | "full" | "orphaned"; /** how the bus keeps it */ "storage-type": "stream" | "key-value" | "object-store" | "ledger"; /** its template; empty for a configuration of its own */ "template"?: "state" | "events" | "work-queue" | "log" | "objects" | "ledger" | ""; + /** why it is in its state, in plain words; empty when held as asked */ + "why"?: string; } export interface Named { diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 5eb57ff..746e817 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -218,6 +218,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -240,6 +252,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ @@ -329,6 +345,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -351,6 +379,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ @@ -479,6 +511,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -501,6 +545,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ @@ -623,6 +671,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -645,6 +705,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ @@ -726,6 +790,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -748,6 +824,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ @@ -839,6 +919,18 @@ export const list: readonly Seat[] = [ "description": "the settings the bus holds", "type": "object" }, + "state": { + "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", + "enum": [ + "held", + "refused", + "waiting", + "drifted", + "full", + "orphaned" + ], + "type": "string" + }, "storage-type": { "description": "how the bus keeps it", "enum": [ @@ -861,6 +953,10 @@ export const list: readonly Seat[] = [ "" ], "type": "string" + }, + "why": { + "description": "why it is in its state, in plain words; empty when held as asked", + "type": "string" } }, "required": [ -- 2.54.0 From cc2d3aafdb4c1d7f24db46549332aa0ff7b22766 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:17:26 +0100 Subject: [PATCH 26/94] SD-M2b, SD-M2d: the checker's and mesh-config's events kept in their own streams are mesh.stream..event.* verdict, tag-wanted and preview.written ride the checker's own streams, renamed, retiring and retired mesh-config's config-events stream, whose subjects are mesh.stream..event.. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshconfig/contract.go | 6 +++--- go/seats/meshconfig/contract_test.go | 12 ++++++------ go/seats/meshdeliverychecker/contract.go | 8 +++++--- go/seats/meshdeliverychecker/contract_test.go | 12 ++++++------ seats/mesh-config/fixtures/events.json | 6 +++--- seats/mesh-config/seat.json | 6 +++--- seats/mesh-delivery-checker/fixtures/events.json | 6 +++--- seats/mesh-delivery-checker/seat.json | 6 +++--- ts/src/mesh-config.ts | 6 +++--- ts/src/mesh-delivery-checker.ts | 6 +++--- 10 files changed, 38 insertions(+), 36 deletions(-) diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go index 207bf63..e5d5005 100644 --- a/go/seats/meshconfig/contract.go +++ b/go/seats/meshconfig/contract.go @@ -30,13 +30,13 @@ func HolderSubjectConfigChanged(holder string) string { } // HolderSubjectRenamed is where the holder publishes renamed, kept in config.events. -func HolderSubjectRenamed(holder string) string { return "mesh.mod." + holder + ".event.renamed" } +func HolderSubjectRenamed(holder string) string { return "mesh.stream." + holder + ".event.renamed" } // HolderSubjectRetiring is where the holder publishes retiring, kept in config.events. -func HolderSubjectRetiring(holder string) string { return "mesh.mod." + holder + ".event.retiring" } +func HolderSubjectRetiring(holder string) string { return "mesh.stream." + holder + ".event.retiring" } // HolderSubjectRetired is where the holder publishes retired, kept in config.events. -func HolderSubjectRetired(holder string) string { return "mesh.mod." + holder + ".event.retired" } +func HolderSubjectRetired(holder string) string { return "mesh.stream." + holder + ".event.retired" } // ConfigInput is what config takes. type ConfigInput struct { diff --git a/go/seats/meshconfig/contract_test.go b/go/seats/meshconfig/contract_test.go index eb8839a..6d82f0f 100644 --- a/go/seats/meshconfig/contract_test.go +++ b/go/seats/meshconfig/contract_test.go @@ -19,12 +19,12 @@ func TestFixtures(t *testing.T) { }{ {name: "config-changed, unchanged", subject: "mesh.mod.mesh-config.event.config-changed", given: "{\"key\":\"x\"}", wire: "{\"key\":\"x\"}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.config-changed$")}, - {name: "renamed", subject: "mesh.mod.mesh-config.event.renamed", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", - decode: func(b []byte) (any, error) { var v RenamedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.renamed$")}, - {name: "retiring", subject: "mesh.mod.mesh-config.event.retiring", given: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", wire: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", - decode: func(b []byte) (any, error) { var v RetiringHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.retiring$")}, - {name: "retired", subject: "mesh.mod.mesh-config.event.retired", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", - decode: func(b []byte) (any, error) { var v RetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.retired$")}, + {name: "renamed", subject: "mesh.stream.mesh-config.event.renamed", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"to\":\"new-key\"}", + decode: func(b []byte) (any, error) { var v RenamedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.renamed$")}, + {name: "retiring", subject: "mesh.stream.mesh-config.event.retiring", given: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", wire: "{\"ask\":\"a1\",\"key\":\"old-key\",\"kind\":\"value\",\"module\":\"messenger\",\"since\":\"2026-11-04T12:00:00Z\"}", + decode: func(b []byte) (any, error) { var v RetiringHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retiring$")}, + {name: "retired", subject: "mesh.stream.mesh-config.event.retired", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", + decode: func(b []byte) (any, error) { var v RetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retired$")}, {name: "renames answers the open windows", subject: "", given: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", wire: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", decode: func(b []byte) (any, error) { var v RenamesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go index af921c3..cbe9997 100644 --- a/go/seats/meshdeliverychecker/contract.go +++ b/go/seats/meshdeliverychecker/contract.go @@ -25,14 +25,16 @@ const ( ) // HolderSubjectVerdict is where the holder publishes verdict, kept in checker.asks. -func HolderSubjectVerdict(holder string) string { return "mesh.mod." + holder + ".event.verdict" } +func HolderSubjectVerdict(holder string) string { return "mesh.stream." + holder + ".event.verdict" } // HolderSubjectTagWanted is where the holder publishes tag-wanted, kept in checker.asks. -func HolderSubjectTagWanted(holder string) string { return "mesh.mod." + holder + ".event.tag-wanted" } +func HolderSubjectTagWanted(holder string) string { + return "mesh.stream." + holder + ".event.tag-wanted" +} // HolderSubjectPreviewWritten is where the holder publishes preview.written, kept in checks. func HolderSubjectPreviewWritten(holder string) string { - return "mesh.mod." + holder + ".event.preview.written" + return "mesh.stream." + holder + ".event.preview.written" } // TryInput is what try takes. diff --git a/go/seats/meshdeliverychecker/contract_test.go b/go/seats/meshdeliverychecker/contract_test.go index b3e334a..5856f27 100644 --- a/go/seats/meshdeliverychecker/contract_test.go +++ b/go/seats/meshdeliverychecker/contract_test.go @@ -17,12 +17,12 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "verdict on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", - decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.verdict$")}, - {name: "tag-wanted on the checker's own family", subject: "mesh.mod.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", - decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, - {name: "preview.written on the checker's stream", subject: "mesh.mod.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", - decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, + {name: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", + decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.verdict$")}, + {name: "tag-wanted on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", + decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, + {name: "preview.written on the checker's stream", subject: "mesh.stream.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", + decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/seats/mesh-config/fixtures/events.json b/seats/mesh-config/fixtures/events.json index 01c3f98..4cbc324 100644 --- a/seats/mesh-config/fixtures/events.json +++ b/seats/mesh-config/fixtures/events.json @@ -10,7 +10,7 @@ { "name": "renamed", "of": "holder-event:renamed", - "subject": "mesh.mod.mesh-config.event.renamed", + "subject": "mesh.stream.mesh-config.event.renamed", "given": { "module": "messenger", "from": "old-key", @@ -21,7 +21,7 @@ { "name": "retiring", "of": "holder-event:retiring", - "subject": "mesh.mod.mesh-config.event.retiring", + "subject": "mesh.stream.mesh-config.event.retiring", "given": { "module": "messenger", "key": "old-key", @@ -33,7 +33,7 @@ { "name": "retired", "of": "holder-event:retired", - "subject": "mesh.mod.mesh-config.event.retired", + "subject": "mesh.stream.mesh-config.event.retired", "given": { "module": "messenger", "name": "old-key", diff --git a/seats/mesh-config/seat.json b/seats/mesh-config/seat.json index 7ddce60..d095825 100644 --- a/seats/mesh-config/seat.json +++ b/seats/mesh-config/seat.json @@ -119,7 +119,7 @@ }, { "name": "renamed", - "subject": "mesh.mod..event.renamed", + "subject": "mesh.stream..event.renamed", "family": "config.events", "body": { "type": "object", @@ -151,7 +151,7 @@ }, { "name": "retiring", - "subject": "mesh.mod..event.retiring", + "subject": "mesh.stream..event.retiring", "family": "config.events", "body": { "type": "object", @@ -190,7 +190,7 @@ }, { "name": "retired", - "subject": "mesh.mod..event.retired", + "subject": "mesh.stream..event.retired", "family": "config.events", "body": { "type": "object", diff --git a/seats/mesh-delivery-checker/fixtures/events.json b/seats/mesh-delivery-checker/fixtures/events.json index cc8ae18..712b5db 100644 --- a/seats/mesh-delivery-checker/fixtures/events.json +++ b/seats/mesh-delivery-checker/fixtures/events.json @@ -2,7 +2,7 @@ { "name": "verdict on the checker's own family", "of": "holder-event:verdict", - "subject": "mesh.mod.mesh-delivery-checker.event.verdict", + "subject": "mesh.stream.mesh-delivery-checker.event.verdict", "given": { "repository": "novox/mesh-contracts", "pull": 3, @@ -16,7 +16,7 @@ { "name": "tag-wanted on the checker's own family", "of": "holder-event:tag-wanted", - "subject": "mesh.mod.mesh-delivery-checker.event.tag-wanted", + "subject": "mesh.stream.mesh-delivery-checker.event.tag-wanted", "given": { "repository": "novox/mesh-sdk", "pull": 170, @@ -30,7 +30,7 @@ { "name": "preview.written on the checker's stream", "of": "holder-event:preview.written", - "subject": "mesh.mod.mesh-delivery-checker.event.preview.written", + "subject": "mesh.stream.mesh-delivery-checker.event.preview.written", "given": {} } ] diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json index fb80b49..67eb7a8 100644 --- a/seats/mesh-delivery-checker/seat.json +++ b/seats/mesh-delivery-checker/seat.json @@ -124,7 +124,7 @@ "holder-events": [ { "name": "verdict", - "subject": "mesh.mod..event.verdict", + "subject": "mesh.stream..event.verdict", "family": "checker.asks", "body": { "type": "object", @@ -162,7 +162,7 @@ }, { "name": "tag-wanted", - "subject": "mesh.mod..event.tag-wanted", + "subject": "mesh.stream..event.tag-wanted", "family": "checker.asks", "body": { "type": "object", @@ -207,7 +207,7 @@ }, { "name": "preview.written", - "subject": "mesh.mod..event.preview.written", + "subject": "mesh.stream..event.preview.written", "family": "checks" } ] diff --git a/ts/src/mesh-config.ts b/ts/src/mesh-config.ts index a9d230f..2c5bb50 100644 --- a/ts/src/mesh-config.ts +++ b/ts/src/mesh-config.ts @@ -21,15 +21,15 @@ export function configChangedHolderSubject(holder: string): string { } export function renamedHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.renamed"; + return "mesh.stream." + holder + ".event.renamed"; } export function retiringHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.retiring"; + return "mesh.stream." + holder + ".event.retiring"; } export function retiredHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.retired"; + return "mesh.stream." + holder + ".event.retired"; } /** is what config takes. */ diff --git a/ts/src/mesh-delivery-checker.ts b/ts/src/mesh-delivery-checker.ts index 4bbc94b..bb5e526 100644 --- a/ts/src/mesh-delivery-checker.ts +++ b/ts/src/mesh-delivery-checker.ts @@ -17,15 +17,15 @@ export const holderEvents = { } as const; export function verdictHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.verdict"; + return "mesh.stream." + holder + ".event.verdict"; } export function tagWantedHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.tag-wanted"; + return "mesh.stream." + holder + ".event.tag-wanted"; } export function previewWrittenHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.preview.written"; + return "mesh.stream." + holder + ".event.preview.written"; } /** is what try takes. */ -- 2.54.0 From 6fd4eca51c76097a1f30b53ede7f6a1cca91ad8e Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:27:00 +0100 Subject: [PATCH 27/94] =?UTF-8?q?ADR=200627=20=C2=A71:=20operator-channel'?= =?UTF-8?q?s=20client=20holds=20a=20process=20connection's=20state=20to=20?= =?UTF-8?q?its=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test moved from mesh-sdk's process package, which no longer names the client. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/clients/operatorchannelclient/client_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go/clients/operatorchannelclient/client_test.go b/go/clients/operatorchannelclient/client_test.go index 0b16073..3739b99 100644 --- a/go/clients/operatorchannelclient/client_test.go +++ b/go/clients/operatorchannelclient/client_test.go @@ -8,6 +8,7 @@ import ( seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/process" ) type mem struct { @@ -70,6 +71,7 @@ func askedWarrant() seat.Warrant { } func TestAnAskIsKeptThenPublishedUnderTheModulesOwnName(t *testing.T) { + var _ Store = process.State{} // a process connection's state keeps the asks (issue 1188 AC2) c, sent := client() if err := c.Ask(askedHeld()); err != nil { t.Fatal(err) -- 2.54.0 From 1bcb18d1c766996ef71683e0f83354b9c0a3daaf Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:29:47 +0100 Subject: [PATCH 28/94] =?UTF-8?q?SD-M2d,=20ADR=200627=20=C2=A71:=20the=20v?= =?UTF-8?q?ault's=20sign,=20enrolment-token=20and=20node-account=20answers?= =?UTF-8?q?=20and=20its=20signed=20event=20are=20typed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Their output schemas replace mesh-sdk's hand-written SignAnswer, EnrolmentTokenAnswer, NodeAccountAnswer and Signed, on the same wire names; signed's time is RFC 3339 text, as the generator writes every time. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshvault/contract.go | 59 +++++++++++- go/seats/meshvault/contract_test.go | 10 +- go/seats/seats.go | 6 +- seats/mesh-vault/fixtures/events.json | 8 +- seats/mesh-vault/fixtures/verbs.json | 30 ++++++ seats/mesh-vault/seat.json | 130 +++++++++++++++++++++++++- ts/src/mesh-vault.ts | 59 +++++++++++- ts/src/seats.ts | 91 +++++++++++++++++- 8 files changed, 374 insertions(+), 19 deletions(-) diff --git a/go/seats/meshvault/contract.go b/go/seats/meshvault/contract.go index e5beead..14bd5c1 100644 --- a/go/seats/meshvault/contract.go +++ b/go/seats/meshvault/contract.go @@ -250,7 +250,22 @@ type SignInput struct { } // SignAnswer is what sign answers. -type SignAnswer = map[string]any +type SignAnswer struct { + // the authority's certificate that signed it, as PEM + Authority string `json:"authority,omitempty"` + // the node certificate as PEM, for a node certificate + Certificate string `json:"certificate,omitempty"` + // sha256: of the bytes signed + Digest string `json:"digest"` + // node-certificate or enrolment, as asked + Kind string `json:"kind"` + // the machine, for a node certificate + Node string `json:"node,omitempty"` + // the signature in base64, for an enrolment + Signature string `json:"signature,omitempty"` + // the signer's fingerprint + Signer string `json:"signer"` +} // EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. type EnrolmentTokenInputTunnel struct { @@ -281,7 +296,20 @@ type EnrolmentTokenInput struct { } // EnrolmentTokenAnswer is what enrolment-token answers. -type EnrolmentTokenAnswer = map[string]any +type EnrolmentTokenAnswer struct { + // the bus's address the machine dials + Broker string `json:"broker,omitempty"` + // a drop left neither the token nor its hash + Dropped bool `json:"dropped,omitempty"` + // the fingerprint of the bus's certificate + Fingerprint string `json:"fingerprint,omitempty"` + // the node the token is for + Node string `json:"node"` + // the token sealed to the operator key, when asked sealed, relayed as it is + Sealed string `json:"sealed,omitempty"` + // the vault's public signing key in base64 + Signer string `json:"signer,omitempty"` +} // NodeAccountInput is what node-account takes. type NodeAccountInput struct { @@ -296,7 +324,16 @@ type NodeAccountInput struct { } // NodeAccountAnswer is what node-account answers. -type NodeAccountAnswer = map[string]any +type NodeAccountAnswer struct { + // the account sealed to the node's sealing key, in base64 + Account string `json:"account,omitempty"` + // the login minted + Login string `json:"login,omitempty"` + // the joining node + Node string `json:"node"` + // a retire left no account + Retired bool `json:"retired,omitempty"` +} // RotateNodeInput is what rotate-node takes. type RotateNodeInput struct { @@ -347,6 +384,22 @@ type RetireAnswer struct { Retired bool `json:"retired"` } +// SignedHolderEvent is the body of the holder's event signed. +type SignedHolderEvent struct { + // when it was signed, RFC 3339 + At string `json:"at"` + // the caller the vault proved + Caller string `json:"caller"` + // sha256: of the bytes signed + Digest string `json:"digest"` + // node-certificate or enrolment + Kind string `json:"kind"` + // the machine, for a node certificate + Node string `json:"node,omitempty"` + // the signer's fingerprint + Signer string `json:"signer"` +} + // OwnRetiredHolderEvent is the body of the holder's event own-retired. type OwnRetiredHolderEvent struct { // the ask's id diff --git a/go/seats/meshvault/contract_test.go b/go/seats/meshvault/contract_test.go index 63f54dc..a358263 100644 --- a/go/seats/meshvault/contract_test.go +++ b/go/seats/meshvault/contract_test.go @@ -29,8 +29,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-replaced$")}, {name: "account-issued under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.account-issued", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.account-issued$")}, - {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{}", wire: "{}", - decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.signed$")}, + {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", + decode: func(b []byte) (any, error) { var v SignedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.signed$")}, {name: "rotation-begun under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotation-begun", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-begun$")}, {name: "node-account-ready under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.node-account-ready", given: "{}", wire: "{}", @@ -41,6 +41,12 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v OwnRetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-retired$")}, {name: "retire on a warrant", subject: "", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", decode: func(b []byte) (any, error) { var v RetireInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node certificate signed", subject: "", given: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", + decode: func(b []byte) (any, error) { var v SignAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an enrolment token's trust", subject: "", given: "{\"broker\":\"tls://192.0.2.1:4222\",\"fingerprint\":\"sha256:2222222222222222222222222222222222222222222222222222222222222222\",\"node\":\"ace\",\"signer\":\"AAAA\"}", wire: "{\"broker\":\"tls://192.0.2.1:4222\",\"fingerprint\":\"sha256:2222222222222222222222222222222222222222222222222222222222222222\",\"node\":\"ace\",\"signer\":\"AAAA\"}", + decode: func(b []byte) (any, error) { var v EnrolmentTokenAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node account retired", subject: "", given: "{\"node\":\"ace\",\"retired\":true}", wire: "{\"node\":\"ace\",\"retired\":true}", + decode: func(b []byte) (any, error) { var v NodeAccountAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/seats.go b/go/seats/seats.go index fb51082..6b5673a 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -197,13 +197,13 @@ func List() []Seat { Output: map[string]any{"description": "the holder's answer", "type": "object"}}, {Name: "sign", Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "node-certificate or enrolment", "type": "string"}, "node": map[string]any{"description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", "type": "string"}, "reply": map[string]any{"description": "the redeem answer's bytes in base64, for an enrolment", "type": "string"}, "serving_key": map[string]any{"description": "from mesh-enrolment: the serving key's public half the enrolling node presented", "type": "string"}}, "required": []string{"kind"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + Output: map[string]any{"description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half", "properties": map[string]any{"authority": map[string]any{"description": "the authority's certificate that signed it, as PEM", "type": "string"}, "certificate": map[string]any{"description": "the node certificate as PEM, for a node certificate", "type": "string"}, "digest": map[string]any{"description": "sha256: of the bytes signed", "type": "string"}, "kind": map[string]any{"description": "node-certificate or enrolment, as asked", "type": "string"}, "node": map[string]any{"description": "the machine, for a node certificate", "type": "string"}, "signature": map[string]any{"description": "the signature in base64, for an enrolment", "type": "string"}, "signer": map[string]any{"description": "the signer's fingerprint", "type": "string"}}, "required": []string{"kind", "signer", "digest"}, "type": "object"}}, {Name: "enrolment-token", Description: "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", Optional: true, Input: map[string]any{"properties": map[string]any{"drop": map[string]any{"description": "\"true\": drop the node's token and its hash", "enum": []string{"true", "false"}, "type": "string"}, "expires": map[string]any{"description": "when the token expires (RFC 3339), as the enrolment's key says", "type": "string"}, "node": map[string]any{"description": "the node the token is for", "type": "string"}, "sealed": map[string]any{"description": "\"true\": answer the token sealed to the operator key", "enum": []string{"true", "false"}, "type": "string"}, "tunnel": map[string]any{"additionalProperties": false, "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", "properties": map[string]any{"address": map[string]any{"description": "the joining node's tunnel address", "type": "string"}, "hub_endpoint": map[string]any{"description": "the hub's endpoint, host:port", "type": "string"}, "hub_key": map[string]any{"description": "the hub's tunnel public key", "type": "string"}, "key": map[string]any{"description": "the joining machine's own tunnel public key, as given at issue", "type": "string"}, "range": map[string]any{"description": "the private network's range, a prefix", "type": "string"}}, "required": []string{"address", "hub_endpoint", "hub_key", "key", "range"}, "type": "object"}}, "required": []string{"node"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + Output: map[string]any{"description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash", "properties": map[string]any{"broker": map[string]any{"description": "the bus's address the machine dials", "type": "string"}, "dropped": map[string]any{"description": "a drop left neither the token nor its hash", "type": "boolean"}, "fingerprint": map[string]any{"description": "the fingerprint of the bus's certificate", "type": "string"}, "node": map[string]any{"description": "the node the token is for", "type": "string"}, "sealed": map[string]any{"description": "the token sealed to the operator key, when asked sealed, relayed as it is", "type": "string"}, "signer": map[string]any{"description": "the vault's public signing key in base64", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, {Name: "node-account", Description: "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", Optional: true, Input: map[string]any{"properties": map[string]any{"keys_digest": map[string]any{"description": "the digest of every public key the node presented (delivery.KeysDigest)", "type": "string"}, "node": map[string]any{"description": "the joining node", "type": "string"}, "retire": map[string]any{"description": "\"true\": retire the account minted for this node", "enum": []string{"true", "false"}, "type": "string"}, "sealing_key": map[string]any{"description": "the sealing key's public half the node presented, which the account is sealed to", "type": "string"}}, "required": []string{"node"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, + Output: map[string]any{"description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account", "properties": map[string]any{"account": map[string]any{"description": "the account sealed to the node's sealing key, in base64", "type": "string"}, "login": map[string]any{"description": "the login minted", "type": "string"}, "node": map[string]any{"description": "the joining node", "type": "string"}, "retired": map[string]any{"description": "a retire left no account", "type": "boolean"}}, "required": []string{"node"}, "type": "object"}}, {Name: "rotate-node", Description: "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", Optional: true, Input: map[string]any{"properties": map[string]any{"act": map[string]any{"description": "the id of the hand act recorded for it in mesh-hand-acts", "type": "string"}, "node": map[string]any{"description": "the node whose account rotates", "type": "string"}, "why": map[string]any{"description": "why, in a sentence", "type": "string"}}, "required": []string{"node", "why", "act"}, "type": "object"}, Output: map[string]any{"description": "the holder's answer", "type": "object"}}, diff --git a/seats/mesh-vault/fixtures/events.json b/seats/mesh-vault/fixtures/events.json index 342fd91..ef46519 100644 --- a/seats/mesh-vault/fixtures/events.json +++ b/seats/mesh-vault/fixtures/events.json @@ -39,7 +39,13 @@ "name": "signed under the vault's own subjects", "of": "holder-event:signed", "subject": "mesh.mod.mesh-vault.event.signed", - "given": {} + "given": { + "caller": "mesh-enrolment", + "kind": "enrolment", + "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "signer": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "at": "2026-11-04T13:00:00Z" + } }, { "name": "rotation-begun under the vault's own subjects", diff --git a/seats/mesh-vault/fixtures/verbs.json b/seats/mesh-vault/fixtures/verbs.json index a031109..49b29f3 100644 --- a/seats/mesh-vault/fixtures/verbs.json +++ b/seats/mesh-vault/fixtures/verbs.json @@ -9,5 +9,35 @@ "warrant": "w-1", "ask": "a1" } + }, + { + "name": "a node certificate signed", + "of": "verb:sign.answer", + "given": { + "kind": "node-certificate", + "node": "ace", + "certificate": "-----BEGIN CERTIFICATE-----", + "authority": "-----BEGIN CERTIFICATE-----", + "signer": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "name": "an enrolment token's trust", + "of": "verb:enrolment-token.answer", + "given": { + "node": "ace", + "broker": "tls://192.0.2.1:4222", + "fingerprint": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "signer": "AAAA" + } + }, + { + "name": "a node account retired", + "of": "verb:node-account.answer", + "given": { + "node": "ace", + "retired": true + } } ] diff --git a/seats/mesh-vault/seat.json b/seats/mesh-vault/seat.json index 4cb5d2c..5a115a6 100644 --- a/seats/mesh-vault/seat.json +++ b/seats/mesh-vault/seat.json @@ -327,7 +327,42 @@ }, "answer": { "type": "object", - "description": "the holder's answer" + "description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half", + "properties": { + "kind": { + "type": "string", + "description": "node-certificate or enrolment, as asked" + }, + "node": { + "type": "string", + "description": "the machine, for a node certificate" + }, + "certificate": { + "type": "string", + "description": "the node certificate as PEM, for a node certificate" + }, + "authority": { + "type": "string", + "description": "the authority's certificate that signed it, as PEM" + }, + "signature": { + "type": "string", + "description": "the signature in base64, for an enrolment" + }, + "signer": { + "type": "string", + "description": "the signer's fingerprint" + }, + "digest": { + "type": "string", + "description": "sha256: of the bytes signed" + } + }, + "required": [ + "kind", + "signer", + "digest" + ] } }, { @@ -402,7 +437,36 @@ }, "answer": { "type": "object", - "description": "the holder's answer" + "description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash", + "properties": { + "node": { + "type": "string", + "description": "the node the token is for" + }, + "broker": { + "type": "string", + "description": "the bus's address the machine dials" + }, + "fingerprint": { + "type": "string", + "description": "the fingerprint of the bus's certificate" + }, + "signer": { + "type": "string", + "description": "the vault's public signing key in base64" + }, + "sealed": { + "type": "string", + "description": "the token sealed to the operator key, when asked sealed, relayed as it is" + }, + "dropped": { + "type": "boolean", + "description": "a drop left neither the token nor its hash" + } + }, + "required": [ + "node" + ] } }, { @@ -439,7 +503,28 @@ }, "answer": { "type": "object", - "description": "the holder's answer" + "description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account", + "properties": { + "node": { + "type": "string", + "description": "the joining node" + }, + "login": { + "type": "string", + "description": "the login minted" + }, + "account": { + "type": "string", + "description": "the account sealed to the node's sealing key, in base64" + }, + "retired": { + "type": "boolean", + "description": "a retire left no account" + } + }, + "required": [ + "node" + ] } }, { @@ -586,7 +671,44 @@ }, { "name": "signed", - "subject": "mesh.mod..event.signed" + "subject": "mesh.mod..event.signed", + "body": { + "type": "object", + "description": "who asked, the kind, the node and the digest of what was signed; never a key", + "properties": { + "caller": { + "type": "string", + "description": "the caller the vault proved" + }, + "kind": { + "type": "string", + "description": "node-certificate or enrolment" + }, + "node": { + "type": "string", + "description": "the machine, for a node certificate" + }, + "digest": { + "type": "string", + "description": "sha256: of the bytes signed" + }, + "signer": { + "type": "string", + "description": "the signer's fingerprint" + }, + "at": { + "type": "string", + "description": "when it was signed, RFC 3339" + } + }, + "required": [ + "caller", + "kind", + "digest", + "signer", + "at" + ] + } }, { "name": "rotation-begun", diff --git a/ts/src/mesh-vault.ts b/ts/src/mesh-vault.ts index 7c51701..c78145a 100644 --- a/ts/src/mesh-vault.ts +++ b/ts/src/mesh-vault.ts @@ -234,7 +234,22 @@ export interface SignInput { } /** is what sign answers. */ -export type SignAnswer = Record; +export interface SignAnswer { + /** the authority's certificate that signed it, as PEM */ + "authority"?: string; + /** the node certificate as PEM, for a node certificate */ + "certificate"?: string; + /** sha256: of the bytes signed */ + "digest": string; + /** node-certificate or enrolment, as asked */ + "kind": string; + /** the machine, for a node certificate */ + "node"?: string; + /** the signature in base64, for an enrolment */ + "signature"?: string; + /** the signer's fingerprint */ + "signer": string; +} /** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ export interface EnrolmentTokenInputTunnel { @@ -265,7 +280,20 @@ export interface EnrolmentTokenInput { } /** is what enrolment-token answers. */ -export type EnrolmentTokenAnswer = Record; +export interface EnrolmentTokenAnswer { + /** the bus's address the machine dials */ + "broker"?: string; + /** a drop left neither the token nor its hash */ + "dropped"?: boolean; + /** the fingerprint of the bus's certificate */ + "fingerprint"?: string; + /** the node the token is for */ + "node": string; + /** the token sealed to the operator key, when asked sealed, relayed as it is */ + "sealed"?: string; + /** the vault's public signing key in base64 */ + "signer"?: string; +} /** is what node-account takes. */ export interface NodeAccountInput { @@ -280,7 +308,16 @@ export interface NodeAccountInput { } /** is what node-account answers. */ -export type NodeAccountAnswer = Record; +export interface NodeAccountAnswer { + /** the account sealed to the node's sealing key, in base64 */ + "account"?: string; + /** the login minted */ + "login"?: string; + /** the joining node */ + "node": string; + /** a retire left no account */ + "retired"?: boolean; +} /** is what rotate-node takes. */ export interface RotateNodeInput { @@ -331,6 +368,22 @@ export interface RetireAnswer { "retired": boolean; } +/** is the body of the holder's event signed. */ +export interface SignedHolderEvent { + /** when it was signed, RFC 3339 */ + "at": string; + /** the caller the vault proved */ + "caller": string; + /** sha256: of the bytes signed */ + "digest": string; + /** node-certificate or enrolment */ + "kind": string; + /** the machine, for a node certificate */ + "node"?: string; + /** the signer's fingerprint */ + "signer": string; +} + /** is the body of the holder's event own-retired. */ export interface OwnRetiredHolderEvent { /** the ask's id */ diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 746e817..a514e20 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -1345,7 +1345,42 @@ export const list: readonly Seat[] = [ "name": "sign", "optional": true, "output": { - "description": "the holder's answer", + "description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half", + "properties": { + "authority": { + "description": "the authority's certificate that signed it, as PEM", + "type": "string" + }, + "certificate": { + "description": "the node certificate as PEM, for a node certificate", + "type": "string" + }, + "digest": { + "description": "sha256: of the bytes signed", + "type": "string" + }, + "kind": { + "description": "node-certificate or enrolment, as asked", + "type": "string" + }, + "node": { + "description": "the machine, for a node certificate", + "type": "string" + }, + "signature": { + "description": "the signature in base64, for an enrolment", + "type": "string" + }, + "signer": { + "description": "the signer's fingerprint", + "type": "string" + } + }, + "required": [ + "kind", + "signer", + "digest" + ], "type": "object" } }, @@ -1420,7 +1455,36 @@ export const list: readonly Seat[] = [ "name": "enrolment-token", "optional": true, "output": { - "description": "the holder's answer", + "description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash", + "properties": { + "broker": { + "description": "the bus's address the machine dials", + "type": "string" + }, + "dropped": { + "description": "a drop left neither the token nor its hash", + "type": "boolean" + }, + "fingerprint": { + "description": "the fingerprint of the bus's certificate", + "type": "string" + }, + "node": { + "description": "the node the token is for", + "type": "string" + }, + "sealed": { + "description": "the token sealed to the operator key, when asked sealed, relayed as it is", + "type": "string" + }, + "signer": { + "description": "the vault's public signing key in base64", + "type": "string" + } + }, + "required": [ + "node" + ], "type": "object" } }, @@ -1457,7 +1521,28 @@ export const list: readonly Seat[] = [ "name": "node-account", "optional": true, "output": { - "description": "the holder's answer", + "description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account", + "properties": { + "account": { + "description": "the account sealed to the node's sealing key, in base64", + "type": "string" + }, + "login": { + "description": "the login minted", + "type": "string" + }, + "node": { + "description": "the joining node", + "type": "string" + }, + "retired": { + "description": "a retire left no account", + "type": "boolean" + } + }, + "required": [ + "node" + ], "type": "object" } }, -- 2.54.0 From 4ce5dec682edc985d185b77b335d184f43ca17f6 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:31:16 +0100 Subject: [PATCH 29/94] SD-M2d: operator-channel's offered and closed are on the router's stream asks, mesh.stream..event.* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The messenger keeps them in its own stream asks (its manifest's streams, design 55 § 11), so they ride that stream's subjects, as the checker's verdict does. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/operatorchannel/contract.go | 8 ++++---- go/seats/operatorchannel/contract_test.go | 8 ++++---- seats/operator-channel/fixtures/events.json | 4 ++-- seats/operator-channel/seat.json | 8 ++++---- ts/src/operator-channel.ts | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go index 104afe6..eece285 100644 --- a/go/seats/operatorchannel/contract.go +++ b/go/seats/operatorchannel/contract.go @@ -68,11 +68,11 @@ const ( // HolderSubjectRefused is where the holder publishes refused. func HolderSubjectRefused(holder string) string { return "mesh.mod." + holder + ".event.refused" } -// HolderSubjectOffered is where the holder publishes offered, kept in the router's restricted stream (design 55 § 11). -func HolderSubjectOffered(holder string) string { return "mesh.mod." + holder + ".event.offered" } +// HolderSubjectOffered is where the holder publishes offered, kept in router.asks. +func HolderSubjectOffered(holder string) string { return "mesh.stream." + holder + ".event.offered" } -// HolderSubjectClosed is where the holder publishes closed, kept in the router's restricted stream (design 55 § 11). -func HolderSubjectClosed(holder string) string { return "mesh.mod." + holder + ".event.closed" } +// HolderSubjectClosed is where the holder publishes closed, kept in router.asks. +func HolderSubjectClosed(holder string) string { return "mesh.stream." + holder + ".event.closed" } const ( RecordAsks = "asks" diff --git a/go/seats/operatorchannel/contract_test.go b/go/seats/operatorchannel/contract_test.go index 2a8c9ce..c5e8a8e 100644 --- a/go/seats/operatorchannel/contract_test.go +++ b/go/seats/operatorchannel/contract_test.go @@ -21,10 +21,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v DecidedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.event\\.decided\\.[A-Za-z0-9_-]+$")}, {name: "refused under the holder's own subjects", subject: "mesh.mod.messenger.event.refused", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.refused$")}, - {name: "offered on the router's restricted stream", subject: "mesh.mod.messenger.event.offered", given: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", - decode: func(b []byte) (any, error) { var v OfferedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.offered$")}, - {name: "closed once per end", subject: "mesh.mod.messenger.event.closed", given: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", - decode: func(b []byte) (any, error) { var v ClosedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.closed$")}, + {name: "offered on the router's restricted stream", subject: "mesh.stream.messenger.event.offered", given: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"channels\":[\"telegram\"],\"headline\":\"release held\",\"id\":\"a1\"}", + decode: func(b []byte) (any, error) { var v OfferedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.offered$")}, + {name: "closed once per end", subject: "mesh.stream.messenger.event.closed", given: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", wire: "{\"asker\":\"mesh-delivery\",\"how\":\"answered\",\"id\":\"a1\"}", + decode: func(b []byte) (any, error) { var v ClosedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.closed$")}, {name: "an ask by its asker", subject: "mesh.seat.operator-channel.accept.ask.mesh-delivery", given: "{\"expires\":\"2026-11-05T12:00:00Z\",\"explanation\":\"A delivery waits on your word.\",\"headline\":\"release held\",\"id\":\"a1\",\"on-expiry\":\"the delivery stays held\",\"options\":[{\"binds\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"does\":\"releases it\",\"id\":\"ok\",\"label\":\"Release\",\"level\":\"approve\"}],\"who\":\"operator\"}", wire: "{\"expires\":\"2026-11-05T12:00:00Z\",\"explanation\":\"A delivery waits on your word.\",\"headline\":\"release held\",\"id\":\"a1\",\"on-expiry\":\"the delivery stays held\",\"options\":[{\"binds\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"does\":\"releases it\",\"id\":\"ok\",\"label\":\"Release\",\"level\":\"approve\"}],\"who\":\"operator\"}", decode: func(b []byte) (any, error) { var v AskAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.ask\\.[A-Za-z0-9_-]+$")}, {name: "a cancel by its asker", subject: "mesh.seat.operator-channel.accept.cancel.mesh-delivery", given: "{\"id\":\"a1\"}", wire: "{\"id\":\"a1\"}", diff --git a/seats/operator-channel/fixtures/events.json b/seats/operator-channel/fixtures/events.json index 971ce14..efa6a5b 100644 --- a/seats/operator-channel/fixtures/events.json +++ b/seats/operator-channel/fixtures/events.json @@ -30,7 +30,7 @@ { "name": "offered on the router's restricted stream", "of": "holder-event:offered", - "subject": "mesh.mod.messenger.event.offered", + "subject": "mesh.stream.messenger.event.offered", "given": { "id": "a1", "asker": "mesh-delivery", @@ -43,7 +43,7 @@ { "name": "closed once per end", "of": "holder-event:closed", - "subject": "mesh.mod.messenger.event.closed", + "subject": "mesh.stream.messenger.event.closed", "given": { "id": "a1", "asker": "mesh-delivery", diff --git a/seats/operator-channel/seat.json b/seats/operator-channel/seat.json index 12fb625..4fe0a0c 100644 --- a/seats/operator-channel/seat.json +++ b/seats/operator-channel/seat.json @@ -189,8 +189,8 @@ }, { "name": "offered", - "subject": "mesh.mod..event.offered", - "family": "the router's restricted stream (design 55 § 11)", + "subject": "mesh.stream..event.offered", + "family": "router.asks", "body": { "type": "object", "properties": { @@ -222,8 +222,8 @@ }, { "name": "closed", - "subject": "mesh.mod..event.closed", - "family": "the router's restricted stream (design 55 § 11)", + "subject": "mesh.stream..event.closed", + "family": "router.asks", "body": { "type": "object", "properties": { diff --git a/ts/src/operator-channel.ts b/ts/src/operator-channel.ts index e8ad2ba..16e5cb0 100644 --- a/ts/src/operator-channel.ts +++ b/ts/src/operator-channel.ts @@ -51,11 +51,11 @@ export function refusedHolderSubject(holder: string): string { } export function offeredHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.offered"; + return "mesh.stream." + holder + ".event.offered"; } export function closedHolderSubject(holder: string): string { - return "mesh.mod." + holder + ".event.closed"; + return "mesh.stream." + holder + ".event.closed"; } export interface Ask { -- 2.54.0 From 3368a055a4126736200ba0514b8349373e49cdaa Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:32:24 +0100 Subject: [PATCH 30/94] SD-M2c (A2d): a registry outcome names the Go module path a Go package is fetched under go-package-registry's published says the module (field module), so a reader fetches the version by its path. The four registry seats take their events whole from the part registry, which refuses a seat redefining one, so the field is the part's, optional and empty for every other kind. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/containerimageregistry/contract.go | 2 ++ go/seats/gopackageregistry/contract.go | 2 ++ go/seats/gopackageregistry/contract_test.go | 2 +- go/seats/npmpackageregistry/contract.go | 2 ++ go/seats/ociartifactregistry/contract.go | 2 ++ parts/registry.json | 4 ++++ seats/go-package-registry/fixtures/events.json | 1 + ts/src/parts/registry.ts | 2 ++ 8 files changed, 16 insertions(+), 1 deletion(-) diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index 8d53626..01fcea6 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -102,6 +102,8 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` + // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 9b6d148..98aea35 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -74,6 +74,8 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` + // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go index f52946b..ff2164e 100644 --- a/go/seats/gopackageregistry/contract_test.go +++ b/go/seats/gopackageregistry/contract_test.go @@ -17,7 +17,7 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")}, {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 8758751..79d2b05 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -74,6 +74,8 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` + // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index 1eb950d..5746c78 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -100,6 +100,8 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` + // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) diff --git a/parts/registry.json b/parts/registry.json index d636cc2..0fcb16b 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -341,6 +341,10 @@ "type": "string", "description": "the package's, image's, bundle's or archive's name" }, + "module": { + "type": "string", + "description": "the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact" + }, "version": { "type": "string", "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json index fde91ce..23d9779 100644 --- a/seats/go-package-registry/fixtures/events.json +++ b/seats/go-package-registry/fixtures/events.json @@ -5,6 +5,7 @@ "subject": "mesh.seat.go-package-registry.event.published", "given": { "name": "mesh-sdk", + "module": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/ts/src/parts/registry.ts b/ts/src/parts/registry.ts index 71cc661..9642c2d 100644 --- a/ts/src/parts/registry.ts +++ b/ts/src/parts/registry.ts @@ -45,6 +45,8 @@ export interface Outcome { "holder": string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; /** the package's, image's, bundle's or archive's name */ "name": string; /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ -- 2.54.0 From 9d57c09594a39465eb053a06ee657855c73c4616 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:34:41 +0100 Subject: [PATCH 31/94] =?UTF-8?q?ADR=200625=20=C2=A710:=20the=20README=20s?= =?UTF-8?q?ays=20where=20a=20seat's=20definition,=20contract=20and=20clien?= =?UTF-8?q?t=20go,=20and=20how=20each=20is=20checked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fca5468..7e28e12 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,32 @@ # mesh-contracts -The mesh's seat contracts (novox/hq ADR 0608 §5, ADR 0613, ADR 0620; design 26 *A seat's definition*): one -language-neutral definition per seat, and the Go and TypeScript types generated from it. +The mesh's seat contracts (novox/hq ADR 0608 §5, ADR 0613, ADR 0620, ADR 0625, ADR 0627; design 26 *A seat's +definition*): one language-neutral definition per seat, the Go and TypeScript code generated from it, and the client +of a seat whose callers share logic over it. This repository holds contracts only and no seat: a seat's holder +implements its contract, and no holder's code lives here. -Every file under `go/` and `ts/` is written by the generators of the Go and the Node build modules, never by hand. -This repository holds contracts only and no seat: a seat's holder implements its contract. +## Where a seat's code goes + +- **The definition**, written by hand: `seats//seat.json`, its fixtures in `seats//fixtures/`, and the + parts several seats include whole in `parts/` (the four registry seats' verbs and events are `parts/registry.json`). + mesh-sdk's `seatdef` is the one reader of the format; the indexer and the generators use it. +- **The seat set**, generated: `go/seats` (module `git.novox.be/novox/mesh-contracts/go`), every seat's name, scope, + verbs and provision at the seat list's grain (ADR 0625 §2). mesh-sdk's `catalogue` is its only importer in mesh-sdk. + A change of it rebuilds every importer of the seat set. +- **A seat defined whole**, generated: `go/seats/`, a Go module of its own importing nothing, + holding the seat's verbs, events and subjects, its message types and a test of its fixtures (ADR 0625 §1). A module + imports only the seats it uses, so a change of one seat rebuilds only its users. The TypeScript side is `ts/src/`. +- **A seat's client**, written by hand: `go/clients/client`, a Go module of its own, only where + two or more callers share logic over the seat (ADR 0625 §4, ADR 0627 §1): operator-channel's (checking an ask, + its digest, a warrant's, and an asking module's keeping of its asks) and the four registry seats' (publishing a file + in parts). A client imports its own seat's contract and mesh-sdk's runtime, never another seat's client, and names + no module's subject or tool. +- **Not here**: the value layouts of seats not yet defined whole (they stay in mesh-sdk until their domain moves, + ADR 0625 §9), and a role only one module could ever play (that module's own tools and events, ADR 0625 §8). + +## How it is checked + +Every file under `go/` and `ts/` outside `go/clients/` is written by the generator (seatgen, the source of the Go and +Node build modules' generate step) and compared on every build: a hand-written file there fails, named (ADR 0625 §5). +`seatgen -check` also runs the client check (each client is for a seat defined whole, a module of its own, importing +only its seat's contract and mesh-sdk) and checks `module.json`'s Go package artifacts against the tree. -- 2.54.0 From a6f859de3f9934baa91022b4471596285a5e9df4 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:40:54 +0100 Subject: [PATCH 32/94] =?UTF-8?q?ADR=200625=20=C2=A71:=20each=20seat=20def?= =?UTF-8?q?ined=20whole=20is=20an=20npm=20package=20of=20its=20own,=20decl?= =?UTF-8?q?ared=20in=20module.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One @novox/mesh-contracts would rebuild every TypeScript user at any seat's change, since the indexer counts the npm package a lock resolves. Generated by seatgen c1fdd7c; ts/ keeps the seat list alone. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 9 +- module.json | 11 +- ts/package.json | 2 +- ts/seats/containerimageregistry/package.json | 19 ++ .../containerimageregistry/src/index.ts} | 0 .../src/parts/registry.ts | 0 ts/seats/containerimageregistry/tsconfig.json | 14 ++ ts/seats/gopackageregistry/package.json | 19 ++ .../gopackageregistry/src/index.ts} | 0 .../gopackageregistry/src/parts/registry.ts | 175 ++++++++++++++++++ ts/seats/gopackageregistry/tsconfig.json | 14 ++ ts/seats/meshbroker/package.json | 19 ++ .../meshbroker/src/index.ts} | 0 ts/seats/meshbroker/tsconfig.json | 14 ++ ts/seats/meshconfig/package.json | 19 ++ .../meshconfig/src/index.ts} | 0 ts/seats/meshconfig/tsconfig.json | 14 ++ ts/seats/meshdeliverychecker/package.json | 19 ++ .../meshdeliverychecker/src/index.ts} | 0 ts/seats/meshdeliverychecker/tsconfig.json | 14 ++ ts/seats/meshvault/package.json | 19 ++ .../meshvault/src/index.ts} | 0 ts/seats/meshvault/tsconfig.json | 14 ++ ts/seats/npmpackageregistry/package.json | 19 ++ .../npmpackageregistry/src/index.ts} | 0 .../npmpackageregistry/src/parts/registry.ts | 175 ++++++++++++++++++ ts/seats/npmpackageregistry/tsconfig.json | 14 ++ ts/seats/ociartifactregistry/package.json | 19 ++ .../ociartifactregistry/src/index.ts} | 0 .../ociartifactregistry/src/parts/registry.ts | 175 ++++++++++++++++++ ts/seats/ociartifactregistry/tsconfig.json | 14 ++ ts/seats/operatorchannel/package.json | 19 ++ .../operatorchannel/src/index.ts} | 0 ts/seats/operatorchannel/tsconfig.json | 14 ++ ts/src/index.ts | 9 - 35 files changed, 839 insertions(+), 14 deletions(-) create mode 100644 ts/seats/containerimageregistry/package.json rename ts/{src/container-image-registry.ts => seats/containerimageregistry/src/index.ts} (100%) rename ts/{ => seats/containerimageregistry}/src/parts/registry.ts (100%) create mode 100644 ts/seats/containerimageregistry/tsconfig.json create mode 100644 ts/seats/gopackageregistry/package.json rename ts/{src/go-package-registry.ts => seats/gopackageregistry/src/index.ts} (100%) create mode 100644 ts/seats/gopackageregistry/src/parts/registry.ts create mode 100644 ts/seats/gopackageregistry/tsconfig.json create mode 100644 ts/seats/meshbroker/package.json rename ts/{src/mesh-broker.ts => seats/meshbroker/src/index.ts} (100%) create mode 100644 ts/seats/meshbroker/tsconfig.json create mode 100644 ts/seats/meshconfig/package.json rename ts/{src/mesh-config.ts => seats/meshconfig/src/index.ts} (100%) create mode 100644 ts/seats/meshconfig/tsconfig.json create mode 100644 ts/seats/meshdeliverychecker/package.json rename ts/{src/mesh-delivery-checker.ts => seats/meshdeliverychecker/src/index.ts} (100%) create mode 100644 ts/seats/meshdeliverychecker/tsconfig.json create mode 100644 ts/seats/meshvault/package.json rename ts/{src/mesh-vault.ts => seats/meshvault/src/index.ts} (100%) create mode 100644 ts/seats/meshvault/tsconfig.json create mode 100644 ts/seats/npmpackageregistry/package.json rename ts/{src/npm-package-registry.ts => seats/npmpackageregistry/src/index.ts} (100%) create mode 100644 ts/seats/npmpackageregistry/src/parts/registry.ts create mode 100644 ts/seats/npmpackageregistry/tsconfig.json create mode 100644 ts/seats/ociartifactregistry/package.json rename ts/{src/oci-artifact-registry.ts => seats/ociartifactregistry/src/index.ts} (100%) create mode 100644 ts/seats/ociartifactregistry/src/parts/registry.ts create mode 100644 ts/seats/ociartifactregistry/tsconfig.json create mode 100644 ts/seats/operatorchannel/package.json rename ts/{src/operator-channel.ts => seats/operatorchannel/src/index.ts} (100%) create mode 100644 ts/seats/operatorchannel/tsconfig.json diff --git a/README.md b/README.md index 7e28e12..f7e7fcc 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,13 @@ implements its contract, and no holder's code lives here. mesh-sdk's `seatdef` is the one reader of the format; the indexer and the generators use it. - **The seat set**, generated: `go/seats` (module `git.novox.be/novox/mesh-contracts/go`), every seat's name, scope, verbs and provision at the seat list's grain (ADR 0625 §2). mesh-sdk's `catalogue` is its only importer in mesh-sdk. - A change of it rebuilds every importer of the seat set. + A change of it rebuilds every importer of the seat set. Its npm package is `ts/` (`@novox/mesh-contracts`, the + seat list alone). - **A seat defined whole**, generated: `go/seats/`, a Go module of its own importing nothing, holding the seat's verbs, events and subjects, its message types and a test of its fixtures (ADR 0625 §1). A module - imports only the seats it uses, so a change of one seat rebuilds only its users. The TypeScript side is `ts/src/`. + imports only the seats it uses, so a change of one seat rebuilds only its users. Its npm package is + `ts/seats/` (`@novox/mesh-contracts-`), holding the parts it includes whole: the indexer + counts the npm package a lock resolves, so here too a module is rebuilt only for the seats it uses. - **A seat's client**, written by hand: `go/clients/client`, a Go module of its own, only where two or more callers share logic over the seat (ADR 0625 §4, ADR 0627 §1): operator-channel's (checking an ask, its digest, a warrant's, and an asking module's keeping of its asks) and the four registry seats' (publishing a file @@ -29,4 +32,4 @@ implements its contract, and no holder's code lives here. Every file under `go/` and `ts/` outside `go/clients/` is written by the generator (seatgen, the source of the Go and Node build modules' generate step) and compared on every build: a hand-written file there fails, named (ADR 0625 §5). `seatgen -check` also runs the client check (each client is for a seat defined whole, a module of its own, importing -only its seat's contract and mesh-sdk) and checks `module.json`'s Go package artifacts against the tree. +only its seat's contract and mesh-sdk) and checks `module.json`'s Go and npm package artifacts against the tree. diff --git a/module.json b/module.json index fd20a00..15e7a19 100644 --- a/module.json +++ b/module.json @@ -19,7 +19,16 @@ { "name": "go-npmpackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/npmpackageregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-ociartifactregistry", "kind": "package", "language": "go", "paths": ["go/seats/ociartifactregistry"], "series": "0.1", "generated-from": "." }, { "name": "go-operatorchannel", "kind": "package", "language": "go", "paths": ["go/seats/operatorchannel"], "series": "0.1", "generated-from": "." }, - { "name": "lib", "kind": "package", "language": "typescript", "paths": ["ts"], "series": "0.1", "generated-from": "." } + { "name": "lib", "kind": "package", "language": "typescript", "paths": ["ts"], "series": "0.1", "generated-from": "." }, + { "name": "ts-containerimageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "ts-gopackageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "ts-meshbroker", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshbroker"], "series": "0.1", "generated-from": "." }, + { "name": "ts-meshconfig", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshconfig"], "series": "0.1", "generated-from": "." }, + { "name": "ts-meshdeliverychecker", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshdeliverychecker"], "series": "0.1", "generated-from": "." }, + { "name": "ts-meshvault", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshvault"], "series": "0.1", "generated-from": "." }, + { "name": "ts-npmpackageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/npmpackageregistry"], "series": "0.1", "generated-from": "." }, + { "name": "ts-ociartifactregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/ociartifactregistry"], "series": "0.1", "generated-from": "." }, + { "name": "ts-operatorchannel", "kind": "package", "language": "typescript", "paths": ["ts/seats/operatorchannel"], "series": "0.1", "generated-from": "." } ] }, "resources": [] diff --git a/ts/package.json b/ts/package.json index 28bd3f5..cd5565a 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,5 +1,5 @@ { - "description": "The mesh's seat contracts, generated from their definitions.", + "description": "The mesh's seat set, generated from every seat's definition.", "devDependencies": { "typescript": "^5.6.0" }, diff --git a/ts/seats/containerimageregistry/package.json b/ts/seats/containerimageregistry/package.json new file mode 100644 index 0000000..5a3ac9d --- /dev/null +++ b/ts/seats/containerimageregistry/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat container-image-registry, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-container-image-registry", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/container-image-registry.ts b/ts/seats/containerimageregistry/src/index.ts similarity index 100% rename from ts/src/container-image-registry.ts rename to ts/seats/containerimageregistry/src/index.ts diff --git a/ts/src/parts/registry.ts b/ts/seats/containerimageregistry/src/parts/registry.ts similarity index 100% rename from ts/src/parts/registry.ts rename to ts/seats/containerimageregistry/src/parts/registry.ts diff --git a/ts/seats/containerimageregistry/tsconfig.json b/ts/seats/containerimageregistry/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/containerimageregistry/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/gopackageregistry/package.json b/ts/seats/gopackageregistry/package.json new file mode 100644 index 0000000..f8dd34e --- /dev/null +++ b/ts/seats/gopackageregistry/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat go-package-registry, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-go-package-registry", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/go-package-registry.ts b/ts/seats/gopackageregistry/src/index.ts similarity index 100% rename from ts/src/go-package-registry.ts rename to ts/seats/gopackageregistry/src/index.ts diff --git a/ts/seats/gopackageregistry/src/parts/registry.ts b/ts/seats/gopackageregistry/src/parts/registry.ts new file mode 100644 index 0000000..9642c2d --- /dev/null +++ b/ts/seats/gopackageregistry/src/parts/registry.ts @@ -0,0 +1,175 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it is gone */ + "removed": boolean; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + diff --git a/ts/seats/gopackageregistry/tsconfig.json b/ts/seats/gopackageregistry/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/gopackageregistry/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/meshbroker/package.json b/ts/seats/meshbroker/package.json new file mode 100644 index 0000000..d93aec0 --- /dev/null +++ b/ts/seats/meshbroker/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-broker, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-mesh-broker", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/mesh-broker.ts b/ts/seats/meshbroker/src/index.ts similarity index 100% rename from ts/src/mesh-broker.ts rename to ts/seats/meshbroker/src/index.ts diff --git a/ts/seats/meshbroker/tsconfig.json b/ts/seats/meshbroker/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/meshbroker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/meshconfig/package.json b/ts/seats/meshconfig/package.json new file mode 100644 index 0000000..af62267 --- /dev/null +++ b/ts/seats/meshconfig/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-config, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-mesh-config", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/mesh-config.ts b/ts/seats/meshconfig/src/index.ts similarity index 100% rename from ts/src/mesh-config.ts rename to ts/seats/meshconfig/src/index.ts diff --git a/ts/seats/meshconfig/tsconfig.json b/ts/seats/meshconfig/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/meshconfig/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/meshdeliverychecker/package.json b/ts/seats/meshdeliverychecker/package.json new file mode 100644 index 0000000..b842d55 --- /dev/null +++ b/ts/seats/meshdeliverychecker/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-delivery-checker, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-mesh-delivery-checker", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/mesh-delivery-checker.ts b/ts/seats/meshdeliverychecker/src/index.ts similarity index 100% rename from ts/src/mesh-delivery-checker.ts rename to ts/seats/meshdeliverychecker/src/index.ts diff --git a/ts/seats/meshdeliverychecker/tsconfig.json b/ts/seats/meshdeliverychecker/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/meshdeliverychecker/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/meshvault/package.json b/ts/seats/meshvault/package.json new file mode 100644 index 0000000..b811690 --- /dev/null +++ b/ts/seats/meshvault/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-vault, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-mesh-vault", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/mesh-vault.ts b/ts/seats/meshvault/src/index.ts similarity index 100% rename from ts/src/mesh-vault.ts rename to ts/seats/meshvault/src/index.ts diff --git a/ts/seats/meshvault/tsconfig.json b/ts/seats/meshvault/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/meshvault/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/npmpackageregistry/package.json b/ts/seats/npmpackageregistry/package.json new file mode 100644 index 0000000..506e958 --- /dev/null +++ b/ts/seats/npmpackageregistry/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat npm-package-registry, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-npm-package-registry", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/npm-package-registry.ts b/ts/seats/npmpackageregistry/src/index.ts similarity index 100% rename from ts/src/npm-package-registry.ts rename to ts/seats/npmpackageregistry/src/index.ts diff --git a/ts/seats/npmpackageregistry/src/parts/registry.ts b/ts/seats/npmpackageregistry/src/parts/registry.ts new file mode 100644 index 0000000..9642c2d --- /dev/null +++ b/ts/seats/npmpackageregistry/src/parts/registry.ts @@ -0,0 +1,175 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it is gone */ + "removed": boolean; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + diff --git a/ts/seats/npmpackageregistry/tsconfig.json b/ts/seats/npmpackageregistry/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/npmpackageregistry/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/ociartifactregistry/package.json b/ts/seats/ociartifactregistry/package.json new file mode 100644 index 0000000..822b791 --- /dev/null +++ b/ts/seats/ociartifactregistry/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat oci-artifact-registry, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-oci-artifact-registry", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/oci-artifact-registry.ts b/ts/seats/ociartifactregistry/src/index.ts similarity index 100% rename from ts/src/oci-artifact-registry.ts rename to ts/seats/ociartifactregistry/src/index.ts diff --git a/ts/seats/ociartifactregistry/src/parts/registry.ts b/ts/seats/ociartifactregistry/src/parts/registry.ts new file mode 100644 index 0000000..9642c2d --- /dev/null +++ b/ts/seats/ociartifactregistry/src/parts/registry.ts @@ -0,0 +1,175 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. + +export const verbs = { + publish: "publish", + publishPart: "publish-part", + versions: "versions", + remove: "remove", +} as const; + +export const events = { + published: "published", + refused: "refused", + removed: "removed", +} as const; + +/** one published thing, as every registry event and answer names it */ +export interface Artifact { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** the body of published, refused and removed */ +export interface Outcome { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** the module holding the seat that said it */ + "holder": string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + "module"?: string; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + "reason"?: string; + /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ + "reference"?: string; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer it ends, when it ended one */ + "transfer"?: string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** one part of a transfer */ +export interface Part { + /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ + "digest": string; + /** its size in bytes */ + "size": number; +} + +/** is what publish takes. */ +export interface PublishInput { + /** the source commit it was built from */ + "commit"?: string; + /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ + "digest": string; + /** the build's fingerprint */ + "fingerprint"?: string; + /** what it is */ + "kind": "package" | "image" | "bundle" | "archive" | "upstream"; + /** the package's, image's, bundle's or archive's name */ + "name": string; + /** every part's digest and size; none when the digest is already held */ + "parts"?: Part[]; + /** the tag it carries, for a tagged version */ + "tag"?: string; + /** the transfer's name, the caller's own; the same name opens the same transfer again */ + "transfer": string; + /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ + "version"?: string; +} + +/** is what publish answers. */ +export interface PublishAnswer { + /** the digest is held already: nothing to send; the outcome follows as an event */ + "held"?: boolean; + /** the digests of the parts the holder lacks, to send by publish-part */ + "lacks"?: string[]; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part takes. */ +export interface PublishPartInput { + /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ + "data": string; + /** this chunk is the part's last */ + "last": boolean; + /** the part's digest */ + "part": string; + /** the chunk's place in its part, from 0 */ + "sequence": number; + /** the transfer */ + "transfer": string; +} + +/** is what publish-part answers. */ +export interface PublishPartAnswer { + /** the part is whole and its bytes match its digest */ + "complete"?: boolean; + /** the part */ + "part": string; + /** the chunks of the part held so far */ + "received"?: number; + /** the transfer */ + "transfer": string; +} + +/** is what versions takes. */ +export interface VersionsInput { + /** narrowed to one kind (optional) */ + "kind"?: string; + /** the name */ + "name": string; +} + +/** is what versions answers. */ +export interface VersionsAnswer { + /** the name */ + "name": string; + /** newest first */ + "versions": Artifact[]; +} + +/** is what remove takes. */ +export interface RemoveInput { + /** the digest (for a bundle, archive or copied base) */ + "digest"?: string; + /** the name */ + "name": string; + /** why it goes */ + "reason": "tag-gone" | "retention"; + /** the tag (for a tagged version) */ + "tag"?: string; + /** the version */ + "version"?: string; +} + +/** is what remove answers. */ +export interface RemoveAnswer { + "artifact"?: Artifact; + /** it is gone */ + "removed": boolean; +} + +/** is the body of the event published. */ +export type PublishedEvent = Outcome; + +/** is the body of the event refused. */ +export type RefusedEvent = Outcome; + +/** is the body of the event removed. */ +export type RemovedEvent = Outcome; + diff --git a/ts/seats/ociartifactregistry/tsconfig.json b/ts/seats/ociartifactregistry/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/ociartifactregistry/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/operatorchannel/package.json b/ts/seats/operatorchannel/package.json new file mode 100644 index 0000000..e1d9bcb --- /dev/null +++ b/ts/seats/operatorchannel/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat operator-channel, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-contracts-operator-channel", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/ts/src/operator-channel.ts b/ts/seats/operatorchannel/src/index.ts similarity index 100% rename from ts/src/operator-channel.ts rename to ts/seats/operatorchannel/src/index.ts diff --git a/ts/seats/operatorchannel/tsconfig.json b/ts/seats/operatorchannel/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/ts/seats/operatorchannel/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/src/index.ts b/ts/src/index.ts index 18984e2..6df2ae8 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -1,12 +1,3 @@ // Code generated by seatgen from the seat definitions; DO NOT EDIT. export * as seats from "./seats.js"; -export * as meshBroker from "./mesh-broker.js"; -export * as meshVault from "./mesh-vault.js"; -export * as meshConfig from "./mesh-config.js"; -export * as meshDeliveryChecker from "./mesh-delivery-checker.js"; -export * as operatorChannel from "./operator-channel.js"; -export * as npmPackageRegistry from "./npm-package-registry.js"; -export * as goPackageRegistry from "./go-package-registry.js"; -export * as containerImageRegistry from "./container-image-registry.js"; -export * as ociArtifactRegistry from "./oci-artifact-registry.js"; -- 2.54.0 From 22abe5c955bd1ffdbedbcd2ab4ab7f500bf190af Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:50:00 +0100 Subject: [PATCH 33/94] =?UTF-8?q?ADR=200627=20=C2=A71:=20node-engine's=20c?= =?UTF-8?q?lient=20holds=20the=20checks=20of=20compose-candidate's=20ask?= =?UTF-8?q?=20and=20answer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checks the node-engine, the checker and the assigner share over the verb (ADR 0593 §7, §9) leave mesh-sdk's delivery package, where they sat over aliases of the seat's types, for go/clients/nodeengineclient, a Go module and package artifact of its own over the seat's generated contract: the verdicts, the module states, the asker's bound, the no-answer words, Encode/DecodeAsk, Encode/DecodeAnswer and CheckAnswer. Its test holds the cases mesh-sdk's held, and that an open input's number keeps its digits; each fails without the code. seatgen -check finds nothing out of step. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 4 +- go/clients/nodeengineclient/client.go | 183 +++++++++++++++++++++ go/clients/nodeengineclient/client_test.go | 97 +++++++++++ go/clients/nodeengineclient/go.mod | 3 + module.json | 1 + 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 go/clients/nodeengineclient/client.go create mode 100644 go/clients/nodeengineclient/client_test.go create mode 100644 go/clients/nodeengineclient/go.mod diff --git a/README.md b/README.md index 1efaec4..5536728 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ implements its contract, and no holder's code lives here. counts the npm package a lock resolves, so here too a module is rebuilt only for the seats it uses. - **A seat's client**, written by hand: `go/clients/client`, a Go module of its own, only where two or more callers share logic over the seat (ADR 0625 §4, ADR 0627 §1): operator-channel's (checking an ask, - its digest, a warrant's, and an asking module's keeping of its asks) and the four registry seats' (publishing a file - in parts). A client imports its own seat's contract and mesh-sdk's runtime, never another seat's client, and names + its digest, a warrant's, and an asking module's keeping of its asks), node-engine's (checking compose-candidate's ask + and answer) and the four registry seats' (publishing a file in parts). A client imports its own seat's contract and mesh-sdk's runtime, never another seat's client, and names no module's subject or tool. - **Not here**: the value layouts of seats not yet defined whole (they stay in mesh-sdk until their domain moves, ADR 0625 §9), and a role only one module could ever play (that module's own tools and events, ADR 0625 §8). diff --git a/go/clients/nodeengineclient/client.go b/go/clients/nodeengineclient/client.go new file mode 100644 index 0000000..0b1939f --- /dev/null +++ b/go/clients/nodeengineclient/client.go @@ -0,0 +1,183 @@ +// Package nodeengineclient is the node-engine seat's client (novox/hq ADR 0627 §1, ADR 0625 §4): the checks its callers +// share over the verb compose-candidate's ask and answer (ADR 0593 §7, §9), over the seat's generated contract. The +// node-engine encodes its answer through it, and the checker and the assigner read answers and the asker's bound +// through it. The composing is the node-engine's own; the call is the caller's own, so it holds no transport. +package nodeengineclient + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "regexp" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const ( + // AnswerBound is the default bound an asker of compose-candidate waits for a node's answer (the checker's setting + // compose-answer-bound, ADR 0593 §7, §9). + AnswerBound = 60 * time.Second + // NoAnswer is the words a check's verdict names a node with that did not answer within the bound (ADR 0593 §9). + NoAnswer = "not composed on that node: no answer" + + // Composed and Refused are an answer's verdict: no module and not the node's own composition is refused by the + // candidate, or at least one is (or the node could not compose with it at all, the answer's Refused). + Composed = "composed" + Refused = "refused" + // Unready is the verdict of a node that cannot compose yet (its shadow not ready, its agent starting): no module is + // named, Refused says why in words, and the checker takes it as a node that did not answer (NoAnswer), deciding by + // this verdict and never by the words. + Unready = "unready" + // NotJudged is the verdict for a node the checker's sandbox composed and could not judge (an older node-engine's + // input that does not say how it composes, ADR 0593 §9): no module is named, Refused says why in words, and the + // checker names the node with them and passes the check with that warning (ADR 0601 §4: a contract is a field, not + // a phrase). + NotJudged = "not-judged" + + // The states of one module's composition with the candidate: composed (Digest set), refused by the candidate + // (Words set), or held, refused without the candidate too (Words set), which counts toward no verdict. + ModuleComposed = "composed" + ModuleRefused = "refused" + ModuleHeld = "held" + + // Own is the Module name of the node's own composition in an answer. + Own = "node" + + // Version is the version an answer is written at and the only one read. + Version = 1 +) + +// ErrUnknownVersion is what DecodeAnswer returns for an answer whose version is missing or not Version. +var ErrUnknownVersion = errors.New("node-engine: unknown compose-candidate answer version") + +var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) + +func checkAsk(a seat.ComposeCandidateInput) error { + if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + return fmt.Errorf("node-engine: a compose-candidate ask names no candidate: %w", err) + } + return nil +} + +func checkModule(m seat.Module, node string) error { + if m.Module == "" { + return fmt.Errorf("node-engine: a module's composition on %s names no module", node) + } + switch m.State { + case ModuleComposed: + if m.Digest == "" { + return fmt.Errorf("node-engine: %s is composed on %s and names no digest", m.Module, node) + } + case ModuleRefused, ModuleHeld: + if m.Words == "" { + return fmt.Errorf("node-engine: %s is %s on %s and says no why", m.Module, m.State, node) + } + default: + return fmt.Errorf("node-engine: %s on %s is %q, neither %s, %s nor %s", m.Module, node, m.State, + ModuleComposed, ModuleRefused, ModuleHeld) + } + return nil +} + +// CheckAnswer refuses an answer naming no node or candidate, a negative time taken, an unknown module state, a composed +// module without a digest, a refused or held one without words, modules not sorted and each once, a refusal of the +// whole naming modules, an unready or not-judged one saying no why, and a verdict the modules do not give. +func CheckAnswer(a seat.ComposeCandidateAnswer) error { + if a.Node == "" || !nameOK.MatchString(a.Node) { + return fmt.Errorf("node-engine: the answer's node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", a.Node) + } + if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + return err + } + if a.Took < 0 { + return fmt.Errorf("node-engine: the answer of %s took %d ms", a.Node, a.Took) + } + if (a.Verdict == Unready || a.Verdict == NotJudged) && a.Refused == "" { + return fmt.Errorf("node-engine: %s's answer is %s and says no why", a.Node, a.Verdict) + } + if a.Refused != "" { + if len(a.Modules) > 0 || a.Verdict != Refused && a.Verdict != Unready && a.Verdict != NotJudged { + return fmt.Errorf("node-engine: %s could not compose the candidate (%s), and its answer names modules or is neither %s, %s nor %s", + a.Node, a.Refused, Refused, Unready, NotJudged) + } + return nil + } + refused := false + for i, m := range append([]seat.Module{a.Own}, a.Modules...) { + if err := checkModule(m, a.Node); err != nil { + return err + } + if i > 1 && a.Modules[i-2].Module >= m.Module { + return fmt.Errorf("node-engine: the modules of %s's answer are not sorted and each once (%s after %s)", a.Node, m.Module, a.Modules[i-2].Module) + } + refused = refused || m.State == ModuleRefused + } + switch { + case a.Own.Module != Own: + return fmt.Errorf("node-engine: the node's own composition in %s's answer is named %q, not %q", a.Node, a.Own.Module, Own) + case refused && a.Verdict != Refused: + return fmt.Errorf("node-engine: %s's answer refuses a module and its verdict is %q", a.Node, a.Verdict) + case !refused && a.Verdict != Composed: + return fmt.Errorf("node-engine: %s's answer refuses no module and its verdict is %q", a.Node, a.Verdict) + } + return nil +} + +// strictDecode reads data into v, refusing an unknown key. +func strictDecode(data []byte, v any, what string) error { + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + d.UseNumber() // a number in an object the contract leaves open (the composition input) keeps its digits as written + if err := d.Decode(v); err != nil { + return fmt.Errorf("node-engine: %s does not fit its shape: %w", what, err) + } + return nil +} + +// EncodeAsk is a as JSON; it refuses an ask naming no candidate key. +func EncodeAsk(a seat.ComposeCandidateInput) ([]byte, error) { + if err := checkAsk(a); err != nil { + return nil, err + } + return json.Marshal(a) +} + +// DecodeAsk reads an ask strictly; it refuses an unknown key and what EncodeAsk refuses. +func DecodeAsk(data []byte) (seat.ComposeCandidateInput, error) { + var a seat.ComposeCandidateInput + if err := strictDecode(data, &a, "a compose-candidate ask"); err != nil { + return seat.ComposeCandidateInput{}, err + } + return a, checkAsk(a) +} + +// EncodeAnswer is a as JSON at Version; it refuses what CheckAnswer refuses. +func EncodeAnswer(a seat.ComposeCandidateAnswer) ([]byte, error) { + if err := CheckAnswer(a); err != nil { + return nil, err + } + a.V = Version + return json.Marshal(a) +} + +// DecodeAnswer reads an answer strictly; it refuses an unknown version (ErrUnknownVersion), an unknown key and what +// CheckAnswer refuses. +func DecodeAnswer(data []byte) (seat.ComposeCandidateAnswer, error) { + var probe struct { + V *int64 `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return seat.ComposeCandidateAnswer{}, fmt.Errorf("node-engine: a compose-candidate answer is not JSON: %w", err) + } + if probe.V == nil || *probe.V != Version { + return seat.ComposeCandidateAnswer{}, fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version) + } + var a seat.ComposeCandidateAnswer + if err := strictDecode(data, &a, "a compose-candidate answer"); err != nil { + return seat.ComposeCandidateAnswer{}, err + } + return a, CheckAnswer(a) +} diff --git a/go/clients/nodeengineclient/client_test.go b/go/clients/nodeengineclient/client_test.go new file mode 100644 index 0000000..542b207 --- /dev/null +++ b/go/clients/nodeengineclient/client_test.go @@ -0,0 +1,97 @@ +package nodeengineclient + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could +// not take, naming it. +func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { + key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab") + if err != nil { + t.Fatal(err) + } + raw, err := EncodeAsk(seat.ComposeCandidateInput{Candidate: key, Input: true}) + if err != nil { + t.Fatal(err) + } + if a, err := DecodeAsk(raw); err != nil || a.Candidate != key || !a.Input { + t.Fatalf("ask round trip: %v, %+v", err, a) + } + if _, err := DecodeAsk([]byte(`{"candidate":"` + key + `","other":1}`)); err == nil { + t.Error("an ask with an unknown key was read") + } + if _, err := EncodeAsk(seat.ComposeCandidateInput{Candidate: "nope"}); err == nil { + t.Error("an ask naming no candidate key was encoded") + } + + good := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Refused, Took: 812, + Own: seat.Module{Module: Own, State: ModuleComposed, Digest: "sha256:00"}, + Modules: []seat.Module{ + {Module: "netcheck", Fp: "src2:x", State: ModuleComposed, Digest: "sha256:01", Changed: true}, + {Module: "route-proxy", State: ModuleRefused, Words: "the setting domain has no value"}, + {Module: "zz", State: ModuleHeld, Words: "held before"}, + }, Input: map[string]any{"machines": []any{"ace"}}} + raw, err = EncodeAnswer(good) + if err != nil { + t.Fatal(err) + } + back, err := DecodeAnswer(raw) + if err != nil || back.Verdict != Refused || len(back.Modules) != 3 || !reflect.DeepEqual(back.Input, good.Input) { + t.Fatalf("answer round trip: %v, %+v", err, back) + } + if _, err := DecodeAnswer([]byte(strings.Replace(string(raw), `"took"`, `"other":1,"took"`, 1))); err == nil { + t.Error("an answer with an unknown key was read") + } + if _, err := DecodeAnswer([]byte(strings.Replace(string(raw), `"v":1`, `"v":2`, 1))); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("an answer at another version read as %v", err) + } + numbered := strings.Replace(string(raw), `"machines":["ace"]`, `"bound":12345678901234567890`, 1) + if back, err := DecodeAnswer([]byte(numbered)); err != nil || fmt.Sprint(back.Input["bound"]) != "12345678901234567890" { + t.Errorf("an open input's number did not keep its digits: %v, %+v", err, back.Input) + } + + whole := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Refused, Refused: "the bound passed"} + unready := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: Unready, Refused: "the agent is starting"} + notJudged := seat.ComposeCandidateAnswer{Node: "ace", Candidate: key, Verdict: NotJudged, Refused: "its node-engine sends no running state"} + for _, a := range []seat.ComposeCandidateAnswer{whole, unready, notJudged} { + if raw, err := EncodeAnswer(a); err != nil { + t.Errorf("a %s answer was not encoded: %v", a.Verdict, err) + } else if back, err := DecodeAnswer(raw); err != nil || back.Verdict != a.Verdict { + t.Errorf("a %s answer read back as %+v, %v", a.Verdict, back, err) + } + } + + for name, c := range map[string]struct { + from seat.ComposeCandidateAnswer + mutate func(a *seat.ComposeCandidateAnswer) + }{ + "verdict not the modules'": {good, func(a *seat.ComposeCandidateAnswer) { a.Verdict = Composed }}, + "unknown state": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0].State = "fine" }}, + "composed without digest": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0].Digest = "" }}, + "refused without words": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[1].Words = "" }}, + "unsorted": {good, func(a *seat.ComposeCandidateAnswer) { a.Modules[0], a.Modules[1] = a.Modules[1], a.Modules[0] }}, + "own misnamed": {good, func(a *seat.ComposeCandidateAnswer) { a.Own.Module = "ace" }}, + "whole refusal with modules": {good, func(a *seat.ComposeCandidateAnswer) { a.Refused = "the bound passed" }}, + "node with a dot": {good, func(a *seat.ComposeCandidateAnswer) { a.Node = "a.b" }}, + "negative took": {good, func(a *seat.ComposeCandidateAnswer) { a.Took = -1 }}, + "unready saying no why": {unready, func(a *seat.ComposeCandidateAnswer) { a.Refused = "" }}, + "unready naming modules": {unready, func(a *seat.ComposeCandidateAnswer) { a.Modules = good.Modules }}, + "not judged saying no why": {notJudged, func(a *seat.ComposeCandidateAnswer) { a.Refused = "" }}, + "not judged naming modules": {notJudged, func(a *seat.ComposeCandidateAnswer) { a.Modules = good.Modules }}, + } { + a := c.from + a.Modules = append([]seat.Module(nil), c.from.Modules...) + c.mutate(&a) + if _, err := EncodeAnswer(a); err == nil { + t.Errorf("%s: encoded", name) + } + } +} diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod new file mode 100644 index 0000000..2ccb4d7 --- /dev/null +++ b/go/clients/nodeengineclient/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient + +go 1.26.0 diff --git a/module.json b/module.json index 52bbde4..849e5c4 100644 --- a/module.json +++ b/module.json @@ -7,6 +7,7 @@ { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, { "name": "go-containerimageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/containerimageregistryclient"], "series": "0.1" }, { "name": "go-gopackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/gopackageregistryclient"], "series": "0.1" }, + { "name": "go-nodeengineclient", "kind": "package", "language": "go", "paths": ["go/clients/nodeengineclient"], "series": "0.1" }, { "name": "go-npmpackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/npmpackageregistryclient"], "series": "0.1" }, { "name": "go-ociartifactregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/ociartifactregistryclient"], "series": "0.1" }, { "name": "go-operatorchannelclient", "kind": "package", "language": "go", "paths": ["go/clients/operatorchannelclient"], "series": "0.1" }, -- 2.54.0 From 4afce1aac809bdaff0a94b0a8348a0f4f23dda34 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 13:52:54 +0100 Subject: [PATCH 34/94] Drop node-engine's optional verb pull-waits: no record gives the seat that verb The lead's ruling (14:2x): the pull waits ENG-9 shows are carried in the engine's have, read through node_haves, as ENG-9 allows, so the seat serves no verb of its own for them. Regenerated; seatgen -check finds nothing out of step. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/nodeengine/contract.go | 35 +---------------- go/seats/seats.go | 3 -- seats/node-engine/seat.json | 66 -------------------------------- ts/seats/nodeengine/src/index.ts | 32 ---------------- ts/src/seats.ts | 66 -------------------------------- 5 files changed, 1 insertion(+), 201 deletions(-) diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index 74daf78..200524c 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -17,7 +17,6 @@ const ( VerbComposition = "composition" VerbCompose = "compose" VerbComposeCandidate = "compose-candidate" - VerbPullWaits = "pull-waits" VerbManifestReads = "manifest-reads" ) @@ -30,7 +29,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbPullWaits, VerbManifestReads} + return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbManifestReads} } // Events is every event published on the seat's subject: a new copy each call. @@ -493,38 +492,6 @@ type ComposeCandidateAnswer struct { Verdict string `json:"verdict"` } -// PullWaitsInput is what pull-waits takes. -type PullWaitsInput struct { -} - -// PullWaitsAnswerWaitsItem is a type of the contract. -type PullWaitsAnswerWaitsItem struct { - // that seat's holder, whose settled the retry waits on - Holder string `json:"holder,omitempty"` - // the image whose pull failed - Image string `json:"image"` - // the module whose apply failed - Module string `json:"module"` - // when the failed pull began, RFC 3339 - PullBegan string `json:"pull-began,omitempty"` - // when it was last tried again, RFC 3339 - RetryLast string `json:"retry-last,omitempty"` - // what started that retry: settled, or the engine's start - RetryTrigger string `json:"retry-trigger,omitempty"` - // the seat the image came through - Seat string `json:"seat,omitempty"` - // the settled events of the holder heard since the engine started - SettledCount int64 `json:"settled-count,omitempty"` - // when the last was heard, RFC 3339 - SettledLast string `json:"settled-last,omitempty"` -} - -// PullWaitsAnswer is what pull-waits answers. -type PullWaitsAnswer struct { - // every failed pull waiting on a holder - Waits []PullWaitsAnswerWaitsItem `json:"waits"` -} - // ManifestReadsInput is what manifest-reads takes. type ManifestReadsInput struct { } diff --git a/go/seats/seats.go b/go/seats/seats.go index ab7d3c8..2eb7cbb 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -694,9 +694,6 @@ func List() []Seat { {Name: "compose-candidate", Description: "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", Optional: true, Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "also answer the composition input the candidate was composed from, for the sandbox path", "type": "boolean"}}, "required": []string{"candidate"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the candidate's key", "type": "string"}, "input": map[string]any{"description": "when asked: the composition input it composed from", "type": "object"}, "modules": map[string]any{"description": "every module placed on the node, sorted by name", "items": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "refused": map[string]any{"description": "why the node could not compose with the candidate at all", "type": "string"}, "took": map[string]any{"description": "how long the engine took, in milliseconds", "type": "integer"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}, "verdict": map[string]any{"description": "the node's verdict (ADR 0593 §7, §9)", "enum": []string{"composed", "refused", "unready", "not-judged"}, "type": "string"}}, "required": []string{"v", "node", "candidate", "verdict", "modules", "own", "took"}, "type": "object"}}, - {Name: "pull-waits", Description: "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"waits": map[string]any{"description": "every failed pull waiting on a holder", "items": map[string]any{"properties": map[string]any{"holder": map[string]any{"description": "that seat's holder, whose settled the retry waits on", "type": "string"}, "image": map[string]any{"description": "the image whose pull failed", "type": "string"}, "module": map[string]any{"description": "the module whose apply failed", "type": "string"}, "pull-began": map[string]any{"description": "when the failed pull began, RFC 3339", "type": "string"}, "retry-last": map[string]any{"description": "when it was last tried again, RFC 3339", "type": "string"}, "retry-trigger": map[string]any{"description": "what started that retry: settled, or the engine's start", "type": "string"}, "seat": map[string]any{"description": "the seat the image came through", "type": "string"}, "settled-count": map[string]any{"description": "the settled events of the holder heard since the engine started", "type": "integer"}, "settled-last": map[string]any{"description": "when the last was heard, RFC 3339", "type": "string"}}, "required": []string{"module", "image"}, "type": "object"}, "type": "array"}}, "required": []string{"waits"}, "type": "object"}}, {Name: "manifest-reads", Description: "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", Optional: true, Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Output: map[string]any{"description": "what this machine's engine reads of a manifest (ADR 0618 §3)", "properties": map[string]any{"manifest-reads": map[string]any{"description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline", "items": map[string]any{"minLength": float64(1), "type": "string"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "stated-at": map[string]any{"description": "when the engine last stated them, RFC 3339", "type": "string"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}}, "required": []string{"v", "node", "stated-at"}, "type": "object"}}, diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 229e220..dfcaeb8 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -634,72 +634,6 @@ ] } }, - { - "name": "pull-waits", - "optional": true, - "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", - "input": { - "type": "object", - "properties": {} - }, - "answer": { - "type": "object", - "required": [ - "waits" - ], - "properties": { - "waits": { - "type": "array", - "description": "every failed pull waiting on a holder", - "items": { - "type": "object", - "required": [ - "module", - "image" - ], - "properties": { - "module": { - "type": "string", - "description": "the module whose apply failed" - }, - "image": { - "type": "string", - "description": "the image whose pull failed" - }, - "seat": { - "type": "string", - "description": "the seat the image came through" - }, - "holder": { - "type": "string", - "description": "that seat's holder, whose settled the retry waits on" - }, - "pull-began": { - "type": "string", - "description": "when the failed pull began, RFC 3339" - }, - "settled-count": { - "type": "integer", - "description": "the settled events of the holder heard since the engine started" - }, - "settled-last": { - "type": "string", - "description": "when the last was heard, RFC 3339" - }, - "retry-last": { - "type": "string", - "description": "when it was last tried again, RFC 3339" - }, - "retry-trigger": { - "type": "string", - "description": "what started that retry: settled, or the engine's start" - } - } - } - } - } - } - }, { "name": "manifest-reads", "optional": true, diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 8c0f71e..d37717b 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -15,7 +15,6 @@ export const verbs = { composition: "composition", compose: "compose", composeCandidate: "compose-candidate", - pullWaits: "pull-waits", manifestReads: "manifest-reads", } as const; @@ -478,37 +477,6 @@ export interface ComposeCandidateAnswer { "verdict": "composed" | "refused" | "unready" | "not-judged"; } -/** is what pull-waits takes. */ -export interface PullWaitsInput { -} - -export interface PullWaitsAnswerWaitsItem { - /** that seat's holder, whose settled the retry waits on */ - "holder"?: string; - /** the image whose pull failed */ - "image": string; - /** the module whose apply failed */ - "module": string; - /** when the failed pull began, RFC 3339 */ - "pull-began"?: string; - /** when it was last tried again, RFC 3339 */ - "retry-last"?: string; - /** what started that retry: settled, or the engine's start */ - "retry-trigger"?: string; - /** the seat the image came through */ - "seat"?: string; - /** the settled events of the holder heard since the engine started */ - "settled-count"?: number; - /** when the last was heard, RFC 3339 */ - "settled-last"?: string; -} - -/** is what pull-waits answers. */ -export interface PullWaitsAnswer { - /** every failed pull waiting on a holder */ - "waits": PullWaitsAnswerWaitsItem[]; -} - /** is what manifest-reads takes. */ export interface ManifestReadsInput { } diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 2d5e4b0..13ff8a5 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -7584,72 +7584,6 @@ export const list: readonly Seat[] = [ "type": "object" } }, - { - "description": "The failed pulls this machine's engine waits on a store's holder for (design 51 *What a pull meets in the swap*): per module, the image, the seat it came through and that seat's holder, when the pull began, how many settled events of that holder it heard and the last, and its last retry and what started it. Read-only. Answered by this machine's engine alone.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "pull-waits", - "optional": true, - "output": { - "properties": { - "waits": { - "description": "every failed pull waiting on a holder", - "items": { - "properties": { - "holder": { - "description": "that seat's holder, whose settled the retry waits on", - "type": "string" - }, - "image": { - "description": "the image whose pull failed", - "type": "string" - }, - "module": { - "description": "the module whose apply failed", - "type": "string" - }, - "pull-began": { - "description": "when the failed pull began, RFC 3339", - "type": "string" - }, - "retry-last": { - "description": "when it was last tried again, RFC 3339", - "type": "string" - }, - "retry-trigger": { - "description": "what started that retry: settled, or the engine's start", - "type": "string" - }, - "seat": { - "description": "the seat the image came through", - "type": "string" - }, - "settled-count": { - "description": "the settled events of the holder heard since the engine started", - "type": "integer" - }, - "settled-last": { - "description": "when the last was heard, RFC 3339", - "type": "string" - } - }, - "required": [ - "module", - "image" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "waits" - ], - "type": "object" - } - }, { "description": "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", "input": { -- 2.54.0 From d9af39256ed29402692114aae2f5cbce90704361 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 14:52:52 +0100 Subject: [PATCH 35/94] J4 (A2j finding 2): the checker's verdict event names each series change, so a reader never loses one to a cut status line Each change of an artifact's series the checked head makes is {module, artifact, from, to} under series; the fixture carries one and the regenerated round trip reads it back (seatgen -check clean). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/meshdeliverychecker/contract.go | 14 +++++++++ go/seats/meshdeliverychecker/contract_test.go | 2 +- .../fixtures/events.json | 8 +++++ seats/mesh-delivery-checker/seat.json | 31 +++++++++++++++++++ ts/seats/meshdeliverychecker/src/index.ts | 13 ++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go index cbe9997..024999d 100644 --- a/go/seats/meshdeliverychecker/contract.go +++ b/go/seats/meshdeliverychecker/contract.go @@ -84,6 +84,18 @@ type TryEndAnswer struct { ID string `json:"id"` } +// VerdictHolderEventSeriesItem is a type of the contract. +type VerdictHolderEventSeriesItem struct { + // its artifact, or the package's ecosystem name as the module's packages name it + Artifact string `json:"artifact"` + // the series on trunk + From string `json:"from"` + // the module whose artifact's series changes + Module string `json:"module"` + // the series the checked head gives it + To string `json:"to"` +} + // VerdictHolderEvent is the body of the holder's event verdict. type VerdictHolderEvent struct { // the commit checked @@ -94,6 +106,8 @@ type VerdictHolderEvent struct { Pull int64 `json:"pull,omitempty"` // owner/repo Repository string `json:"repository"` + // each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none + Series []VerdictHolderEventSeriesItem `json:"series,omitempty"` // pass or fail Verdict string `json:"verdict"` } diff --git a/go/seats/meshdeliverychecker/contract_test.go b/go/seats/meshdeliverychecker/contract_test.go index 5856f27..1b17187 100644 --- a/go/seats/meshdeliverychecker/contract_test.go +++ b/go/seats/meshdeliverychecker/contract_test.go @@ -17,7 +17,7 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"verdict\":\"fail\"}", + {name: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.verdict$")}, {name: "tag-wanted on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, diff --git a/seats/mesh-delivery-checker/fixtures/events.json b/seats/mesh-delivery-checker/fixtures/events.json index 712b5db..7df46f6 100644 --- a/seats/mesh-delivery-checker/fixtures/events.json +++ b/seats/mesh-delivery-checker/fixtures/events.json @@ -10,6 +10,14 @@ "verdict": "fail", "out-of-step": [ "go/seats/meshbroker/contract.go" + ], + "series": [ + { + "module": "mesh-contracts", + "artifact": "go", + "from": "0.1", + "to": "0.2" + } ] } }, diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json index 67eb7a8..7b56ab5 100644 --- a/seats/mesh-delivery-checker/seat.json +++ b/seats/mesh-delivery-checker/seat.json @@ -151,6 +151,37 @@ "type": "string" }, "description": "the generated files out of step with their seat definitions, each path (design 26)" + }, + "series": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string", + "description": "the module whose artifact's series changes" + }, + "artifact": { + "type": "string", + "description": "its artifact, or the package's ecosystem name as the module's packages name it" + }, + "from": { + "type": "string", + "description": "the series on trunk" + }, + "to": { + "type": "string", + "description": "the series the checked head gives it" + } + }, + "required": [ + "module", + "artifact", + "from", + "to" + ] + }, + "description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none" } }, "required": [ diff --git a/ts/seats/meshdeliverychecker/src/index.ts b/ts/seats/meshdeliverychecker/src/index.ts index bb5e526..bdd9486 100644 --- a/ts/seats/meshdeliverychecker/src/index.ts +++ b/ts/seats/meshdeliverychecker/src/index.ts @@ -75,6 +75,17 @@ export interface TryEndAnswer { "id": string; } +export interface VerdictHolderEventSeriesItem { + /** its artifact, or the package's ecosystem name as the module's packages name it */ + "artifact": string; + /** the series on trunk */ + "from": string; + /** the module whose artifact's series changes */ + "module": string; + /** the series the checked head gives it */ + "to": string; +} + /** is the body of the holder's event verdict. */ export interface VerdictHolderEvent { /** the commit checked */ @@ -85,6 +96,8 @@ export interface VerdictHolderEvent { "pull"?: number; /** owner/repo */ "repository": string; + /** each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none */ + "series"?: VerdictHolderEventSeriesItem[]; /** pass or fail */ "verdict": string; } -- 2.54.0 From 5118c5f4259660733707361bb9e6f32061a5e524 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 15:38:46 +0100 Subject: [PATCH 36/94] J7 (1): the operator-channel client reads an ask its keeper carries back, held to its id and an option The assigner, the teller, the view and the gate each wrote the same two checks over the ask a value of theirs carries; the fourth copy made it a kind, so it lives once in the seat's client (Carried) and the four call it. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/clients/operatorchannelclient/ask.go | 19 +++++++++++++++++++ go/clients/operatorchannelclient/ask_test.go | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/go/clients/operatorchannelclient/ask.go b/go/clients/operatorchannelclient/ask.go index a886bb4..ee93987 100644 --- a/go/clients/operatorchannelclient/ask.go +++ b/go/clients/operatorchannelclient/ask.go @@ -9,6 +9,7 @@ package operatorchannelclient import ( "crypto/sha256" "encoding/hex" + "encoding/json" "errors" "fmt" "regexp" @@ -301,6 +302,24 @@ func Check(a seat.Ask, now time.Time) error { return nil } +// Carried reads an ask a module keeps or hands on as the seat's JSON (a value of its own state that carries the ask it +// publishes, novox/hq ADR 0627 §4) and holds it to what every keeper of an ask owes, whoever reads it back: its id is the +// one the keeper gave it (id), and it offers at least one option. It checks nothing else of the ask: Check is what an +// ask is held to before it is shown, and an ask kept long ago is read back even when the bounds of Check moved since. +func Carried(raw []byte, id string) (seat.Ask, error) { + var a seat.Ask + if err := json.Unmarshal(raw, &a); err != nil { + return seat.Ask{}, fmt.Errorf("the value carries no operator-channel ask: %w", err) + } + if a.ID != id { + return seat.Ask{}, fmt.Errorf("the ask's id %q is not %q, the id it was kept under", a.ID, id) + } + if len(a.Options) == 0 { + return seat.Ask{}, fmt.Errorf("the ask %q offers no option", a.ID) + } + return a, nil +} + // Says is the warrant in the words an asker records with its act: "the operator, via telegram (user id // verified), chose Release". func Says(w seat.Warrant) string { diff --git a/go/clients/operatorchannelclient/ask_test.go b/go/clients/operatorchannelclient/ask_test.go index 1a68435..7166bbe 100644 --- a/go/clients/operatorchannelclient/ask_test.go +++ b/go/clients/operatorchannelclient/ask_test.go @@ -69,6 +69,26 @@ func TestAnAskIsRefusedInWordsForEachFault(t *testing.T) { if err := Check(ack, now); err != nil { t.Errorf("an ask that only acknowledges needs no expiry words and may last a week: %v", err) } + // Carried: an ask read back from a keeper's value is its own id's, and offers an option. + raw, _ := json.Marshal(held()) + if a, err := Carried(raw, "d-1"); err != nil || !reflect.DeepEqual(a, held()) { + t.Errorf("a carried sound ask reads back whole: %v %+v", err, a) + } + none := held() + none.Options = nil + noOptions, _ := json.Marshal(none) + for want, c := range map[string]struct { + raw []byte + id string + }{ + "carries no operator-channel ask": {[]byte("[1]"), "d-1"}, + `is not "d-2"`: {raw, "d-2"}, + "offers no option": {noOptions, "d-1"}, + } { + if _, err := Carried(c.raw, c.id); err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("Carried: want a refusal saying %q, got %v", want, err) + } + } } func TestAnAskerActsOnlyOnAWarrantForItsOwnAskAndOption(t *testing.T) { -- 2.54.0 From 7d8f090b75dd0c7ddc67e3279103690ae04d09d0 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 16:32:15 +0100 Subject: [PATCH 37/94] Registry seats: the removal reasons are constants, a release of nothing held answers released false, packages carry their own language's names seatgen writes a string's enum as Go constants, so the sweep names RemoveInputReasonRetention instead of the literal. release of what is not held is no error. The package seats' fixtures name a package as design 32 does (git.novox.be/novox/mesh-sdk/go, @novox/mesh-sdk), name says so, and module equals name for a Go package. Generated with mesh-catalog's seatgen (wip/registry-a3); seatgen -check passes. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/containerimageregistry/contract.go | 43 ++++++++++-- .../containerimageregistry/contract_test.go | 2 + go/seats/gopackageregistry/contract.go | 41 ++++++++++-- go/seats/gopackageregistry/contract_test.go | 12 ++-- go/seats/meshbroker/contract.go | 67 +++++++++++++++++++ go/seats/meshconfig/contract.go | 6 ++ go/seats/meshvault/contract.go | 18 +++++ go/seats/nodeengine/contract.go | 42 ++++++++++++ go/seats/npmpackageregistry/contract.go | 41 ++++++++++-- go/seats/npmpackageregistry/contract_test.go | 12 ++-- go/seats/ociartifactregistry/contract.go | 43 ++++++++++-- go/seats/ociartifactregistry/contract_test.go | 2 + go/seats/operatorchannel/contract.go | 24 +++++++ go/seats/seats.go | 32 ++++----- parts/registry.json | 8 +-- .../fixtures/verbs.json | 8 +++ seats/container-image-registry/seat.json | 4 +- .../go-package-registry/fixtures/events.json | 12 ++-- seats/go-package-registry/fixtures/verbs.json | 10 +-- .../npm-package-registry/fixtures/events.json | 12 ++-- .../npm-package-registry/fixtures/verbs.json | 10 +-- .../oci-artifact-registry/fixtures/verbs.json | 8 +++ seats/oci-artifact-registry/seat.json | 4 +- ts/seats/containerimageregistry/src/index.ts | 2 +- .../src/parts/registry.ts | 8 +-- .../gopackageregistry/src/parts/registry.ts | 8 +-- .../npmpackageregistry/src/parts/registry.ts | 8 +-- ts/seats/ociartifactregistry/src/index.ts | 2 +- .../ociartifactregistry/src/parts/registry.ts | 8 +-- ts/src/seats.ts | 32 ++++----- 30 files changed, 419 insertions(+), 110 deletions(-) diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index 01fcea6..ef6d3f2 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -66,7 +66,7 @@ type Artifact struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` @@ -74,6 +74,15 @@ type Artifact struct { Version string `json:"version,omitempty"` } +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + // Hold is the body of held and released. type Hold struct { // when, RFC 3339 @@ -102,9 +111,9 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` - // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) Reason string `json:"reason,omitempty"` @@ -118,6 +127,15 @@ type Outcome struct { Version string `json:"version,omitempty"` } +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + // Part is one part of a transfer. type Part struct { // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob @@ -136,7 +154,7 @@ type PublishInput struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // every part's digest and size; none when the digest is already held Parts []Part `json:"parts,omitempty"` @@ -148,6 +166,15 @@ type PublishInput struct { Version string `json:"version,omitempty"` } +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + // PublishAnswer is what publish answers. type PublishAnswer struct { // the digest is held already: nothing to send; the outcome follows as an event @@ -214,6 +241,12 @@ type RemoveInput struct { Version string `json:"version,omitempty"` } +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` @@ -291,7 +324,7 @@ type ReleaseInput struct { // ReleaseAnswer is what release answers. type ReleaseAnswer struct { - // the hold is gone + // a hold was let go; false when there was none Released bool `json:"released"` } diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go index 7141141..6b5e2c0 100644 --- a/go/seats/containerimageregistry/contract_test.go +++ b/go/seats/containerimageregistry/contract_test.go @@ -37,6 +37,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", + decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 98aea35..54ea7a4 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -54,7 +54,7 @@ type Artifact struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` @@ -62,6 +62,15 @@ type Artifact struct { Version string `json:"version,omitempty"` } +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + // Outcome is the body of published, refused and removed. type Outcome struct { // the source commit it was built from @@ -74,9 +83,9 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` - // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) Reason string `json:"reason,omitempty"` @@ -90,6 +99,15 @@ type Outcome struct { Version string `json:"version,omitempty"` } +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + // Part is one part of a transfer. type Part struct { // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob @@ -108,7 +126,7 @@ type PublishInput struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // every part's digest and size; none when the digest is already held Parts []Part `json:"parts,omitempty"` @@ -120,6 +138,15 @@ type PublishInput struct { Version string `json:"version,omitempty"` } +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + // PublishAnswer is what publish answers. type PublishAnswer struct { // the digest is held already: nothing to send; the outcome follows as an event @@ -186,6 +213,12 @@ type RemoveInput struct { Version string `json:"version,omitempty"` } +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go index ff2164e..8f127f0 100644 --- a/go/seats/gopackageregistry/contract_test.go +++ b/go/seats/gopackageregistry/contract_test.go @@ -17,21 +17,21 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + {name: "the versions held", subject: "", given: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 1dcdb23..5313b13 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -100,6 +100,25 @@ type Entry struct { Template string `json:"template,omitempty"` } +// The values of Entry's storage-type. +const ( + EntryStorageTypeStream = "stream" + EntryStorageTypeKeyValue = "key-value" + EntryStorageTypeObjectStore = "object-store" + EntryStorageTypeLedger = "ledger" +) + +// The values of Entry's template. +const ( + EntryTemplateState = "state" + EntryTemplateEvents = "events" + EntryTemplateWorkQueue = "work-queue" + EntryTemplateLog = "log" + EntryTemplateObjects = "objects" + EntryTemplateLedger = "ledger" + EntryTemplate = "" +) + // Fill is a type of the contract. type Fill struct { // bytes held @@ -135,6 +154,35 @@ type Held struct { Why string `json:"why,omitempty"` } +// The values of Held's state. +const ( + HeldStateHeld = "held" + HeldStateRefused = "refused" + HeldStateWaiting = "waiting" + HeldStateDrifted = "drifted" + HeldStateFull = "full" + HeldStateOrphaned = "orphaned" +) + +// The values of Held's storage-type. +const ( + HeldStorageTypeStream = "stream" + HeldStorageTypeKeyValue = "key-value" + HeldStorageTypeObjectStore = "object-store" + HeldStorageTypeLedger = "ledger" +) + +// The values of Held's template. +const ( + HeldTemplateState = "state" + HeldTemplateEvents = "events" + HeldTemplateWorkQueue = "work-queue" + HeldTemplateLog = "log" + HeldTemplateObjects = "objects" + HeldTemplateLedger = "ledger" + HeldTemplate = "" +) + // Named is a type of the contract. type Named struct { // the entry's local name @@ -173,6 +221,25 @@ type Said struct { Why string `json:"why,omitempty"` } +// The values of Said's storage-type. +const ( + SaidStorageTypeStream = "stream" + SaidStorageTypeKeyValue = "key-value" + SaidStorageTypeObjectStore = "object-store" + SaidStorageTypeLedger = "ledger" +) + +// The values of Said's template. +const ( + SaidTemplateState = "state" + SaidTemplateEvents = "events" + SaidTemplateWorkQueue = "work-queue" + SaidTemplateLog = "log" + SaidTemplateObjects = "objects" + SaidTemplateLedger = "ledger" + SaidTemplate = "" +) + // Warranted is a type of the contract. type Warranted struct { // the entry's local name diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go index e5d5005..0ab1684 100644 --- a/go/seats/meshconfig/contract.go +++ b/go/seats/meshconfig/contract.go @@ -112,6 +112,12 @@ type RetiringHolderEvent struct { Since string `json:"since"` } +// The values of RetiringHolderEvent's kind. +const ( + RetiringHolderEventKindValue = "value" + RetiringHolderEventKindSecret = "secret" +) + // RetiredHolderEvent is the body of the holder's event retired. type RetiredHolderEvent struct { // the ask's id diff --git a/go/seats/meshvault/contract.go b/go/seats/meshvault/contract.go index 14bd5c1..df00f7c 100644 --- a/go/seats/meshvault/contract.go +++ b/go/seats/meshvault/contract.go @@ -295,6 +295,18 @@ type EnrolmentTokenInput struct { Tunnel *EnrolmentTokenInputTunnel `json:"tunnel,omitempty"` } +// The values of EnrolmentTokenInput's drop. +const ( + EnrolmentTokenInputDropTrue = "true" + EnrolmentTokenInputDropFalse = "false" +) + +// The values of EnrolmentTokenInput's sealed. +const ( + EnrolmentTokenInputSealedTrue = "true" + EnrolmentTokenInputSealedFalse = "false" +) + // EnrolmentTokenAnswer is what enrolment-token answers. type EnrolmentTokenAnswer struct { // the bus's address the machine dials @@ -323,6 +335,12 @@ type NodeAccountInput struct { SealingKey string `json:"sealing_key,omitempty"` } +// The values of NodeAccountInput's retire. +const ( + NodeAccountInputRetireTrue = "true" + NodeAccountInputRetireFalse = "false" +) + // NodeAccountAnswer is what node-account answers. type NodeAccountAnswer struct { // the account sealed to the node's sealing key, in base64 diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index 200524c..8e6a947 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -97,6 +97,14 @@ type Act struct { Why string `json:"why,omitempty"` } +// The values of Act's state. +const ( + ActStateAsked = "asked" + ActStateDone = "done" + ActStateRefused = "refused" + ActStateAnswered = "answered" +) + // ActSaid is what the seat says of an act: the act, the path or access, the caller as the runtime proved it and the ask (ADR 0563 §1). type ActSaid struct { // the act @@ -115,6 +123,13 @@ type ActSaid struct { Subject string `json:"subject,omitempty"` } +// The values of ActSaid's state. +const ( + ActSaidStateAsked = "asked" + ActSaidStateDone = "done" + ActSaidStateRefused = "refused" +) + // Acts is the acts kept. type Acts struct { // the acts, newest first @@ -147,6 +162,12 @@ type ManifestReadsChanged struct { Why string `json:"why"` } +// The values of ManifestReadsChanged's why. +const ( + ManifestReadsChangedWhyStarted = "started" + ManifestReadsChangedWhyReturned = "returned" +) + // Module is one module's composition with the candidate. type Module struct { // the candidate changes it from the one composed without it @@ -163,6 +184,13 @@ type Module struct { Words string `json:"words,omitempty"` } +// The values of Module's state. +const ( + ModuleStateComposed = "composed" + ModuleStateRefused = "refused" + ModuleStateHeld = "held" +) + // Revision is one revision of a composition key. type Revision struct { // its digest @@ -266,6 +294,12 @@ type PackagesInput struct { Write string `json:"write,omitempty"` } +// The values of PackagesInput's write. +const ( + PackagesInputWriteTrue = "true" + PackagesInputWriteFalse = "false" +) + // PackagesAnswerKeyPackagesItem is a type of the contract. type PackagesAnswerKeyPackagesItem struct { // declared absent @@ -492,6 +526,14 @@ type ComposeCandidateAnswer struct { Verdict string `json:"verdict"` } +// The values of ComposeCandidateAnswer's verdict. +const ( + ComposeCandidateAnswerVerdictComposed = "composed" + ComposeCandidateAnswerVerdictRefused = "refused" + ComposeCandidateAnswerVerdictUnready = "unready" + ComposeCandidateAnswerVerdictNotJudged = "not-judged" +) + // ManifestReadsInput is what manifest-reads takes. type ManifestReadsInput struct { } diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 79d2b05..1237a9e 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -54,7 +54,7 @@ type Artifact struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` @@ -62,6 +62,15 @@ type Artifact struct { Version string `json:"version,omitempty"` } +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + // Outcome is the body of published, refused and removed. type Outcome struct { // the source commit it was built from @@ -74,9 +83,9 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` - // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) Reason string `json:"reason,omitempty"` @@ -90,6 +99,15 @@ type Outcome struct { Version string `json:"version,omitempty"` } +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + // Part is one part of a transfer. type Part struct { // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob @@ -108,7 +126,7 @@ type PublishInput struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // every part's digest and size; none when the digest is already held Parts []Part `json:"parts,omitempty"` @@ -120,6 +138,15 @@ type PublishInput struct { Version string `json:"version,omitempty"` } +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + // PublishAnswer is what publish answers. type PublishAnswer struct { // the digest is held already: nothing to send; the outcome follows as an event @@ -186,6 +213,12 @@ type RemoveInput struct { Version string `json:"version,omitempty"` } +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` diff --git a/go/seats/npmpackageregistry/contract_test.go b/go/seats/npmpackageregistry/contract_test.go index 66b39b2..ed57d7f 100644 --- a/go/seats/npmpackageregistry/contract_test.go +++ b/go/seats/npmpackageregistry/contract_test.go @@ -17,21 +17,21 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", + {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", + {name: "the versions held", subject: "", given: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index 5746c78..a02d700 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -64,7 +64,7 @@ type Artifact struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` @@ -72,6 +72,15 @@ type Artifact struct { Version string `json:"version,omitempty"` } +// The values of Artifact's kind. +const ( + ArtifactKindPackage = "package" + ArtifactKindImage = "image" + ArtifactKindBundle = "bundle" + ArtifactKindArchive = "archive" + ArtifactKindUpstream = "upstream" +) + // Hold is the body of held and released. type Hold struct { // when, RFC 3339 @@ -100,9 +109,9 @@ type Outcome struct { Holder string `json:"holder"` // what it is Kind string `json:"kind"` - // the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact + // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) Reason string `json:"reason,omitempty"` @@ -116,6 +125,15 @@ type Outcome struct { Version string `json:"version,omitempty"` } +// The values of Outcome's kind. +const ( + OutcomeKindPackage = "package" + OutcomeKindImage = "image" + OutcomeKindBundle = "bundle" + OutcomeKindArchive = "archive" + OutcomeKindUpstream = "upstream" +) + // Part is one part of a transfer. type Part struct { // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob @@ -134,7 +152,7 @@ type PublishInput struct { Fingerprint string `json:"fingerprint,omitempty"` // what it is Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name + // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` // every part's digest and size; none when the digest is already held Parts []Part `json:"parts,omitempty"` @@ -146,6 +164,15 @@ type PublishInput struct { Version string `json:"version,omitempty"` } +// The values of PublishInput's kind. +const ( + PublishInputKindPackage = "package" + PublishInputKindImage = "image" + PublishInputKindBundle = "bundle" + PublishInputKindArchive = "archive" + PublishInputKindUpstream = "upstream" +) + // PublishAnswer is what publish answers. type PublishAnswer struct { // the digest is held already: nothing to send; the outcome follows as an event @@ -212,6 +239,12 @@ type RemoveInput struct { Version string `json:"version,omitempty"` } +// The values of RemoveInput's reason. +const ( + RemoveInputReasonTagGone = "tag-gone" + RemoveInputReasonRetention = "retention" +) + // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` @@ -249,7 +282,7 @@ type ReleaseInput struct { // ReleaseAnswer is what release answers. type ReleaseAnswer struct { - // the hold is gone + // a hold was let go; false when there was none Released bool `json:"released"` } diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go index f8e04e6..8c8cdd6 100644 --- a/go/seats/ociartifactregistry/contract_test.go +++ b/go/seats/ociartifactregistry/contract_test.go @@ -37,6 +37,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", + decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/operatorchannel/contract.go b/go/seats/operatorchannel/contract.go index eece285..bb087ee 100644 --- a/go/seats/operatorchannel/contract.go +++ b/go/seats/operatorchannel/contract.go @@ -210,6 +210,15 @@ type Warrant struct { Words string `json:"words,omitempty"` } +// The values of Warrant's outcome. +const ( + WarrantOutcomeChosen = "chosen" + WarrantOutcomeExpired = "expired" + WarrantOutcomeCancelled = "cancelled" + WarrantOutcomeReplaced = "replaced" + WarrantOutcomeRefused = "refused" +) + // OpenInput is what open takes. type OpenInput struct { } @@ -245,6 +254,12 @@ type NotifyInput struct { Summary string `json:"summary,omitempty"` } +// The values of NotifyInput's severity. +const ( + NotifyInputSeverityUrgent = "urgent" + NotifyInputSeverityWarning = "warning" +) + // NotifyAnswer is what notify answers. type NotifyAnswer = map[string]any @@ -304,5 +319,14 @@ type ClosedHolderEvent struct { ID string `json:"id"` } +// The values of ClosedHolderEvent's how. +const ( + ClosedHolderEventHowAnswered = "answered" + ClosedHolderEventHowExpired = "expired" + ClosedHolderEventHowCancelled = "cancelled" + ClosedHolderEventHowReplaced = "replaced" + ClosedHolderEventHowUnlinked = "unlinked" +) + // AsksRecord is one value of the record asks. type AsksRecord = map[string]any diff --git a/go/seats/seats.go b/go/seats/seats.go index 2eb7cbb..3464d0d 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -384,47 +384,47 @@ func List() []Seat { {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, }}, {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, }}, {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, @@ -434,30 +434,30 @@ func List() []Seat { {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, + {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, + Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, + {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "the hold is gone", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, {Name: Git, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0111", Delivers: "git"}, {Name: NodeBuildGo, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, diff --git a/parts/registry.json b/parts/registry.json index 0fcb16b..af1249c 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -12,7 +12,7 @@ "properties": { "name": { "type": "string", - "description": "the package's, image's, bundle's or archive's name" + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" }, "version": { "type": "string", @@ -275,7 +275,7 @@ "properties": { "name": { "type": "string", - "description": "the package's, image's, bundle's or archive's name" + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" }, "version": { "type": "string", @@ -339,11 +339,11 @@ "properties": { "name": { "type": "string", - "description": "the package's, image's, bundle's or archive's name" + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" }, "module": { "type": "string", - "description": "the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact" + "description": "the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact" }, "version": { "type": "string", diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json index 2c03cdc..2d94ee7 100644 --- a/seats/container-image-registry/fixtures/verbs.json +++ b/seats/container-image-registry/fixtures/verbs.json @@ -93,5 +93,13 @@ } ] } + }, + { + "name": "a release of what is not held answers released false", + "of": "verb:release.answer", + "why": "A3t: the outcome asked holds already, so it is no error", + "given": { + "released": false + } } ] diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json index 651ad69..8877a5b 100644 --- a/seats/container-image-registry/seat.json +++ b/seats/container-image-registry/seat.json @@ -153,7 +153,7 @@ }, { "name": "release", - "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", "input": { "type": "object", @@ -182,7 +182,7 @@ "properties": { "released": { "type": "boolean", - "description": "the hold is gone" + "description": "a hold was let go; false when there was none" } }, "required": [ diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json index 23d9779..37579ae 100644 --- a/seats/go-package-registry/fixtures/events.json +++ b/seats/go-package-registry/fixtures/events.json @@ -4,7 +4,7 @@ "of": "event:published", "subject": "mesh.seat.go-package-registry.event.published", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "module": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", @@ -13,7 +13,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19" + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19" } }, { @@ -21,7 +21,7 @@ "of": "event:refused", "subject": "mesh.seat.go-package-registry.event.refused", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -29,7 +29,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", "reason": "retention" } }, @@ -38,7 +38,7 @@ "of": "event:removed", "subject": "mesh.seat.go-package-registry.event.removed", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -46,7 +46,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", "reason": "retention" } } diff --git a/seats/go-package-registry/fixtures/verbs.json b/seats/go-package-registry/fixtures/verbs.json index 338bf36..716fe9f 100644 --- a/seats/go-package-registry/fixtures/verbs.json +++ b/seats/go-package-registry/fixtures/verbs.json @@ -3,7 +3,7 @@ "name": "publish opens a transfer", "of": "verb:publish.input", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -23,7 +23,7 @@ "of": "verb:publish.input", "why": "ADR 0608 §6: a reader ignores a field it does not know", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -41,7 +41,7 @@ } }, "wire": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -81,10 +81,10 @@ "name": "the versions held", "of": "verb:versions.answer", "given": { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "versions": [ { - "name": "mesh-sdk", + "name": "git.novox.be/novox/mesh-sdk/go", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/seats/npm-package-registry/fixtures/events.json b/seats/npm-package-registry/fixtures/events.json index f7cca82..0fe1c76 100644 --- a/seats/npm-package-registry/fixtures/events.json +++ b/seats/npm-package-registry/fixtures/events.json @@ -4,7 +4,7 @@ "of": "event:published", "subject": "mesh.seat.npm-package-registry.event.published", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -12,7 +12,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19" + "reference": "@novox/mesh-sdk@0.1.19" } }, { @@ -20,7 +20,7 @@ "of": "event:refused", "subject": "mesh.seat.npm-package-registry.event.refused", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -28,7 +28,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19", + "reference": "@novox/mesh-sdk@0.1.19", "reason": "retention" } }, @@ -37,7 +37,7 @@ "of": "event:removed", "subject": "mesh.seat.npm-package-registry.event.removed", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -45,7 +45,7 @@ "commit": "cccccccccccccccccccccccccccccccccccccccc", "holder": "gitea", "transfer": "t1", - "reference": "mesh-sdk@0.1.19", + "reference": "@novox/mesh-sdk@0.1.19", "reason": "retention" } } diff --git a/seats/npm-package-registry/fixtures/verbs.json b/seats/npm-package-registry/fixtures/verbs.json index 338bf36..529d486 100644 --- a/seats/npm-package-registry/fixtures/verbs.json +++ b/seats/npm-package-registry/fixtures/verbs.json @@ -3,7 +3,7 @@ "name": "publish opens a transfer", "of": "verb:publish.input", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -23,7 +23,7 @@ "of": "verb:publish.input", "why": "ADR 0608 §6: a reader ignores a field it does not know", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -41,7 +41,7 @@ } }, "wire": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -81,10 +81,10 @@ "name": "the versions held", "of": "verb:versions.answer", "given": { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "versions": [ { - "name": "mesh-sdk", + "name": "@novox/mesh-sdk", "version": "0.1.19", "kind": "package", "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json index 697b2ca..b571a88 100644 --- a/seats/oci-artifact-registry/fixtures/verbs.json +++ b/seats/oci-artifact-registry/fixtures/verbs.json @@ -93,5 +93,13 @@ } ] } + }, + { + "name": "a release of what is not held answers released false", + "of": "verb:release.answer", + "why": "A3t: the outcome asked holds already, so it is no error", + "given": { + "released": false + } } ] diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json index c826e5c..df5f58c 100644 --- a/seats/oci-artifact-registry/seat.json +++ b/seats/oci-artifact-registry/seat.json @@ -56,7 +56,7 @@ }, { "name": "release", - "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", "input": { "type": "object", @@ -85,7 +85,7 @@ "properties": { "released": { "type": "boolean", - "description": "the hold is gone" + "description": "a hold was let go; false when there was none" } }, "required": [ diff --git a/ts/seats/containerimageregistry/src/index.ts b/ts/seats/containerimageregistry/src/index.ts index ca92d41..b20e37e 100644 --- a/ts/seats/containerimageregistry/src/index.ts +++ b/ts/seats/containerimageregistry/src/index.ts @@ -137,7 +137,7 @@ export interface ReleaseInput { /** is what release answers. */ export interface ReleaseAnswer { - /** the hold is gone */ + /** a hold was let go; false when there was none */ "released": boolean; } diff --git a/ts/seats/containerimageregistry/src/parts/registry.ts b/ts/seats/containerimageregistry/src/parts/registry.ts index 9642c2d..544dc55 100644 --- a/ts/seats/containerimageregistry/src/parts/registry.ts +++ b/ts/seats/containerimageregistry/src/parts/registry.ts @@ -25,7 +25,7 @@ export interface Artifact { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** the tag it carries, for a tagged version */ "tag"?: string; @@ -45,9 +45,9 @@ export interface Outcome { "holder": string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ "module"?: string; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ "reason"?: string; @@ -79,7 +79,7 @@ export interface PublishInput { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** every part's digest and size; none when the digest is already held */ "parts"?: Part[]; diff --git a/ts/seats/gopackageregistry/src/parts/registry.ts b/ts/seats/gopackageregistry/src/parts/registry.ts index 9642c2d..544dc55 100644 --- a/ts/seats/gopackageregistry/src/parts/registry.ts +++ b/ts/seats/gopackageregistry/src/parts/registry.ts @@ -25,7 +25,7 @@ export interface Artifact { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** the tag it carries, for a tagged version */ "tag"?: string; @@ -45,9 +45,9 @@ export interface Outcome { "holder": string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ "module"?: string; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ "reason"?: string; @@ -79,7 +79,7 @@ export interface PublishInput { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** every part's digest and size; none when the digest is already held */ "parts"?: Part[]; diff --git a/ts/seats/npmpackageregistry/src/parts/registry.ts b/ts/seats/npmpackageregistry/src/parts/registry.ts index 9642c2d..544dc55 100644 --- a/ts/seats/npmpackageregistry/src/parts/registry.ts +++ b/ts/seats/npmpackageregistry/src/parts/registry.ts @@ -25,7 +25,7 @@ export interface Artifact { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** the tag it carries, for a tagged version */ "tag"?: string; @@ -45,9 +45,9 @@ export interface Outcome { "holder": string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ "module"?: string; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ "reason"?: string; @@ -79,7 +79,7 @@ export interface PublishInput { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** every part's digest and size; none when the digest is already held */ "parts"?: Part[]; diff --git a/ts/seats/ociartifactregistry/src/index.ts b/ts/seats/ociartifactregistry/src/index.ts index 974cccd..1467090 100644 --- a/ts/seats/ociartifactregistry/src/index.ts +++ b/ts/seats/ociartifactregistry/src/index.ts @@ -96,7 +96,7 @@ export interface ReleaseInput { /** is what release answers. */ export interface ReleaseAnswer { - /** the hold is gone */ + /** a hold was let go; false when there was none */ "released": boolean; } diff --git a/ts/seats/ociartifactregistry/src/parts/registry.ts b/ts/seats/ociartifactregistry/src/parts/registry.ts index 9642c2d..544dc55 100644 --- a/ts/seats/ociartifactregistry/src/parts/registry.ts +++ b/ts/seats/ociartifactregistry/src/parts/registry.ts @@ -25,7 +25,7 @@ export interface Artifact { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** the tag it carries, for a tagged version */ "tag"?: string; @@ -45,9 +45,9 @@ export interface Outcome { "holder": string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under (go-package-registry's published); empty for every other kind of artifact */ + /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ "module"?: string; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ "reason"?: string; @@ -79,7 +79,7 @@ export interface PublishInput { "fingerprint"?: string; /** what it is */ "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name */ + /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; /** every part's digest and size; none when the digest is already held */ "parts"?: Part[]; diff --git a/ts/src/seats.ts b/ts/src/seats.ts index 13ff8a5..c4f7292 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -3097,7 +3097,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "parts": { @@ -3286,7 +3286,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -3381,7 +3381,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -3453,7 +3453,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "parts": { @@ -3642,7 +3642,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -3737,7 +3737,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -3811,7 +3811,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "parts": { @@ -4000,7 +4000,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -4095,7 +4095,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -4264,7 +4264,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", "input": { "properties": { "digest": { @@ -4292,7 +4292,7 @@ export const list: readonly Seat[] = [ "output": { "properties": { "released": { - "description": "the hold is gone", + "description": "a hold was let go; false when there was none", "type": "boolean" } }, @@ -4346,7 +4346,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "parts": { @@ -4535,7 +4535,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -4630,7 +4630,7 @@ export const list: readonly Seat[] = [ "type": "string" }, "name": { - "description": "the package's, image's, bundle's or archive's name", + "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string" }, "tag": { @@ -4704,7 +4704,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Let a hold go; emits released. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", + "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", "input": { "properties": { "digest": { @@ -4732,7 +4732,7 @@ export const list: readonly Seat[] = [ "output": { "properties": { "released": { - "description": "the hold is gone", + "description": "a hold was let go; false when there was none", "type": "boolean" } }, -- 2.54.0 From 5b440ca8773cdca16581ded371834520482eed65 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 16:58:34 +0100 Subject: [PATCH 38/94] README: name where seatgen -check now lives The generator moved into mesh-catalog's Go build module; the line named no path, so a reader could not find what checks the tree. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5536728..4109509 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ implements its contract, and no holder's code lives here. Every file under `go/` and `ts/` outside `go/clients/` is written by the generator (seatgen, the source of the Go and Node build modules' generate step) and compared on every build: a hand-written file there fails, named (ADR 0625 §5). -`seatgen -check` also runs the client check (each client is for a seat defined whole, a module of its own, importing +`seatgen -check` (mesh-catalog `modules/mesh-build-go/buildstep/cmd/seatgen`) also runs the client check (each client is for a seat defined whole, a module of its own, importing only its seat's contract and mesh-sdk) and checks `module.json`'s Go and npm package artifacts against the tree. Made by the mesh from the gitea module's config value `repositories` (novox/hq ADR 0428). Main takes no push: a change reaches it by a pull request whose `mesh/check` passed: the checker's verdict on its head merged onto main, its tests run by their language's convention (novox/hq ADR 0447, ADR 0445). -- 2.54.0 From ec9529022c4460a2e5a944670ba0d30662319ebf Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 17:40:00 +0100 Subject: [PATCH 39/94] Pin the clients' requires: mesh-sdk 719956b7451a and this repository's seats at 5b440ca8773c, the group's heads (issue 1966) A client requires the seat it speaks and mesh-sdk; the seats and the seat list require nothing. The pins are the commits before this one, as a module cannot name the commit it is part of; the group's check builds against its members' heads. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../containerimageregistryclient/go.mod | 15 ++++++++++ .../containerimageregistryclient/go.sum | 18 +++++++++++ go/clients/gopackageregistryclient/go.mod | 15 ++++++++++ go/clients/gopackageregistryclient/go.sum | 18 +++++++++++ go/clients/nodeengineclient/go.mod | 7 +++++ go/clients/nodeengineclient/go.sum | 18 +++++++++++ go/clients/npmpackageregistryclient/go.mod | 15 ++++++++++ go/clients/npmpackageregistryclient/go.sum | 18 +++++++++++ go/clients/ociartifactregistryclient/go.mod | 15 ++++++++++ go/clients/ociartifactregistryclient/go.sum | 18 +++++++++++ go/clients/operatorchannelclient/go.mod | 15 ++++++++++ go/clients/operatorchannelclient/go.sum | 30 +++++++++++++++++++ 12 files changed, 202 insertions(+) create mode 100644 go/clients/containerimageregistryclient/go.sum create mode 100644 go/clients/gopackageregistryclient/go.sum create mode 100644 go/clients/nodeengineclient/go.sum create mode 100644 go/clients/npmpackageregistryclient/go.sum create mode 100644 go/clients/ociartifactregistryclient/go.sum create mode 100644 go/clients/operatorchannelclient/go.sum diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod index ea5bb60..f32cbd3 100644 --- a/go/clients/containerimageregistryclient/go.mod +++ b/go/clients/containerimageregistryclient/go.mod @@ -1,3 +1,18 @@ module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/containerimageregistryclient/go.sum b/go/clients/containerimageregistryclient/go.sum new file mode 100644 index 0000000..5b725ac --- /dev/null +++ b/go/clients/containerimageregistryclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c h1:uKavQC+Fv3QFCLIOBc4nBiaaIL/VWUeqpuGyKXWwKBI= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod index 7678fd3..c39b615 100644 --- a/go/clients/gopackageregistryclient/go.mod +++ b/go/clients/gopackageregistryclient/go.mod @@ -1,3 +1,18 @@ module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/gopackageregistryclient/go.sum b/go/clients/gopackageregistryclient/go.sum new file mode 100644 index 0000000..9e4e4c5 --- /dev/null +++ b/go/clients/gopackageregistryclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c h1:sYtHSBc7HHvCAd1XHYxmCXPQJrqyYZ9o1cMsQUaD3Js= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod index 2ccb4d7..9bed638 100644 --- a/go/clients/nodeengineclient/go.mod +++ b/go/clients/nodeengineclient/go.mod @@ -1,3 +1,10 @@ module git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect diff --git a/go/clients/nodeengineclient/go.sum b/go/clients/nodeengineclient/go.sum new file mode 100644 index 0000000..0546b69 --- /dev/null +++ b/go/clients/nodeengineclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c h1:TaZUybyjez6gsmDVZ0yP3d2UMU6IuxMJFnF/jdavH4Y= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod index 94d8c30..2fd62e7 100644 --- a/go/clients/npmpackageregistryclient/go.mod +++ b/go/clients/npmpackageregistryclient/go.mod @@ -1,3 +1,18 @@ module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/npmpackageregistryclient/go.sum b/go/clients/npmpackageregistryclient/go.sum new file mode 100644 index 0000000..e0a2c5c --- /dev/null +++ b/go/clients/npmpackageregistryclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c h1:fi3TxT9GISaa97mkc+NoN26scOGsETFrqkj7qBDTJ+s= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod index a37d128..ba28eee 100644 --- a/go/clients/ociartifactregistryclient/go.mod +++ b/go/clients/ociartifactregistryclient/go.mod @@ -1,3 +1,18 @@ module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/ociartifactregistryclient/go.sum b/go/clients/ociartifactregistryclient/go.sum new file mode 100644 index 0000000..cc82228 --- /dev/null +++ b/go/clients/ociartifactregistryclient/go.sum @@ -0,0 +1,18 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c h1:6J1fY4kz8xD/TI+KG87RnPdv0nOB+iS0QP3ixwd56rU= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/operatorchannelclient/go.mod b/go/clients/operatorchannelclient/go.mod index d7217d2..3f779bf 100644 --- a/go/clients/operatorchannelclient/go.mod +++ b/go/clients/operatorchannelclient/go.mod @@ -1,3 +1,18 @@ module git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a +) + +require ( + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/operatorchannelclient/go.sum b/go/clients/operatorchannelclient/go.sum new file mode 100644 index 0000000..a726be7 --- /dev/null +++ b/go/clients/operatorchannelclient/go.sum @@ -0,0 +1,30 @@ +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c h1:4QeMhlpHEWSK3R2PtcZsVf6B0c7ylkWcHHPe9FB6xRI= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= +github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= +github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= -- 2.54.0 From 859da7b1ceb153e49d4a26d9349c7eeae7614e07 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 18:12:34 +0100 Subject: [PATCH 40/94] Registry seats: remove answers and says kept when a tagged version shares its digest A store that cannot remove a tag alone (another tag names its digest) kept the tag and the holder said removed, so the builder and every follower believed the tagged version gone while it stayed pullable: work reported done that was not done. The shared registry part gains the outcome kept: in remove's answer (kept, the reason shared-digest, the tags still naming the digest) and as its own event, in all four registry seats. Regenerated with mesh-catalog's seatgen (join-mesh-catalog ccb7eaff); seatgen -check passes; fixtures for the answer and the event in each seat. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/containerimageregistry/contract.go | 24 ++++- .../containerimageregistry/contract_test.go | 4 + go/seats/gopackageregistry/contract.go | 24 ++++- go/seats/gopackageregistry/contract_test.go | 4 + go/seats/npmpackageregistry/contract.go | 24 ++++- go/seats/npmpackageregistry/contract_test.go | 4 + go/seats/ociartifactregistry/contract.go | 24 ++++- go/seats/ociartifactregistry/contract_test.go | 4 + go/seats/seats.go | 24 ++--- parts/registry.json | 35 +++++++- .../fixtures/events.json | 20 +++++ .../fixtures/verbs.json | 13 +++ .../go-package-registry/fixtures/events.json | 20 +++++ seats/go-package-registry/fixtures/verbs.json | 13 +++ .../npm-package-registry/fixtures/events.json | 20 +++++ .../npm-package-registry/fixtures/verbs.json | 13 +++ .../fixtures/events.json | 20 +++++ .../oci-artifact-registry/fixtures/verbs.json | 13 +++ ts/seats/containerimageregistry/src/index.ts | 4 + .../src/parts/registry.ts | 14 ++- ts/seats/gopackageregistry/src/index.ts | 4 + .../gopackageregistry/src/parts/registry.ts | 14 ++- ts/seats/npmpackageregistry/src/index.ts | 4 + .../npmpackageregistry/src/parts/registry.ts | 14 ++- ts/seats/ociartifactregistry/src/index.ts | 4 + .../ociartifactregistry/src/parts/registry.ts | 14 ++- ts/src/seats.ts | 88 +++++++++++++++++-- 27 files changed, 431 insertions(+), 32 deletions(-) diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index ef6d3f2..ac06122 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -21,6 +21,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" EventHeld = "held" EventReleased = "released" ) @@ -32,7 +33,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -50,6 +51,9 @@ const SubjectRefused = "mesh.seat.container-image-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.container-image-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.container-image-registry.event.kept" + // SubjectHeld is where held is published. const SubjectHeld = "mesh.seat.container-image-registry.event.held" @@ -115,12 +119,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -250,10 +256,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // MirrorInput is what mirror takes. type MirrorInput struct { // its pinned digest, sha256: @@ -337,6 +354,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go index 6b5e2c0..3ad30c7 100644 --- a/go/seats/containerimageregistry/contract_test.go +++ b/go/seats/containerimageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.container-image-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.container-image-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.kept$")}, {name: "held on the seat's subject", subject: "mesh.seat.container-image-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.held$")}, {name: "released on the seat's subject", subject: "mesh.seat.container-image-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", @@ -39,6 +41,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go index 54ea7a4..eb53461 100644 --- a/go/seats/gopackageregistry/contract.go +++ b/go/seats/gopackageregistry/contract.go @@ -17,6 +17,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -26,7 +27,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved} + return []string{EventPublished, EventRefused, EventRemoved, EventKept} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -44,6 +45,9 @@ const SubjectRefused = "mesh.seat.go-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.go-package-registry.event.kept" + // Artifact is one published thing, as every registry event and answer names it. type Artifact struct { // the source commit it was built from @@ -87,12 +91,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -222,10 +228,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -235,6 +252,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // GoPackageRegistryForm is the form of the provision go-package-registry. type GoPackageRegistryForm struct { // the read side's address diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go index 8f127f0..e5f69ec 100644 --- a/go/seats/gopackageregistry/contract_test.go +++ b/go/seats/gopackageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.go-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.kept$")}, {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", @@ -33,6 +35,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go index 1237a9e..5f39cb8 100644 --- a/go/seats/npmpackageregistry/contract.go +++ b/go/seats/npmpackageregistry/contract.go @@ -17,6 +17,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -26,7 +27,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved} + return []string{EventPublished, EventRefused, EventRemoved, EventKept} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -44,6 +45,9 @@ const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.npm-package-registry.event.kept" + // Artifact is one published thing, as every registry event and answer names it. type Artifact struct { // the source commit it was built from @@ -87,12 +91,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -222,10 +228,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -235,6 +252,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // NpmPackageRegistryForm is the form of the provision npm-package-registry. type NpmPackageRegistryForm struct { // the read side's address diff --git a/go/seats/npmpackageregistry/contract_test.go b/go/seats/npmpackageregistry/contract_test.go index ed57d7f..3092b37 100644 --- a/go/seats/npmpackageregistry/contract_test.go +++ b/go/seats/npmpackageregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.npm-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.kept$")}, {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", @@ -33,6 +35,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "the versions held", subject: "", given: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index a02d700..39bba8b 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -19,6 +19,7 @@ const ( EventPublished = "published" EventRefused = "refused" EventRemoved = "removed" + EventKept = "kept" EventHeld = "held" EventReleased = "released" ) @@ -30,7 +31,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventHeld, EventReleased} + return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -48,6 +49,9 @@ const SubjectRefused = "mesh.seat.oci-artifact-registry.event.refused" // SubjectRemoved is where removed is published. const SubjectRemoved = "mesh.seat.oci-artifact-registry.event.removed" +// SubjectKept is where kept is published. +const SubjectKept = "mesh.seat.oci-artifact-registry.event.kept" + // SubjectHeld is where held is published. const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" @@ -113,12 +117,14 @@ type Outcome struct { Module string `json:"module,omitempty"` // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) Name string `json:"name"` - // why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) + // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) Reason string `json:"reason,omitempty"` // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) Reference string `json:"reference,omitempty"` // the tag it carries, for a tagged version Tag string `json:"tag,omitempty"` + // for kept: the other tags still naming its digest + Tags []string `json:"tags,omitempty"` // the transfer it ends, when it ended one Transfer string `json:"transfer,omitempty"` // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone @@ -248,10 +254,21 @@ const ( // RemoveAnswer is what remove answers. type RemoveAnswer struct { Artifact *Artifact `json:"artifact,omitempty"` + // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) + Kept bool `json:"kept,omitempty"` + // why it was kept: shared-digest, another tag names its digest + Reason string `json:"reason,omitempty"` // it is gone Removed bool `json:"removed"` + // the other tags still naming its digest, when kept + Tags []string `json:"tags,omitempty"` } +// The values of RemoveAnswer's reason. +const ( + RemoveAnswerReasonSharedDigest = "shared-digest" +) + // HoldInput is what hold takes. type HoldInput struct { // the digest @@ -295,6 +312,9 @@ type RefusedEvent = Outcome // RemovedEvent is the body of the event removed. type RemovedEvent = Outcome +// KeptEvent is the body of the event kept. +type KeptEvent = Outcome + // HeldEvent is the body of the event held. type HeldEvent = Hold diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go index 8c8cdd6..9a2f3a2 100644 --- a/go/seats/ociartifactregistry/contract_test.go +++ b/go/seats/ociartifactregistry/contract_test.go @@ -23,6 +23,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.refused$")}, {name: "removed on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.removed$")}, + {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.oci-artifact-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", + decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.kept$")}, {name: "held on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.held$")}, {name: "released on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", @@ -39,6 +41,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", + decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/seats.go b/go/seats/seats.go index 3464d0d..125f4fe 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -381,7 +381,7 @@ func List() []Seat { {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: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the machine's name", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, }}, - {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed"}, + {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -392,11 +392,11 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, }}, - {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed"}, + {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -407,11 +407,11 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, }}, - {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -422,9 +422,9 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, @@ -438,7 +438,7 @@ func List() []Seat { Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, }}, - {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "held", "released"}, + {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -449,9 +449,9 @@ func List() []Seat { {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", Optional: true, + {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}}, "required": []string{"removed"}, "type": "object"}}, + Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, diff --git a/parts/registry.json b/parts/registry.json index af1249c..ca0022f 100644 --- a/parts/registry.json +++ b/parts/registry.json @@ -196,7 +196,7 @@ }, { "name": "remove", - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "callers": "the builder, for a tagged version; the artifact sweep's holder, for retention", "input": { "type": "object", @@ -238,6 +238,24 @@ "type": "boolean", "description": "it is gone" }, + "kept": { + "type": "boolean", + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)" + }, + "reason": { + "type": "string", + "enum": [ + "shared-digest" + ], + "description": "why it was kept: shared-digest, another tag names its digest" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the other tags still naming its digest, when kept" + }, "artifact": { "$ref": "#/defs/Artifact" } @@ -267,6 +285,12 @@ "body": { "$ref": "#/defs/Outcome" } + }, + { + "name": "kept", + "body": { + "$ref": "#/defs/Outcome" + } } ], "defs": { @@ -390,7 +414,14 @@ }, "reason": { "type": "string", - "description": "why: a refusal's cause, or a removal's (retention, tag-gone, by-hand)" + "description": "why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest)" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for kept: the other tags still naming its digest" } }, "required": [ diff --git a/seats/container-image-registry/fixtures/events.json b/seats/container-image-registry/fixtures/events.json index 8bd60c9..65e98e5 100644 --- a/seats/container-image-registry/fixtures/events.json +++ b/seats/container-image-registry/fixtures/events.json @@ -49,6 +49,26 @@ "reason": "retention" } }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.container-image-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "image", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, { "name": "held on the seat's subject", "of": "event:held", diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json index 2d94ee7..6e50d36 100644 --- a/seats/container-image-registry/fixtures/verbs.json +++ b/seats/container-image-registry/fixtures/verbs.json @@ -101,5 +101,18 @@ "given": { "released": false } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json index 37579ae..c3a8b16 100644 --- a/seats/go-package-registry/fixtures/events.json +++ b/seats/go-package-registry/fixtures/events.json @@ -49,5 +49,25 @@ "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", "reason": "retention" } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.go-package-registry.event.kept", + "given": { + "name": "git.novox.be/novox/mesh-sdk/go", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/go-package-registry/fixtures/verbs.json b/seats/go-package-registry/fixtures/verbs.json index 716fe9f..be6a53a 100644 --- a/seats/go-package-registry/fixtures/verbs.json +++ b/seats/go-package-registry/fixtures/verbs.json @@ -93,5 +93,18 @@ } ] } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/npm-package-registry/fixtures/events.json b/seats/npm-package-registry/fixtures/events.json index 0fe1c76..4cadc30 100644 --- a/seats/npm-package-registry/fixtures/events.json +++ b/seats/npm-package-registry/fixtures/events.json @@ -48,5 +48,25 @@ "reference": "@novox/mesh-sdk@0.1.19", "reason": "retention" } + }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.npm-package-registry.event.kept", + "given": { + "name": "@novox/mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "package", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "gitea", + "reference": "@novox/mesh-sdk@0.1.19", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/npm-package-registry/fixtures/verbs.json b/seats/npm-package-registry/fixtures/verbs.json index 529d486..0b88f08 100644 --- a/seats/npm-package-registry/fixtures/verbs.json +++ b/seats/npm-package-registry/fixtures/verbs.json @@ -93,5 +93,18 @@ } ] } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/seats/oci-artifact-registry/fixtures/events.json b/seats/oci-artifact-registry/fixtures/events.json index 0928933..e0a8cde 100644 --- a/seats/oci-artifact-registry/fixtures/events.json +++ b/seats/oci-artifact-registry/fixtures/events.json @@ -49,6 +49,26 @@ "reason": "retention" } }, + { + "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", + "of": "event:kept", + "subject": "mesh.seat.oci-artifact-registry.event.kept", + "given": { + "name": "mesh-sdk", + "version": "0.1.19-feat-x.2", + "kind": "bundle", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "fingerprint": "f1", + "commit": "cccccccccccccccccccccccccccccccccccccccc", + "holder": "distribution", + "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "reason": "shared-digest", + "tag": "0.1.19-feat-x.2", + "tags": [ + "0.1.19" + ] + } + }, { "name": "held on the seat's subject", "of": "event:held", diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json index b571a88..e0121b7 100644 --- a/seats/oci-artifact-registry/fixtures/verbs.json +++ b/seats/oci-artifact-registry/fixtures/verbs.json @@ -101,5 +101,18 @@ "given": { "released": false } + }, + { + "name": "a tagged version sharing its digest with another tag is kept, not removed", + "of": "verb:remove.answer", + "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", + "given": { + "removed": false, + "kept": true, + "reason": "shared-digest", + "tags": [ + "0.1.19" + ] + } } ] diff --git a/ts/seats/containerimageregistry/src/index.ts b/ts/seats/containerimageregistry/src/index.ts index b20e37e..9baf704 100644 --- a/ts/seats/containerimageregistry/src/index.ts +++ b/ts/seats/containerimageregistry/src/index.ts @@ -21,6 +21,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", held: "held", released: "released", } as const; @@ -33,11 +34,14 @@ export const refusedSubject = "mesh.seat.container-image-registry.event.refused" export const removedSubject = "mesh.seat.container-image-registry.event.removed"; +export const keptSubject = "mesh.seat.container-image-registry.event.kept"; + export const heldSubject = "mesh.seat.container-image-registry.event.held"; export const releasedSubject = "mesh.seat.container-image-registry.event.released"; export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/containerimageregistry/src/parts/registry.ts b/ts/seats/containerimageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/containerimageregistry/src/parts/registry.ts +++ b/ts/seats/containerimageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/gopackageregistry/src/index.ts b/ts/seats/gopackageregistry/src/index.ts index 834ecb3..f072090 100644 --- a/ts/seats/gopackageregistry/src/index.ts +++ b/ts/seats/gopackageregistry/src/index.ts @@ -17,6 +17,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; export const family = { name: "go-package-registry.events", maxAgeDays: 7 } as const; @@ -27,7 +28,10 @@ export const refusedSubject = "mesh.seat.go-package-registry.event.refused"; export const removedSubject = "mesh.seat.go-package-registry.event.removed"; +export const keptSubject = "mesh.seat.go-package-registry.event.kept"; + export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/gopackageregistry/src/parts/registry.ts b/ts/seats/gopackageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/gopackageregistry/src/parts/registry.ts +++ b/ts/seats/gopackageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/npmpackageregistry/src/index.ts b/ts/seats/npmpackageregistry/src/index.ts index 0834e45..a704e8b 100644 --- a/ts/seats/npmpackageregistry/src/index.ts +++ b/ts/seats/npmpackageregistry/src/index.ts @@ -17,6 +17,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; export const family = { name: "npm-package-registry.events", maxAgeDays: 7 } as const; @@ -27,7 +28,10 @@ export const refusedSubject = "mesh.seat.npm-package-registry.event.refused"; export const removedSubject = "mesh.seat.npm-package-registry.event.removed"; +export const keptSubject = "mesh.seat.npm-package-registry.event.kept"; + export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/npmpackageregistry/src/parts/registry.ts b/ts/seats/npmpackageregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/npmpackageregistry/src/parts/registry.ts +++ b/ts/seats/npmpackageregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/seats/ociartifactregistry/src/index.ts b/ts/seats/ociartifactregistry/src/index.ts index 1467090..c1fa55e 100644 --- a/ts/seats/ociartifactregistry/src/index.ts +++ b/ts/seats/ociartifactregistry/src/index.ts @@ -19,6 +19,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", held: "held", released: "released", } as const; @@ -31,11 +32,14 @@ export const refusedSubject = "mesh.seat.oci-artifact-registry.event.refused"; export const removedSubject = "mesh.seat.oci-artifact-registry.event.removed"; +export const keptSubject = "mesh.seat.oci-artifact-registry.event.kept"; + export const heldSubject = "mesh.seat.oci-artifact-registry.event.held"; export const releasedSubject = "mesh.seat.oci-artifact-registry.event.released"; export type Artifact = registry.Artifact; +export type KeptEvent = registry.KeptEvent; export type Outcome = registry.Outcome; export type Part = registry.Part; export type PublishAnswer = registry.PublishAnswer; diff --git a/ts/seats/ociartifactregistry/src/parts/registry.ts b/ts/seats/ociartifactregistry/src/parts/registry.ts index 544dc55..17a8de4 100644 --- a/ts/seats/ociartifactregistry/src/parts/registry.ts +++ b/ts/seats/ociartifactregistry/src/parts/registry.ts @@ -13,6 +13,7 @@ export const events = { published: "published", refused: "refused", removed: "removed", + kept: "kept", } as const; /** one published thing, as every registry event and answer names it */ @@ -49,12 +50,14 @@ export interface Outcome { "module"?: string; /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ "name": string; - /** why: a refusal's cause, or a removal's (retention, tag-gone, by-hand) */ + /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ "reason"?: string; /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ "reference"?: string; /** the tag it carries, for a tagged version */ "tag"?: string; + /** for kept: the other tags still naming its digest */ + "tags"?: string[]; /** the transfer it ends, when it ended one */ "transfer"?: string; /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ @@ -160,8 +163,14 @@ export interface RemoveInput { /** is what remove answers. */ export interface RemoveAnswer { "artifact"?: Artifact; + /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ + "kept"?: boolean; + /** why it was kept: shared-digest, another tag names its digest */ + "reason"?: "shared-digest"; /** it is gone */ "removed": boolean; + /** the other tags still naming its digest, when kept */ + "tags"?: string[]; } /** is the body of the event published. */ @@ -173,3 +182,6 @@ export type RefusedEvent = Outcome; /** is the body of the event removed. */ export type RemovedEvent = Outcome; +/** is the body of the event kept. */ +export type KeptEvent = Outcome; + diff --git a/ts/src/seats.ts b/ts/src/seats.ts index c4f7292..efe9c39 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -3063,7 +3063,8 @@ export const list: readonly Seat[] = [ "emits": [ "published", "refused", - "removed" + "removed", + "kept" ], "grain": "whole", "name": "npm-package-registry", @@ -3316,7 +3317,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -3400,9 +3401,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -3419,7 +3438,8 @@ export const list: readonly Seat[] = [ "emits": [ "published", "refused", - "removed" + "removed", + "kept" ], "grain": "whole", "name": "go-package-registry", @@ -3672,7 +3692,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -3756,9 +3776,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -3776,6 +3814,7 @@ export const list: readonly Seat[] = [ "published", "refused", "removed", + "kept", "held", "released" ], @@ -4030,7 +4069,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -4114,9 +4153,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -4311,6 +4368,7 @@ export const list: readonly Seat[] = [ "published", "refused", "removed", + "kept", "held", "released" ], @@ -4565,7 +4623,7 @@ export const list: readonly Seat[] = [ } }, { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason.", + "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", "input": { "properties": { "digest": { @@ -4649,9 +4707,27 @@ export const list: readonly Seat[] = [ ], "type": "object" }, + "kept": { + "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", + "type": "boolean" + }, + "reason": { + "description": "why it was kept: shared-digest, another tag names its digest", + "enum": [ + "shared-digest" + ], + "type": "string" + }, "removed": { "description": "it is gone", "type": "boolean" + }, + "tags": { + "description": "the other tags still naming its digest, when kept", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ -- 2.54.0 From 347a3053580a6d48d34e986e0857773908005b63 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 18:22:18 +0100 Subject: [PATCH 41/94] Pin the clients' requires again: mesh-sdk 3526e9ef2a42 and this repository's seats at 859da7b1ceb1, the group's heads after review round 2 (issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/clients/containerimageregistryclient/go.mod | 6 +++--- go/clients/containerimageregistryclient/go.sum | 12 ++++++------ go/clients/gopackageregistryclient/go.mod | 6 +++--- go/clients/gopackageregistryclient/go.sum | 12 ++++++------ go/clients/nodeengineclient/go.mod | 6 +++--- go/clients/nodeengineclient/go.sum | 12 ++++++------ go/clients/npmpackageregistryclient/go.mod | 6 +++--- go/clients/npmpackageregistryclient/go.sum | 12 ++++++------ go/clients/ociartifactregistryclient/go.mod | 6 +++--- go/clients/ociartifactregistryclient/go.sum | 12 ++++++------ go/clients/operatorchannelclient/go.mod | 6 +++--- go/clients/operatorchannelclient/go.sum | 12 ++++++------ 12 files changed, 54 insertions(+), 54 deletions(-) diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod index f32cbd3..9087a58 100644 --- a/go/clients/containerimageregistryclient/go.mod +++ b/go/clients/containerimageregistryclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/containerimageregistryclient/go.sum b/go/clients/containerimageregistryclient/go.sum index 5b725ac..87ae239 100644 --- a/go/clients/containerimageregistryclient/go.sum +++ b/go/clients/containerimageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c h1:uKavQC+Fv3QFCLIOBc4nBiaaIL/VWUeqpuGyKXWwKBI= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:cbXPHz2qRlQe/7J4zjRridcXo8CVc5UHlmc4Cwvhks4= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod index c39b615..270cb93 100644 --- a/go/clients/gopackageregistryclient/go.mod +++ b/go/clients/gopackageregistryclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/gopackageregistryclient/go.sum b/go/clients/gopackageregistryclient/go.sum index 9e4e4c5..7779e1b 100644 --- a/go/clients/gopackageregistryclient/go.sum +++ b/go/clients/gopackageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c h1:sYtHSBc7HHvCAd1XHYxmCXPQJrqyYZ9o1cMsQUaD3Js= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:ADOs9m0dNqEyvXepF8uA8rcb9KjOvEiMLMvN+sB8RSQ= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod index 9bed638..0ac068f 100644 --- a/go/clients/nodeengineclient/go.mod +++ b/go/clients/nodeengineclient/go.mod @@ -3,8 +3,8 @@ module git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) -require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect +require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect diff --git a/go/clients/nodeengineclient/go.sum b/go/clients/nodeengineclient/go.sum index 0546b69..461fe54 100644 --- a/go/clients/nodeengineclient/go.sum +++ b/go/clients/nodeengineclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c h1:TaZUybyjez6gsmDVZ0yP3d2UMU6IuxMJFnF/jdavH4Y= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104155834-5b440ca8773c/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1 h1:1g+A4Bzt4pNe/jcZoY1j0EaGqHI7dYSDOBJIEDnAirw= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod index 2fd62e7..c80d589 100644 --- a/go/clients/npmpackageregistryclient/go.mod +++ b/go/clients/npmpackageregistryclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/npmpackageregistryclient/go.sum b/go/clients/npmpackageregistryclient/go.sum index e0a2c5c..24c3a8b 100644 --- a/go/clients/npmpackageregistryclient/go.sum +++ b/go/clients/npmpackageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c h1:fi3TxT9GISaa97mkc+NoN26scOGsETFrqkj7qBDTJ+s= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:0z4H18dM7KuTOwuGmUTSdoig+N6T6MkckOUYuWCwYjE= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod index ba28eee..257fb3d 100644 --- a/go/clients/ociartifactregistryclient/go.mod +++ b/go/clients/ociartifactregistryclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/ociartifactregistryclient/go.sum b/go/clients/ociartifactregistryclient/go.sum index cc82228..c7b36ac 100644 --- a/go/clients/ociartifactregistryclient/go.sum +++ b/go/clients/ociartifactregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c h1:6J1fY4kz8xD/TI+KG87RnPdv0nOB+iS0QP3ixwd56rU= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104155834-5b440ca8773c/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1 h1:nEolvX9Lpmj99boSWB2gtvRSEjEGfGvQu67ycdYnnSY= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/operatorchannelclient/go.mod b/go/clients/operatorchannelclient/go.mod index 3f779bf..c312107 100644 --- a/go/clients/operatorchannelclient/go.mod +++ b/go/clients/operatorchannelclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/operatorchannelclient/go.sum b/go/clients/operatorchannelclient/go.sum index a726be7..2bd3d5e 100644 --- a/go/clients/operatorchannelclient/go.sum +++ b/go/clients/operatorchannelclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c h1:Kx2rM0yJ7bop5KmU4nV8WujEY3PK5vquw2dwFTvYKN4= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104155834-5b440ca8773c/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c h1:4QeMhlpHEWSK3R2PtcZsVf6B0c7ylkWcHHPe9FB6xRI= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104155834-5b440ca8773c/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a h1:cKQJ9VN+HW0eUIOOt8jJ09A1FqzMpAsNePCQpC54qzM= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104162455-719956b7451a/go.mod h1:oPDnrS9TLTJqIili/WD0MjmwkVCAhm2NLHeJ+plfQik= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1 h1:Hm3ew22XTxggG8QzMh9/YUvRzTPlcNAqoUr7AjL+xa0= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= -- 2.54.0 From ae5266e776395330e2339a0972da8ce9fd5131a7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 18:43:44 +0100 Subject: [PATCH 42/94] Pin the clients' requires: mesh-sdk f94abb95 and this repository's seats at 347a3053, the group's final heads (issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/clients/containerimageregistryclient/go.mod | 5 ++--- go/clients/containerimageregistryclient/go.sum | 12 ++++++------ go/clients/gopackageregistryclient/go.mod | 5 ++--- go/clients/gopackageregistryclient/go.sum | 12 ++++++------ go/clients/nodeengineclient/go.mod | 6 +++--- go/clients/nodeengineclient/go.sum | 12 ++++++------ go/clients/npmpackageregistryclient/go.mod | 5 ++--- go/clients/npmpackageregistryclient/go.sum | 12 ++++++------ go/clients/ociartifactregistryclient/go.mod | 5 ++--- go/clients/ociartifactregistryclient/go.sum | 12 ++++++------ go/clients/operatorchannelclient/go.mod | 6 +++--- go/clients/operatorchannelclient/go.sum | 12 ++++++------ 12 files changed, 50 insertions(+), 54 deletions(-) diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod index 9087a58..6e28d16 100644 --- a/go/clients/containerimageregistryclient/go.mod +++ b/go/clients/containerimageregistryclient/go.mod @@ -3,12 +3,11 @@ module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/containerimageregistryclient/go.sum b/go/clients/containerimageregistryclient/go.sum index 87ae239..a93fc2b 100644 --- a/go/clients/containerimageregistryclient/go.sum +++ b/go/clients/containerimageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:cbXPHz2qRlQe/7J4zjRridcXo8CVc5UHlmc4Cwvhks4= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA= +git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod index 270cb93..ab2eab4 100644 --- a/go/clients/gopackageregistryclient/go.mod +++ b/go/clients/gopackageregistryclient/go.mod @@ -3,12 +3,11 @@ module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/gopackageregistryclient/go.sum b/go/clients/gopackageregistryclient/go.sum index 7779e1b..0234cfd 100644 --- a/go/clients/gopackageregistryclient/go.sum +++ b/go/clients/gopackageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:ADOs9m0dNqEyvXepF8uA8rcb9KjOvEiMLMvN+sB8RSQ= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a h1:Zl6CzMg9AZtk7RU/DSJxSGGoUg5fSW3t7Yf8zyfXbVY= +git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod index 0ac068f..cae4c9e 100644 --- a/go/clients/nodeengineclient/go.mod +++ b/go/clients/nodeengineclient/go.mod @@ -3,8 +3,8 @@ module git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) -require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect +require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a // indirect diff --git a/go/clients/nodeengineclient/go.sum b/go/clients/nodeengineclient/go.sum index 461fe54..5c5298e 100644 --- a/go/clients/nodeengineclient/go.sum +++ b/go/clients/nodeengineclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1 h1:1g+A4Bzt4pNe/jcZoY1j0EaGqHI7dYSDOBJIEDnAirw= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a h1:ZdYc+f3A8l2oDjN+LMngI8grXvT01Ud75/gPIAnuTko= +git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod index c80d589..7a07bb9 100644 --- a/go/clients/npmpackageregistryclient/go.mod +++ b/go/clients/npmpackageregistryclient/go.mod @@ -3,12 +3,11 @@ module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/npmpackageregistryclient/go.sum b/go/clients/npmpackageregistryclient/go.sum index 24c3a8b..aeb3cba 100644 --- a/go/clients/npmpackageregistryclient/go.sum +++ b/go/clients/npmpackageregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1 h1:0z4H18dM7KuTOwuGmUTSdoig+N6T6MkckOUYuWCwYjE= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a h1:JdEqNAv+s0ut6JEcCtXN5RIoGpuYg0jIVteFZSJJ36s= +git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod index 257fb3d..c3f364d 100644 --- a/go/clients/ociartifactregistryclient/go.mod +++ b/go/clients/ociartifactregistryclient/go.mod @@ -3,12 +3,11 @@ module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/ociartifactregistryclient/go.sum b/go/clients/ociartifactregistryclient/go.sum index c7b36ac..737e62d 100644 --- a/go/clients/ociartifactregistryclient/go.sum +++ b/go/clients/ociartifactregistryclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1 h1:nEolvX9Lpmj99boSWB2gtvRSEjEGfGvQu67ycdYnnSY= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a h1:fhQ+q40fnPwuBBU+7Bpv13ktzoPxHtQiO6mpHR9oVoo= +git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/go/clients/operatorchannelclient/go.mod b/go/clients/operatorchannelclient/go.mod index c312107..375590f 100644 --- a/go/clients/operatorchannelclient/go.mod +++ b/go/clients/operatorchannelclient/go.mod @@ -3,12 +3,12 @@ module git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1 - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 + git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 // indirect + git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/operatorchannelclient/go.sum b/go/clients/operatorchannelclient/go.sum index 2bd3d5e..e8e153a 100644 --- a/go/clients/operatorchannelclient/go.sum +++ b/go/clients/operatorchannelclient/go.sum @@ -1,9 +1,9 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1 h1:QlHIjyY5gPAiN1zsNdWHtt+Lo020AdWKOcXgri5sXhw= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1 h1:Hm3ew22XTxggG8QzMh9/YUvRzTPlcNAqoUr7AjL+xa0= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104171234-859da7b1ceb1/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42 h1:v745Zcs+BCQB9LihMEPIR+KJLyPOhaidk4/KgLNwleE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104171712-3526e9ef2a42/go.mod h1:RGdgpJp9b7aZJABMNVSFrmQdm9AAco4pmmjiKduyLK0= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= +git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a h1:UzJfgbyGlZJzsOFevu/c7stv1ccejrS4nAMfYytSd8s= +git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= -- 2.54.0 From 4d7b2a3778787d9f04739d0c5da5b42c40c8d254 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 20:52:15 +0100 Subject: [PATCH 43/94] =?UTF-8?q?Registry=20seats:=20no=20seat=20delivers?= =?UTF-8?q?=20artifact-store,=20and=20the=20two=20store=20seats=20serve=20?= =?UTF-8?q?list=20(ADR=200632=20=C2=A72,=20=C2=A73;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The store's address is the registry seat holder's listen, so the provision artifact-store retires; the sweep and the read tools read the store through list, never by its files or its read side. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/seats/containerimageregistry/contract.go | 47 ++++- .../containerimageregistry/contract_test.go | 2 + go/seats/ociartifactregistry/contract.go | 47 ++++- go/seats/ociartifactregistry/contract_test.go | 2 + go/seats/seats.go | 12 +- .../fixtures/verbs.json | 31 +++ seats/container-image-registry/seat.json | 114 +++++++++-- seats/mesh-artifact-store/seat.json | 3 +- .../oci-artifact-registry/fixtures/verbs.json | 20 ++ seats/oci-artifact-registry/seat.json | 114 +++++++++-- ts/seats/containerimageregistry/src/index.ts | 43 ++++- ts/seats/ociartifactregistry/src/index.ts | 43 ++++- ts/src/seats.ts | 179 +++++++++++++++++- 13 files changed, 585 insertions(+), 72 deletions(-) diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go index ac06122..6eb1f99 100644 --- a/go/seats/containerimageregistry/contract.go +++ b/go/seats/containerimageregistry/contract.go @@ -15,6 +15,7 @@ const ( VerbTags = "tags" VerbHold = "hold" VerbRelease = "release" + VerbList = "list" ) const ( @@ -28,7 +29,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease} + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease, VerbList} } // Events is every event published on the seat's subject: a new copy each call. @@ -103,6 +104,24 @@ type Hold struct { Reason string `json:"reason,omitempty"` } +// Listed is a type of the contract. +type Listed struct { + // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by + Artifact string `json:"artifact,omitempty"` + // the manifest's digest, sha256: + Digest string `json:"digest"` + // a hold keeps it from retention + Held bool `json:"held"` + // it is an index, naming a manifest per platform + Index bool `json:"index"` + // for an index, the digests of the manifests it names + Platforms []string `json:"platforms,omitempty"` + // its bytes: its own content and every blob it names + Size int64 `json:"size"` + // the tags naming it, none for an untagged one + Tags []string `json:"tags,omitempty"` +} + // Outcome is the body of published, refused and removed. type Outcome struct { // the source commit it was built from @@ -150,6 +169,14 @@ type Part struct { Size int64 `json:"size"` } +// Repository is a type of the contract. +type Repository struct { + // each manifest it holds, by digest + Manifests []Listed `json:"manifests"` + // the repository + Name string `json:"name"` +} + // PublishInput is what publish takes. type PublishInput struct { // the source commit it was built from @@ -345,6 +372,18 @@ type ReleaseAnswer struct { Released bool `json:"released"` } +// ListInput is what list takes. +type ListInput struct { + // the repository, /; every repository of the seat when not given + Name string `json:"name,omitempty"` +} + +// ListAnswer is what list answers. +type ListAnswer struct { + // each repository named, in order of its name + Repositories []Repository `json:"repositories"` +} + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -362,9 +401,3 @@ type HeldEvent = Hold // ReleasedEvent is the body of the event released. type ReleasedEvent = Hold - -// ArtifactStoreForm is the form of the provision artifact-store. -type ArtifactStoreForm struct { - // the store's address - URL string `json:"url"` -} diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go index 3ad30c7..8f4d543 100644 --- a/go/seats/containerimageregistry/contract_test.go +++ b/go/seats/containerimageregistry/contract_test.go @@ -43,6 +43,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "list answers an index with its platforms, its tag and its hold", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", + decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go index 39bba8b..edade5c 100644 --- a/go/seats/ociartifactregistry/contract.go +++ b/go/seats/ociartifactregistry/contract.go @@ -13,6 +13,7 @@ const ( VerbRemove = "remove" VerbHold = "hold" VerbRelease = "release" + VerbList = "list" ) const ( @@ -26,7 +27,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease} + return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList} } // Events is every event published on the seat's subject: a new copy each call. @@ -101,6 +102,24 @@ type Hold struct { Reason string `json:"reason,omitempty"` } +// Listed is a type of the contract. +type Listed struct { + // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by + Artifact string `json:"artifact,omitempty"` + // the manifest's digest, sha256: + Digest string `json:"digest"` + // a hold keeps it from retention + Held bool `json:"held"` + // it is an index, naming a manifest per platform + Index bool `json:"index"` + // for an index, the digests of the manifests it names + Platforms []string `json:"platforms,omitempty"` + // its bytes: its own content and every blob it names + Size int64 `json:"size"` + // the tags naming it, none for an untagged one + Tags []string `json:"tags,omitempty"` +} + // Outcome is the body of published, refused and removed. type Outcome struct { // the source commit it was built from @@ -148,6 +167,14 @@ type Part struct { Size int64 `json:"size"` } +// Repository is a type of the contract. +type Repository struct { + // each manifest it holds, by digest + Manifests []Listed `json:"manifests"` + // the repository + Name string `json:"name"` +} + // PublishInput is what publish takes. type PublishInput struct { // the source commit it was built from @@ -303,6 +330,18 @@ type ReleaseAnswer struct { Released bool `json:"released"` } +// ListInput is what list takes. +type ListInput struct { + // the repository, /; every repository of the seat when not given + Name string `json:"name,omitempty"` +} + +// ListAnswer is what list answers. +type ListAnswer struct { + // each repository named, in order of its name + Repositories []Repository `json:"repositories"` +} + // PublishedEvent is the body of the event published. type PublishedEvent = Outcome @@ -320,9 +359,3 @@ type HeldEvent = Hold // ReleasedEvent is the body of the event released. type ReleasedEvent = Hold - -// ArtifactStoreForm is the form of the provision artifact-store. -type ArtifactStoreForm struct { - // the store's address - URL string `json:"url"` -} diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go index 9a2f3a2..f6d90b3 100644 --- a/go/seats/ociartifactregistry/contract_test.go +++ b/go/seats/ociartifactregistry/contract_test.go @@ -43,6 +43,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "list answers an untagged archive by the blob it keeps", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", + decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/go/seats/seats.go b/go/seats/seats.go index 125f4fe..a545cdd 100644 --- a/go/seats/seats.go +++ b/go/seats/seats.go @@ -231,7 +231,7 @@ func List() []Seat { {Name: "provisioning", Description: "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", Optional: true, Input: map[string]any{"properties": map[string]any{"collection": map[string]any{"description": "for collect: a collection", "type": "string"}, "module": map[string]any{"description": "a module: for collect, a contributor", "type": "string"}, "node": map[string]any{"description": "a node", "type": "string"}, "what": map[string]any{"description": "registry (when not given), needs, applied or collect", "type": "string"}}, "type": "object"}}, }}, - {Name: MeshArtifactStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0075", Delivers: "artifact-store"}, + {Name: MeshArtifactStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0075"}, {Name: MeshCatalog, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0604", Serves: []Verb{ {Name: "modules", Description: "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", Optional: true, @@ -411,7 +411,7 @@ func List() []Seat { Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, }}, - {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, + {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -437,8 +437,11 @@ func List() []Seat { {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + {Name: "list", Description: "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", Optional: true, + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the repository, /; every repository of the seat when not given", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"repositories": map[string]any{"description": "each repository named, in order of its name", "items": map[string]any{"properties": map[string]any{"manifests": map[string]any{"description": "each manifest it holds, by digest", "items": map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", "type": "string"}, "digest": map[string]any{"description": "the manifest's digest, sha256:", "type": "string"}, "held": map[string]any{"description": "a hold keeps it from retention", "type": "boolean"}, "index": map[string]any{"description": "it is an index, naming a manifest per platform", "type": "boolean"}, "platforms": map[string]any{"description": "for an index, the digests of the manifests it names", "items": map[string]any{"type": "string"}, "type": "array"}, "size": map[string]any{"description": "its bytes: its own content and every blob it names", "type": "integer"}, "tags": map[string]any{"description": "the tags naming it, none for an untagged one", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"digest", "index", "size", "held"}, "type": "object"}, "type": "array"}, "name": map[string]any{"description": "the repository", "type": "string"}}, "required": []string{"name", "manifests"}, "type": "object"}, "type": "array"}}, "required": []string{"repositories"}, "type": "object"}}, }}, - {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "artifact-store", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, + {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, Serves: []Verb{ {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, @@ -458,6 +461,9 @@ func List() []Seat { {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, + {Name: "list", Description: "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", Optional: true, + Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the repository, /; every repository of the seat when not given", "type": "string"}}, "type": "object"}, + Output: map[string]any{"properties": map[string]any{"repositories": map[string]any{"description": "each repository named, in order of its name", "items": map[string]any{"properties": map[string]any{"manifests": map[string]any{"description": "each manifest it holds, by digest", "items": map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", "type": "string"}, "digest": map[string]any{"description": "the manifest's digest, sha256:", "type": "string"}, "held": map[string]any{"description": "a hold keeps it from retention", "type": "boolean"}, "index": map[string]any{"description": "it is an index, naming a manifest per platform", "type": "boolean"}, "platforms": map[string]any{"description": "for an index, the digests of the manifests it names", "items": map[string]any{"type": "string"}, "type": "array"}, "size": map[string]any{"description": "its bytes: its own content and every blob it names", "type": "integer"}, "tags": map[string]any{"description": "the tags naming it, none for an untagged one", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"digest", "index", "size", "held"}, "type": "object"}, "type": "array"}, "name": map[string]any{"description": "the repository", "type": "string"}}, "required": []string{"name", "manifests"}, "type": "object"}, "type": "array"}}, "required": []string{"repositories"}, "type": "object"}}, }}, {Name: Git, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0111", Delivers: "git"}, {Name: NodeBuildGo, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json index 6e50d36..e024a81 100644 --- a/seats/container-image-registry/fixtures/verbs.json +++ b/seats/container-image-registry/fixtures/verbs.json @@ -114,5 +114,36 @@ "0.1.19" ] } + }, + { + "name": "list answers an index with its platforms, its tag and its hold", + "of": "verb:list.answer", + "given": { + "repositories": [ + { + "name": "mail/server", + "manifests": [ + { + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": true, + "platforms": [ + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "tags": [ + "3" + ], + "size": 2048, + "held": false + }, + { + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "index": false, + "size": 1024, + "held": false + } + ] + } + ] + } } ] diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json index 8877a5b..b1a595f 100644 --- a/seats/container-image-registry/seat.json +++ b/seats/container-image-registry/seat.json @@ -5,7 +5,6 @@ "grain": "whole", "decision": "novox/hq ADR 0605", "description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.", - "delivers": "artifact-store", "includes": [ "registry" ], @@ -190,6 +189,36 @@ ] }, "optional": true + }, + { + "name": "list", + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository, /; every repository of the seat when not given" + } + } + }, + "answer": { + "type": "object", + "properties": { + "repositories": { + "type": "array", + "items": { + "$ref": "#/defs/Repository" + }, + "description": "each repository named, in order of its name" + } + }, + "required": [ + "repositories" + ] + }, + "optional": true } ], "events": [ @@ -210,24 +239,6 @@ "name": "container-image-registry.events", "max-age-days": 7 }, - "provisions": [ - { - "name": "artifact-store", - "form": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "the store's address" - } - }, - "required": [ - "url" - ], - "description": "the artifact store's read side, the OCI distribution protocol's GET and HEAD, open on the mesh network (design 51)" - } - } - ], "defs": { "Hold": { "type": "object", @@ -263,6 +274,71 @@ "holder" ], "description": "the body of held and released" + }, + "Repository": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository" + }, + "manifests": { + "type": "array", + "items": { + "$ref": "#/defs/Listed" + }, + "description": "each manifest it holds, by digest" + } + }, + "required": [ + "name", + "manifests" + ] + }, + "Listed": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the manifest's digest, sha256:" + }, + "index": { + "type": "boolean", + "description": "it is an index, naming a manifest per platform" + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for an index, the digests of the manifests it names" + }, + "artifact": { + "type": "string", + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the tags naming it, none for an untagged one" + }, + "size": { + "type": "integer", + "description": "its bytes: its own content and every blob it names" + }, + "held": { + "type": "boolean", + "description": "a hold keeps it from retention" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ] } } } diff --git a/seats/mesh-artifact-store/seat.json b/seats/mesh-artifact-store/seat.json index b1efa95..84c8a57 100644 --- a/seats/mesh-artifact-store/seat.json +++ b/seats/mesh-artifact-store/seat.json @@ -3,6 +3,5 @@ "seat": "mesh-artifact-store", "scope": "mesh", "grain": "seat-list", - "decision": "novox/hq ADR 0075", - "delivers": "artifact-store" + "decision": "novox/hq ADR 0075" } diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json index e0121b7..039c38b 100644 --- a/seats/oci-artifact-registry/fixtures/verbs.json +++ b/seats/oci-artifact-registry/fixtures/verbs.json @@ -114,5 +114,25 @@ "0.1.19" ] } + }, + { + "name": "list answers an untagged archive by the blob it keeps", + "of": "verb:list.answer", + "given": { + "repositories": [ + { + "name": "mail/files", + "manifests": [ + { + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": false, + "artifact": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 4096, + "held": true + } + ] + } + ] + } } ] diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json index df5f58c..9d37fce 100644 --- a/seats/oci-artifact-registry/seat.json +++ b/seats/oci-artifact-registry/seat.json @@ -5,7 +5,6 @@ "grain": "whole", "decision": "novox/hq ADR 0605", "description": "The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.", - "delivers": "artifact-store", "includes": [ "registry" ], @@ -93,6 +92,36 @@ ] }, "optional": true + }, + { + "name": "list", + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "callers": "any caller whose role grants it", + "input": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository, /; every repository of the seat when not given" + } + } + }, + "answer": { + "type": "object", + "properties": { + "repositories": { + "type": "array", + "items": { + "$ref": "#/defs/Repository" + }, + "description": "each repository named, in order of its name" + } + }, + "required": [ + "repositories" + ] + }, + "optional": true } ], "events": [ @@ -113,24 +142,6 @@ "name": "oci-artifact-registry.events", "max-age-days": 7 }, - "provisions": [ - { - "name": "artifact-store", - "form": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "the store's address" - } - }, - "required": [ - "url" - ], - "description": "the artifact store's read side, the OCI distribution protocol's GET and HEAD, open on the mesh network (design 51)" - } - } - ], "defs": { "Hold": { "type": "object", @@ -166,6 +177,71 @@ "holder" ], "description": "the body of held and released" + }, + "Repository": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the repository" + }, + "manifests": { + "type": "array", + "items": { + "$ref": "#/defs/Listed" + }, + "description": "each manifest it holds, by digest" + } + }, + "required": [ + "name", + "manifests" + ] + }, + "Listed": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "description": "the manifest's digest, sha256:" + }, + "index": { + "type": "boolean", + "description": "it is an index, naming a manifest per platform" + }, + "platforms": { + "type": "array", + "items": { + "type": "string" + }, + "description": "for an index, the digests of the manifests it names" + }, + "artifact": { + "type": "string", + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the tags naming it, none for an untagged one" + }, + "size": { + "type": "integer", + "description": "its bytes: its own content and every blob it names" + }, + "held": { + "type": "boolean", + "description": "a hold keeps it from retention" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ] } } } diff --git a/ts/seats/containerimageregistry/src/index.ts b/ts/seats/containerimageregistry/src/index.ts index 9baf704..941ed44 100644 --- a/ts/seats/containerimageregistry/src/index.ts +++ b/ts/seats/containerimageregistry/src/index.ts @@ -15,6 +15,7 @@ export const verbs = { tags: "tags", hold: "hold", release: "release", + list: "list", } as const; export const events = { @@ -72,6 +73,30 @@ export interface Hold { "reason"?: string; } +export interface Listed { + /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ + "artifact"?: string; + /** the manifest's digest, sha256: */ + "digest": string; + /** a hold keeps it from retention */ + "held": boolean; + /** it is an index, naming a manifest per platform */ + "index": boolean; + /** for an index, the digests of the manifests it names */ + "platforms"?: string[]; + /** its bytes: its own content and every blob it names */ + "size": number; + /** the tags naming it, none for an untagged one */ + "tags"?: string[]; +} + +export interface Repository { + /** each manifest it holds, by digest */ + "manifests": Listed[]; + /** the repository */ + "name": string; +} + /** is what mirror takes. */ export interface MirrorInput { /** its pinned digest, sha256: */ @@ -145,15 +170,21 @@ export interface ReleaseAnswer { "released": boolean; } +/** is what list takes. */ +export interface ListInput { + /** the repository, /; every repository of the seat when not given */ + "name"?: string; +} + +/** is what list answers. */ +export interface ListAnswer { + /** each repository named, in order of its name */ + "repositories": Repository[]; +} + /** is the body of the event held. */ export type HeldEvent = Hold; /** is the body of the event released. */ export type ReleasedEvent = Hold; -/** is the form of the provision artifact-store. */ -export interface ArtifactStoreForm { - /** the store's address */ - "url": string; -} - diff --git a/ts/seats/ociartifactregistry/src/index.ts b/ts/seats/ociartifactregistry/src/index.ts index c1fa55e..ebf6242 100644 --- a/ts/seats/ociartifactregistry/src/index.ts +++ b/ts/seats/ociartifactregistry/src/index.ts @@ -13,6 +13,7 @@ export const verbs = { remove: "remove", hold: "hold", release: "release", + list: "list", } as const; export const events = { @@ -70,6 +71,30 @@ export interface Hold { "reason"?: string; } +export interface Listed { + /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ + "artifact"?: string; + /** the manifest's digest, sha256: */ + "digest": string; + /** a hold keeps it from retention */ + "held": boolean; + /** it is an index, naming a manifest per platform */ + "index": boolean; + /** for an index, the digests of the manifests it names */ + "platforms"?: string[]; + /** its bytes: its own content and every blob it names */ + "size": number; + /** the tags naming it, none for an untagged one */ + "tags"?: string[]; +} + +export interface Repository { + /** each manifest it holds, by digest */ + "manifests": Listed[]; + /** the repository */ + "name": string; +} + /** is what hold takes. */ export interface HoldInput { /** the digest */ @@ -104,15 +129,21 @@ export interface ReleaseAnswer { "released": boolean; } +/** is what list takes. */ +export interface ListInput { + /** the repository, /; every repository of the seat when not given */ + "name"?: string; +} + +/** is what list answers. */ +export interface ListAnswer { + /** each repository named, in order of its name */ + "repositories": Repository[]; +} + /** is the body of the event held. */ export type HeldEvent = Hold; /** is the body of the event released. */ export type ReleasedEvent = Hold; -/** is the form of the provision artifact-store. */ -export interface ArtifactStoreForm { - /** the store's address */ - "url": string; -} - diff --git a/ts/src/seats.ts b/ts/src/seats.ts index efe9c39..6c51e36 100644 --- a/ts/src/seats.ts +++ b/ts/src/seats.ts @@ -1810,7 +1810,6 @@ export const list: readonly Seat[] = [ }, { "decision": "novox/hq ADR 0075", - "delivers": "artifact-store", "grain": "seat-list", "name": "mesh-artifact-store", "scope": "mesh" @@ -3809,7 +3808,6 @@ export const list: readonly Seat[] = [ }, { "decision": "novox/hq ADR 0605", - "delivers": "artifact-store", "emits": [ "published", "refused", @@ -4358,12 +4356,99 @@ export const list: readonly Seat[] = [ ], "type": "object" } + }, + { + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "input": { + "properties": { + "name": { + "description": "the repository, /; every repository of the seat when not given", + "type": "string" + } + }, + "type": "object" + }, + "name": "list", + "optional": true, + "output": { + "properties": { + "repositories": { + "description": "each repository named, in order of its name", + "items": { + "properties": { + "manifests": { + "description": "each manifest it holds, by digest", + "items": { + "properties": { + "artifact": { + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", + "type": "string" + }, + "digest": { + "description": "the manifest's digest, sha256:", + "type": "string" + }, + "held": { + "description": "a hold keeps it from retention", + "type": "boolean" + }, + "index": { + "description": "it is an index, naming a manifest per platform", + "type": "boolean" + }, + "platforms": { + "description": "for an index, the digests of the manifests it names", + "items": { + "type": "string" + }, + "type": "array" + }, + "size": { + "description": "its bytes: its own content and every blob it names", + "type": "integer" + }, + "tags": { + "description": "the tags naming it, none for an untagged one", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ], + "type": "object" + }, + "type": "array" + }, + "name": { + "description": "the repository", + "type": "string" + } + }, + "required": [ + "name", + "manifests" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "repositories" + ], + "type": "object" + } } ] }, { "decision": "novox/hq ADR 0605", - "delivers": "artifact-store", "emits": [ "published", "refused", @@ -4817,6 +4902,94 @@ export const list: readonly Seat[] = [ ], "type": "object" } + }, + { + "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", + "input": { + "properties": { + "name": { + "description": "the repository, /; every repository of the seat when not given", + "type": "string" + } + }, + "type": "object" + }, + "name": "list", + "optional": true, + "output": { + "properties": { + "repositories": { + "description": "each repository named, in order of its name", + "items": { + "properties": { + "manifests": { + "description": "each manifest it holds, by digest", + "items": { + "properties": { + "artifact": { + "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", + "type": "string" + }, + "digest": { + "description": "the manifest's digest, sha256:", + "type": "string" + }, + "held": { + "description": "a hold keeps it from retention", + "type": "boolean" + }, + "index": { + "description": "it is an index, naming a manifest per platform", + "type": "boolean" + }, + "platforms": { + "description": "for an index, the digests of the manifests it names", + "items": { + "type": "string" + }, + "type": "array" + }, + "size": { + "description": "its bytes: its own content and every blob it names", + "type": "integer" + }, + "tags": { + "description": "the tags naming it, none for an untagged one", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "digest", + "index", + "size", + "held" + ], + "type": "object" + }, + "type": "array" + }, + "name": { + "description": "the repository", + "type": "string" + } + }, + "required": [ + "name", + "manifests" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "repositories" + ], + "type": "object" + } } ] }, -- 2.54.0 From b6c127f237871fc0d4632880ff7336337fceaa14 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 12:46:33 +0100 Subject: [PATCH 44/94] =?UTF-8?q?Seat=20definition=20format:=20states=20re?= =?UTF-8?q?place=20records,=20operator-channel's=20asks=20first=20(ADR=200?= =?UTF-8?q?608=20=C2=A71,=20ADR=200634=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- README.md | 5 ++ format/seat-definition.schema.json | 99 ++++++++++++++++++++++++------ seats/operator-channel/seat.json | 19 ++++-- 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 4109509..5d494ab 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ implements its contract, and no holder's code lives here. - **The definition**, written by hand: `seats//seat.json`, its fixtures in `seats//fixtures/`, and the parts several seats include whole in `parts/` (the four registry seats' verbs and events are `parts/registry.json`). mesh-sdk's `seatdef` is the one reader of the format; the indexer and the generators use it. +- **A state**, in the definition's `states` (ADR 0608 §1, ADR 0634 §6): `{name, template, writer, key-forms, history, + max-age-days, description}`. `template` is one of ADR 0595's six (`state`, `events`, `work-queue`, `log`, `objects`, + `ledger`); `writer` is `holder` (the default) or `caller`; each key form is `{key, value, description}`, the key a + pattern of named parts between angle brackets (`.`) and the value its JSON Schema. A definition names no + holder module: a reader takes the holder from its own manifest's `reads`. A fixture of a state is `state:/`. - **The seat set**, generated: `go/seats` (module `git.novox.be/novox/mesh-contracts/go`), every seat's name, scope, verbs and provision at the seat list's grain (ADR 0625 §2). mesh-sdk's `catalogue` is its only importer in mesh-sdk. A change of it rebuilds every importer of the seat set. Its npm package is `ts/` (`@novox/mesh-contracts`, the diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index 2c64151..3202f33 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -130,27 +130,11 @@ }, "description": "the holder's own events listed beside the seat, keeping the holder's subjects (ADR 0620 \u00a72)" }, - "records": { + "states": { "type": "array", + "description": "the layouts of the state the holder keeps on the bus for the seat (ADR 0608 \u00a71, ADR 0634 \u00a76); a whole definition's. No holder module is named: each reader takes the holder from its own manifest's reads", "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "pattern": "^[a-z][a-z0-9-]*$" - }, - "key": { - "type": "string" - }, - "value": { - "type": "object", - "description": "a JSON Schema" - } - } + "$ref": "#/$defs/state" } }, "provisions": { @@ -377,7 +361,7 @@ }, "of": { "type": "string", - "pattern": "^(verb:[a-z][a-z0-9-]*\\.(input|answer)|accept:[a-z][a-z0-9-]*|event:[a-z][a-z0-9-]*|holder-event:[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*)$" + "pattern": "^(verb:[a-z][a-z0-9-]*\\.(input|answer)|accept:[a-z][a-z0-9-]*|event:[a-z][a-z0-9-]*|state:[a-z][a-z0-9-]*/.+|holder-event:[a-z][a-z0-9-]*(\\.[a-z][a-z0-9-]*)*)$" }, "subject": { "type": "string" @@ -410,6 +394,81 @@ "uniqueItems": true } } + }, + "state": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "template", + "key-forms" + ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "template": { + "enum": [ + "state", + "events", + "work-queue", + "log", + "objects", + "ledger" + ], + "description": "ADR 0595 \u00a72" + }, + "writer": { + "enum": [ + "holder", + "caller" + ], + "description": "holder when not given; caller when the seat's callers write their own keys" + }, + "key-forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/keyForm" + } + }, + "history": { + "type": "integer", + "minimum": 0, + "description": "past values a key keeps; the state template only" + }, + "max-age-days": { + "type": "integer", + "minimum": 0, + "description": "days a message is kept; the events, work-queue and log templates only" + }, + "description": { + "type": "string" + } + } + }, + "keyForm": { + "type": "object", + "additionalProperties": false, + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string", + "minLength": 1, + "description": "a pattern of named parts between angle brackets, the rest literal: ." + }, + "value": { + "type": "object", + "description": "a JSON Schema" + }, + "description": { + "type": "string" + } + } } } } diff --git a/seats/operator-channel/seat.json b/seats/operator-channel/seat.json index 4fe0a0c..9770349 100644 --- a/seats/operator-channel/seat.json +++ b/seats/operator-channel/seat.json @@ -172,14 +172,21 @@ "name": "operator-channel.decided", "max-age-days": 7 }, - "records": [ + "states": [ { "name": "asks", - "key": ".", - "value": { - "type": "object", - "description": "one ask the router holds and how it stands" - } + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "one ask the router holds and how it stands" + }, + "description": "one ask the router holds and how it stands" + } + ], + "description": "the asks a module raised and the router holds, one key per asker and ask" } ], "holder-events": [ -- 2.54.0 From 23a759c78b093df1abb75356983435d606026ef7 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 12:51:40 +0100 Subject: [PATCH 45/94] Domain contracts folders: health-and-repair, provisioning, connectivity, operator-and-conversation hold their seats' definitions with states; node-engine and mesh-config gain states (ADR 0634, ADR 0635; issue 1966) Generated code, clients and the seat-set removal wait for the seatgen change. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- connectivity/module.json | 38 ++ .../seats}/node-packet-filter/seat.json | 0 .../seats}/node-resolver/seat.json | 0 .../seats/the-private-network/seat.json | 18 + health-and-repair/module.json | 127 ++++ .../seats/mesh-conditions/seat.json | 646 ++++++++++++++++++ .../seats/mesh-hand-acts/seat.json | 320 +++++++++ .../seats/mesh-healers/seat.json | 249 +++++++ .../seats/mesh-self-check/seat.json | 164 +++++ .../seats/mesh-watchdogs/seat.json | 242 +++++++ operator-and-conversation/module.json | 38 ++ .../operator-channel/fixtures/events.json | 0 .../seats}/operator-channel/seat.json | 0 provisioning/module.json | 38 ++ provisioning/seats/mesh-provisioner/seat.json | 298 ++++++++ .../seats}/node-mounts/seat.json | 0 .../seats}/node-nfs-server/seat.json | 0 .../seats}/node-provisioner/seat.json | 0 seats/index.json | 13 - seats/mesh-conditions/seat.json | 95 --- seats/mesh-config/seat.json | 49 ++ seats/mesh-hand-acts/seat.json | 97 --- seats/mesh-healers/seat.json | 73 -- seats/mesh-provisioner/seat.json | 35 - seats/mesh-self-check/seat.json | 43 -- seats/mesh-watchdogs/seat.json | 93 --- seats/node-engine/seat.json | 361 ++++++++++ seats/the-private-network/seat.json | 7 - 28 files changed, 2588 insertions(+), 456 deletions(-) create mode 100644 connectivity/module.json rename {seats => connectivity/seats}/node-packet-filter/seat.json (100%) rename {seats => connectivity/seats}/node-resolver/seat.json (100%) create mode 100644 connectivity/seats/the-private-network/seat.json create mode 100644 health-and-repair/module.json create mode 100644 health-and-repair/seats/mesh-conditions/seat.json create mode 100644 health-and-repair/seats/mesh-hand-acts/seat.json create mode 100644 health-and-repair/seats/mesh-healers/seat.json create mode 100644 health-and-repair/seats/mesh-self-check/seat.json create mode 100644 health-and-repair/seats/mesh-watchdogs/seat.json create mode 100644 operator-and-conversation/module.json rename {seats => operator-and-conversation/seats}/operator-channel/fixtures/events.json (100%) rename {seats => operator-and-conversation/seats}/operator-channel/seat.json (100%) create mode 100644 provisioning/module.json create mode 100644 provisioning/seats/mesh-provisioner/seat.json rename {seats => provisioning/seats}/node-mounts/seat.json (100%) rename {seats => provisioning/seats}/node-nfs-server/seat.json (100%) rename {seats => provisioning/seats}/node-provisioner/seat.json (100%) delete mode 100644 seats/mesh-conditions/seat.json delete mode 100644 seats/mesh-hand-acts/seat.json delete mode 100644 seats/mesh-healers/seat.json delete mode 100644 seats/mesh-provisioner/seat.json delete mode 100644 seats/mesh-self-check/seat.json delete mode 100644 seats/mesh-watchdogs/seat.json delete mode 100644 seats/the-private-network/seat.json diff --git a/connectivity/module.json b/connectivity/module.json new file mode 100644 index 0000000..5581790 --- /dev/null +++ b/connectivity/module.json @@ -0,0 +1,38 @@ +{ + "module": "mesh-connectivity-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-theprivatenetwork", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/theprivatenetwork" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-theprivatenetworkclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/theprivatenetworkclient" + ], + "series": "0.1" + }, + { + "name": "ts-theprivatenetwork", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/theprivatenetwork" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/seats/node-packet-filter/seat.json b/connectivity/seats/node-packet-filter/seat.json similarity index 100% rename from seats/node-packet-filter/seat.json rename to connectivity/seats/node-packet-filter/seat.json diff --git a/seats/node-resolver/seat.json b/connectivity/seats/node-resolver/seat.json similarity index 100% rename from seats/node-resolver/seat.json rename to connectivity/seats/node-resolver/seat.json diff --git a/connectivity/seats/the-private-network/seat.json b/connectivity/seats/the-private-network/seat.json new file mode 100644 index 0000000..2646620 --- /dev/null +++ b/connectivity/seats/the-private-network/seat.json @@ -0,0 +1,18 @@ +{ + "format": 1, + "seat": "the-private-network", + "scope": "node", + "grain": "whole", + "decision": "novox/hq ADR 0110, ADR 0634, ADR 0635", + "description": "The seat of the private network (ADR 0110): the one network the mesh runs over, which a machine can have only one of; the mesh provides its holder itself and the provision carries no fields.", + "verbs": [], + "provisions": [ + { + "name": "private-network", + "form": { + "type": "object", + "description": "the provision has no fields: it is the machine's reach to the others" + } + } + ] +} diff --git a/health-and-repair/module.json b/health-and-repair/module.json new file mode 100644 index 0000000..d712009 --- /dev/null +++ b/health-and-repair/module.json @@ -0,0 +1,127 @@ +{ + "module": "mesh-health-and-repair-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshconditions", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshconditions" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshselfcheck", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshselfcheck" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshhealers", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshhealers" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshwatchdogs", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshwatchdogs" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshhandacts", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshhandacts" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshhandactsclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshhandactsclient" + ], + "series": "0.1" + }, + { + "name": "go-meshconditionsclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshconditionsclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshconditions", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshconditions" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshselfcheck", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshselfcheck" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshhealers", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshhealers" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshwatchdogs", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshwatchdogs" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshhandacts", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshhandacts" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/health-and-repair/seats/mesh-conditions/seat.json b/health-and-repair/seats/mesh-conditions/seat.json new file mode 100644 index 0000000..ee5b697 --- /dev/null +++ b/health-and-repair/seats/mesh-conditions/seat.json @@ -0,0 +1,646 @@ +{ + "format": 1, + "seat": "mesh-conditions", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", + "description": "What is wrong with the mesh now (ADR 0564): the open conditions raised from the observers' findings and cleared when a finding goes.", + "verbs": [ + { + "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.", + "input": { + "properties": { + "days": { + "description": "with history: how many days back (default 7, at most 90)", + "type": "string" + }, + "history": { + "description": "\"true\": every raising, change, silence and clearing lately, oldest first", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "key": { + "description": "a condition's key: that one whole; with history, only its transitions", + "type": "string" + }, + "machine": { + "description": "only those about this machine", + "type": "string" + }, + "scope": { + "description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", + "type": "string" + }, + "severity": { + "description": "only urgent, or only warning", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the open conditions, or one whole, or the 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": { + "properties": { + "cause": { + "description": "the cause in a word (the condition's kind when absent)", + "type": "string" + }, + "for": { + "description": "how long: 30m, 4h, 2d; at most 7d", + "type": "string" + }, + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why, required and recorded", + "type": "string" + } + }, + "required": [ + "key", + "for", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the condition now silenced" + } + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what was raised and cleared again" + } + } + ], + "events": [ + { + "name": "condition-raised", + "body": { + "$ref": "#/defs/ConditionEvent" + } + }, + { + "name": "condition-changed", + "body": { + "$ref": "#/defs/ConditionEvent" + } + }, + { + "name": "condition-cleared", + "body": { + "$ref": "#/defs/ConditionEvent" + } + } + ], + "family": { + "name": "mesh-conditions.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "conditions", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one open condition", + "properties": { + "key": { + "type": "string", + "description": "scope.id.token" + }, + "kind": { + "type": "string" + }, + "subject": { + "$ref": "#/defs/Subject" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string" + }, + "explanation": { + "type": "string" + }, + "resolved": { + "type": "string" + }, + "needs": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/defs/Action" + } + }, + "evidence": { + "type": "array", + "description": "newest first, at most 10", + "items": { + "$ref": "#/defs/Evidence" + } + }, + "source": { + "type": "string", + "description": "the row, probe or event that saw it" + }, + "resolver": { + "type": "string", + "description": "self, operator, agent or healer:" + }, + "raised": { + "type": "string", + "format": "date-time" + }, + "last-observed": { + "type": "string", + "format": "date-time" + }, + "first": { + "type": "string", + "format": "date-time" + }, + "gaps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "cleared": { + "type": "string", + "format": "date-time" + }, + "reopened": { + "type": "string", + "format": "date-time" + } + } + } + }, + "observations": { + "type": "integer" + }, + "count": { + "type": "integer" + }, + "tried": { + "type": "array", + "items": { + "type": "object", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "what": { + "type": "string" + }, + "outcome": { + "type": "string" + }, + "by": { + "type": "string" + } + } + } + }, + "silenced": { + "type": "object", + "properties": { + "until": { + "type": "string", + "format": "date-time" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "since": { + "type": "string", + "format": "date-time" + } + } + }, + "epoch": { + "type": "integer" + }, + "looks-unread": { + "type": "string" + } + }, + "required": [ + "key", + "kind", + "subject", + "severity", + "raised", + "last-observed" + ] + }, + "description": "one open condition, without its looks it keeps from its finding" + } + ], + "description": "the open conditions the keeper holds, one key each" + }, + { + "name": "findings", + "template": "state", + "writer": "caller", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one thing an observer sees wrong now", + "properties": { + "v": { + "type": "integer", + "description": "layout version, 1" + }, + "key": { + "type": "string", + "description": "scope.id.token" + }, + "kind": { + "type": "string" + }, + "subject": { + "$ref": "#/defs/Subject" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string" + }, + "explanation": { + "type": "string" + }, + "resolved": { + "type": "string" + }, + "needs": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/defs/Action" + } + }, + "evidence": { + "type": "array", + "description": "newest first, at most 10", + "items": { + "$ref": "#/defs/Evidence" + } + }, + "source": { + "type": "string", + "description": "the row, probe or event that saw it" + }, + "resolver": { + "type": "string", + "description": "self, operator, agent or healer:" + }, + "ref": { + "type": "string", + "description": "the key's last part when it is not the kind" + }, + "observer": { + "type": "string", + "description": "the module that wrote it" + }, + "run": { + "type": "string", + "description": "what of its own saw it last" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "v", + "key", + "kind", + "subject", + "severity", + "summary", + "source", + "observer", + "at" + ] + }, + "description": "one finding of the observer that wrote it" + } + ], + "description": "every observer's findings, one key per finding; a key written raises or changes the condition of the same key, a key deleted clears it" + } + ], + "defs": { + "Subject": { + "type": "object", + "description": "what the condition is about", + "properties": { + "scope": { + "type": "string", + "description": "machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module" + }, + "id": { + "type": "string", + "description": "its id within the scope" + }, + "machine": { + "type": "string", + "description": "the machine it concerns, when there is one" + }, + "also": { + "type": "array", + "description": "the other machines it concerns", + "items": { + "type": "string" + } + } + }, + "required": [ + "scope", + "id" + ] + }, + "Evidence": { + "type": "object", + "description": "one observation, as it was said", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "said": { + "type": "string" + } + }, + "required": [ + "at", + "said" + ] + }, + "Action": { + "type": "object", + "description": "one answer the operator may give from a notification", + "properties": { + "label": { + "type": "string" + }, + "verb": { + "type": "string", + "description": "the seat verb the operator channel calls" + }, + "machine": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "level": { + "type": "string", + "enum": [ + "acknowledge", + "approve" + ] + } + }, + "required": [ + "label", + "verb" + ] + }, + "ConditionEvent": { + "type": "object", + "description": "a condition and what changed", + "properties": { + "key": { + "type": "string", + "description": "scope.id.token" + }, + "kind": { + "type": "string" + }, + "subject": { + "$ref": "#/defs/Subject" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string" + }, + "explanation": { + "type": "string" + }, + "resolved": { + "type": "string" + }, + "needs": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/defs/Action" + } + }, + "evidence": { + "type": "array", + "description": "newest first, at most 10", + "items": { + "$ref": "#/defs/Evidence" + } + }, + "source": { + "type": "string", + "description": "the row, probe or event that saw it" + }, + "resolver": { + "type": "string", + "description": "self, operator, agent or healer:" + }, + "raised": { + "type": "string", + "format": "date-time" + }, + "last-observed": { + "type": "string", + "format": "date-time" + }, + "first": { + "type": "string", + "format": "date-time" + }, + "gaps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "cleared": { + "type": "string", + "format": "date-time" + }, + "reopened": { + "type": "string", + "format": "date-time" + } + } + } + }, + "observations": { + "type": "integer" + }, + "count": { + "type": "integer" + }, + "tried": { + "type": "array", + "items": { + "type": "object", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "what": { + "type": "string" + }, + "outcome": { + "type": "string" + }, + "by": { + "type": "string" + } + } + } + }, + "silenced": { + "type": "object", + "properties": { + "until": { + "type": "string", + "format": "date-time" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "since": { + "type": "string", + "format": "date-time" + } + } + }, + "epoch": { + "type": "integer" + }, + "looks-unread": { + "type": "string" + }, + "event": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "change": { + "type": "string", + "enum": [ + "raised", + "reopened", + "severity", + "resolver", + "silenced", + "silence-ended", + "cleared", + "words" + ] + }, + "was": { + "type": "string" + }, + "why": { + "type": "string" + }, + "cleared": { + "type": "string", + "format": "date-time" + }, + "show": { + "type": "string", + "description": "the tool call that shows it" + } + }, + "required": [ + "key", + "event", + "at", + "change" + ] + } + } +} diff --git a/health-and-repair/seats/mesh-hand-acts/seat.json b/health-and-repair/seats/mesh-hand-acts/seat.json new file mode 100644 index 0000000..e635d7a --- /dev/null +++ b/health-and-repair/seats/mesh-hand-acts/seat.json @@ -0,0 +1,320 @@ +{ + "format": 1, + "seat": "mesh-hand-acts", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", + "description": "Acts done by hand, recorded with who, why and cause (ADR 0565): the log every module records through.", + "verbs": [ + { + "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": { + "properties": { + "cause": { + "description": "the cause in a word, or a condition's kind", + "type": "string" + }, + "condition": { + "description": "the key of the condition it addressed, if any (optional)", + "type": "string" + }, + "verb": { + "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", + "type": "string" + }, + "what": { + "description": "what was done, in a line", + "type": "string" + }, + "why": { + "description": "why it had to be done by hand", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the act recorded" + } + }, + { + "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": { + "properties": { + "cause": { + "description": "only this cause", + "type": "string" + }, + "days": { + "description": "how many days back (default 14)", + "type": "string" + }, + "warrants": { + "description": "\"true\": every warrant recorded", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "who": { + "description": "only acts whose who contains this", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the acts lately" + } + }, + { + "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": { + "properties": { + "ask": { + "description": "its ask's id", + "type": "string" + }, + "asker": { + "description": "the module that asked", + "type": "string" + } + }, + "required": [ + "asker", + "ask" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the warrant recorded" + } + } + ], + "events": [ + { + "name": "act-recorded", + "body": { + "$ref": "#/defs/Act" + } + } + ], + "family": { + "name": "mesh-hand-acts.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "acts", + "template": "log", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one act done by hand", + "properties": { + "id": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "by": { + "type": "string", + "description": "the caller the runtime proved" + }, + "verb": { + "type": "string" + }, + "arguments": { + "type": "array", + "items": { + "type": "string" + } + }, + "why": { + "type": "string" + }, + "cause": { + "type": "string" + }, + "condition": { + "type": "string" + }, + "kind": { + "type": "string", + "description": "what the act was read to be from what it did" + }, + "carried": { + "type": "array", + "items": { + "type": "string" + } + }, + "via": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + } + }, + "requested-by": { + "type": "string" + }, + "outcome": { + "type": "string" + }, + "log": { + "type": "string", + "description": "which log holds it, set when it is read" + } + }, + "required": [ + "id", + "at", + "by", + "verb", + "why", + "cause" + ] + }, + "description": "one act" + } + ], + "max-age-days": 400, + "description": "every act, kept 400 days" + }, + { + "name": "warrants", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "what a module did on the operator's warrant, made once per ask", + "properties": { + "ask": { + "type": "string" + }, + "asker": { + "type": "string" + }, + "option": { + "type": "string" + }, + "by": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "ask", + "asker" + ] + }, + "description": "one warrant" + } + ], + "description": "warrants recorded, one key per asker and ask" + } + ], + "defs": { + "Act": { + "type": "object", + "description": "one act done by hand", + "properties": { + "id": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "by": { + "type": "string", + "description": "the caller the runtime proved" + }, + "verb": { + "type": "string" + }, + "arguments": { + "type": "array", + "items": { + "type": "string" + } + }, + "why": { + "type": "string" + }, + "cause": { + "type": "string" + }, + "condition": { + "type": "string" + }, + "kind": { + "type": "string", + "description": "what the act was read to be from what it did" + }, + "carried": { + "type": "array", + "items": { + "type": "string" + } + }, + "via": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "proofs": { + "type": "array", + "items": { + "type": "string" + } + }, + "requested-by": { + "type": "string" + }, + "outcome": { + "type": "string" + }, + "log": { + "type": "string", + "description": "which log holds it, set when it is read" + } + }, + "required": [ + "id", + "at", + "by", + "verb", + "why", + "cause" + ] + } + } +} diff --git a/health-and-repair/seats/mesh-healers/seat.json b/health-and-repair/seats/mesh-healers/seat.json new file mode 100644 index 0000000..e402fe6 --- /dev/null +++ b/health-and-repair/seats/mesh-healers/seat.json @@ -0,0 +1,249 @@ +{ + "format": 1, + "seat": "mesh-healers", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", + "description": "The healers (ADR 0564): each a registered response to one kind of condition, within a budget and the mesh-wide brake.", + "verbs": [ + { + "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, and the mesh-wide brake.", + "input": { + "properties": { + "days": { + "description": "how many days of acts back (default 7)", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the registered healers, acts lately and the brake" + } + }, + { + "name": "heal", + "description": "Take one healer's act for one open condition now, within its budget and the brake.", + "input": { + "properties": { + "key": { + "description": "the condition's key", + "type": "string" + }, + "why": { + "description": "why now, kept with the act", + "type": "string" + } + }, + "required": [ + "key", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the act taken" + } + }, + { + "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": { + "properties": { + "condition": { + "description": "the key of the condition the drill is meant to raise, if any (optional)", + "type": "string" + }, + "what": { + "description": "what was done on purpose, in a line", + "type": "string" + }, + "why": { + "description": "what the drill tests", + "type": "string" + } + }, + "required": [ + "what", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the drill recorded" + } + } + ], + "events": [ + { + "name": "healer-acted", + "body": { + "type": "object", + "description": "a healer's act", + "properties": { + "event": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "healer": { + "type": "string" + }, + "by": { + "type": "string" + }, + "condition": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "act": { + "type": "string" + }, + "outcome": { + "type": "string", + "enum": [ + "acting", + "acted", + "failed", + "escalated" + ] + }, + "said": { + "type": "string" + }, + "budget": { + "type": "string" + }, + "show": { + "type": "string" + } + }, + "required": [ + "event", + "at", + "healer", + "condition", + "outcome" + ] + } + } + ], + "family": { + "name": "mesh-healers.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "attempts", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one act of a healer, created before it is made and finished after, so a second instance cannot begin the same act", + "properties": { + "healer": { + "type": "string" + }, + "condition": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "budget-key": { + "type": "string", + "description": "what the budget counts by: the condition's key unless the healer says another" + }, + "act": { + "type": "string" + }, + "outcome": { + "type": "string", + "enum": [ + "acting", + "acted", + "failed", + "escalated" + ] + }, + "said": { + "type": "string" + }, + "begun": { + "type": "string", + "format": "date-time" + }, + "finished": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "healer", + "condition", + "kind", + "budget-key", + "act", + "outcome", + "begun" + ] + }, + "description": "one act, under the healers' own key" + } + ], + "description": "the acts begun, which the keeper reads to write a condition's tries" + }, + { + "name": "would", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "an act the healers would have taken while the controller's lease names a controller; it counts for no budget", + "properties": { + "healer": { + "type": "string" + }, + "condition": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "act": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "why": { + "type": "string" + } + }, + "required": [ + "healer", + "condition", + "act", + "at" + ] + }, + "description": "one act not taken" + } + ], + "description": "the acts the healers would have taken" + } + ] +} diff --git a/health-and-repair/seats/mesh-self-check/seat.json b/health-and-repair/seats/mesh-self-check/seat.json new file mode 100644 index 0000000..d73fc2d --- /dev/null +++ b/health-and-repair/seats/mesh-self-check/seat.json @@ -0,0 +1,164 @@ +{ + "format": 1, + "seat": "mesh-self-check", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", + "description": "The mesh checking itself (ADR 0564): probes run every five minutes, each failure a finding the seat's conditions raise.", + "verbs": [ + { + "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": { + "properties": { + "probe": { + "description": "one probe's id (D6): its verdict alone, with every finding", + "type": "string" + }, + "run": { + "description": "\"true\": run every probe now and answer the verdict", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the last run's verdict, or one probe's, or a run now" + } + }, + { + "name": "probes", + "description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the registry of probes" + } + } + ], + "events": [ + { + "name": "checked", + "body": { + "type": "object", + "description": "one run of the self-check", + "properties": { + "run": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "started": { + "type": "string", + "format": "date-time" + }, + "took": { + "type": "string" + }, + "interval-seconds": { + "type": "integer" + }, + "counts": { + "type": "object", + "properties": { + "passed": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "failed-to-run": { + "type": "integer" + }, + "deferred": { + "type": "integer" + } + } + }, + "probes": { + "type": "array", + "items": { + "$ref": "#/defs/ProbeVerdict" + } + }, + "controller": { + "type": "string" + }, + "why": { + "type": "string" + }, + "unsaid": { + "type": "integer" + } + }, + "required": [ + "run", + "at", + "probes" + ] + } + } + ], + "family": { + "name": "mesh-self-check.events", + "max-age-days": 7 + }, + "defs": { + "ProbeVerdict": { + "type": "object", + "description": "one probe's verdict", + "properties": { + "id": { + "type": "string" + }, + "verdict": { + "type": "string", + "enum": [ + "passed", + "failed", + "failed-to-run", + "deferred" + ] + }, + "found": { + "type": "array", + "items": { + "type": "string" + } + }, + "unconfirmed": { + "type": "array", + "items": { + "type": "string" + } + }, + "error": { + "type": "string" + }, + "took": { + "type": "string" + }, + "found-count": { + "type": "integer" + }, + "more": { + "type": "string" + } + }, + "required": [ + "id", + "verdict" + ] + } + } +} diff --git a/health-and-repair/seats/mesh-watchdogs/seat.json b/health-and-repair/seats/mesh-watchdogs/seat.json new file mode 100644 index 0000000..ecf09a3 --- /dev/null +++ b/health-and-repair/seats/mesh-watchdogs/seat.json @@ -0,0 +1,242 @@ +{ + "format": 1, + "seat": "mesh-watchdogs", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", + "description": "The watchman's seat (ADR 0564, ADR 0433): signals that expire unless renewed, what stopped moving, who is present, the dead letters.", + "verbs": [ + { + "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": { + "properties": { + "row": { + "description": "one row, e.g. S1", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the watchdog table" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "only those on this machine", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what stopped moving" + } + }, + { + "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": { + "properties": { + "machine": { + "description": "one machine", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "who is connected and who is silent" + } + }, + { + "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.", + "input": { + "properties": { + "cause": { + "description": "with deliver or drop: the cause in a word (dead-letter when absent)", + "type": "string" + }, + "consumer": { + "description": "a consumer's name, or .: only what that one gave up on", + "type": "string" + }, + "deliver": { + "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", + "type": "string" + }, + "drop": { + "description": "a dead letter's id: drop it for good (needs why)", + "type": "string" + }, + "id": { + "description": "a dead letter's id, as the list gives it: that one whole, with its message", + "type": "string" + }, + "limit": { + "description": "how many to list, newest first (default 50); only when listing", + "type": "string" + }, + "why": { + "description": "with deliver or drop: why, required and recorded", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the dead letters, one whole, or the act done" + } + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what was rebuilt" + } + } + ], + "states": [ + { + "name": "presence", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "whether a node is online", + "properties": { + "v": { + "type": "integer" + }, + "online": { + "type": "boolean" + } + }, + "required": [ + "online" + ] + }, + "description": "a node" + }, + { + "key": "part..", + "value": { + "type": "object", + "description": "whether a part's instance is online", + "properties": { + "v": { + "type": "integer" + }, + "online": { + "type": "boolean" + } + }, + "required": [ + "online" + ] + }, + "description": "a part's instance" + } + ], + "history": 20, + "description": "who is connected; the entry's own time says since when" + }, + { + "name": "pulse", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "a watched key mirrored, its revision, and who owes the next move", + "properties": { + "v": { + "type": "integer" + }, + "bucket": { + "type": "string" + }, + "key": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "owes": { + "type": "string" + } + }, + "required": [ + "bucket", + "key", + "revision" + ] + }, + "description": "the move watched at a key of a state" + } + ], + "description": "one key per watched move, living for the move's bound minus its age" + }, + { + "name": "stuck", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "what stopped moving", + "properties": { + "v": { + "type": "integer" + }, + "bucket": { + "type": "string" + }, + "key": { + "type": "string" + }, + "waited": { + "type": "string", + "description": "what it waited for, in words" + }, + "since": { + "type": "string", + "format": "date-time" + }, + "moved": { + "type": "boolean", + "description": "it has moved again" + } + }, + "required": [ + "bucket", + "key", + "since" + ] + }, + "description": "a key whose pulse expired" + } + ], + "description": "what stopped moving; no key is removed, one that moves again is written as moved" + } + ] +} diff --git a/operator-and-conversation/module.json b/operator-and-conversation/module.json new file mode 100644 index 0000000..2498f04 --- /dev/null +++ b/operator-and-conversation/module.json @@ -0,0 +1,38 @@ +{ + "module": "mesh-operator-and-conversation-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-operatorchannel", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/operatorchannel" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-operatorchannelclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/operatorchannelclient" + ], + "series": "0.1" + }, + { + "name": "ts-operatorchannel", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/operatorchannel" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/seats/operator-channel/fixtures/events.json b/operator-and-conversation/seats/operator-channel/fixtures/events.json similarity index 100% rename from seats/operator-channel/fixtures/events.json rename to operator-and-conversation/seats/operator-channel/fixtures/events.json diff --git a/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json similarity index 100% rename from seats/operator-channel/seat.json rename to operator-and-conversation/seats/operator-channel/seat.json diff --git a/provisioning/module.json b/provisioning/module.json new file mode 100644 index 0000000..f5796f5 --- /dev/null +++ b/provisioning/module.json @@ -0,0 +1,38 @@ +{ + "module": "mesh-provisioning-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshprovisioner", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshprovisioner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshprovisionerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshprovisionerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshprovisioner", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshprovisioner" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/provisioning/seats/mesh-provisioner/seat.json b/provisioning/seats/mesh-provisioner/seat.json new file mode 100644 index 0000000..8641fa8 --- /dev/null +++ b/provisioning/seats/mesh-provisioner/seat.json @@ -0,0 +1,298 @@ +{ + "format": 1, + "seat": "mesh-provisioner", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0349, ADR 0634, ADR 0635", + "description": "The provisioner (ADR 0349): which domain answers which kind of placeholder, what each module needs, what is applied where.", + "verbs": [ + { + "name": "provisioning", + "description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", + "optional": true, + "input": { + "properties": { + "collection": { + "description": "for collect: a collection", + "type": "string" + }, + "module": { + "description": "a module: for collect, a contributor", + "type": "string" + }, + "node": { + "description": "a node", + "type": "string" + }, + "what": { + "description": "registry (when not given), needs, applied or collect", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the registry, the needs, what is applied, or the collections' items" + } + } + ], + "states": [ + { + "name": "plans", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "what a module needs on a node", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "plan": { + "type": "object", + "description": "the module's plan" + } + }, + "required": [ + "node", + "module", + "plan" + ] + }, + "description": "one module's plan on one node" + } + ], + "description": "the needs recorded for each module on each node" + }, + { + "name": "provisions", + "template": "state", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "description": "one provision between a consumer and a provider", + "properties": { + "v": { + "type": "integer" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "consumer": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + } + }, + "provider": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + } + }, + "depends-on": { + "type": "array", + "items": { + "type": "string" + } + }, + "state": { + "type": "string", + "enum": [ + "asked", + "provided", + "waits", + "failed", + "retiring" + ] + }, + "why": { + "type": "string" + }, + "reference": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "generation": { + "type": "integer" + } + } + }, + "ask": { + "type": "string" + }, + "warrant": { + "type": "object", + "properties": { + "ask": { + "type": "string" + }, + "option": { + "type": "string" + }, + "by": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + } + } + }, + "plan": { + "type": "object", + "description": "the plan's provision" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "provision", + "consumer", + "provider", + "state", + "at" + ] + }, + "description": "a provision with no local name" + }, + { + "key": "...", + "value": { + "type": "object", + "description": "one provision between a consumer and a provider", + "properties": { + "v": { + "type": "integer" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "consumer": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + } + }, + "provider": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + } + }, + "depends-on": { + "type": "array", + "items": { + "type": "string" + } + }, + "state": { + "type": "string", + "enum": [ + "asked", + "provided", + "waits", + "failed", + "retiring" + ] + }, + "why": { + "type": "string" + }, + "reference": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "generation": { + "type": "integer" + } + } + }, + "ask": { + "type": "string" + }, + "warrant": { + "type": "object", + "properties": { + "ask": { + "type": "string" + }, + "option": { + "type": "string" + }, + "by": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + } + } + }, + "plan": { + "type": "object", + "description": "the plan's provision" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "provision", + "consumer", + "provider", + "state", + "at" + ] + }, + "description": "a provision of a local name" + } + ], + "description": "every provision between a consumer and a provider, and how it stands" + } + ] +} diff --git a/seats/node-mounts/seat.json b/provisioning/seats/node-mounts/seat.json similarity index 100% rename from seats/node-mounts/seat.json rename to provisioning/seats/node-mounts/seat.json diff --git a/seats/node-nfs-server/seat.json b/provisioning/seats/node-nfs-server/seat.json similarity index 100% rename from seats/node-nfs-server/seat.json rename to provisioning/seats/node-nfs-server/seat.json diff --git a/seats/node-provisioner/seat.json b/provisioning/seats/node-provisioner/seat.json similarity index 100% rename from seats/node-provisioner/seat.json rename to provisioning/seats/node-provisioner/seat.json diff --git a/seats/index.json b/seats/index.json index 28eb0cf..d027d6f 100644 --- a/seats/index.json +++ b/seats/index.json @@ -5,21 +5,14 @@ "mesh-broker", "mesh-vault", "mesh-config", - "mesh-provisioner", "mesh-artifact-store", "mesh-catalog", "mesh-delivery", "mesh-placement", "mesh-delivery-checker", - "mesh-conditions", - "mesh-watchdogs", - "mesh-self-check", - "mesh-healers", "mesh-wants", - "mesh-hand-acts", "mesh-enrolment", "mesh-artifact-sweep", - "operator-channel", "npm-package-registry", "go-package-registry", "container-image-registry", @@ -31,22 +24,16 @@ "mesh-dns-resolver", "node-hostname", "node-intrusion-prevention", - "node-packet-filter", "node-service-manager", "node-package-manager", "node-container-runtime", "node-environment", - "node-provisioner", "node-login-shell", "node-power", "node-hotkeys", "node-message-bus", "node-backup", - "the-private-network", "node-uplink", - "node-resolver", - "node-nfs-server", - "node-mounts", "node-reads", "node-files", "node-engine", diff --git a/seats/mesh-conditions/seat.json b/seats/mesh-conditions/seat.json deleted file mode 100644 index 5513eba..0000000 --- a/seats/mesh-conditions/seat.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "format": 1, - "seat": "mesh-conditions", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0564", - "verbs": [ - { - "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.", - "input": { - "properties": { - "days": { - "description": "with history: how many days back (default 7, at most 90)", - "type": "string" - }, - "history": { - "description": "\"true\": every raising, change, silence and clearing lately, oldest first", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "key": { - "description": "a condition's key: that one whole; with history, only its transitions", - "type": "string" - }, - "machine": { - "description": "only those about this machine", - "type": "string" - }, - "scope": { - "description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", - "type": "string" - }, - "severity": { - "description": "only urgent, or only warning", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": { - "cause": { - "description": "the cause in a word (the condition's kind when absent)", - "type": "string" - }, - "for": { - "description": "how long: 30m, 4h, 2d; at most 7d", - "type": "string" - }, - "key": { - "description": "the condition's key", - "type": "string" - }, - "why": { - "description": "why, required and recorded", - "type": "string" - } - }, - "required": [ - "key", - "for", - "why" - ], - "type": "object" - } - }, - { - "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": { - "properties": {}, - "type": "object" - } - } - ], - "events": [ - { - "name": "condition-raised" - }, - { - "name": "condition-changed" - }, - { - "name": "condition-cleared" - } - ] -} diff --git a/seats/mesh-config/seat.json b/seats/mesh-config/seat.json index d095825..640ccbb 100644 --- a/seats/mesh-config/seat.json +++ b/seats/mesh-config/seat.json @@ -218,5 +218,54 @@ ] } } + ], + "states": [ + { + "name": "resolved", + "template": "state", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "description": "the value a module on a node takes for a config key, and where it came from", + "properties": { + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": {}, + "source": { + "type": "string" + }, + "predefined": { + "type": "string", + "description": "the predefined key the value falls back to" + }, + "required": { + "type": "boolean", + "description": "an unset value is the operator's own to set" + }, + "trusted": { + "type": "boolean" + } + }, + "required": [ + "node", + "module", + "key", + "source" + ] + }, + "description": "one resolved value" + } + ], + "description": "the value each module on each node takes for each config key, built from the sources alone" + } ] } diff --git a/seats/mesh-hand-acts/seat.json b/seats/mesh-hand-acts/seat.json deleted file mode 100644 index a789dbb..0000000 --- a/seats/mesh-hand-acts/seat.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "format": 1, - "seat": "mesh-hand-acts", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0565", - "verbs": [ - { - "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": { - "properties": { - "cause": { - "description": "the cause in a word, or a condition's kind", - "type": "string" - }, - "condition": { - "description": "the key of the condition it addressed, if any (optional)", - "type": "string" - }, - "verb": { - "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", - "type": "string" - }, - "what": { - "description": "what was done, in a line", - "type": "string" - }, - "why": { - "description": "why it had to be done by hand", - "type": "string" - } - }, - "required": [ - "what", - "why" - ], - "type": "object" - } - }, - { - "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": { - "properties": { - "cause": { - "description": "only this cause", - "type": "string" - }, - "days": { - "description": "how many days back (default 14)", - "type": "string" - }, - "warrants": { - "description": "\"true\": every warrant recorded", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "who": { - "description": "only acts whose who contains this", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": { - "ask": { - "description": "its ask's id", - "type": "string" - }, - "asker": { - "description": "the module that asked", - "type": "string" - } - }, - "required": [ - "asker", - "ask" - ], - "type": "object" - } - } - ], - "events": [ - { - "name": "act-recorded" - } - ] -} diff --git a/seats/mesh-healers/seat.json b/seats/mesh-healers/seat.json deleted file mode 100644 index 0f463da..0000000 --- a/seats/mesh-healers/seat.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "format": 1, - "seat": "mesh-healers", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0564", - "verbs": [ - { - "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, and the mesh-wide brake.", - "input": { - "properties": { - "days": { - "description": "how many days of acts back (default 7)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "heal", - "description": "Take one healer's act for one open condition now, within its budget and the brake.", - "input": { - "properties": { - "key": { - "description": "the condition's key", - "type": "string" - }, - "why": { - "description": "why now, kept with the act", - "type": "string" - } - }, - "required": [ - "key", - "why" - ], - "type": "object" - } - }, - { - "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": { - "properties": { - "condition": { - "description": "the key of the condition the drill is meant to raise, if any (optional)", - "type": "string" - }, - "what": { - "description": "what was done on purpose, in a line", - "type": "string" - }, - "why": { - "description": "what the drill tests", - "type": "string" - } - }, - "required": [ - "what", - "why" - ], - "type": "object" - } - } - ], - "events": [ - { - "name": "healer-acted" - } - ] -} diff --git a/seats/mesh-provisioner/seat.json b/seats/mesh-provisioner/seat.json deleted file mode 100644 index fe16d81..0000000 --- a/seats/mesh-provisioner/seat.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "format": 1, - "seat": "mesh-provisioner", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0349", - "verbs": [ - { - "name": "provisioning", - "description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", - "optional": true, - "input": { - "properties": { - "collection": { - "description": "for collect: a collection", - "type": "string" - }, - "module": { - "description": "a module: for collect, a contributor", - "type": "string" - }, - "node": { - "description": "a node", - "type": "string" - }, - "what": { - "description": "registry (when not given), needs, applied or collect", - "type": "string" - } - }, - "type": "object" - } - } - ] -} diff --git a/seats/mesh-self-check/seat.json b/seats/mesh-self-check/seat.json deleted file mode 100644 index 56ba074..0000000 --- a/seats/mesh-self-check/seat.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "format": 1, - "seat": "mesh-self-check", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0564", - "verbs": [ - { - "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": { - "properties": { - "probe": { - "description": "one probe's id (D6): its verdict alone, with every finding", - "type": "string" - }, - "run": { - "description": "\"true\": run every probe now and answer the verdict", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "probes", - "description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", - "input": { - "properties": {}, - "type": "object" - } - } - ], - "events": [ - { - "name": "checked" - } - ] -} diff --git a/seats/mesh-watchdogs/seat.json b/seats/mesh-watchdogs/seat.json deleted file mode 100644 index 3ad9f3c..0000000 --- a/seats/mesh-watchdogs/seat.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "format": 1, - "seat": "mesh-watchdogs", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0564", - "verbs": [ - { - "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": { - "properties": { - "row": { - "description": "one row, e.g. S1", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": { - "machine": { - "description": "only those on this machine", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": { - "machine": { - "description": "one machine", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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.", - "input": { - "properties": { - "cause": { - "description": "with deliver or drop: the cause in a word (dead-letter when absent)", - "type": "string" - }, - "consumer": { - "description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on", - "type": "string" - }, - "deliver": { - "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", - "type": "string" - }, - "drop": { - "description": "a dead letter's id: drop it for good (needs why)", - "type": "string" - }, - "id": { - "description": "a dead letter's id, as the list gives it: that one whole, with its message", - "type": "string" - }, - "limit": { - "description": "how many to list, newest first (default 50); only when listing", - "type": "string" - }, - "why": { - "description": "with deliver or drop: why, required and recorded", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": {}, - "type": "object" - } - } - ] -} diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index dfcaeb8..0987936 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -709,6 +709,367 @@ "name": "node-engine.events", "max-age-days": 7 }, + "states": [ + { + "name": "have", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "one module's state on a node: what the engine has built, composed and run", + "properties": { + "v": { + "type": "integer" + }, + "fp": { + "type": "string", + "description": "the fingerprint held" + }, + "composed": { + "type": "string", + "description": "the composition's digest" + }, + "build": { + "type": "string" + }, + "phase": { + "type": "string", + "description": "how far it has come" + }, + "health": { + "type": "string", + "description": "how it is" + }, + "detail": { + "type": "string" + }, + "user-list": { + "type": "object", + "description": "the user list written" + }, + "judgement": { + "type": "string" + }, + "waits": { + "type": "string", + "description": "what it waits for" + }, + "build-alone": { + "type": "boolean" + }, + "kept": { + "type": "array", + "items": { + "type": "string" + } + }, + "sealed": { + "type": "array", + "items": { + "type": "string" + } + }, + "sealed-at": { + "type": "object" + }, + "pull-wait": { + "type": "object", + "description": "a pull the engine waits for" + }, + "handed": { + "type": "object", + "description": "the accounts handed" + } + }, + "required": [ + "v", + "phase", + "health" + ] + }, + "description": "one module on a node" + }, + { + "key": ".node", + "value": { + "type": "object", + "description": "a node's own record", + "properties": { + "v": { + "type": "integer" + }, + "composed": { + "type": "string" + }, + "windows": { + "type": "object" + }, + "busy": { + "type": "object" + }, + "strays": { + "type": "object" + }, + "filters": { + "type": "object" + }, + "profile": { + "type": "object" + }, + "record": { + "type": "object", + "description": "the node's record: its tunnel and certificate" + }, + "sealed": { + "type": "array", + "items": { + "type": "string" + } + }, + "sealed-at": { + "type": "object" + }, + "candidates": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "composes": { + "type": "string", + "description": "own, or the controller's" + }, + "composes-why": { + "type": "string" + } + }, + "required": [ + "v" + ] + }, + "description": "the node's own record" + } + ], + "history": 20, + "description": "what each node has of each module, written by its engine" + }, + { + "name": "composed", + "template": "state", + "key-forms": [ + { + "key": "..", + "value": { + "type": "object", + "description": "one composition of a module on a node under a fingerprint", + "properties": { + "V": { + "type": "integer" + }, + "Node": { + "type": "string" + }, + "Module": { + "type": "string" + }, + "FP": { + "type": "string" + }, + "Data": { + "type": "boolean" + }, + "Resources": { + "type": "array", + "items": { + "type": "object" + } + }, + "Filled": { + "type": "array", + "items": { + "type": "string" + } + }, + "Own": { + "type": "object", + "description": "the node's own adoption, identities, groups, libraries and provisioner account" + }, + "Plan": { + "type": "object", + "description": "the module's plan" + }, + "Refused": { + "type": "string", + "description": "why the composition was refused" + } + }, + "required": [ + "Node", + "Module", + "FP" + ] + }, + "description": "one composition" + } + ], + "description": "the compositions of one node, kept in a bucket per node named composed-" + }, + { + "name": "data", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "the data items a module has on a node", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "node", + "module", + "digest", + "items" + ] + }, + "description": "a module's data on a node" + } + ], + "description": "the data items of each module on each node" + }, + { + "name": "packages", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "the packages a node declares", + "properties": { + "node": { + "type": "string" + }, + "packages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "package": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "type": "string" + } + }, + "absent": { + "type": "boolean" + } + } + } + } + }, + "required": [ + "node", + "packages" + ] + }, + "description": "a node's declared packages" + } + ], + "description": "the packages each node declares" + }, + { + "name": "health", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "a node's health as its engine reports it", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "network": { + "type": "object" + }, + "peers": { + "type": "array", + "items": { + "type": "object" + } + }, + "units": { + "type": "object" + }, + "relogin": { + "type": "array", + "items": { + "type": "object" + } + }, + "found": { + "type": "array", + "items": { + "type": "object" + } + }, + "fallback": { + "type": "object" + }, + "root": { + "type": "array", + "items": { + "type": "object" + } + }, + "contract": { + "type": "integer" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "v", + "node", + "at" + ] + }, + "description": "a node's health" + } + ], + "description": "each node's health as its engine reports it" + } + ], "defs": { "Act": { "type": "object", diff --git a/seats/the-private-network/seat.json b/seats/the-private-network/seat.json deleted file mode 100644 index 858f02e..0000000 --- a/seats/the-private-network/seat.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "format": 1, - "seat": "the-private-network", - "scope": "node", - "grain": "seat-list", - "decision": "novox/hq ADR 0110" -} -- 2.54.0 From 211bdcaa5ece175170e02f0d325dbcc99f6b5b9f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 13:19:37 +0100 Subject: [PATCH 46/94] Regenerate the domain contracts; hand-acts and conditions clients; operator-channel client moves; the seat-set package ends (ADR 0634, ADR 0635; issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/go.mod | 3 - go/seats/meshconfig/contract.go | 68 + go/seats/nodeengine/contract.go | 324 + go/seats/operatorchannel/go.mod | 3 - go/seats/seats.go | 765 -- .../go/clients/meshconditionsclient/client.go | 172 + .../meshconditionsclient/client_test.go | 62 + .../go/clients/meshconditionsclient/go.mod | 5 + .../go/clients/meshhandactsclient/client.go | 88 + .../clients/meshhandactsclient/client_test.go | 73 + .../go/clients/meshhandactsclient/go.mod | 5 + .../go/seats/meshconditions/contract.go | 384 + .../go/seats/meshconditions/go.mod | 3 + .../go/seats/meshhandacts/contract.go | 222 + .../go/seats/meshhandacts/go.mod | 3 + .../go/seats/meshhealers/contract.go | 197 + health-and-repair/go/seats/meshhealers/go.mod | 3 + .../go/seats/meshselfcheck/contract.go | 101 + .../go/seats/meshselfcheck/go.mod | 3 + .../go/seats/meshwatchdogs/contract.go | 266 + .../go/seats/meshwatchdogs/go.mod | 3 + health-and-repair/seats/index.json | 10 + .../ts/seats/meshconditions/package.json | 19 + .../ts/seats/meshconditions/src/index.ts | 280 + .../ts/seats/meshconditions}/tsconfig.json | 0 .../ts/seats/meshhandacts/package.json | 19 + .../ts/seats/meshhandacts/src/index.ts | 160 + .../ts/seats/meshhandacts}/tsconfig.json | 0 .../ts/seats/meshhealers}/package.json | 4 +- .../ts/seats/meshhealers/src/index.ts | 127 + .../ts/seats/meshhealers/tsconfig.json | 14 + .../ts/seats/meshselfcheck/package.json | 19 + .../ts/seats/meshselfcheck/src/index.ts | 70 + .../ts/seats/meshselfcheck/tsconfig.json | 14 + .../ts/seats/meshwatchdogs/package.json | 19 + .../ts/seats/meshwatchdogs/src/index.ts | 182 + .../ts/seats/meshwatchdogs/tsconfig.json | 14 + .../go}/clients/operatorchannelclient/ask.go | 2 +- .../clients/operatorchannelclient/ask_test.go | 2 +- .../clients/operatorchannelclient/client.go | 2 +- .../operatorchannelclient/client_test.go | 2 +- .../operatorchannelclient/condition.go | 2 +- .../operatorchannelclient/condition_test.go | 2 +- .../go}/clients/operatorchannelclient/go.mod | 5 +- .../go}/clients/operatorchannelclient/go.sum | 0 .../go}/seats/operatorchannel/contract.go | 49 +- .../seats/operatorchannel/contract_test.go | 0 .../go/seats/operatorchannel/go.mod | 3 + operator-and-conversation/seats/index.json | 6 + .../ts}/seats/operatorchannel/package.json | 2 +- .../ts}/seats/operatorchannel/src/index.ts | 27 +- .../ts/seats/operatorchannel/tsconfig.json | 14 + .../go/seats/meshprovisioner/contract.go | 285 + provisioning/go/seats/meshprovisioner/go.mod | 3 + provisioning/seats/index.json | 9 + .../ts/seats/meshprovisioner/package.json | 19 + .../ts/seats/meshprovisioner/src/index.ts | 188 + .../ts/seats/meshprovisioner/tsconfig.json | 14 + ts/seats/meshconfig/src/index.ts | 41 + ts/seats/nodeengine/src/index.ts | 215 + ts/src/index.ts | 3 - ts/src/seats.ts | 8185 ----------------- 62 files changed, 3808 insertions(+), 8976 deletions(-) delete mode 100644 go/go.mod delete mode 100644 go/seats/operatorchannel/go.mod delete mode 100644 go/seats/seats.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/client.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/client_test.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/go.mod create mode 100644 health-and-repair/go/clients/meshhandactsclient/client.go create mode 100644 health-and-repair/go/clients/meshhandactsclient/client_test.go create mode 100644 health-and-repair/go/clients/meshhandactsclient/go.mod create mode 100644 health-and-repair/go/seats/meshconditions/contract.go create mode 100644 health-and-repair/go/seats/meshconditions/go.mod create mode 100644 health-and-repair/go/seats/meshhandacts/contract.go create mode 100644 health-and-repair/go/seats/meshhandacts/go.mod create mode 100644 health-and-repair/go/seats/meshhealers/contract.go create mode 100644 health-and-repair/go/seats/meshhealers/go.mod create mode 100644 health-and-repair/go/seats/meshselfcheck/contract.go create mode 100644 health-and-repair/go/seats/meshselfcheck/go.mod create mode 100644 health-and-repair/go/seats/meshwatchdogs/contract.go create mode 100644 health-and-repair/go/seats/meshwatchdogs/go.mod create mode 100644 health-and-repair/seats/index.json create mode 100644 health-and-repair/ts/seats/meshconditions/package.json create mode 100644 health-and-repair/ts/seats/meshconditions/src/index.ts rename {ts/seats/operatorchannel => health-and-repair/ts/seats/meshconditions}/tsconfig.json (100%) create mode 100644 health-and-repair/ts/seats/meshhandacts/package.json create mode 100644 health-and-repair/ts/seats/meshhandacts/src/index.ts rename {ts => health-and-repair/ts/seats/meshhandacts}/tsconfig.json (100%) rename {ts => health-and-repair/ts/seats/meshhealers}/package.json (60%) create mode 100644 health-and-repair/ts/seats/meshhealers/src/index.ts create mode 100644 health-and-repair/ts/seats/meshhealers/tsconfig.json create mode 100644 health-and-repair/ts/seats/meshselfcheck/package.json create mode 100644 health-and-repair/ts/seats/meshselfcheck/src/index.ts create mode 100644 health-and-repair/ts/seats/meshselfcheck/tsconfig.json create mode 100644 health-and-repair/ts/seats/meshwatchdogs/package.json create mode 100644 health-and-repair/ts/seats/meshwatchdogs/src/index.ts create mode 100644 health-and-repair/ts/seats/meshwatchdogs/tsconfig.json rename {go => operator-and-conversation/go}/clients/operatorchannelclient/ask.go (99%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/ask_test.go (99%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/client.go (98%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/client_test.go (98%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/condition.go (97%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/condition_test.go (98%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/go.mod (59%) rename {go => operator-and-conversation/go}/clients/operatorchannelclient/go.sum (100%) rename {go => operator-and-conversation/go}/seats/operatorchannel/contract.go (87%) rename {go => operator-and-conversation/go}/seats/operatorchannel/contract_test.go (100%) create mode 100644 operator-and-conversation/go/seats/operatorchannel/go.mod create mode 100644 operator-and-conversation/seats/index.json rename {ts => operator-and-conversation/ts}/seats/operatorchannel/package.json (80%) rename {ts => operator-and-conversation/ts}/seats/operatorchannel/src/index.ts (89%) create mode 100644 operator-and-conversation/ts/seats/operatorchannel/tsconfig.json create mode 100644 provisioning/go/seats/meshprovisioner/contract.go create mode 100644 provisioning/go/seats/meshprovisioner/go.mod create mode 100644 provisioning/seats/index.json create mode 100644 provisioning/ts/seats/meshprovisioner/package.json create mode 100644 provisioning/ts/seats/meshprovisioner/src/index.ts create mode 100644 provisioning/ts/seats/meshprovisioner/tsconfig.json delete mode 100644 ts/src/index.ts delete mode 100644 ts/src/seats.ts diff --git a/go/go.mod b/go/go.mod deleted file mode 100644 index efb2a0d..0000000 --- a/go/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go - -go 1.26.0 diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go index 0ab1684..3c126d1 100644 --- a/go/seats/meshconfig/contract.go +++ b/go/seats/meshconfig/contract.go @@ -3,6 +3,13 @@ // Package meshconfig is the contract of the seat "mesh-config", defined whole (seats/mesh-config/seat.json): The mesh's config values: read and set one, and the renames still open. mesh-config's own events stay under its own subjects (ADR 0620 §2). package meshconfig +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + // Seat is the seat's name. const Seat = "mesh-config" @@ -38,6 +45,53 @@ func HolderSubjectRetiring(holder string) string { return "mesh.stream." + holde // HolderSubjectRetired is where the holder publishes retired, kept in config.events. func HolderSubjectRetired(holder string) string { return "mesh.stream." + holder + ".event.retired" } +const ( + StateResolved = "resolved" +) + +// ResolvedByNodeModuleKeyKey is the key of one value of the state resolved under the form "..". +func ResolvedByNodeModuleKeyKey(node, module, key string) string { + return node + "." + module + "." + key +} + +// ParseResolvedByNodeModuleKeyKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseResolvedByNodeModuleKeyKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeResolvedByNodeModuleKeyValue reads one value of the state resolved under the form "..", refusing a field ResolvedByNodeModuleKeyValue does not know and any text after the value. +func DecodeResolvedByNodeModuleKeyValue(raw []byte) (ResolvedByNodeModuleKeyValue, error) { + var v ResolvedByNodeModuleKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // ConfigInput is what config takes. type ConfigInput struct { // list (when not given), set, clear or history @@ -129,3 +183,17 @@ type RetiredHolderEvent struct { // the node Node string `json:"node,omitempty"` } + +// ResolvedByNodeModuleKeyValue is one value of the state resolved under the key ... +type ResolvedByNodeModuleKeyValue struct { + Key string `json:"key"` + Module string `json:"module"` + Node string `json:"node"` + // the predefined key the value falls back to + Predefined string `json:"predefined,omitempty"` + // an unset value is the operator's own to set + Required bool `json:"required,omitempty"` + Source string `json:"source"` + Trusted bool `json:"trusted,omitempty"` + Value any `json:"value,omitempty"` +} diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index 8e6a947..b06da58 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -3,6 +3,13 @@ // Package nodeengine is the contract of the seat "node-engine", defined whole (seats/node-engine/seat.json): The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token. package nodeengine +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + // Seat is the seat's name. const Seat = "node-engine" @@ -57,6 +64,222 @@ func SubjectManifestReadsChanged(caller string) string { return "mesh.seat.node-engine.event.manifest-reads-changed." + caller } +const ( + StateHave = "have" + StateComposed = "composed" + StateData = "data" + StatePackages = "packages" + StateHealth = "health" +) + +// HaveByNodeModuleKey is the key of one value of the state have under the form ".". +func HaveByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParseHaveByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseHaveByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeHaveByNodeModuleValue reads one value of the state have under the form ".", refusing a field HaveByNodeModuleValue does not know and any text after the value. +func DecodeHaveByNodeModuleValue(raw []byte) (HaveByNodeModuleValue, error) { + var v HaveByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// HaveByNodeNodeKey is the key of one value of the state have under the form ".node". +func HaveByNodeNodeKey(node string) string { return node + ".node" } + +// ParseHaveByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. +func ParseHaveByNodeNodeKey(key string) (string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".node") + if !found0 || v0 == "" { + return "", false + } + rest = after0 + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeHaveByNodeNodeValue reads one value of the state have under the form ".node", refusing a field HaveByNodeNodeValue does not know and any text after the value. +func DecodeHaveByNodeNodeValue(raw []byte) (HaveByNodeNodeValue, error) { + var v HaveByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ComposedByNodeModuleFingerprintKey is the key of one value of the state composed under the form "..". +func ComposedByNodeModuleFingerprintKey(node, module, fingerprint string) string { + return node + "." + module + "." + fingerprint +} + +// ParseComposedByNodeModuleFingerprintKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseComposedByNodeModuleFingerprintKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeComposedByNodeModuleFingerprintValue reads one value of the state composed under the form "..", refusing a field ComposedByNodeModuleFingerprintValue does not know and any text after the value. +func DecodeComposedByNodeModuleFingerprintValue(raw []byte) (ComposedByNodeModuleFingerprintValue, error) { + var v ComposedByNodeModuleFingerprintValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// DataByNodeModuleKey is the key of one value of the state data under the form ".". +func DataByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParseDataByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseDataByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeDataByNodeModuleValue reads one value of the state data under the form ".", refusing a field DataByNodeModuleValue does not know and any text after the value. +func DecodeDataByNodeModuleValue(raw []byte) (DataByNodeModuleValue, error) { + var v DataByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PackagesByNodeKey is the key of one value of the state packages under the form "". +func PackagesByNodeKey(node string) string { return node } + +// ParsePackagesByNodeKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParsePackagesByNodeKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodePackagesByNodeValue reads one value of the state packages under the form "", refusing a field PackagesByNodeValue does not know and any text after the value. +func DecodePackagesByNodeValue(raw []byte) (PackagesByNodeValue, error) { + var v PackagesByNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// HealthByNodeKey is the key of one value of the state health under the form "". +func HealthByNodeKey(node string) string { return node } + +// ParseHealthByNodeKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseHealthByNodeKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeHealthByNodeValue reads one value of the state health under the form "", refusing a field HealthByNodeValue does not know and any text after the value. +func DecodeHealthByNodeValue(raw []byte) (HealthByNodeValue, error) { + var v HealthByNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // Act is one act asked through the seat, as acts reads it (ADR 0563 §9). type Act struct { // the act: hand-over, access-act, judge-directories or setuid-search @@ -561,3 +784,104 @@ type RefusedEvent = ActSaid // ManifestReadsChangedEvent is the body of the event manifest-reads-changed. type ManifestReadsChangedEvent = ManifestReadsChanged + +// HaveByNodeModuleValue is one value of the state have under the key .. +type HaveByNodeModuleValue struct { + Build string `json:"build,omitempty"` + BuildAlone bool `json:"build-alone,omitempty"` + // the composition's digest + Composed string `json:"composed,omitempty"` + Detail string `json:"detail,omitempty"` + // the fingerprint held + Fp string `json:"fp,omitempty"` + // the accounts handed + Handed map[string]any `json:"handed,omitempty"` + // how it is + Health string `json:"health"` + Judgement string `json:"judgement,omitempty"` + Kept []string `json:"kept,omitempty"` + // how far it has come + Phase string `json:"phase"` + // a pull the engine waits for + PullWait map[string]any `json:"pull-wait,omitempty"` + Sealed []string `json:"sealed,omitempty"` + SealedAt map[string]any `json:"sealed-at,omitempty"` + // the user list written + UserList map[string]any `json:"user-list,omitempty"` + V int64 `json:"v"` + // what it waits for + Waits string `json:"waits,omitempty"` +} + +// HaveByNodeNodeValue is one value of the state have under the key .node. +type HaveByNodeNodeValue struct { + Busy map[string]any `json:"busy,omitempty"` + Candidates map[string]map[string]any `json:"candidates,omitempty"` + Composed string `json:"composed,omitempty"` + // own, or the controller's + Composes string `json:"composes,omitempty"` + ComposesWhy string `json:"composes-why,omitempty"` + Filters map[string]any `json:"filters,omitempty"` + Profile map[string]any `json:"profile,omitempty"` + // the node's record: its tunnel and certificate + Record map[string]any `json:"record,omitempty"` + Sealed []string `json:"sealed,omitempty"` + SealedAt map[string]any `json:"sealed-at,omitempty"` + Strays map[string]any `json:"strays,omitempty"` + V int64 `json:"v"` + Windows map[string]any `json:"windows,omitempty"` +} + +// ComposedByNodeModuleFingerprintValue is one value of the state composed under the key ... +type ComposedByNodeModuleFingerprintValue struct { + Data bool `json:"Data,omitempty"` + FP string `json:"FP"` + Filled []string `json:"Filled,omitempty"` + Module string `json:"Module"` + Node string `json:"Node"` + // the node's own adoption, identities, groups, libraries and provisioner account + Own map[string]any `json:"Own,omitempty"` + // the module's plan + Plan map[string]any `json:"Plan,omitempty"` + // why the composition was refused + Refused string `json:"Refused,omitempty"` + Resources []map[string]any `json:"Resources,omitempty"` + V int64 `json:"V,omitempty"` +} + +// DataByNodeModuleValue is one value of the state data under the key .. +type DataByNodeModuleValue struct { + Digest string `json:"digest"` + Items []map[string]any `json:"items"` + Module string `json:"module"` + Node string `json:"node"` + V int64 `json:"v,omitempty"` +} + +// PackagesByNodeValuePackagesItem is a type of the contract. +type PackagesByNodeValuePackagesItem struct { + Absent bool `json:"absent,omitempty"` + Modules []string `json:"modules,omitempty"` + Package string `json:"package,omitempty"` +} + +// PackagesByNodeValue is one value of the state packages under the key . +type PackagesByNodeValue struct { + Node string `json:"node"` + Packages []PackagesByNodeValuePackagesItem `json:"packages"` +} + +// HealthByNodeValue is one value of the state health under the key . +type HealthByNodeValue struct { + At string `json:"at"` + Contract int64 `json:"contract,omitempty"` + Fallback map[string]any `json:"fallback,omitempty"` + Found []map[string]any `json:"found,omitempty"` + Network map[string]any `json:"network,omitempty"` + Node string `json:"node"` + Peers []map[string]any `json:"peers,omitempty"` + Relogin []map[string]any `json:"relogin,omitempty"` + Root []map[string]any `json:"root,omitempty"` + Units map[string]any `json:"units,omitempty"` + V int64 `json:"v"` +} diff --git a/go/seats/operatorchannel/go.mod b/go/seats/operatorchannel/go.mod deleted file mode 100644 index 894ee5e..0000000 --- a/go/seats/operatorchannel/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/operatorchannel - -go 1.26.0 diff --git a/go/seats/seats.go b/go/seats/seats.go deleted file mode 100644 index a545cdd..0000000 --- a/go/seats/seats.go +++ /dev/null @@ -1,765 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package seats is the mesh's seat list, generated from every seat's definition (design 26 *The seat set has one -// source*): what each seat is, where it is held, the provision it delivers, its verbs with their inputs, what it -// accepts and emits, what it receives and the paths it owns. mesh-sdk's runtime reads it; a seat its holder declares -// itself has a name here and no entry in the list. -package seats - -// Seat is one role the mesh defines. -type Seat struct { - Name string - Scope string - Grain string - Replicated bool - Delivers string - Accepts []string - Emits []string - PulledByKind bool - Yields bool - Serves []Verb - Receives []Receivable - Paths []Path - HeldByTheEngine bool - Decision string -} - -// Verb is one verb a seat serves: its input and answer as JSON Schema. -type Verb struct { - Name string - Description string - Input map[string]any - Output map[string]any - Optional bool - Replaces []string -} - -// Receivable is one kind of contribution a seat receives. -type Receivable struct { - Kind string - Comment string - Dirs bool - Shape *Shape - Offered bool -} - -// Shape is a kind given as data. Its checks are the runtime's. -type Shape struct { - Places []PlaceField - Order string - Examples []map[string]any - Text bool -} - -// PlaceField is one field a holder places by, and the values it takes. -type PlaceField struct { - Field string - Values []string -} - -// Path is one path on its machine a seat's holder owns. -type Path struct { - Path string - StepsBackFor string -} - -// The name of every seat defined. -const ( - MeshStore = "mesh-store" - MeshBroker = "mesh-broker" - MeshVault = "mesh-vault" - MeshConfig = "mesh-config" - MeshProvisioner = "mesh-provisioner" - MeshArtifactStore = "mesh-artifact-store" - MeshCatalog = "mesh-catalog" - MeshDelivery = "mesh-delivery" - MeshPlacement = "mesh-placement" - MeshDeliveryChecker = "mesh-delivery-checker" - MeshConditions = "mesh-conditions" - MeshWatchdogs = "mesh-watchdogs" - MeshSelfCheck = "mesh-self-check" - MeshHealers = "mesh-healers" - MeshWants = "mesh-wants" - MeshHandActs = "mesh-hand-acts" - MeshEnrolment = "mesh-enrolment" - MeshArtifactSweep = "mesh-artifact-sweep" - OperatorChannel = "operator-channel" - NpmPackageRegistry = "npm-package-registry" - GoPackageRegistry = "go-package-registry" - ContainerImageRegistry = "container-image-registry" - OciArtifactRegistry = "oci-artifact-registry" - Git = "git" - NodeBuildGo = "node-build-go" - NodeBuildNode = "node-build-node" - NodeBuildImage = "node-build-image" - MeshDnsResolver = "mesh-dns-resolver" - NodeHostname = "node-hostname" - NodeIntrusionPrevention = "node-intrusion-prevention" - NodePacketFilter = "node-packet-filter" - NodeServiceManager = "node-service-manager" - NodePackageManager = "node-package-manager" - NodeContainerRuntime = "node-container-runtime" - NodeEnvironment = "node-environment" - NodeProvisioner = "node-provisioner" - NodeLoginShell = "node-login-shell" - NodePower = "node-power" - NodeHotkeys = "node-hotkeys" - NodeMessageBus = "node-message-bus" - NodeBackup = "node-backup" - ThePrivateNetwork = "the-private-network" - NodeUplink = "node-uplink" - NodeResolver = "node-resolver" - NodeNfsServer = "node-nfs-server" - NodeMounts = "node-mounts" - NodeReads = "node-reads" - NodeFiles = "node-files" - NodeEngine = "node-engine" - NodeLoginManager = "node-login-manager" - NodeDisplayServer = "node-display-server" - NodeDisplaySession = "node-display-session" - NodeTerminalEmulator = "node-terminal-emulator" - NodeLauncher = "node-launcher" - NodeNotifier = "node-notifier" - NodeLockScreen = "node-lock-screen" - NodeClipboard = "node-clipboard" - NodeBar = "node-bar" - NodeCompositor = "node-compositor" - NodeSecretService = "node-secret-service" -) - -// Names is every seat defined, in reading order, the ones their holders declare included. -func Names() []string { - return []string{MeshStore, MeshBroker, MeshVault, MeshConfig, MeshProvisioner, MeshArtifactStore, MeshCatalog, MeshDelivery, MeshPlacement, MeshDeliveryChecker, MeshConditions, MeshWatchdogs, MeshSelfCheck, MeshHealers, MeshWants, MeshHandActs, MeshEnrolment, MeshArtifactSweep, OperatorChannel, NpmPackageRegistry, GoPackageRegistry, ContainerImageRegistry, OciArtifactRegistry, Git, NodeBuildGo, NodeBuildNode, NodeBuildImage, MeshDnsResolver, NodeHostname, NodeIntrusionPrevention, NodePacketFilter, NodeServiceManager, NodePackageManager, NodeContainerRuntime, NodeEnvironment, NodeProvisioner, NodeLoginShell, NodePower, NodeHotkeys, NodeMessageBus, NodeBackup, ThePrivateNetwork, NodeUplink, NodeResolver, NodeNfsServer, NodeMounts, NodeReads, NodeFiles, NodeEngine, NodeLoginManager, NodeDisplayServer, NodeDisplaySession, NodeTerminalEmulator, NodeLauncher, NodeNotifier, NodeLockScreen, NodeClipboard, NodeBar, NodeCompositor, NodeSecretService} -} - -// List is the seat list, in reading order: a new copy each call. -func List() []Seat { - return []Seat{ - {Name: MeshStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0079", Delivers: "postgres-database", - Serves: []Verb{ - {Name: "databases", Description: "Every database the store holds, with its on-disk size.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"psql -l"}}, - {Name: "query", Description: "One read-only statement against one database the store holds.", - Input: map[string]any{"properties": map[string]any{"database": map[string]any{"description": "the database to query", "type": "string"}, "sql": map[string]any{"description": "the read-only statement", "type": "string"}}, "required": []string{"database", "sql"}, "type": "object"}, Replaces: []string{"psql"}}, - }}, - {Name: MeshBroker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0079", Delivers: "mesh-bus", Emits: []string{"created", "refused", "drifted", "drift-cleared", "full", "full-cleared", "orphaned", "recreated", "dropped"}, - Serves: []Verb{ - {Name: "list", Description: "Every entry the bus module created, its template, its settings and its fill against its cap.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every entry", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, - {Name: "show", Description: "One entry, its template, settings and fill.", Optional: true, - Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "check", Description: "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"differences": map[string]any{"description": "every difference", "items": map[string]any{"properties": map[string]any{"asked": map[string]any{"description": "asked", "type": "object"}, "grows": map[string]any{"description": "the difference only grows: apply may act on it", "type": "boolean"}, "held": map[string]any{"description": "held", "type": "object"}, "local-name": map[string]any{"description": "the entry", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, "type": "array"}}, "required": []string{"differences"}, "type": "object"}}, - {Name: "orphans", Description: "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", Optional: true, - Input: map[string]any{"description": "no arguments", "properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"entries": map[string]any{"description": "every orphan", "items": map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}, "type": "array"}}, "required": []string{"entries"}, "type": "object"}}, - {Name: "create", Description: "Create one entry now.", Optional: true, - Input: map[string]any{"description": "one piece of bus state as a manifest asks it", "properties": map[string]any{"configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}}, "required": []string{"module", "local-name", "storage-type"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "apply", Description: "Apply a difference that only grows: a larger cap, a longer age.", Optional: true, - Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}}, "required": []string{"module", "local-name"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "recreate", Description: "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", Optional: true, - Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, - Output: map[string]any{"description": "one entry as the bus holds it", "properties": map[string]any{"bus-name": map[string]any{"description": "its name on the bus, _", "type": "string"}, "configuration": map[string]any{"description": "its own configuration, where no template fits", "type": "object"}, "fill": map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "bytes held", "type": "integer"}, "cap": map[string]any{"description": "its cap in bytes", "type": "integer"}, "count": map[string]any{"description": "messages, keys or objects held", "type": "integer"}}, "required": []string{"count", "bytes", "cap"}, "type": "object"}, "local-name": map[string]any{"description": "its local name in the module's manifest", "type": "string"}, "module": map[string]any{"description": "the module asking it", "type": "string"}, "options": map[string]any{"description": "the template's options the module set, within their bounds", "type": "object"}, "settings": map[string]any{"description": "the settings the bus holds", "type": "object"}, "state": map[string]any{"description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", "enum": []string{"held", "refused", "waiting", "drifted", "full", "orphaned"}, "type": "string"}, "storage-type": map[string]any{"description": "how the bus keeps it", "enum": []string{"stream", "key-value", "object-store", "ledger"}, "type": "string"}, "template": map[string]any{"description": "its template; empty for a configuration of its own", "enum": []string{"state", "events", "work-queue", "log", "objects", "ledger", ""}, "type": "string"}, "why": map[string]any{"description": "why it is in its state, in plain words; empty when held as asked", "type": "string"}}, "required": []string{"module", "local-name", "storage-type", "bus-name"}, "type": "object"}}, - {Name: "drop", Description: "Remove one orphan, after a stream snapshot.", Optional: true, - Input: map[string]any{"properties": map[string]any{"local-name": map[string]any{"description": "the entry's local name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", "type": "string"}, "why": map[string]any{"description": "why, kept in the record", "type": "string"}}, "required": []string{"module", "local-name", "warrant", "why"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"dropped": map[string]any{"description": "it is gone", "type": "boolean"}, "snapshot": map[string]any{"description": "the snapshot taken first", "type": "string"}}, "required": []string{"dropped"}, "type": "object"}}, - }}, - {Name: MeshVault, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0161", Delivers: "secret", - Serves: []Verb{ - {Name: "get", Description: "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.", Optional: true, - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the secret's name", "type": "string"}, "public_key": map[string]any{"description": "the asker's X25519 public key as SPKI PEM — never a private key", "type": "string"}}, "required": []string{"name", "public_key"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "secret-ask", Description: "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", Optional: true, - Input: map[string]any{"properties": map[string]any{"asked_by": map[string]any{"description": "who asked, as the bus named its caller", "type": "string"}, "call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "desk": map[string]any{"description": "the machine whose prompt asks", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the secret's name in the module", "type": "string"}, "node": map[string]any{"description": "the machine the secret is for", "type": "string"}, "outcome": map[string]any{"description": "instead of sealed: how the prompt ended", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "issued", Description: "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.", Optional: true, - Input: map[string]any{"properties": map[string]any{"call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "holders": map[string]any{"description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone", "items": map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the consuming module", "type": "string"}, "node": map[string]any{"description": "the holder's machine", "type": "string"}}, "type": "object"}, "type": "array"}, "module": map[string]any{"description": "the provider's module", "type": "string"}, "provider": map[string]any{"description": "the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "rotate", Description: "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.", Optional: true, - Input: map[string]any{"properties": map[string]any{"by": map[string]any{"description": "who asked, when the controller carries the act", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the secret's name in the module", "type": "string"}, "node": map[string]any{"description": "the machine, for a module's own secret", "type": "string"}, "provider": map[string]any{"description": "the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "move-in", Description: "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.", Optional: true, - Input: map[string]any{"properties": map[string]any{"call": map[string]any{"description": "the second step: the call the first step answered", "type": "string"}, "consumer": map[string]any{"description": "the consumer's machine", "type": "string"}, "consumer_module": map[string]any{"description": "the consuming module", "type": "string"}, "generation": map[string]any{"description": "the controller's generation for the value", "type": "string"}, "local": map[string]any{"description": "the credential's local name, when it has one", "type": "string"}, "made_at": map[string]any{"description": "when it was made (RFC 3339)", "type": "string"}, "module": map[string]any{"description": "its module", "type": "string"}, "name": map[string]any{"description": "its name in the module", "type": "string"}, "node": map[string]any{"description": "a module's own secret: its machine", "type": "string"}, "origin": map[string]any{"description": "made, accepted or issued, as the controller's store says", "type": "string"}, "provider": map[string]any{"description": "a pair credential: the provider's machine", "type": "string"}, "provision": map[string]any{"description": "the provision", "type": "string"}, "read_back_to": map[string]any{"description": "the second step: a raw X25519 public key the read-back is sealed to", "type": "string"}, "sealed": map[string]any{"description": "the second step: the value sealed to the call's key — never the value", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "node-key", Description: "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.", Optional: true, - Input: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the machine's sealing key, a raw X25519 public key in base64", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "own", Description: "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.", Optional: true, - Input: map[string]any{"properties": map[string]any{"credentials": map[string]any{"description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}", "items": map[string]any{"type": "object"}, "type": "array"}}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "issue-account", Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", Optional: true, - Input: map[string]any{"properties": map[string]any{"user": map[string]any{"description": "the bus user, e.g. ace.mesh-build-go", "type": "string"}}, "required": []string{"user"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "sign", Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", Optional: true, - Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "node-certificate or enrolment", "type": "string"}, "node": map[string]any{"description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", "type": "string"}, "reply": map[string]any{"description": "the redeem answer's bytes in base64, for an enrolment", "type": "string"}, "serving_key": map[string]any{"description": "from mesh-enrolment: the serving key's public half the enrolling node presented", "type": "string"}}, "required": []string{"kind"}, "type": "object"}, - Output: map[string]any{"description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half", "properties": map[string]any{"authority": map[string]any{"description": "the authority's certificate that signed it, as PEM", "type": "string"}, "certificate": map[string]any{"description": "the node certificate as PEM, for a node certificate", "type": "string"}, "digest": map[string]any{"description": "sha256: of the bytes signed", "type": "string"}, "kind": map[string]any{"description": "node-certificate or enrolment, as asked", "type": "string"}, "node": map[string]any{"description": "the machine, for a node certificate", "type": "string"}, "signature": map[string]any{"description": "the signature in base64, for an enrolment", "type": "string"}, "signer": map[string]any{"description": "the signer's fingerprint", "type": "string"}}, "required": []string{"kind", "signer", "digest"}, "type": "object"}}, - {Name: "enrolment-token", Description: "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", Optional: true, - Input: map[string]any{"properties": map[string]any{"drop": map[string]any{"description": "\"true\": drop the node's token and its hash", "enum": []string{"true", "false"}, "type": "string"}, "expires": map[string]any{"description": "when the token expires (RFC 3339), as the enrolment's key says", "type": "string"}, "node": map[string]any{"description": "the node the token is for", "type": "string"}, "sealed": map[string]any{"description": "\"true\": answer the token sealed to the operator key", "enum": []string{"true", "false"}, "type": "string"}, "tunnel": map[string]any{"additionalProperties": false, "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", "properties": map[string]any{"address": map[string]any{"description": "the joining node's tunnel address", "type": "string"}, "hub_endpoint": map[string]any{"description": "the hub's endpoint, host:port", "type": "string"}, "hub_key": map[string]any{"description": "the hub's tunnel public key", "type": "string"}, "key": map[string]any{"description": "the joining machine's own tunnel public key, as given at issue", "type": "string"}, "range": map[string]any{"description": "the private network's range, a prefix", "type": "string"}}, "required": []string{"address", "hub_endpoint", "hub_key", "key", "range"}, "type": "object"}}, "required": []string{"node"}, "type": "object"}, - Output: map[string]any{"description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash", "properties": map[string]any{"broker": map[string]any{"description": "the bus's address the machine dials", "type": "string"}, "dropped": map[string]any{"description": "a drop left neither the token nor its hash", "type": "boolean"}, "fingerprint": map[string]any{"description": "the fingerprint of the bus's certificate", "type": "string"}, "node": map[string]any{"description": "the node the token is for", "type": "string"}, "sealed": map[string]any{"description": "the token sealed to the operator key, when asked sealed, relayed as it is", "type": "string"}, "signer": map[string]any{"description": "the vault's public signing key in base64", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, - {Name: "node-account", Description: "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", Optional: true, - Input: map[string]any{"properties": map[string]any{"keys_digest": map[string]any{"description": "the digest of every public key the node presented (delivery.KeysDigest)", "type": "string"}, "node": map[string]any{"description": "the joining node", "type": "string"}, "retire": map[string]any{"description": "\"true\": retire the account minted for this node", "enum": []string{"true", "false"}, "type": "string"}, "sealing_key": map[string]any{"description": "the sealing key's public half the node presented, which the account is sealed to", "type": "string"}}, "required": []string{"node"}, "type": "object"}, - Output: map[string]any{"description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account", "properties": map[string]any{"account": map[string]any{"description": "the account sealed to the node's sealing key, in base64", "type": "string"}, "login": map[string]any{"description": "the login minted", "type": "string"}, "node": map[string]any{"description": "the joining node", "type": "string"}, "retired": map[string]any{"description": "a retire left no account", "type": "boolean"}}, "required": []string{"node"}, "type": "object"}}, - {Name: "rotate-node", Description: "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", Optional: true, - Input: map[string]any{"properties": map[string]any{"act": map[string]any{"description": "the id of the hand act recorded for it in mesh-hand-acts", "type": "string"}, "node": map[string]any{"description": "the node whose account rotates", "type": "string"}, "why": map[string]any{"description": "why, in a sentence", "type": "string"}}, "required": []string{"node", "why", "act"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "node-account-confirm", Description: "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "abandon", Description: "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.", Optional: true, - Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node whose rotation is abandoned", "type": "string"}}, "required": []string{"node"}, "type": "object"}, - Output: map[string]any{"description": "the holder's answer", "type": "object"}}, - {Name: "retire", Description: "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", Optional: true, - Input: map[string]any{"properties": map[string]any{"ask": map[string]any{"description": "the id of the operator's ask the warrant answers", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "name": map[string]any{"description": "the own secret's name", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "warrant": map[string]any{"description": "the operator's warrant for this node, module and name: the hand act's id", "type": "string"}}, "required": []string{"node", "module", "name", "warrant", "ask"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"retired": map[string]any{"description": "the secret is gone", "type": "boolean"}}, "required": []string{"retired"}, "type": "object"}}, - }}, - {Name: MeshConfig, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0348, ADR 0565", - Serves: []Verb{ - {Name: "config", 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.", Optional: true, - Input: map[string]any{"properties": map[string]any{"action": map[string]any{"description": "list (when not given), set, clear or history", "type": "string"}, "by": map[string]any{"description": "who gave the value at the controller's terminal", "type": "string"}, "key": map[string]any{"description": "the config key", "type": "string"}, "limit": map[string]any{"description": "for history: at most this many, newest", "type": "number"}, "module": map[string]any{"description": "a module: its own value, with node", "type": "string"}, "node": map[string]any{"description": "a node", "type": "string"}, "value": map[string]any{"description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list"}, "why": map[string]any{"description": "for set and clear: why", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the values asked, or the one set or cleared, or its history", "type": "object"}}, - {Name: "renames", Description: "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"renames": map[string]any{"description": "every open rename", "items": map[string]any{"properties": map[string]any{"at": map[string]any{"description": "when it moved, RFC 3339", "type": "string"}, "from": map[string]any{"description": "the old name", "type": "string"}, "module": map[string]any{"description": "the module", "type": "string"}, "open": map[string]any{"description": "the nodes still holding the old name open", "items": map[string]any{"type": "string"}, "type": "array"}, "to": map[string]any{"description": "the new name", "type": "string"}}, "required": []string{"module", "from", "to"}, "type": "object"}, "type": "array"}}, "required": []string{"renames"}, "type": "object"}}, - }}, - {Name: MeshProvisioner, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0349", - Serves: []Verb{ - {Name: "provisioning", Description: "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", Optional: true, - Input: map[string]any{"properties": map[string]any{"collection": map[string]any{"description": "for collect: a collection", "type": "string"}, "module": map[string]any{"description": "a module: for collect, a contributor", "type": "string"}, "node": map[string]any{"description": "a node", "type": "string"}, "what": map[string]any{"description": "registry (when not given), needs, applied or collect", "type": "string"}}, "type": "object"}}, - }}, - {Name: MeshArtifactStore, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0075"}, - {Name: MeshCatalog, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0604", - Serves: []Verb{ - {Name: "modules", Description: "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "module", Description: "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module's name", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, - {Name: "provides", Description: "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", Optional: true, - Input: map[string]any{"properties": map[string]any{"provision": map[string]any{"description": "the provision, such as postgres-database", "type": "string"}}, "required": []string{"provision"}, "type": "object"}}, - {Name: "dependents", Description: "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module's name", "type": "string"}}, "required": []string{"module"}, "type": "object"}}, - }}, - {Name: MeshDelivery, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0239", - Serves: []Verb{ - {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", - Input: map[string]any{"properties": map[string]any{"all": map[string]any{"description": "\"true\": the final ones of the last thirty days too", "enum": []string{"true", "false"}, "type": "string"}, "group": map[string]any{"description": "a group's id (its branch name)", "type": "string"}, "repository": map[string]any{"description": "owner/repository", "type": "string"}, "state": map[string]any{"description": "one state, e.g. delivering or held", "type": "string"}}, "type": "object"}}, - {Name: "times", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "a delivery's id, or a group's", "type": "string"}}, "required": []string{"id"}, "type": "object"}}, - {Name: "groups", Description: "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", - Input: map[string]any{"properties": map[string]any{"all": map[string]any{"description": "\"true\": the groups that ended too", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, - {Name: "what-if", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "a commit's sha, or the start of one, instead of a pull request", "type": "string"}, "number": map[string]any{"description": "a pull request's number: its head is read", "type": "string"}, "repository": map[string]any{"description": "owner/repository", "type": "string"}}, "required": []string{"repository"}, "type": "object"}}, - {Name: "table", Description: "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "stalled", Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "recheck", Description: "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the delivery's id", "type": "string"}, "why": map[string]any{"description": "why, kept with the transition", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, - {Name: "release", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "stop", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the delivery's id", "type": "string"}, "why": map[string]any{"description": "the condition it answers", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, - {Name: "retire-history", Description: "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "settle", Description: "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", Optional: true, - Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"ids": map[string]any{"description": "the held deliveries' ids (owner/repository@commit)", "items": map[string]any{"type": "string"}, "minItems": float64(1), "type": "array"}, "why": map[string]any{"description": "why they may be settled, shown to the operator", "type": "string"}}, "required": []string{"ids", "why"}, "type": "object"}}, - }}, - {Name: MeshPlacement, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0567", - Serves: []Verb{ - {Name: "placement", Description: "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", Optional: true, - Input: map[string]any{"properties": map[string]any{"history": map[string]any{"description": "\"true\": the changes in order, from the stream and the state's revisions", "enum": []string{"true", "false"}, "type": "string"}, "module": map[string]any{"description": "one module: the nodes it is placed on", "type": "string"}, "node": map[string]any{"description": "one node", "type": "string"}, "seats": map[string]any{"description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, - {Name: "proposals", 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.", Optional: true, - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one proposal's id", "type": "string"}}, "type": "object"}}, - {Name: "propose", Description: "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", Optional: true, - Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"changes": map[string]any{"description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", "items": map[string]any{"required": []string{"op", "node"}, "type": "object"}, "minItems": float64(1), "type": "array"}, "warrant": map[string]any{"description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", "required": []string{"node", "address", "ask", "warrant"}, "type": "object"}, "why": map[string]any{"description": "why, shown to the operator with the proposal", "type": "string"}}, "required": []string{"why", "changes"}, "type": "object"}}, - {Name: "withdraw", Description: "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", Optional: true, - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the proposal's id", "type": "string"}, "why": map[string]any{"description": "why, kept in the event", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}}, - {Name: "again", Description: "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", Optional: true, - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the proposal's id", "type": "string"}}, "required": []string{"id"}, "type": "object"}}, - }}, - {Name: MeshDeliveryChecker, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0547", - Serves: []Verb{ - {Name: "try", Description: "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", Optional: true, - Input: map[string]any{"properties": map[string]any{"head": map[string]any{"description": "the full commit to try, 40 or 64 hex digits", "type": "string"}, "level": map[string]any{"description": "unit or integration: only this level; both when absent", "type": "string"}, "repository": map[string]any{"description": "/", "type": "string"}, "tests": map[string]any{"description": "a go test -run pattern: only those tests run, and nothing is kept", "type": "string"}, "trunk": map[string]any{"description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", "type": "string"}}, "required": []string{"repository", "head"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id", "type": "string"}, "state": map[string]any{"description": "its state", "type": "string"}}, "required": []string{"id"}, "type": "object"}, Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, - {Name: "try-read", Description: "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", Optional: true, - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id, try-", "type": "string"}}, "required": []string{"id"}, "type": "object"}, - Output: map[string]any{"description": "the try: its state, its steps and its verdict when it has one", "type": "object"}}, - {Name: "try-end", Description: "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", Optional: true, - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "the try's id, try-", "type": "string"}, "why": map[string]any{"description": "why it is ended, in words", "type": "string"}}, "required": []string{"id", "why"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"ended": map[string]any{"description": "it is ended", "type": "boolean"}, "id": map[string]any{"description": "the try", "type": "string"}}, "required": []string{"id", "ended"}, "type": "object"}}, - }}, - {Name: MeshConditions, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"condition-raised", "condition-changed", "condition-cleared"}, - Serves: []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.", - Input: map[string]any{"properties": map[string]any{"days": map[string]any{"description": "with history: how many days back (default 7, at most 90)", "type": "string"}, "history": map[string]any{"description": "\"true\": every raising, change, silence and clearing lately, oldest first", "enum": []string{"true", "false"}, "type": "string"}, "key": map[string]any{"description": "a condition's key: that one whole; with history, only its transitions", "type": "string"}, "machine": map[string]any{"description": "only those about this machine", "type": "string"}, "scope": map[string]any{"description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", "type": "string"}, "severity": map[string]any{"description": "only urgent, or only warning", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "the cause in a word (the condition's kind when absent)", "type": "string"}, "for": map[string]any{"description": "how long: 30m, 4h, 2d; at most 7d", "type": "string"}, "key": map[string]any{"description": "the condition's key", "type": "string"}, "why": map[string]any{"description": "why, required and recorded", "type": "string"}}, "required": []string{"key", "for", "why"}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: MeshWatchdogs, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", - Serves: []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: map[string]any{"properties": map[string]any{"row": map[string]any{"description": "one row, e.g. S1", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"machine": map[string]any{"description": "only those on this machine", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"machine": map[string]any{"description": "one machine", "type": "string"}}, "type": "object"}}, - {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.", - Input: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "with deliver or drop: the cause in a word (dead-letter when absent)", "type": "string"}, "consumer": map[string]any{"description": "a consumer's name, or .: only what that one gave up on", "type": "string"}, "deliver": map[string]any{"description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", "type": "string"}, "drop": map[string]any{"description": "a dead letter's id: drop it for good (needs why)", "type": "string"}, "id": map[string]any{"description": "a dead letter's id, as the list gives it: that one whole, with its message", "type": "string"}, "limit": map[string]any{"description": "how many to list, newest first (default 50); only when listing", "type": "string"}, "why": map[string]any{"description": "with deliver or drop: why, required and recorded", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: MeshSelfCheck, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"checked"}, - Serves: []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: map[string]any{"properties": map[string]any{"probe": map[string]any{"description": "one probe's id (D6): its verdict alone, with every finding", "type": "string"}, "run": map[string]any{"description": "\"true\": run every probe now and answer the verdict", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}}, - {Name: "probes", Description: "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: MeshHealers, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0564", Emits: []string{"healer-acted"}, - Serves: []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, and the mesh-wide brake.", - Input: map[string]any{"properties": map[string]any{"days": map[string]any{"description": "how many days of acts back (default 7)", "type": "string"}}, "type": "object"}}, - {Name: "heal", Description: "Take one healer's act for one open condition now, within its budget and the brake.", - Input: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the condition's key", "type": "string"}, "why": map[string]any{"description": "why now, kept with the act", "type": "string"}}, "required": []string{"key", "why"}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"condition": map[string]any{"description": "the key of the condition the drill is meant to raise, if any (optional)", "type": "string"}, "what": map[string]any{"description": "what was done on purpose, in a line", "type": "string"}, "why": map[string]any{"description": "what the drill tests", "type": "string"}}, "required": []string{"what", "why"}, "type": "object"}}, - }}, - {Name: MeshWants, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0565", Emits: []string{"policy-changed"}, - Serves: []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, each node's own composition and the assigner's findings.", - Input: map[string]any{"properties": map[string]any{"compare": map[string]any{"description": "\"true\": each node's own composition, by its digest, and the assigner's findings", "enum": []string{"true", "false"}, "type": "string"}, "module": map[string]any{"description": "one module", "type": "string"}, "policy": map[string]any{"description": "with module: roll, together, record, or default to take a person's choice back", "type": "string"}, "why": map[string]any{"description": "with policy: why, required and kept", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: MeshHandActs, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0565", Emits: []string{"act-recorded"}, - Serves: []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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "the cause in a word, or a condition's kind", "type": "string"}, "condition": map[string]any{"description": "the key of the condition it addressed, if any (optional)", "type": "string"}, "verb": map[string]any{"description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", "type": "string"}, "what": map[string]any{"description": "what was done, in a line", "type": "string"}, "why": map[string]any{"description": "why it had to be done by hand", "type": "string"}}, "required": []string{"what", "why"}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"cause": map[string]any{"description": "only this cause", "type": "string"}, "days": map[string]any{"description": "how many days back (default 14)", "type": "string"}, "warrants": map[string]any{"description": "\"true\": every warrant recorded", "enum": []string{"true", "false"}, "type": "string"}, "who": map[string]any{"description": "only acts whose who contains this", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"ask": map[string]any{"description": "its ask's id", "type": "string"}, "asker": map[string]any{"description": "the module that asked", "type": "string"}}, "required": []string{"asker", "ask"}, "type": "object"}}, - }}, - {Name: MeshEnrolment, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0587", Emits: []string{"enrolment-issued", "node-enrolled", "enrolment-refused", "enrolment-revoked", "enrolment-expired"}, - Serves: []Verb{ - {Name: "issue", Description: "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.", - Input: map[string]any{"properties": map[string]any{"expires": map[string]any{"description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", "type": "string"}, "node": map[string]any{"description": "the new machine's name in the mesh", "type": "string"}, "overlay_key": map[string]any{"description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", "type": "string"}, "why": map[string]any{"description": "why it joins, shown to the operator with the ask", "type": "string"}}, "required": []string{"node", "why", "overlay_key"}, "type": "object"}}, - {Name: "redeem", Description: "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", - Input: map[string]any{"additionalProperties": false, "properties": map[string]any{"node": map[string]any{"description": "the node the token was issued for", "type": "string"}, "overlay_key": map[string]any{"description": "the private network key's public half", "type": "string"}, "profile": map[string]any{"description": "what the machine is, as its record says it", "type": "object"}, "proof": map[string]any{"description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64", "type": "string"}, "public_key": map[string]any{"description": "the identity key's public half, base64", "type": "string"}, "sealing_key": map[string]any{"description": "the sealing key's public half, which the account is sealed to", "type": "string"}, "serving_key": map[string]any{"description": "the serving key's public half, which the certificate binds", "type": "string"}, "tunnel": map[string]any{"description": "a tunnel found on the machine: everything but its private key", "type": "object"}}, "required": []string{"node", "public_key", "sealing_key", "proof"}, "type": "object"}}, - {Name: "confirm", Description: "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "revoke", Description: "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.", - Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "why": map[string]any{"description": "why, recorded with the hand act", "type": "string"}}, "required": []string{"node", "why"}, "type": "object"}}, - {Name: "tokens", Description: "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.", - Input: map[string]any{"properties": map[string]any{"history": map[string]any{"description": "\"true\": the ended enrolments too", "enum": []string{"true", "false"}, "type": "string"}, "node": map[string]any{"description": "one node", "type": "string"}}, "type": "object"}}, - {Name: "again", Description: "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.", - Input: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the node whose ask is raised again", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, - }}, - {Name: MeshArtifactSweep, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"}, - Serves: []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: map[string]any{"properties": map[string]any{"collected": map[string]any{"description": "\"true\": list the collected references too (counted always)", "enum": []string{"true", "false"}, "type": "string"}, "repository": map[string]any{"description": "only this repository's references, as / (the counts stay whole)", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"references": map[string]any{"description": "the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"references"}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": let go of what is eligible (needs why); without it nothing is done", "enum": []string{"true", "false"}, "type": "string"}, "most": map[string]any{"description": "let go of at most this many (500 by default, at most 5000)", "type": "string"}, "why": map[string]any{"description": "with confirm: why, required and recorded", "type": "string"}}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {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: map[string]any{"properties": map[string]any{"node": map[string]any{"description": "the machine's name", "type": "string"}}, "required": []string{"node"}, "type": "object"}}, - }}, - {Name: NpmPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0109", Delivers: "npm-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, - Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, - Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, - }}, - {Name: GoPackageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Delivers: "go-package-registry", Emits: []string{"published", "refused", "removed", "kept"}, - Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, - Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, - }}, - {Name: ContainerImageRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, - Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, - Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, - {Name: "mirror", Description: "Copy an outside image into the store by its pinned digest; emits published or refused.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its pinned digest, sha256:", "type": "string"}, "name": map[string]any{"description": "the name it is kept under", "type": "string"}, "source": map[string]any{"description": "the outside image's reference", "type": "string"}}, "required": []string{"source", "digest", "name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest stored", "type": "string"}, "reference": map[string]any{"description": "the reference the seat answers", "type": "string"}}, "required": []string{"reference", "digest"}, "type": "object"}}, - {Name: "tags", Description: "The tags of one image and the digest each names.", Optional: true, - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the image", "type": "string"}, "tags": map[string]any{"description": "every tag", "items": map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "tag": map[string]any{"description": "the tag", "type": "string"}}, "required": []string{"tag", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "tags"}, "type": "object"}}, - {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, - {Name: "list", Description: "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", Optional: true, - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the repository, /; every repository of the seat when not given", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"repositories": map[string]any{"description": "each repository named, in order of its name", "items": map[string]any{"properties": map[string]any{"manifests": map[string]any{"description": "each manifest it holds, by digest", "items": map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", "type": "string"}, "digest": map[string]any{"description": "the manifest's digest, sha256:", "type": "string"}, "held": map[string]any{"description": "a hold keeps it from retention", "type": "boolean"}, "index": map[string]any{"description": "it is an index, naming a manifest per platform", "type": "boolean"}, "platforms": map[string]any{"description": "for an index, the digests of the manifests it names", "items": map[string]any{"type": "string"}, "type": "array"}, "size": map[string]any{"description": "its bytes: its own content and every blob it names", "type": "integer"}, "tags": map[string]any{"description": "the tags naming it, none for an untagged one", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"digest", "index", "size", "held"}, "type": "object"}, "type": "array"}, "name": map[string]any{"description": "the repository", "type": "string"}}, "required": []string{"name", "manifests"}, "type": "object"}, "type": "array"}}, "required": []string{"repositories"}, "type": "object"}}, - }}, - {Name: OciArtifactRegistry, Scope: "mesh", Grain: "whole", Decision: "novox/hq ADR 0605", Emits: []string{"published", "refused", "removed", "kept", "held", "released"}, - Serves: []Verb{ - {Name: "publish", Description: "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", Optional: true, - Input: map[string]any{"properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "parts": map[string]any{"description": "every part's digest and size; none when the digest is already held", "items": map[string]any{"description": "one part of a transfer", "properties": map[string]any{"digest": map[string]any{"description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", "type": "string"}, "size": map[string]any{"description": "its size in bytes", "type": "integer"}}, "required": []string{"digest", "size"}, "type": "object"}, "type": "array"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "transfer": map[string]any{"description": "the transfer's name, the caller's own; the same name opens the same transfer again", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"transfer", "name", "kind", "digest"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "the digest is held already: nothing to send; the outcome follows as an event", "type": "boolean"}, "lacks": map[string]any{"description": "the digests of the parts the holder lacks, to send by publish-part", "items": map[string]any{"type": "string"}, "type": "array"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer"}, "type": "object"}}, - {Name: "publish-part", Description: "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", Optional: true, - Input: map[string]any{"properties": map[string]any{"data": map[string]any{"description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", "type": "string"}, "last": map[string]any{"description": "this chunk is the part's last", "type": "boolean"}, "part": map[string]any{"description": "the part's digest", "type": "string"}, "sequence": map[string]any{"description": "the chunk's place in its part, from 0", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part", "sequence", "last", "data"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"complete": map[string]any{"description": "the part is whole and its bytes match its digest", "type": "boolean"}, "part": map[string]any{"description": "the part", "type": "string"}, "received": map[string]any{"description": "the chunks of the part held so far", "type": "integer"}, "transfer": map[string]any{"description": "the transfer", "type": "string"}}, "required": []string{"transfer", "part"}, "type": "object"}}, - {Name: "versions", Description: "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", Optional: true, - Input: map[string]any{"properties": map[string]any{"kind": map[string]any{"description": "narrowed to one kind (optional)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the name", "type": "string"}, "versions": map[string]any{"description": "newest first", "items": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "type": "array"}}, "required": []string{"name", "versions"}, "type": "object"}}, - {Name: "remove", Description: "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest (for a bundle, archive or copied base)", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it goes", "enum": []string{"tag-gone", "retention"}, "type": "string"}, "tag": map[string]any{"description": "the tag (for a tagged version)", "type": "string"}, "version": map[string]any{"description": "the version", "type": "string"}}, "required": []string{"name", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "one published thing, as every registry event and answer names it", "properties": map[string]any{"commit": map[string]any{"description": "the source commit it was built from", "type": "string"}, "digest": map[string]any{"description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", "type": "string"}, "fingerprint": map[string]any{"description": "the build's fingerprint", "type": "string"}, "kind": map[string]any{"description": "what it is", "enum": []string{"package", "image", "bundle", "archive", "upstream"}, "type": "string"}, "name": map[string]any{"description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", "type": "string"}, "tag": map[string]any{"description": "the tag it carries, for a tagged version", "type": "string"}, "version": map[string]any{"description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", "type": "string"}}, "required": []string{"name", "kind", "digest"}, "type": "object"}, "kept": map[string]any{"description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", "type": "boolean"}, "reason": map[string]any{"description": "why it was kept: shared-digest, another tag names its digest", "enum": []string{"shared-digest"}, "type": "string"}, "removed": map[string]any{"description": "it is gone", "type": "boolean"}, "tags": map[string]any{"description": "the other tags still naming its digest, when kept", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"removed"}, "type": "object"}}, - {Name: "hold", Description: "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "which of the sweep's rules keeps it", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"held": map[string]any{"description": "it is held", "type": "boolean"}, "since": map[string]any{"description": "when the hold began, RFC 3339", "type": "string"}}, "required": []string{"held"}, "type": "object"}}, - {Name: "release", Description: "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", Optional: true, - Input: map[string]any{"properties": map[string]any{"digest": map[string]any{"description": "the digest", "type": "string"}, "name": map[string]any{"description": "the name", "type": "string"}, "reason": map[string]any{"description": "why it is let go", "type": "string"}}, "required": []string{"name", "digest", "reason"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"released": map[string]any{"description": "a hold was let go; false when there was none", "type": "boolean"}}, "required": []string{"released"}, "type": "object"}}, - {Name: "list", Description: "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", Optional: true, - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the repository, /; every repository of the seat when not given", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"repositories": map[string]any{"description": "each repository named, in order of its name", "items": map[string]any{"properties": map[string]any{"manifests": map[string]any{"description": "each manifest it holds, by digest", "items": map[string]any{"properties": map[string]any{"artifact": map[string]any{"description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", "type": "string"}, "digest": map[string]any{"description": "the manifest's digest, sha256:", "type": "string"}, "held": map[string]any{"description": "a hold keeps it from retention", "type": "boolean"}, "index": map[string]any{"description": "it is an index, naming a manifest per platform", "type": "boolean"}, "platforms": map[string]any{"description": "for an index, the digests of the manifests it names", "items": map[string]any{"type": "string"}, "type": "array"}, "size": map[string]any{"description": "its bytes: its own content and every blob it names", "type": "integer"}, "tags": map[string]any{"description": "the tags naming it, none for an untagged one", "items": map[string]any{"type": "string"}, "type": "array"}}, "required": []string{"digest", "index", "size", "held"}, "type": "object"}, "type": "array"}, "name": map[string]any{"description": "the repository", "type": "string"}}, "required": []string{"name", "manifests"}, "type": "object"}, "type": "array"}}, "required": []string{"repositories"}, "type": "object"}}, - }}, - {Name: Git, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0111", Delivers: "git"}, - {Name: NodeBuildGo, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, - Serves: []Verb{ - {Name: "current", Description: "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "pause", Description: "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "resume", Description: "Take Go asks again on this node.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: NodeBuildNode, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, - Serves: []Verb{ - {Name: "current", Description: "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "pause", Description: "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "resume", Description: "Take Node asks again on this node.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: NodeBuildImage, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0525, ADR 0536, ADR 0553", Accepts: []string{"delivery", "check", "recheck"}, PulledByKind: true, Yields: true, - Serves: []Verb{ - {Name: "current", Description: "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "pause", Description: "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "resume", Description: "Take image asks again on this node.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: MeshDnsResolver, Scope: "mesh", Grain: "seat-list", Decision: "novox/hq ADR 0194, ADR 0223", Replicated: true, Delivers: "wildcard-resolution"}, - {Name: NodeHostname, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0199, ADR 0223", - Serves: []Verb{ - {Name: "entries", Description: "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"cat /etc/hosts", "getent hosts"}}, - {Name: "add", Description: "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", - Input: map[string]any{"properties": map[string]any{"address": map[string]any{"description": "the IPv4 or IPv6 address", "type": "string"}, "names": map[string]any{"description": "the names for it, separated by spaces", "type": "string"}}, "required": []string{"address", "names"}, "type": "object"}, Replaces: []string{"edit /etc/hosts", "HOSTALIASES"}}, - {Name: "remove", Description: "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "a host name, or an address to remove every line of", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"sed -i /etc/hosts"}}, - }, Paths: []Path{{Path: "/etc/hosts", StepsBackFor: ""}, {Path: "/etc/hostname", StepsBackFor: ""}}}, - {Name: NodeIntrusionPrevention, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0121", - Serves: []Verb{ - {Name: "status", Description: "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", - Input: map[string]any{"properties": map[string]any{"jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"fail2ban-client status"}}, - {Name: "banned", Description: "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", - Input: map[string]any{"properties": map[string]any{"jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"fail2ban-client banned"}}, - {Name: "ban", Description: "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", - Input: map[string]any{"properties": map[string]any{"ip": map[string]any{"description": "the address", "type": "string"}, "jail": map[string]any{"description": "the jail to hold it", "type": "string"}}, "required": []string{"ip", "jail"}, "type": "object"}, Replaces: []string{"fail2ban-client set banip"}}, - {Name: "unban", Description: "Let one address go, from one jail or from every jail when none is named.", - Input: map[string]any{"properties": map[string]any{"ip": map[string]any{"description": "the address", "type": "string"}, "jail": map[string]any{"description": "one jail (optional)", "type": "string"}}, "required": []string{"ip"}, "type": "object"}, Replaces: []string{"fail2ban-client unban", "fail2ban-client set unbanip"}}, - }}, - {Name: NodePacketFilter, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0121", - Serves: []Verb{ - {Name: "rules", Description: "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", - Input: map[string]any{"properties": map[string]any{"chain": map[string]any{"description": "one chain of that table (optional)", "type": "string"}, "table": map[string]any{"description": "one nftables table, as `family name` (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"nft list ruleset", "iptables -L", "iptables-save"}}, - {Name: "reload", Description: "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"nft -f"}}, - {Name: "remove", Description: "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", - Input: map[string]any{"properties": map[string]any{"where": map[string]any{"description": "the rule set, as `node show` lists it", "type": "string"}}, "required": []string{"where"}, "type": "object"}, Replaces: []string{"nft delete", "iptables -X"}}, - }}, - {Name: NodeServiceManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0177", - Serves: []Verb{ - {Name: "units", Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", - Input: map[string]any{"properties": map[string]any{"pattern": map[string]any{"description": "a glob the unit's name must match (optional)", "type": "string"}, "scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}}, "type": "object"}, Replaces: []string{"systemctl list-units"}}, - {Name: "status", Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl status", "systemctl is-active"}}, - {Name: "start", Description: "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl start"}}, - {Name: "stop", Description: "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl stop"}}, - {Name: "restart", Description: "Restart one unit.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl restart"}}, - {Name: "enable", Description: "Make one unit start at boot (or at the account's login, in user scope).", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl enable"}}, - {Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl disable"}}, - {Name: "journal", Description: "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: ].", - Input: map[string]any{"properties": map[string]any{"lines": map[string]any{"description": "how many lines from the end of what matches (default 100, at most 2000)", "type": "string"}, "match": map[string]any{"description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", "type": "string"}, "priority": map[string]any{"description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", "type": "string"}, "scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "since": map[string]any{"description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}, "until": map[string]any{"description": "the window's end, in the same forms (optional; now when absent)", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"journalctl"}}, - {Name: "failed", Description: "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.", Optional: true, - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" or \"user\": only that manager (both when absent)", "type": "string"}}, "type": "object"}, Replaces: []string{"systemctl --failed"}}, - {Name: "reset-failed", Description: "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl reset-failed"}}, - {Name: "wanted-by", Description: "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}}, "required": []string{"unit"}, "type": "object"}, Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy", "ls ~/.config/systemd/user/*.wants"}}, - {Name: "unlink-dangling", Description: "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.", - Input: map[string]any{"properties": map[string]any{"scope": map[string]any{"description": "\"system\" (the default) or \"user\": the operator account's own manager", "type": "string"}, "unit": map[string]any{"description": "the unit's name, as the service manager knows it", "type": "string"}, "why": map[string]any{"description": "the person's reason for removing the links, kept in the record", "type": "string"}}, "required": []string{"unit", "why"}, "type": "object"}, Replaces: []string{"rm ~/.config/systemd/user/*.wants/", "unlink /etc/systemd/system/*.wants/"}}, - }}, - {Name: NodePackageManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0207"}, - {Name: NodeContainerRuntime, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0166, ADR 0207"}, - {Name: NodeEnvironment, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0203"}, - {Name: NodeProvisioner, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0349"}, - {Name: NodeLoginShell, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0204", - Serves: []Verb{ - {Name: "execute", Description: "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.", Optional: true, - Input: map[string]any{"properties": map[string]any{"command": map[string]any{"description": "the command line, as you would type it", "type": "string"}, "timeout_seconds": map[string]any{"description": "give up after this long, at most 25 (default 20)", "type": "string"}}, "required": []string{"command"}, "type": "object"}}, - }}, - {Name: NodePower, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0211"}, - {Name: NodeHotkeys, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0212", - Receives: []Receivable{{Kind: "trigger", Comment: "#"}}}, - {Name: NodeMessageBus, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0215"}, - {Name: NodeBackup, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0214", - Serves: []Verb{ - {Name: "backed-up", Description: "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.", - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"restic snapshots"}}, - {Name: "now", Description: "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.", - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "one module (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"restic backup"}}, - {Name: "restore", Description: "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as .restored-. Swapping it in is a person's act. Lists the restore points when none is named.", - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module", "type": "string"}, "path": map[string]any{"description": "one of the module's directories (all of them when omitted)", "type": "string"}, "snapshot": map[string]any{"description": "the restore point (from `backed-up`; the newest when omitted)", "type": "string"}}, "required": []string{"module"}, "type": "object"}, Replaces: []string{"restic restore"}}, - }, - Receives: []Receivable{{Kind: "backup", Comment: "#", Dirs: true}, {Kind: "data", Comment: "#", Dirs: true}}}, - {Name: ThePrivateNetwork, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0110"}, - {Name: NodeUplink, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0117", - Serves: []Verb{ - {Name: "resolvers", Description: "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "links", Description: "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }, Paths: []Path{{Path: "/etc/resolv.conf", StepsBackFor: "node-resolver"}}}, - {Name: NodeResolver, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0247", - Serves: []Verb{ - {Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}}, - {Name: "route", Description: "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", - Input: map[string]any{"properties": map[string]any{"domains": map[string]any{"description": "the domains to route there, separated by spaces or commas", "type": "string"}, "link": map[string]any{"description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", "type": "string"}, "servers": map[string]any{"description": "the servers' addresses, separated by spaces or commas", "type": "string"}}, "required": []string{"link", "domains", "servers"}, "type": "object"}, Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}}, - {Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", - Input: map[string]any{"properties": map[string]any{"link": map[string]any{"description": "the network link, by name", "type": "string"}}, "required": []string{"link"}, "type": "object"}, Replaces: []string{"resolvectl revert"}}, - }, Paths: []Path{{Path: "/etc/resolv.conf", StepsBackFor: ""}}}, - {Name: NodeNfsServer, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0263", - Serves: []Verb{ - {Name: "exports", Description: "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"exportfs -v", "cat /etc/exports", "zfs get sharenfs"}}, - {Name: "clients", Description: "Which machines have mounted which share now, as the NFS server knows its clients.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"ss -tn sport = :2049", "cat /proc/fs/nfsd/clients/*/info"}}, - {Name: "test", Description: "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.", - Input: map[string]any{"properties": map[string]any{"address": map[string]any{"description": "a node's private address, to ask whether the share is exported to it (optional)", "type": "string"}, "share": map[string]any{"description": "the share, by its name", "type": "string"}}, "required": []string{"share"}, "type": "object"}, Replaces: []string{"showmount -e"}}, - {Name: "reload", Description: "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"exportfs -ra"}}, - {Name: "adopt", Description: "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.", - Input: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": change it (needs why); anything else is a dry run", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "the shared path whose hand-made export is taken over", "type": "string"}, "why": map[string]any{"description": "why, for the record", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"zfs set sharenfs=off"}}, - }}, - {Name: NodeMounts, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0263", - Serves: []Verb{ - {Name: "list", Description: "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"cat /etc/fstab", "findmnt", "systemctl list-units --type=mount"}}, - {Name: "test", Description: "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.", - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"showmount -e", "ping"}}, - {Name: "mount", Description: "Mount one share or occasional source now, rather than at its first access.", - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"mount"}}, - {Name: "unmount", Description: "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.", - Input: map[string]any{"properties": map[string]any{"name": map[string]any{"description": "the share or source, by its name", "type": "string"}}, "required": []string{"name"}, "type": "object"}, Replaces: []string{"umount"}}, - {Name: "adopt", Description: "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", - Input: map[string]any{"properties": map[string]any{"confirm": map[string]any{"description": "\"true\": change it (needs why); anything else is a dry run", "enum": []string{"true", "false"}, "type": "string"}, "mountpoint": map[string]any{"description": "the mount point whose fstab line is taken over", "type": "string"}, "why": map[string]any{"description": "why, for the record", "type": "string"}}, "required": []string{"mountpoint"}, "type": "object"}, Replaces: []string{"sed -i /etc/fstab"}}, - }}, - {Name: NodeReads, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0310", - Serves: []Verb{ - {Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.", - Input: map[string]any{"properties": map[string]any{"follow": map[string]any{"description": "\"true\": also say where the path leads once every link is followed", "enum": []string{"true", "false"}, "type": "string"}, "paths": map[string]any{"description": "one absolute path, or several separated by line breaks", "type": "string"}}, "required": []string{"paths"}, "type": "object"}, Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}}, - {Name: "readlink", Description: "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"readlink", "readlink -f", "realpath"}}, - {Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path to a directory", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"ls", "ls -la"}}, - {Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.", - Input: map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", "type": "string"}, "from": map[string]any{"description": "the first line to answer, counted from 1 (optional)", "type": "string"}, "lines": map[string]any{"description": "how many lines to answer (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"cat", "head", "tail", "less"}}, - {Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.", - Input: map[string]any{"properties": map[string]any{"case": map[string]any{"description": "\"false\": match whatever the case (optional)", "enum": []string{"true", "false"}, "type": "string"}, "depth": map[string]any{"description": "how many directories deep, up to 8 (optional)", "type": "string"}, "matches": map[string]any{"description": "the most matches to answer, up to 500 (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file or a directory", "type": "string"}, "text": map[string]any{"description": "the fixed string to find", "type": "string"}}, "required": []string{"text", "path"}, "type": "object"}, Replaces: []string{"grep", "grep -r", "grep -F"}}, - {Name: "account", Description: "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.", - Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "type": "string"}, "name": map[string]any{"description": "an account's name or number (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"getent passwd", "getent group", "id", "groups"}}, - }}, - {Name: NodeFiles, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0394", - Serves: []Verb{ - {Name: "stat", Description: "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"follow": map[string]any{"description": "\"true\": also say where the path leads once every link is followed", "enum": []string{"true", "false"}, "type": "string"}, "paths": map[string]any{"description": "one absolute path, or several separated by line breaks", "type": "string"}}, "required": []string{"paths"}, "type": "object"}, Replaces: []string{"stat", "ls -ld", "namei -l", "findmnt -T"}}, - {Name: "readlink", Description: "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"readlink", "readlink -f", "realpath"}}, - {Name: "list", Description: "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "an absolute path to a directory", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"ls", "ls -la"}}, - {Name: "read", Description: "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"bytes": map[string]any{"description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", "type": "string"}, "from": map[string]any{"description": "the first line to answer, counted from 1 (optional)", "type": "string"}, "lines": map[string]any{"description": "how many lines to answer (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path"}, "type": "object"}, Replaces: []string{"cat", "head", "tail", "less"}}, - {Name: "search", Description: "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"case": map[string]any{"description": "\"false\": match whatever the case (optional)", "enum": []string{"true", "false"}, "type": "string"}, "depth": map[string]any{"description": "how many directories deep, up to 8 (optional)", "type": "string"}, "matches": map[string]any{"description": "the most matches to answer, up to 500 (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file or a directory", "type": "string"}, "text": map[string]any{"description": "the fixed string to find", "type": "string"}}, "required": []string{"text", "path"}, "type": "object"}, Replaces: []string{"grep", "grep -r", "grep -F"}}, - {Name: "account", Description: "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "type": "string"}, "name": map[string]any{"description": "an account's name or number (optional)", "type": "string"}}, "type": "object"}, Replaces: []string{"getent passwd", "getent group", "id", "groups"}}, - {Name: "write", Description: "Write one text file whole, replacing what it holds; the old content goes to the node's trash. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"content": map[string]any{"description": "the text to write", "type": "string"}, "mode": map[string]any{"description": "the file's mode, such as 0644 (optional)", "enum": []string{"true", "false"}, "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"path", "content", "reason"}, "type": "object"}, Replaces: []string{"tee", ">"}}, - {Name: "copy", Description: "Copy a file or directory to a path on this node. A target on another node (:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"from": map[string]any{"description": "an absolute path", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "to": map[string]any{"description": "an absolute path on this node", "type": "string"}}, "required": []string{"from", "to", "reason"}, "type": "object"}, Replaces: []string{"cp", "cp -a", "ssh cp"}}, - {Name: "move", Description: "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"from": map[string]any{"description": "an absolute path", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "to": map[string]any{"description": "an absolute path", "type": "string"}}, "required": []string{"from", "to", "reason"}, "type": "object"}, Replaces: []string{"mv"}}, - {Name: "delete", Description: "Delete a file or directory into the node's trash, where restore can bring it back. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": delete a directory with what it holds", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "reason"}, "type": "object"}, Replaces: []string{"rm", "rm -r"}}, - {Name: "mkdir", Description: "Make a directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "parents": map[string]any{"description": "\"true\": make the missing parents too", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"path", "reason"}, "type": "object"}, Replaces: []string{"mkdir", "mkdir -p"}}, - {Name: "chmod", Description: "Change the mode of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"mode": map[string]any{"description": "the mode, such as 0644 or u+x", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": also everything under a directory", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "mode", "reason"}, "type": "object"}, Replaces: []string{"chmod"}}, - {Name: "chown", Description: "Change the owner and group of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"group": map[string]any{"description": "a group's name or number (optional)", "enum": []string{"true", "false"}, "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "owner": map[string]any{"description": "an account's name or number", "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "recursive": map[string]any{"description": "\"true\": also everything under a directory", "enum": []string{"true", "false"}, "type": "string"}}, "required": []string{"path", "owner", "reason"}, "type": "object"}, Replaces: []string{"chown", "chgrp"}}, - {Name: "upload", Description: "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - Input: map[string]any{"properties": map[string]any{"bucket": map[string]any{"description": "the bucket the file goes into", "type": "string"}, "key": map[string]any{"description": "the object's key (optional; the file's SHA-256 when not given)", "type": "string"}, "path": map[string]any{"description": "an absolute path to a file", "type": "string"}}, "required": []string{"path", "bucket"}, "type": "object"}}, - {Name: "download", Description: "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"bucket": map[string]any{"description": "the bucket", "type": "string"}, "key": map[string]any{"description": "the object's key", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "type": "string"}, "path": map[string]any{"description": "an absolute path", "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}, "sha256": map[string]any{"description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", "type": "string"}}, "required": []string{"bucket", "key", "path", "reason"}, "type": "object"}}, - {Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, Replaces: []string{"ls ~/.local/share/Trash"}}, - {Name: "restore", Description: "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - Input: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "an id from trash", "type": "string"}, "override": map[string]any{"description": "a reason, to change a path a module owns anyway (optional)", "enum": []string{"true", "false"}, "type": "string"}, "reason": map[string]any{"description": "why the file is changed, for the audit trail", "type": "string"}}, "required": []string{"id", "reason"}, "type": "object"}, Replaces: []string{"trash-restore"}}, - }}, - {Name: NodeEngine, Scope: "node", Grain: "whole", Decision: "novox/hq ADR 0563", Emits: []string{"asked", "done", "refused", "manifest-reads-changed"}, HeldByTheEngine: true, - Serves: []Verb{ - {Name: "hand-over", Description: "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", - Input: map[string]any{"properties": map[string]any{"path": map[string]any{"description": "the directory's absolute path, as the engine states it", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"path"}, "type": "object"}, - Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, - {Name: "access-act", Description: "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", - Input: map[string]any{"properties": map[string]any{"access": map[string]any{"description": "the access, library:", "type": "string"}, "act": map[string]any{"description": "the act: take", "type": "string"}, "why": map[string]any{"description": "why it is asked, in words the operator reads", "type": "string"}}, "required": []string{"act", "access"}, "type": "object"}, - Output: map[string]any{"description": "the answer of an act the operator is asked", "properties": map[string]any{"asked": map[string]any{"description": "the ask's id: acts shows where it stands", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "says": map[string]any{"description": "what happens next, in words", "type": "string"}}, "required": []string{"asked", "node", "says"}, "type": "object"}}, - {Name: "judge-directories", Description: "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, - {Name: "setuid-search", Description: "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", - Input: map[string]any{"properties": map[string]any{"why": map[string]any{"description": "why it is asked, in words", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the answer of a read-only judgement", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "said": map[string]any{"description": "the engine's judgement or search, one line per directory or program", "type": "string"}}, "required": []string{"node", "said"}, "type": "object"}}, - {Name: "root-free", Description: "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"machines": map[string]any{"description": "this machine's verdict", "items": map[string]any{"description": "the engine's root-free verdict (ADR 0563 §6)", "properties": map[string]any{"free": map[string]any{"description": "the machine is root-free", "type": "boolean"}, "judged": map[string]any{"description": "when the engine judged it, RFC 3339", "type": "string"}, "machine": map[string]any{"description": "the machine", "type": "string"}, "why": map[string]any{"description": "why, in words", "type": "string"}}, "required": []string{"machine", "free", "why", "judged"}, "type": "object"}, "type": "array"}}, "required": []string{"machines"}, "type": "object"}}, - {Name: "acts", Description: "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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: map[string]any{"properties": map[string]any{"id": map[string]any{"description": "one act, by its id", "type": "string"}, "limit": map[string]any{"description": "at most this many, newest", "type": "string"}}, "type": "object"}, - Output: map[string]any{"description": "the acts kept", "properties": map[string]any{"acts": map[string]any{"description": "the acts, newest first", "items": map[string]any{"description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", "properties": map[string]any{"act": map[string]any{"description": "the act: hand-over, access-act, judge-directories or setuid-search", "type": "string"}, "ask": map[string]any{"description": "the operator's ask of a hand-over or an access act", "type": "string"}, "at": map[string]any{"description": "when it was asked, RFC 3339", "type": "string"}, "auto": map[string]any{"description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", "type": "boolean"}, "by": map[string]any{"description": "who answered, as the operator's warrant names them", "type": "string"}, "caller": map[string]any{"description": "the caller as the runtime proved it", "type": "string"}, "disk": map[string]any{"description": "what the engine read on disk when it judged", "type": "string"}, "ended": map[string]any{"description": "when it ended, RFC 3339", "type": "string"}, "expires": map[string]any{"description": "when the ask ends unanswered, RFC 3339", "type": "string"}, "id": map[string]any{"description": "the act's id", "type": "string"}, "path": map[string]any{"description": "the directory the act changes", "type": "string"}, "proofs": map[string]any{"description": "the warrant's proofs", "items": map[string]any{"type": "string"}, "type": "array"}, "said": map[string]any{"description": "what the engine said of it: its outcome or why it was refused", "type": "string"}, "sets": map[string]any{"description": "the owner, group and mode it sets, in words", "type": "string"}, "state": map[string]any{"description": "where it stands", "enum": []string{"asked", "done", "refused", "answered"}, "type": "string"}, "subject": map[string]any{"description": "the path of a hand-over, or the access of an access act", "type": "string"}, "via": map[string]any{"description": "the channel the warrant came on", "type": "string"}, "why": map[string]any{"description": "why it was asked, in words", "type": "string"}}, "required": []string{"id", "act", "caller", "at", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}}, "required": []string{"node", "acts"}, "type": "object"}}, - {Name: "packages", 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.", Optional: true, - Input: map[string]any{"properties": map[string]any{"write": map[string]any{"description": "\"true\": write this machine's key again from what the engine composed", "enum": []string{"true", "false"}, "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"key": map[string]any{"description": "the node's key as the bus holds it; absent when the engine wrote none", "properties": map[string]any{"node": map[string]any{"description": "the node", "type": "string"}, "packages": map[string]any{"description": "its packages sorted by name, absent after present", "items": map[string]any{"properties": map[string]any{"absent": map[string]any{"description": "declared absent", "type": "boolean"}, "modules": map[string]any{"description": "the modules that declare it", "items": map[string]any{"type": "string"}, "type": "array"}, "package": map[string]any{"description": "the package", "type": "string"}}, "required": []string{"package", "modules", "absent"}, "type": "object"}, "type": "array"}}, "required": []string{"node", "packages"}, "type": "object"}, "key_error": map[string]any{"description": "why the key could not be read", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "note": map[string]any{"description": "a note, such as that no key is written yet", "type": "string"}, "source": map[string]any{"description": "where the key is read: the engine's state packages", "type": "string"}, "wrote": map[string]any{"description": "with write: the key was written again (false: it already held what the engine composed)", "type": "boolean"}}, "required": []string{"node", "source"}, "type": "object"}}, - {Name: "composition", Description: "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", Optional: true, - Input: map[string]any{"properties": map[string]any{"diff": map[string]any{"description": "\"true\": list the resources that differ from what runs", "type": "string"}, "digest": map[string]any{"description": "a composition's digest (sha256:…), to read that revision rather than the latest", "type": "string"}, "module": map[string]any{"description": "the module, as placed on this machine; node for the node's own", "type": "string"}}, "required": []string{"module"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"attempt": map[string]any{"description": "the module's last apply that did not end running; absent when none", "properties": map[string]any{"answered_revision": map[string]any{"description": "the want's revision the hold took", "type": "integer"}, "composed": map[string]any{"description": "the digest tried", "type": "string"}, "composition_revision": map[string]any{"description": "the composition's revision it was tried at", "type": "integer"}, "fp": map[string]any{"description": "the fingerprint tried", "type": "string"}, "held": map[string]any{"description": "held for a failed backup", "type": "boolean"}, "transient": map[string]any{"description": "it failed because the bus did not answer for a moment", "type": "boolean"}, "want_revision": map[string]any{"description": "the want's revision it was tried at", "type": "integer"}}, "required": []string{"fp", "want_revision", "composition_revision"}, "type": "object"}, "composition": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "diff": map[string]any{"description": "every resource that differs from what runs, when asked", "items": map[string]any{"description": "one resource added, removed or changed, as the engine's judge says it", "type": "object"}, "type": "array"}, "have": map[string]any{"description": "what the module's have key says now; absent when it says nothing", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "detail": map[string]any{"description": "detail", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "judgement": map[string]any{"description": "judgement", "type": "string"}, "phase": map[string]any{"description": "running, failed, held or waiting-yes", "type": "string"}, "waits": map[string]any{"description": "the digest that waits for a yes", "type": "string"}}, "required": []string{"phase"}, "type": "object"}, "judged": map[string]any{"description": "the move from what runs to the composition read", "properties": map[string]any{"judgement": map[string]any{"description": "the judgement", "type": "string"}, "waits_for_yes": map[string]any{"description": "the agent would wait for a person's yes before applying it", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}}, "required": []string{"judgement", "words", "waits_for_yes"}, "type": "object"}, "module": map[string]any{"description": "the module", "type": "string"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "of the node's own composition, its own fields beside what runs names", "properties": map[string]any{"adoption": map[string]any{"description": "the revision's adoption; absent for a converged node", "type": "object"}, "groups": map[string]any{"description": "groups", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module the node's composer held in its last pass, with why", "type": "object"}, "identities": map[string]any{"description": "identities the revision names", "type": "integer"}, "libraries": map[string]any{"description": "libraries", "type": "integer"}, "provisioner_account": map[string]any{"description": "the account the provisioner's client runs as, as the revision names it", "type": "string"}, "runs_provisioner_account": map[string]any{"description": "as what runs names it", "type": "string"}}, "required": []string{"provisioner_account"}, "type": "object"}, "revisions": map[string]any{"description": "the revisions of its key the bucket keeps, oldest first", "items": map[string]any{"description": "one revision of a composition key", "properties": map[string]any{"digest": map[string]any{"description": "its digest", "type": "string"}, "key": map[string]any{"description": "the composition's key", "type": "string"}, "latest": map[string]any{"description": "the latest revision", "type": "boolean"}, "refused": map[string]any{"description": "why it cannot be read or trusted", "type": "string"}, "resources": map[string]any{"description": "how many resources it names", "type": "integer"}, "revision": map[string]any{"description": "its revision", "type": "integer"}}, "required": []string{"key"}, "type": "object"}, "type": "array"}, "runs": map[string]any{"description": "what runs of the module; absent when nothing runs", "properties": map[string]any{"composed": map[string]any{"description": "digest", "type": "string"}, "fp": map[string]any{"description": "fingerprint", "type": "string"}, "from": map[string]any{"description": "applied, have or kept", "type": "string"}, "phase": map[string]any{"description": "phase", "type": "string"}, "resources": map[string]any{"description": "how many resources run", "type": "integer"}}, "required": []string{"from", "resources"}, "type": "object"}, "started": map[string]any{"description": "the agent has looked at the module once since it started", "type": "boolean"}, "want": map[string]any{"description": "the module's want as the agent holds it; absent when it holds none", "properties": map[string]any{"backup": map[string]any{"description": "the operator's answer to a failed backup, again or skip", "type": "string"}, "composed": map[string]any{"description": "the composition's digest it names", "type": "string"}, "fp": map[string]any{"description": "its build's fingerprint", "type": "string"}, "removed": map[string]any{"description": "the module is removed", "type": "boolean"}, "revision": map[string]any{"description": "the revision of the want's key read last", "type": "integer"}, "self": map[string]any{"description": "the node composes the module itself (ADR 0547 §2)", "type": "boolean"}, "words": map[string]any{"description": "its words", "type": "string"}, "yes": map[string]any{"description": "the digest a person said yes to", "type": "string"}}, "required": []string{"fp", "revision"}, "type": "object"}}, "required": []string{"node", "module", "started", "composition", "judged"}, "type": "object"}}, - {Name: "compose", Description: "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "a module placed on this machine whose apply is also tried again (optional)", "type": "string"}}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"pass": map[string]any{"description": "how the pass went", "properties": map[string]any{"again": map[string]any{"description": "when the node composes again on its own, RFC 3339", "type": "string"}, "at": map[string]any{"description": "when the pass ended, RFC 3339", "type": "string"}, "earlier": map[string]any{"description": "the answer is the last pass, made moments ago", "type": "boolean"}, "handed": map[string]any{"description": "how many compositions the pass handed the agent that differ from before", "type": "integer"}, "held": map[string]any{"additionalProperties": map[string]any{"type": "string"}, "description": "each module refused alone, with why", "type": "object"}, "refused": map[string]any{"description": "why the node composed none of its modules", "type": "string"}}, "required": []string{"at", "handed"}, "type": "object"}, "retried": map[string]any{"description": "the module whose apply was tried again", "type": "string"}}, "required": []string{"pass"}, "type": "object"}}, - {Name: "compose-candidate", Description: "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", Optional: true, - Input: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the key of the indexer's candidate, .", "type": "string"}, "input": map[string]any{"description": "also answer the composition input the candidate was composed from, for the sandbox path", "type": "boolean"}}, "required": []string{"candidate"}, "type": "object"}, - Output: map[string]any{"properties": map[string]any{"candidate": map[string]any{"description": "the candidate's key", "type": "string"}, "input": map[string]any{"description": "when asked: the composition input it composed from", "type": "object"}, "modules": map[string]any{"description": "every module placed on the node, sorted by name", "items": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "own": map[string]any{"description": "one module's composition with the candidate", "properties": map[string]any{"changed": map[string]any{"description": "the candidate changes it from the one composed without it", "type": "boolean"}, "digest": map[string]any{"description": "the composition's digest when composed", "type": "string"}, "fp": map[string]any{"description": "the fingerprint it was composed at", "type": "string"}, "module": map[string]any{"description": "the module, node for the node's own composition", "type": "string"}, "state": map[string]any{"description": "composed (digest set), refused by the candidate, or held without it too (words set)", "enum": []string{"composed", "refused", "held"}, "type": "string"}, "words": map[string]any{"description": "why, for a refused or held one", "type": "string"}}, "required": []string{"module", "state"}, "type": "object"}, "refused": map[string]any{"description": "why the node could not compose with the candidate at all", "type": "string"}, "took": map[string]any{"description": "how long the engine took, in milliseconds", "type": "integer"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}, "verdict": map[string]any{"description": "the node's verdict (ADR 0593 §7, §9)", "enum": []string{"composed", "refused", "unready", "not-judged"}, "type": "string"}}, "required": []string{"v", "node", "candidate", "verdict", "modules", "own", "took"}, "type": "object"}}, - {Name: "manifest-reads", Description: "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", Optional: true, - Input: map[string]any{"properties": map[string]any{}, "type": "object"}, - Output: map[string]any{"description": "what this machine's engine reads of a manifest (ADR 0618 §3)", "properties": map[string]any{"manifest-reads": map[string]any{"description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline", "items": map[string]any{"minLength": float64(1), "type": "string"}, "type": "array"}, "node": map[string]any{"description": "the node", "type": "string"}, "stated-at": map[string]any{"description": "when the engine last stated them, RFC 3339", "type": "string"}, "v": map[string]any{"description": "the layout's version", "type": "integer"}}, "required": []string{"v", "node", "stated-at"}, "type": "object"}}, - }}, - {Name: NodeLoginManager, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "sessions", Description: "The sessions the login manager offers on this machine, and which one the operator account starts by default.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: NodeDisplayServer, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "displays", Description: "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "layout", Description: "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.", - Input: map[string]any{"properties": map[string]any{"action": map[string]any{"description": "list, save or apply", "enum": []string{"list", "save", "apply"}, "type": "string"}, "name": map[string]any{"description": "the profile to save or apply (save and apply only)", "type": "string"}}, "required": []string{"action"}, "type": "object"}}, - }}, - {Name: NodeDisplaySession, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether it is visible or focused.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - {Name: "windows", Description: "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.", - Input: map[string]any{"properties": map[string]any{"workspace": map[string]any{"description": "one workspace (optional)", "type": "string"}}, "type": "object"}}, - }, - Receives: []Receivable{{Kind: "config", Comment: "#"}}}, - {Name: NodeTerminalEmulator, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "open", Description: "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.", - Input: map[string]any{"properties": map[string]any{"command": map[string]any{"description": "what to run in it (optional; the login shell when absent)", "type": "string"}, "directory": map[string]any{"description": "where it starts (optional; the account's home when absent)", "type": "string"}}, "type": "object"}}, - }}, - {Name: NodeLauncher, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "menu", Description: "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", - Input: map[string]any{"properties": map[string]any{"choices": map[string]any{"description": "the lines to choose between, in order", "items": map[string]any{"type": "string"}, "type": "array"}, "prompt": map[string]any{"description": "what the menu asks (optional)", "type": "string"}}, "required": []string{"choices"}, "type": "object"}}, - {Name: "secret", Description: "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.", Optional: true, - Input: map[string]any{"properties": map[string]any{"module": map[string]any{"description": "the module whose own secret is asked for", "type": "string"}, "node": map[string]any{"description": "the machine the module runs on", "type": "string"}, "seal_to": map[string]any{"description": "the asker's public sealing key: the answer is sealed to it", "type": "string"}, "secret": map[string]any{"description": "the own secret's name", "type": "string"}, "timeout_seconds": map[string]any{"description": "give up after this long (optional)", "type": "string"}}, "required": []string{"module", "secret", "node", "seal_to"}, "type": "object"}}, - }}, - {Name: NodeNotifier, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "send", Description: "Show the operator a notification.", - Input: map[string]any{"properties": map[string]any{"body": map[string]any{"description": "its text (optional)", "type": "string"}, "title": map[string]any{"description": "the notification's summary", "type": "string"}, "urgency": map[string]any{"description": "low, normal (the default) or critical", "enum": []string{"low", "normal", "critical"}, "type": "string"}}, "required": []string{"title"}, "type": "object"}}, - {Name: "history", Description: "The notifications shown lately, newest first.", - Input: map[string]any{"properties": map[string]any{"limit": map[string]any{"description": "how many (optional, default 20)", "type": "string"}}, "type": "object"}}, - }}, - {Name: NodeLockScreen, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "lock", Description: "Lock the operator's session now.", - Input: map[string]any{"properties": map[string]any{}, "type": "object"}}, - }}, - {Name: NodeClipboard, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Serves: []Verb{ - {Name: "history", Description: "What the clipboard held lately, newest first.", - Input: map[string]any{"properties": map[string]any{"limit": map[string]any{"description": "how many (optional, default 20)", "type": "string"}}, "type": "object"}}, - {Name: "copy", Description: "Put text on the operator's clipboard.", - Input: map[string]any{"properties": map[string]any{"text": map[string]any{"description": "the text", "type": "string"}}, "required": []string{"text"}, "type": "object"}}, - }}, - {Name: NodeBar, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208", - Receives: []Receivable{{Kind: "block", Comment: "#", Offered: true, Shape: &Shape{Places: []PlaceField{{Field: "bar", Values: []string{"bottom", "top"}}, {Field: "place", Values: []string{"resources", "status"}}}, Order: "order", Examples: []map[string]any{map[string]any{"bar": "bottom", "place": "status", "shows": "battery"}, map[string]any{"bar": "bottom", "options": map[string]any{"device": "BAT0"}, "place": "status", "shows": "battery"}, map[string]any{"bar": "bottom", "options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}, "place": "status", "shows": "command"}, map[string]any{"bar": "bottom", "options": map[string]any{"state": "power.draw"}, "place": "status", "shows": "state"}, map[string]any{"bar": "bottom", "options": map[string]any{"key": "a-machine", "state": "power.draw"}, "place": "status", "shows": "state"}}, Text: true}}}}, - {Name: NodeCompositor, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208"}, - {Name: NodeSecretService, Scope: "node", Grain: "seat-list", Decision: "novox/hq ADR 0208"}, - } -} diff --git a/health-and-repair/go/clients/meshconditionsclient/client.go b/health-and-repair/go/clients/meshconditionsclient/client.go new file mode 100644 index 0000000..5a28c7b --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/client.go @@ -0,0 +1,172 @@ +// Package meshconditionsclient is the logic every observer, healer and reader of conditions shares over the seat +// mesh-conditions (novox/hq ADR 0564, ADR 0634 §4): how a finding's key is made and checked, the scopes and the bounds, +// the order a person reads conditions in, and the answers a notification offers. The layouts are the seat's contract's. +package meshconditionsclient + +import ( + "fmt" + "regexp" + "sort" + "strings" + "time" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" +) + +// The two severities, how soon the operator is needed. +const ( + Urgent = "urgent" + Warning = "warning" +) + +// The scopes a condition's key starts with: what kind of thing is wrong. +const ( + ScopeMachine = "machine" + ScopePlan = "plan" + ScopeCall = "call" + ScopeBuild = "build" + ScopeMerge = "merge" + ScopeProvider = "provider" + ScopeSeat = "seat" + ScopeBus = "bus" + ScopeCore = "core" + ScopeProbe = "probe" + ScopeMesh = "mesh" + // ScopeDelivery is a delivery by its id (novox/hq ADR 0239); ScopeModule a module on a machine, `.`. + ScopeDelivery = "delivery" + ScopeModule = "module" +) + +// Scopes is every scope, in the order a person reads them. +var Scopes = []string{ScopeMachine, ScopePlan, ScopeCall, ScopeBuild, ScopeMerge, ScopeProvider, + ScopeSeat, ScopeBus, ScopeCore, ScopeProbe, ScopeMesh, ScopeDelivery, ScopeModule} + +// Who resolves a condition. +const ( + ResolverSelf = "self" + ResolverOperator = "operator" + ResolverAgent = "agent" +) + +// ResolverHealer is the resolver of a condition a registered healer works on. +func ResolverHealer(name string) string { return "healer:" + name } + +// The bounds a condition keeps. +const ( + KeptEvidence = 10 + KeptTried = 10 + KeptGaps = 8 + MaxSilence = 7 * 24 * time.Hour + ReopenWithin = 10 * time.Minute +) + +// CauseOperatorAnswer is the cause of an act the operator chose. +const CauseOperatorAnswer = "operator-answer" + +// The changes a condition event says. +const ( + ChangeRaised = "raised" + ChangeReopened = "reopened" + ChangeSeverity = "severity" + ChangeResolver = "resolver" + ChangeSilenced = "silenced" + ChangeUnsilenced = "silence-ended" + ChangeCleared = "cleared" + ChangeWords = "words" +) + +var unsafeKey = regexp.MustCompile(`[^A-Za-z0-9_=/-]`) + +// Key is a condition's key: its scope, the parts of its id and a token, each made safe for a bus key. +func Key(scope, id, token string) string { + var parts []string + for _, p := range append(append([]string{scope}, strings.Split(id, ".")...), token) { + p = unsafeKey.ReplaceAllString(strings.TrimSpace(p), "_") + if p == "" { + p = "_" + } + parts = append(parts, p) + } + return strings.Join(parts, ".") +} + +// EventFor is the seat's event that says a change. +func EventFor(change string) string { + switch change { + case ChangeRaised, ChangeReopened: + return seat.EventConditionRaised + case ChangeCleared: + return seat.EventConditionCleared + } + return seat.EventConditionChanged +} + +// ShowKeeper is the call that shows one condition whole. +func ShowKeeper(key string) string { return seat.Seat + "." + seat.VerbConditions + " key=" + key } + +// SilenceAction is the answer a notification offers to stop a condition's messages for a week. +func SilenceAction(key string) seat.Action { + return seat.Action{Label: "Silence for a week", Verb: seat.Seat + "." + seat.VerbSilence, Level: seat.ActionLevelAcknowledge, + Arguments: map[string]string{"key": key, "for": "7d", "why": "", "cause": CauseOperatorAnswer}} +} + +// CheckFinding refuses a finding the keeper could not make a condition of, naming why. +func CheckFinding(f seat.FindingsByKeyValue) error { + known := false + for _, s := range Scopes { + known = known || s == f.Subject.Scope + } + token := f.Ref + if token == "" { + token = f.Kind + } + switch { + case !known: + return fmt.Errorf("health: a finding's scope is one of %s, not %q", strings.Join(Scopes, ", "), f.Subject.Scope) + case strings.TrimSpace(f.Subject.ID) == "": + return fmt.Errorf("health: a %s finding names what it is about", f.Subject.Scope) + case strings.TrimSpace(f.Kind) == "": + return fmt.Errorf("health: the finding %s has no kind", f.Key) + case f.Key != Key(f.Subject.Scope, f.Subject.ID, token): + return fmt.Errorf("health: the finding's key %q is not %q, its scope, id and kind", f.Key, Key(f.Subject.Scope, f.Subject.ID, token)) + case f.Severity != Urgent && f.Severity != Warning: + return fmt.Errorf("health: the finding %s is urgent or a warning, not %q", f.Key, f.Severity) + case strings.TrimSpace(f.Summary) == "": + return fmt.Errorf("health: the finding %s says nothing", f.Key) + case strings.TrimSpace(f.Source) == "": + return fmt.Errorf("health: the finding %s does not say what saw it", f.Key) + case strings.TrimSpace(f.Observer) == "": + return fmt.Errorf("health: the finding %s does not name its observer", f.Key) + case len(f.Evidence) > KeptEvidence: + return fmt.Errorf("health: the finding %s keeps %d observations, at most %d", f.Key, len(f.Evidence), KeptEvidence) + } + return nil +} + +// Order sorts conditions as a person reads them: urgent first, then oldest raised, then by key. +func Order(list []seat.ConditionsByKeyValue) { + raised := func(c seat.ConditionsByKeyValue) time.Time { t, _ := time.Parse(time.RFC3339Nano, c.Raised); return t } + sort.SliceStable(list, func(i, j int) bool { + if list[i].Severity != list[j].Severity { + return list[i].Severity == Urgent + } + if ri, rj := raised(list[i]), raised(list[j]); !ri.Equal(rj) { + return ri.Before(rj) + } + return list[i].Key < list[j].Key + }) +} + +// SilencedAt says whether the condition's messages are silenced at now. +func SilencedAt(c seat.ConditionsByKeyValue, now time.Time) bool { + if c.Silenced == nil { + return false + } + until, err := time.Parse(time.RFC3339Nano, c.Silenced.Until) + return err == nil && now.Before(until) +} + +// Escalated says the condition waits on the operator after healers tried. +func Escalated(c seat.ConditionsByKeyValue) bool { + return c.Resolver == ResolverOperator && len(c.Tried) > 0 +} diff --git a/health-and-repair/go/clients/meshconditionsclient/client_test.go b/health-and-repair/go/clients/meshconditionsclient/client_test.go new file mode 100644 index 0000000..7baef69 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -0,0 +1,62 @@ +package meshconditionsclient + +import ( + "strings" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" +) + +// ADR 0564 clause 3: the key rule, the checks of a finding, the order and the silence a person reads conditions by. +func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { + if k := Key("machine", "g14.local", "silent"); k != "machine.g14.local.silent" { + t.Errorf("key %s", k) + } + if k := Key("call", "a b", ""); k != "call.a_b._" { + t.Errorf("an unsafe part and an empty token: %s", k) + } + good := seat.FindingsByKeyValue{Key: "machine.g14.silent", Kind: "silent", Severity: Warning, Summary: "x", Source: "S1", + Observer: "mesh-delivery-watchman", Subject: seat.Subject{Scope: ScopeMachine, ID: "g14"}} + for _, c := range []struct { + name string + change func(*seat.FindingsByKeyValue) + refuse string + }{ + {"a good finding", func(*seat.FindingsByKeyValue) {}, ""}, + {"an unknown scope", func(f *seat.FindingsByKeyValue) { f.Subject.Scope = "moon" }, "scope is one of"}, + {"a key that is not its subject's", func(f *seat.FindingsByKeyValue) { f.Key = "machine.g14.other" }, "is not"}, + {"a reference as the key's last part", func(f *seat.FindingsByKeyValue) { f.Ref = "gone"; f.Key = "machine.g14.gone" }, ""}, + {"no summary", func(f *seat.FindingsByKeyValue) { f.Summary = " " }, "says nothing"}, + {"no observer", func(f *seat.FindingsByKeyValue) { f.Observer = "" }, "observer"}, + {"a severity of neither kind", func(f *seat.FindingsByKeyValue) { f.Severity = "info" }, "urgent or a warning"}, + {"too much evidence", func(f *seat.FindingsByKeyValue) { f.Evidence = make([]seat.Evidence, KeptEvidence+1) }, "keeps 11"}, + } { + f := good + c.change(&f) + err := CheckFinding(f) + if (c.refuse == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.refuse)) { + t.Errorf("%s: %v", c.name, err) + } + } + list := []seat.ConditionsByKeyValue{ + {Key: "b", Severity: Warning, Raised: "2026-11-01T10:00:00Z"}, + {Key: "c", Severity: Urgent, Raised: "2026-11-02T10:00:00Z"}, + {Key: "a", Severity: Urgent, Raised: "2026-11-01T10:00:00.5Z", Silenced: &seat.ConditionsByKeyValueSilenced{Until: "2026-11-03T00:00:00Z"}}, + } + Order(list) + if got := list[0].Key + list[1].Key + list[2].Key; got != "acb" { + t.Errorf("order %s, want urgent oldest first", got) + } + now := time.Date(2026, 11, 2, 0, 0, 0, 0, time.UTC) + if !SilencedAt(list[0], now) || SilencedAt(list[0], now.Add(48*time.Hour)) || SilencedAt(list[1], now) { + t.Error("silence is until its end") + } + if EventFor(ChangeReopened) != seat.EventConditionRaised || EventFor(ChangeSilenced) != seat.EventConditionChanged || + EventFor(ChangeCleared) != seat.EventConditionCleared { + t.Error("a change is said as its event") + } + if a := SilenceAction("k"); a.Verb != "mesh-conditions.silence" || ShowKeeper("k") != "mesh-conditions.conditions key=k" { + t.Errorf("the seat's addresses: %+v", a) + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod new file mode 100644 index 0000000..8ee6c8e --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient + +go 1.26.0 + +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0 diff --git a/health-and-repair/go/clients/meshhandactsclient/client.go b/health-and-repair/go/clients/meshhandactsclient/client.go new file mode 100644 index 0000000..e9fbaa7 --- /dev/null +++ b/health-and-repair/go/clients/meshhandactsclient/client.go @@ -0,0 +1,88 @@ +// Package meshhandactsclient is the logic every module that records an act by hand shares over the seat mesh-hand-acts +// (novox/hq ADR 0565 clauses 10 and 11, ADR 0634 §4): the recorder, which asks the seat alone, and what makes an act a +// repair. The act's layout is the seat's contract's. +package meshhandactsclient + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" +) + +// The verbs an act is recorded under in the log, and the causes and kinds that are no repair. +const ( + VerbRecord = "hand-act record" + VerbDrill = "hand-act drill" + VerbWarrant = "warrant" + // CauseDrill is a drill's cause, never counted as a repair; CauseOperatorAnswer an act the operator chose. + CauseDrill = "drill" + CauseOperatorAnswer = "operator-answer" + // KindRecordedBuilds is a push that only moved recorded builds: no repair (ADR 0242). + KindRecordedBuilds = "recorded-builds" +) + +// Repair says the act counts toward a cause repeated: not a drill, not an operator's answer, not recorded builds. +func Repair(a seat.Act) bool { + return a.Cause != CauseDrill && a.Verb != VerbDrill && a.Cause != CauseOperatorAnswer && a.Kind != KindRecordedBuilds +} + +// Request is one act a recorder records: what, why, its cause, the condition it addresses, and its verb in the log (a +// repair when empty). +type Request struct { + What string `json:"what"` + Why string `json:"why"` + Cause string `json:"cause,omitempty"` + Condition string `json:"condition,omitempty"` + Verb string `json:"verb,omitempty"` +} + +// Taken is which log took an act, and what its holder answered. +type Taken struct { + Log string `json:"log"` + Answer json.RawMessage `json:"answer,omitempty"` +} + +// Recorder records acts for a module through the seat mesh-hand-acts alone. An act the seat does not take is an error +// that says nothing was recorded, never a quiet success. +type Recorder struct { + // Ask calls a seat verb by its address (`seat:.`): the runtime's ask. + Ask func(key string, body any) (json.RawMessage, error) +} + +// NoResponder says err is the bus finding nobody to answer: the seat has no holder now. +func NoResponder(err error) bool { + return err != nil && strings.Contains(err.Error(), "no responders") +} + +// Record records one act. +func (r Recorder) Record(q Request) (Taken, error) { + if strings.TrimSpace(q.What) == "" || strings.TrimSpace(q.Why) == "" { + return Taken{}, errors.New("a hand act says what was done and why: nothing was recorded") + } + body := map[string]any{"what": q.What, "why": q.Why} + for k, v := range map[string]string{"cause": q.Cause, "condition": q.Condition, "verb": q.Verb} { + if v != "" { + body[k] = v + } + } + return r.record(seat.VerbHandAct, body) +} + +// RecordWarrant records what the asking module did on the operator's warrant for its ask. +func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) { + return r.record(seat.VerbWarranted, map[string]any{"asker": asker, "ask": ask}) +} + +func (r Recorder) record(verb string, body any) (Taken, error) { + answer, err := r.Ask("seat:"+seat.Seat+"."+verb, body) + if err == nil { + return Taken{Log: seat.Seat, Answer: answer}, nil + } + if NoResponder(err) { + return Taken{}, fmt.Errorf("%s has no holder that answers: nothing was recorded (%w)", seat.Seat, err) + } + return Taken{}, fmt.Errorf("%s.%s refused, nothing was recorded: %w", seat.Seat, verb, err) +} diff --git a/health-and-repair/go/clients/meshhandactsclient/client_test.go b/health-and-repair/go/clients/meshhandactsclient/client_test.go new file mode 100644 index 0000000..a85870e --- /dev/null +++ b/health-and-repair/go/clients/meshhandactsclient/client_test.go @@ -0,0 +1,73 @@ +package meshhandactsclient + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + "testing" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" +) + +// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a +// recorder records through the seat alone, the controller's verbs being gone, and says loudly when nothing was recorded. +func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { + // The shape of mesh-controller internal/link HandAct, as its bucket keeps an entry (handacts.go at 0fbb5ef3). + raw := `{"id":"act-1","at":"2026-10-28T09:00:00Z","by":"person.jochen","verb":"hand-act record","why":"a stuck consumer",` + + `"cause":"consumer-lost","condition":"bus.EVENTS.x.consumer-lost","via":"telegram","ask":"a-1","proofs":["P1"],` + + `"requested-by":"bus.EVENTS.x.consumer-lost","outcome":"done"}` + var a seat.Act + if err := json.Unmarshal([]byte(raw), &a); err != nil { + t.Fatal(err) + } + again, _ := json.Marshal(a) + var want, got map[string]any + _ = json.Unmarshal([]byte(raw), &want) + _ = json.Unmarshal(again, &got) + if !reflect.DeepEqual(want, got) || !Repair(a) || Repair(seat.Act{Cause: CauseDrill}) || Repair(seat.Act{Verb: VerbDrill}) { + t.Errorf("the controller's entry reads back as %s", again) + } + noResponders := errors.New("nats: no responders available for request") + for _, c := range []struct { + name string + seat error + ctrl error + log string + refused string + asked string + warrants bool + }{ + {name: "the seat answers", log: seat.Seat, asked: "seat:mesh-hand-acts.hand-act"}, + {name: "no holder: nothing recorded, the controller never asked", seat: noResponders, refused: "nothing was recorded", + asked: "seat:mesh-hand-acts.hand-act"}, + {name: "the seat refuses", seat: errors.New("why is required"), refused: "refused, nothing was recorded", + asked: "seat:mesh-hand-acts.hand-act"}, + {name: "a warrant without a holder", seat: noResponders, warrants: true, refused: "nothing was recorded", + asked: "seat:mesh-hand-acts.warranted"}, + {name: "a warrant recorded", warrants: true, log: seat.Seat, asked: "seat:mesh-hand-acts.warranted"}, + } { + var asked []string + r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key) + if strings.HasPrefix(key, "seat:mesh-hand-acts.") { + return json.RawMessage(`{}`), c.seat + } + return json.RawMessage(`{}`), c.ctrl + }} + var taken Taken + var err error + if c.warrants { + taken, err = r.RecordWarrant("mesh-config", "ask-1") + } else { + taken, err = r.Record(Request{What: "silenced", Why: "known", Cause: "silent", Verb: "conditions silence"}) + } + if (c.refused != "") != (err != nil) || (err != nil && !strings.Contains(err.Error(), c.refused)) || taken.Log != c.log || + strings.Join(asked, ",") != c.asked { + t.Errorf("%s: took %q, %v, asked %v", c.name, taken.Log, err, asked) + } + } + if _, err := (Recorder{}).Record(Request{What: "x"}); err == nil { + t.Error("an act with no why was recorded") + } +} diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod new file mode 100644 index 0000000..575a706 --- /dev/null +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient + +go 1.26.0 + +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0 diff --git a/health-and-repair/go/seats/meshconditions/contract.go b/health-and-repair/go/seats/meshconditions/contract.go new file mode 100644 index 0000000..eddd19d --- /dev/null +++ b/health-and-repair/go/seats/meshconditions/contract.go @@ -0,0 +1,384 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshconditions is the contract of the seat "mesh-conditions", defined whole (seats/mesh-conditions/seat.json): What is wrong with the mesh now (ADR 0564): the open conditions raised from the observers' findings and cleared when a finding goes. +package meshconditions + +import ( + "bytes" + "encoding/json" + "errors" +) + +// Seat is the seat's name. +const Seat = "mesh-conditions" + +const ( + VerbConditions = "conditions" + VerbSilence = "silence" + VerbResync = "resync" +) + +const ( + EventConditionRaised = "condition-raised" + EventConditionChanged = "condition-changed" + EventConditionCleared = "condition-cleared" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbConditions, VerbSilence, VerbResync} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventConditionRaised, EventConditionChanged, EventConditionCleared} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-conditions.events" + FamilyMaxAgeDays = 7 +) + +// SubjectConditionRaised is where condition-raised is published. +const SubjectConditionRaised = "mesh.seat.mesh-conditions.event.condition-raised" + +// SubjectConditionChanged is where condition-changed is published. +const SubjectConditionChanged = "mesh.seat.mesh-conditions.event.condition-changed" + +// SubjectConditionCleared is where condition-cleared is published. +const SubjectConditionCleared = "mesh.seat.mesh-conditions.event.condition-cleared" + +const ( + StateConditions = "conditions" + StateFindings = "findings" +) + +// ConditionsByKeyKey is the key of one value of the state conditions under the form "". +func ConditionsByKeyKey(key string) string { return key } + +// ParseConditionsByKeyKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseConditionsByKeyKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeConditionsByKeyValue reads one value of the state conditions under the form "", refusing a field ConditionsByKeyValue does not know and any text after the value. +func DecodeConditionsByKeyValue(raw []byte) (ConditionsByKeyValue, error) { + var v ConditionsByKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// FindingsByKeyKey is the key of one value of the state findings under the form "". +func FindingsByKeyKey(key string) string { return key } + +// ParseFindingsByKeyKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseFindingsByKeyKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeFindingsByKeyValue reads one value of the state findings under the form "", refusing a field FindingsByKeyValue does not know and any text after the value. +func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) { + var v FindingsByKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Action is one answer the operator may give from a notification. +type Action struct { + Arguments map[string]string `json:"arguments,omitempty"` + Label string `json:"label"` + Level string `json:"level,omitempty"` + Machine string `json:"machine,omitempty"` + // the seat verb the operator channel calls + Verb string `json:"verb"` +} + +// The values of Action's level. +const ( + ActionLevelAcknowledge = "acknowledge" + ActionLevelApprove = "approve" +) + +// ConditionEventGapsItem is a type of the contract. +type ConditionEventGapsItem struct { + Cleared string `json:"cleared,omitempty"` + Reopened string `json:"reopened,omitempty"` +} + +// ConditionEventSilenced is a type of the contract. +type ConditionEventSilenced struct { + By string `json:"by,omitempty"` + Since string `json:"since,omitempty"` + Until string `json:"until,omitempty"` + Why string `json:"why,omitempty"` +} + +// ConditionEventTriedItem is a type of the contract. +type ConditionEventTriedItem struct { + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Outcome string `json:"outcome,omitempty"` + What string `json:"what,omitempty"` +} + +// ConditionEvent is a condition and what changed. +type ConditionEvent struct { + Actions []Action `json:"actions,omitempty"` + At string `json:"at"` + Change string `json:"change"` + Cleared string `json:"cleared,omitempty"` + Count int64 `json:"count,omitempty"` + Epoch int64 `json:"epoch,omitempty"` + Event string `json:"event"` + // newest first, at most 10 + Evidence []Evidence `json:"evidence,omitempty"` + Explanation string `json:"explanation,omitempty"` + First string `json:"first,omitempty"` + Gaps []ConditionEventGapsItem `json:"gaps,omitempty"` + Headline string `json:"headline,omitempty"` + // scope.id.token + Key string `json:"key"` + Kind string `json:"kind,omitempty"` + LastObserved string `json:"last-observed,omitempty"` + LooksUnread string `json:"looks-unread,omitempty"` + Needs string `json:"needs,omitempty"` + Observations int64 `json:"observations,omitempty"` + Raised string `json:"raised,omitempty"` + Resolved string `json:"resolved,omitempty"` + // self, operator, agent or healer: + Resolver string `json:"resolver,omitempty"` + Severity string `json:"severity,omitempty"` + // the tool call that shows it + Show string `json:"show,omitempty"` + Silenced *ConditionEventSilenced `json:"silenced,omitempty"` + // the row, probe or event that saw it + Source string `json:"source,omitempty"` + Subject *Subject `json:"subject,omitempty"` + // one line + Summary string `json:"summary,omitempty"` + Tried []ConditionEventTriedItem `json:"tried,omitempty"` + Was string `json:"was,omitempty"` + Why string `json:"why,omitempty"` +} + +// The values of ConditionEvent's change. +const ( + ConditionEventChangeRaised = "raised" + ConditionEventChangeReopened = "reopened" + ConditionEventChangeSeverity = "severity" + ConditionEventChangeResolver = "resolver" + ConditionEventChangeSilenced = "silenced" + ConditionEventChangeSilenceEnded = "silence-ended" + ConditionEventChangeCleared = "cleared" + ConditionEventChangeWords = "words" +) + +// The values of ConditionEvent's severity. +const ( + ConditionEventSeverityUrgent = "urgent" + ConditionEventSeverityWarning = "warning" +) + +// Evidence is one observation, as it was said. +type Evidence struct { + At string `json:"at"` + Said string `json:"said"` +} + +// Subject is what the condition is about. +type Subject struct { + // the other machines it concerns + Also []string `json:"also,omitempty"` + // its id within the scope + ID string `json:"id"` + // the machine it concerns, when there is one + Machine string `json:"machine,omitempty"` + // machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module + Scope string `json:"scope"` +} + +// ConditionsInput is what conditions takes. +type ConditionsInput struct { + // with history: how many days back (default 7, at most 90) + Days string `json:"days,omitempty"` + // "true": every raising, change, silence and clearing lately, oldest first + History string `json:"history,omitempty"` + // a condition's key: that one whole; with history, only its transitions + Key string `json:"key,omitempty"` + // only those about this machine + Machine string `json:"machine,omitempty"` + // only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module + Scope string `json:"scope,omitempty"` + // only urgent, or only warning + Severity string `json:"severity,omitempty"` +} + +// The values of ConditionsInput's history. +const ( + ConditionsInputHistoryTrue = "true" + ConditionsInputHistoryFalse = "false" +) + +// ConditionsAnswer is what conditions answers. +type ConditionsAnswer = map[string]any + +// SilenceInput is what silence takes. +type SilenceInput struct { + // the cause in a word (the condition's kind when absent) + Cause string `json:"cause,omitempty"` + // how long: 30m, 4h, 2d; at most 7d + For string `json:"for"` + // the condition's key + Key string `json:"key"` + // why, required and recorded + Why string `json:"why"` +} + +// SilenceAnswer is what silence answers. +type SilenceAnswer = map[string]any + +// ResyncInput is what resync takes. +type ResyncInput struct { +} + +// ResyncAnswer is what resync answers. +type ResyncAnswer = map[string]any + +// ConditionRaisedEvent is the body of the event condition-raised. +type ConditionRaisedEvent = ConditionEvent + +// ConditionChangedEvent is the body of the event condition-changed. +type ConditionChangedEvent = ConditionEvent + +// ConditionClearedEvent is the body of the event condition-cleared. +type ConditionClearedEvent = ConditionEvent + +// ConditionsByKeyValueGapsItem is a type of the contract. +type ConditionsByKeyValueGapsItem struct { + Cleared string `json:"cleared,omitempty"` + Reopened string `json:"reopened,omitempty"` +} + +// ConditionsByKeyValueSilenced is a type of the contract. +type ConditionsByKeyValueSilenced struct { + By string `json:"by,omitempty"` + Since string `json:"since,omitempty"` + Until string `json:"until,omitempty"` + Why string `json:"why,omitempty"` +} + +// ConditionsByKeyValueTriedItem is a type of the contract. +type ConditionsByKeyValueTriedItem struct { + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Outcome string `json:"outcome,omitempty"` + What string `json:"what,omitempty"` +} + +// ConditionsByKeyValue is one value of the state conditions under the key . +type ConditionsByKeyValue struct { + Actions []Action `json:"actions,omitempty"` + Count int64 `json:"count,omitempty"` + Epoch int64 `json:"epoch,omitempty"` + // newest first, at most 10 + Evidence []Evidence `json:"evidence,omitempty"` + Explanation string `json:"explanation,omitempty"` + First string `json:"first,omitempty"` + Gaps []ConditionsByKeyValueGapsItem `json:"gaps,omitempty"` + Headline string `json:"headline,omitempty"` + // scope.id.token + Key string `json:"key"` + Kind string `json:"kind"` + LastObserved string `json:"last-observed"` + LooksUnread string `json:"looks-unread,omitempty"` + Needs string `json:"needs,omitempty"` + Observations int64 `json:"observations,omitempty"` + Raised string `json:"raised"` + Resolved string `json:"resolved,omitempty"` + // self, operator, agent or healer: + Resolver string `json:"resolver,omitempty"` + Severity string `json:"severity"` + Silenced *ConditionsByKeyValueSilenced `json:"silenced,omitempty"` + // the row, probe or event that saw it + Source string `json:"source,omitempty"` + Subject Subject `json:"subject"` + // one line + Summary string `json:"summary,omitempty"` + Tried []ConditionsByKeyValueTriedItem `json:"tried,omitempty"` +} + +// The values of ConditionsByKeyValue's severity. +const ( + ConditionsByKeyValueSeverityUrgent = "urgent" + ConditionsByKeyValueSeverityWarning = "warning" +) + +// FindingsByKeyValue is one value of the state findings under the key . +type FindingsByKeyValue struct { + Actions []Action `json:"actions,omitempty"` + At string `json:"at"` + // newest first, at most 10 + Evidence []Evidence `json:"evidence,omitempty"` + Explanation string `json:"explanation,omitempty"` + Headline string `json:"headline,omitempty"` + // scope.id.token + Key string `json:"key"` + Kind string `json:"kind"` + Needs string `json:"needs,omitempty"` + // the module that wrote it + Observer string `json:"observer"` + // the key's last part when it is not the kind + Ref string `json:"ref,omitempty"` + Resolved string `json:"resolved,omitempty"` + // self, operator, agent or healer: + Resolver string `json:"resolver,omitempty"` + // what of its own saw it last + Run string `json:"run,omitempty"` + Severity string `json:"severity"` + // the row, probe or event that saw it + Source string `json:"source"` + Subject Subject `json:"subject"` + // one line + Summary string `json:"summary"` + // layout version, 1 + V int64 `json:"v"` +} + +// The values of FindingsByKeyValue's severity. +const ( + FindingsByKeyValueSeverityUrgent = "urgent" + FindingsByKeyValueSeverityWarning = "warning" +) diff --git a/health-and-repair/go/seats/meshconditions/go.mod b/health-and-repair/go/seats/meshconditions/go.mod new file mode 100644 index 0000000..4662aa6 --- /dev/null +++ b/health-and-repair/go/seats/meshconditions/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions + +go 1.26.0 diff --git a/health-and-repair/go/seats/meshhandacts/contract.go b/health-and-repair/go/seats/meshhandacts/contract.go new file mode 100644 index 0000000..dfe00ce --- /dev/null +++ b/health-and-repair/go/seats/meshhandacts/contract.go @@ -0,0 +1,222 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshhandacts is the contract of the seat "mesh-hand-acts", defined whole (seats/mesh-hand-acts/seat.json): Acts done by hand, recorded with who, why and cause (ADR 0565): the log every module records through. +package meshhandacts + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-hand-acts" + +const ( + VerbHandAct = "hand-act" + VerbHandActs = "hand-acts" + VerbWarranted = "warranted" +) + +const ( + EventActRecorded = "act-recorded" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbHandAct, VerbHandActs, VerbWarranted} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventActRecorded} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-hand-acts.events" + FamilyMaxAgeDays = 7 +) + +// SubjectActRecorded is where act-recorded is published. +const SubjectActRecorded = "mesh.seat.mesh-hand-acts.event.act-recorded" + +const ( + StateActs = "acts" + StateWarrants = "warrants" +) + +// ActsByIDKey is the key of one value of the state acts under the form "". +func ActsByIDKey(id string) string { return id } + +// ParseActsByIDKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseActsByIDKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeActsByIDValue reads one value of the state acts under the form "", refusing a field ActsByIDValue does not know and any text after the value. +func DecodeActsByIDValue(raw []byte) (ActsByIDValue, error) { + var v ActsByIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WarrantsByAskerAskKey is the key of one value of the state warrants under the form ".". +func WarrantsByAskerAskKey(asker, ask string) string { return asker + "." + ask } + +// ParseWarrantsByAskerAskKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseWarrantsByAskerAskKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeWarrantsByAskerAskValue reads one value of the state warrants under the form ".", refusing a field WarrantsByAskerAskValue does not know and any text after the value. +func DecodeWarrantsByAskerAskValue(raw []byte) (WarrantsByAskerAskValue, error) { + var v WarrantsByAskerAskValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Act is one act done by hand. +type Act struct { + Arguments []string `json:"arguments,omitempty"` + Ask string `json:"ask,omitempty"` + At string `json:"at"` + // the caller the runtime proved + By string `json:"by"` + Carried []string `json:"carried,omitempty"` + Cause string `json:"cause"` + Condition string `json:"condition,omitempty"` + ID string `json:"id"` + // what the act was read to be from what it did + Kind string `json:"kind,omitempty"` + // which log holds it, set when it is read + Log string `json:"log,omitempty"` + Outcome string `json:"outcome,omitempty"` + Proofs []string `json:"proofs,omitempty"` + RequestedBy string `json:"requested-by,omitempty"` + Verb string `json:"verb"` + Via string `json:"via,omitempty"` + Why string `json:"why"` +} + +// HandActInput is what hand-act takes. +type HandActInput struct { + // the cause in a word, or a condition's kind + Cause string `json:"cause,omitempty"` + // the key of the condition it addressed, if any (optional) + Condition string `json:"condition,omitempty"` + // the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own + Verb string `json:"verb,omitempty"` + // what was done, in a line + What string `json:"what"` + // why it had to be done by hand + Why string `json:"why"` +} + +// HandActAnswer is what hand-act answers. +type HandActAnswer = map[string]any + +// HandActsInput is what hand-acts takes. +type HandActsInput struct { + // only this cause + Cause string `json:"cause,omitempty"` + // how many days back (default 14) + Days string `json:"days,omitempty"` + // "true": every warrant recorded + Warrants string `json:"warrants,omitempty"` + // only acts whose who contains this + Who string `json:"who,omitempty"` +} + +// The values of HandActsInput's warrants. +const ( + HandActsInputWarrantsTrue = "true" + HandActsInputWarrantsFalse = "false" +) + +// HandActsAnswer is what hand-acts answers. +type HandActsAnswer = map[string]any + +// WarrantedInput is what warranted takes. +type WarrantedInput struct { + // its ask's id + Ask string `json:"ask"` + // the module that asked + Asker string `json:"asker"` +} + +// WarrantedAnswer is what warranted answers. +type WarrantedAnswer = map[string]any + +// ActRecordedEvent is the body of the event act-recorded. +type ActRecordedEvent = Act + +// ActsByIDValue is one value of the state acts under the key . +type ActsByIDValue struct { + Arguments []string `json:"arguments,omitempty"` + Ask string `json:"ask,omitempty"` + At string `json:"at"` + // the caller the runtime proved + By string `json:"by"` + Carried []string `json:"carried,omitempty"` + Cause string `json:"cause"` + Condition string `json:"condition,omitempty"` + ID string `json:"id"` + // what the act was read to be from what it did + Kind string `json:"kind,omitempty"` + // which log holds it, set when it is read + Log string `json:"log,omitempty"` + Outcome string `json:"outcome,omitempty"` + Proofs []string `json:"proofs,omitempty"` + RequestedBy string `json:"requested-by,omitempty"` + Verb string `json:"verb"` + Via string `json:"via,omitempty"` + Why string `json:"why"` +} + +// WarrantsByAskerAskValue is one value of the state warrants under the key .. +type WarrantsByAskerAskValue struct { + Ask string `json:"ask"` + Asker string `json:"asker"` + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` +} diff --git a/health-and-repair/go/seats/meshhandacts/go.mod b/health-and-repair/go/seats/meshhandacts/go.mod new file mode 100644 index 0000000..101f623 --- /dev/null +++ b/health-and-repair/go/seats/meshhandacts/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts + +go 1.26.0 diff --git a/health-and-repair/go/seats/meshhealers/contract.go b/health-and-repair/go/seats/meshhealers/contract.go new file mode 100644 index 0000000..b26eaaf --- /dev/null +++ b/health-and-repair/go/seats/meshhealers/contract.go @@ -0,0 +1,197 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshhealers is the contract of the seat "mesh-healers", defined whole (seats/mesh-healers/seat.json): The healers (ADR 0564): each a registered response to one kind of condition, within a budget and the mesh-wide brake. +package meshhealers + +import ( + "bytes" + "encoding/json" + "errors" +) + +// Seat is the seat's name. +const Seat = "mesh-healers" + +const ( + VerbHealers = "healers" + VerbHeal = "heal" + VerbDrill = "drill" +) + +const ( + EventHealerActed = "healer-acted" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbHealers, VerbHeal, VerbDrill} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventHealerActed} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-healers.events" + FamilyMaxAgeDays = 7 +) + +// SubjectHealerActed is where healer-acted is published. +const SubjectHealerActed = "mesh.seat.mesh-healers.event.healer-acted" + +const ( + StateAttempts = "attempts" + StateWould = "would" +) + +// AttemptsByAttemptKey is the key of one value of the state attempts under the form "". +func AttemptsByAttemptKey(attempt string) string { return attempt } + +// ParseAttemptsByAttemptKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseAttemptsByAttemptKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeAttemptsByAttemptValue reads one value of the state attempts under the form "", refusing a field AttemptsByAttemptValue does not know and any text after the value. +func DecodeAttemptsByAttemptValue(raw []byte) (AttemptsByAttemptValue, error) { + var v AttemptsByAttemptValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WouldByWouldKey is the key of one value of the state would under the form "". +func WouldByWouldKey(would string) string { return would } + +// ParseWouldByWouldKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseWouldByWouldKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeWouldByWouldValue reads one value of the state would under the form "", refusing a field WouldByWouldValue does not know and any text after the value. +func DecodeWouldByWouldValue(raw []byte) (WouldByWouldValue, error) { + var v WouldByWouldValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// HealersInput is what healers takes. +type HealersInput struct { + // how many days of acts back (default 7) + Days string `json:"days,omitempty"` +} + +// HealersAnswer is what healers answers. +type HealersAnswer = map[string]any + +// HealInput is what heal takes. +type HealInput struct { + // the condition's key + Key string `json:"key"` + // why now, kept with the act + Why string `json:"why"` +} + +// HealAnswer is what heal answers. +type HealAnswer = map[string]any + +// DrillInput is what drill takes. +type DrillInput struct { + // the key of the condition the drill is meant to raise, if any (optional) + Condition string `json:"condition,omitempty"` + // what was done on purpose, in a line + What string `json:"what"` + // what the drill tests + Why string `json:"why"` +} + +// DrillAnswer is what drill answers. +type DrillAnswer = map[string]any + +// HealerActedEvent is the body of the event healer-acted. +type HealerActedEvent struct { + Act string `json:"act,omitempty"` + At string `json:"at"` + Budget string `json:"budget,omitempty"` + By string `json:"by,omitempty"` + Condition string `json:"condition"` + Event string `json:"event"` + Healer string `json:"healer"` + Kind string `json:"kind,omitempty"` + Outcome string `json:"outcome"` + Said string `json:"said,omitempty"` + Show string `json:"show,omitempty"` +} + +// The values of HealerActedEvent's outcome. +const ( + HealerActedEventOutcomeActing = "acting" + HealerActedEventOutcomeActed = "acted" + HealerActedEventOutcomeFailed = "failed" + HealerActedEventOutcomeEscalated = "escalated" +) + +// AttemptsByAttemptValue is one value of the state attempts under the key . +type AttemptsByAttemptValue struct { + Act string `json:"act"` + Begun string `json:"begun"` + // what the budget counts by: the condition's key unless the healer says another + BudgetKey string `json:"budget-key"` + Condition string `json:"condition"` + Finished string `json:"finished,omitempty"` + Healer string `json:"healer"` + Kind string `json:"kind"` + Outcome string `json:"outcome"` + Said string `json:"said,omitempty"` +} + +// The values of AttemptsByAttemptValue's outcome. +const ( + AttemptsByAttemptValueOutcomeActing = "acting" + AttemptsByAttemptValueOutcomeActed = "acted" + AttemptsByAttemptValueOutcomeFailed = "failed" + AttemptsByAttemptValueOutcomeEscalated = "escalated" +) + +// WouldByWouldValue is one value of the state would under the key . +type WouldByWouldValue struct { + Act string `json:"act"` + At string `json:"at"` + Condition string `json:"condition"` + Healer string `json:"healer"` + Kind string `json:"kind,omitempty"` + Why string `json:"why,omitempty"` +} diff --git a/health-and-repair/go/seats/meshhealers/go.mod b/health-and-repair/go/seats/meshhealers/go.mod new file mode 100644 index 0000000..9eb5a51 --- /dev/null +++ b/health-and-repair/go/seats/meshhealers/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers + +go 1.26.0 diff --git a/health-and-repair/go/seats/meshselfcheck/contract.go b/health-and-repair/go/seats/meshselfcheck/contract.go new file mode 100644 index 0000000..2c67551 --- /dev/null +++ b/health-and-repair/go/seats/meshselfcheck/contract.go @@ -0,0 +1,101 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshselfcheck is the contract of the seat "mesh-self-check", defined whole (seats/mesh-self-check/seat.json): The mesh checking itself (ADR 0564): probes run every five minutes, each failure a finding the seat's conditions raise. +package meshselfcheck + +// Seat is the seat's name. +const Seat = "mesh-self-check" + +const ( + VerbDoctor = "doctor" + VerbProbes = "probes" +) + +const ( + EventChecked = "checked" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbDoctor, VerbProbes} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventChecked} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-self-check.events" + FamilyMaxAgeDays = 7 +) + +// SubjectChecked is where checked is published. +const SubjectChecked = "mesh.seat.mesh-self-check.event.checked" + +// ProbeVerdict is one probe's verdict. +type ProbeVerdict struct { + Error string `json:"error,omitempty"` + Found []string `json:"found,omitempty"` + FoundCount int64 `json:"found-count,omitempty"` + ID string `json:"id"` + More string `json:"more,omitempty"` + Took string `json:"took,omitempty"` + Unconfirmed []string `json:"unconfirmed,omitempty"` + Verdict string `json:"verdict"` +} + +// The values of ProbeVerdict's verdict. +const ( + ProbeVerdictVerdictPassed = "passed" + ProbeVerdictVerdictFailed = "failed" + ProbeVerdictVerdictFailedToRun = "failed-to-run" + ProbeVerdictVerdictDeferred = "deferred" +) + +// DoctorInput is what doctor takes. +type DoctorInput struct { + // one probe's id (D6): its verdict alone, with every finding + Probe string `json:"probe,omitempty"` + // "true": run every probe now and answer the verdict + Run string `json:"run,omitempty"` +} + +// The values of DoctorInput's run. +const ( + DoctorInputRunTrue = "true" + DoctorInputRunFalse = "false" +) + +// DoctorAnswer is what doctor answers. +type DoctorAnswer = map[string]any + +// ProbesInput is what probes takes. +type ProbesInput struct { +} + +// ProbesAnswer is what probes answers. +type ProbesAnswer = map[string]any + +// CheckedEventCounts is a type of the contract. +type CheckedEventCounts struct { + Deferred int64 `json:"deferred,omitempty"` + Failed int64 `json:"failed,omitempty"` + FailedToRun int64 `json:"failed-to-run,omitempty"` + Passed int64 `json:"passed,omitempty"` +} + +// CheckedEvent is the body of the event checked. +type CheckedEvent struct { + At string `json:"at"` + Controller string `json:"controller,omitempty"` + Counts *CheckedEventCounts `json:"counts,omitempty"` + IntervalSeconds int64 `json:"interval-seconds,omitempty"` + Probes []ProbeVerdict `json:"probes"` + Run string `json:"run"` + Started string `json:"started,omitempty"` + Took string `json:"took,omitempty"` + Unsaid int64 `json:"unsaid,omitempty"` + Why string `json:"why,omitempty"` +} diff --git a/health-and-repair/go/seats/meshselfcheck/go.mod b/health-and-repair/go/seats/meshselfcheck/go.mod new file mode 100644 index 0000000..a45e4c3 --- /dev/null +++ b/health-and-repair/go/seats/meshselfcheck/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck + +go 1.26.0 diff --git a/health-and-repair/go/seats/meshwatchdogs/contract.go b/health-and-repair/go/seats/meshwatchdogs/contract.go new file mode 100644 index 0000000..05c76ca --- /dev/null +++ b/health-and-repair/go/seats/meshwatchdogs/contract.go @@ -0,0 +1,266 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshwatchdogs is the contract of the seat "mesh-watchdogs", defined whole (seats/mesh-watchdogs/seat.json): The watchman's seat (ADR 0564, ADR 0433): signals that expire unless renewed, what stopped moving, who is present, the dead letters. +package meshwatchdogs + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-watchdogs" + +const ( + VerbSignals = "signals" + VerbStuck = "stuck" + VerbPresence = "presence" + VerbDeadLetters = "dead-letters" + VerbResync = "resync" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbSignals, VerbStuck, VerbPresence, VerbDeadLetters, VerbResync} +} + +const ( + StatePresence = "presence" + StatePulse = "pulse" + StateStuck = "stuck" +) + +// PresenceByNodeKey is the key of one value of the state presence under the form "". +func PresenceByNodeKey(node string) string { return node } + +// ParsePresenceByNodeKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParsePresenceByNodeKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodePresenceByNodeValue reads one value of the state presence under the form "", refusing a field PresenceByNodeValue does not know and any text after the value. +func DecodePresenceByNodeValue(raw []byte) (PresenceByNodeValue, error) { + var v PresenceByNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PresenceByPartPartInstanceKey is the key of one value of the state presence under the form "part..". +func PresenceByPartPartInstanceKey(part, instance string) string { + return "part." + part + "." + instance +} + +// ParsePresenceByPartPartInstanceKey reads the parts of a key of the form "part..", in the order of the form; the last result is false for a key of another form. +func ParsePresenceByPartPartInstanceKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "part.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodePresenceByPartPartInstanceValue reads one value of the state presence under the form "part..", refusing a field PresenceByPartPartInstanceValue does not know and any text after the value. +func DecodePresenceByPartPartInstanceValue(raw []byte) (PresenceByPartPartInstanceValue, error) { + var v PresenceByPartPartInstanceValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PulseByBucketKeyKey is the key of one value of the state pulse under the form ".". +func PulseByBucketKeyKey(bucket, key string) string { return bucket + "." + key } + +// ParsePulseByBucketKeyKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParsePulseByBucketKeyKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodePulseByBucketKeyValue reads one value of the state pulse under the form ".", refusing a field PulseByBucketKeyValue does not know and any text after the value. +func DecodePulseByBucketKeyValue(raw []byte) (PulseByBucketKeyValue, error) { + var v PulseByBucketKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// StuckByBucketKeyKey is the key of one value of the state stuck under the form ".". +func StuckByBucketKeyKey(bucket, key string) string { return bucket + "." + key } + +// ParseStuckByBucketKeyKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseStuckByBucketKeyKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeStuckByBucketKeyValue reads one value of the state stuck under the form ".", refusing a field StuckByBucketKeyValue does not know and any text after the value. +func DecodeStuckByBucketKeyValue(raw []byte) (StuckByBucketKeyValue, error) { + var v StuckByBucketKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SignalsInput is what signals takes. +type SignalsInput struct { + // one row, e.g. S1 + Row string `json:"row,omitempty"` +} + +// SignalsAnswer is what signals answers. +type SignalsAnswer = map[string]any + +// StuckInput is what stuck takes. +type StuckInput struct { + // only those on this machine + Machine string `json:"machine,omitempty"` +} + +// StuckAnswer is what stuck answers. +type StuckAnswer = map[string]any + +// PresenceInput is what presence takes. +type PresenceInput struct { + // one machine + Machine string `json:"machine,omitempty"` +} + +// PresenceAnswer is what presence answers. +type PresenceAnswer = map[string]any + +// DeadLettersInput is what dead-letters takes. +type DeadLettersInput struct { + // with deliver or drop: the cause in a word (dead-letter when absent) + Cause string `json:"cause,omitempty"` + // a consumer's name, or .: only what that one gave up on + Consumer string `json:"consumer,omitempty"` + // a dead letter's id: deliver it again to the consumer that gave it up (needs why) + Deliver string `json:"deliver,omitempty"` + // a dead letter's id: drop it for good (needs why) + Drop string `json:"drop,omitempty"` + // a dead letter's id, as the list gives it: that one whole, with its message + ID string `json:"id,omitempty"` + // how many to list, newest first (default 50); only when listing + Limit string `json:"limit,omitempty"` + // with deliver or drop: why, required and recorded + Why string `json:"why,omitempty"` +} + +// DeadLettersAnswer is what dead-letters answers. +type DeadLettersAnswer = map[string]any + +// ResyncInput is what resync takes. +type ResyncInput struct { +} + +// ResyncAnswer is what resync answers. +type ResyncAnswer = map[string]any + +// PresenceByNodeValue is one value of the state presence under the key . +type PresenceByNodeValue struct { + Online bool `json:"online"` + V int64 `json:"v,omitempty"` +} + +// PresenceByPartPartInstanceValue is one value of the state presence under the key part... +type PresenceByPartPartInstanceValue struct { + Online bool `json:"online"` + V int64 `json:"v,omitempty"` +} + +// PulseByBucketKeyValue is one value of the state pulse under the key .. +type PulseByBucketKeyValue struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + Owes string `json:"owes,omitempty"` + Revision int64 `json:"revision"` + V int64 `json:"v,omitempty"` +} + +// StuckByBucketKeyValue is one value of the state stuck under the key .. +type StuckByBucketKeyValue struct { + Bucket string `json:"bucket"` + Key string `json:"key"` + // it has moved again + Moved bool `json:"moved,omitempty"` + Since string `json:"since"` + V int64 `json:"v,omitempty"` + // what it waited for, in words + Waited string `json:"waited,omitempty"` +} diff --git a/health-and-repair/go/seats/meshwatchdogs/go.mod b/health-and-repair/go/seats/meshwatchdogs/go.mod new file mode 100644 index 0000000..e2e0cf2 --- /dev/null +++ b/health-and-repair/go/seats/meshwatchdogs/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs + +go 1.26.0 diff --git a/health-and-repair/seats/index.json b/health-and-repair/seats/index.json new file mode 100644 index 0000000..8cbd1f8 --- /dev/null +++ b/health-and-repair/seats/index.json @@ -0,0 +1,10 @@ +{ + "format": 1, + "seats": [ + "mesh-conditions", + "mesh-hand-acts", + "mesh-healers", + "mesh-self-check", + "mesh-watchdogs" + ] +} diff --git a/health-and-repair/ts/seats/meshconditions/package.json b/health-and-repair/ts/seats/meshconditions/package.json new file mode 100644 index 0000000..f02f3b7 --- /dev/null +++ b/health-and-repair/ts/seats/meshconditions/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-conditions, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-health-and-repair-contracts-mesh-conditions", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshconditions/src/index.ts b/health-and-repair/ts/seats/meshconditions/src/index.ts new file mode 100644 index 0000000..2371334 --- /dev/null +++ b/health-and-repair/ts/seats/meshconditions/src/index.ts @@ -0,0 +1,280 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-conditions", defined whole (seats/mesh-conditions/seat.json): What is wrong with the mesh now (ADR 0564): the open conditions raised from the observers' findings and cleared when a finding goes. + +export const seat = "mesh-conditions"; + +export const verbs = { + conditions: "conditions", + silence: "silence", + resync: "resync", +} as const; + +export const events = { + conditionRaised: "condition-raised", + conditionChanged: "condition-changed", + conditionCleared: "condition-cleared", +} as const; + +export const family = { name: "mesh-conditions.events", maxAgeDays: 7 } as const; + +export const conditionRaisedSubject = "mesh.seat.mesh-conditions.event.condition-raised"; + +export const conditionChangedSubject = "mesh.seat.mesh-conditions.event.condition-changed"; + +export const conditionClearedSubject = "mesh.seat.mesh-conditions.event.condition-cleared"; + +export const states = { + conditions: "conditions", + findings: "findings", +} as const; + +/** The key of one value of the state conditions under the form "". */ +export function conditionsByKeyKey(key: string): string { + return key; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseConditionsByKeyKey(key: string): { key: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { key }; +} + +/** The key of one value of the state findings under the form "". */ +export function findingsByKeyKey(key: string): string { + return key; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseFindingsByKeyKey(key: string): { key: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { key }; +} + +/** one answer the operator may give from a notification */ +export interface Action { + "arguments"?: Record; + "label": string; + "level"?: "acknowledge" | "approve"; + "machine"?: string; + /** the seat verb the operator channel calls */ + "verb": string; +} + +export interface ConditionEventGapsItem { + "cleared"?: string; + "reopened"?: string; +} + +export interface ConditionEventSilenced { + "by"?: string; + "since"?: string; + "until"?: string; + "why"?: string; +} + +export interface ConditionEventTriedItem { + "at"?: string; + "by"?: string; + "outcome"?: string; + "what"?: string; +} + +/** a condition and what changed */ +export interface ConditionEvent { + "actions"?: Action[]; + "at": string; + "change": "raised" | "reopened" | "severity" | "resolver" | "silenced" | "silence-ended" | "cleared" | "words"; + "cleared"?: string; + "count"?: number; + "epoch"?: number; + "event": string; + /** newest first, at most 10 */ + "evidence"?: Evidence[]; + "explanation"?: string; + "first"?: string; + "gaps"?: ConditionEventGapsItem[]; + "headline"?: string; + /** scope.id.token */ + "key": string; + "kind"?: string; + "last-observed"?: string; + "looks-unread"?: string; + "needs"?: string; + "observations"?: number; + "raised"?: string; + "resolved"?: string; + /** self, operator, agent or healer: */ + "resolver"?: string; + "severity"?: "urgent" | "warning"; + /** the tool call that shows it */ + "show"?: string; + "silenced"?: ConditionEventSilenced; + /** the row, probe or event that saw it */ + "source"?: string; + "subject"?: Subject; + /** one line */ + "summary"?: string; + "tried"?: ConditionEventTriedItem[]; + "was"?: string; + "why"?: string; +} + +/** one observation, as it was said */ +export interface Evidence { + "at": string; + "said": string; +} + +/** what the condition is about */ +export interface Subject { + /** the other machines it concerns */ + "also"?: string[]; + /** its id within the scope */ + "id": string; + /** the machine it concerns, when there is one */ + "machine"?: string; + /** machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module */ + "scope": string; +} + +/** is what conditions takes. */ +export interface ConditionsInput { + /** with history: how many days back (default 7, at most 90) */ + "days"?: string; + /** "true": every raising, change, silence and clearing lately, oldest first */ + "history"?: "true" | "false"; + /** a condition's key: that one whole; with history, only its transitions */ + "key"?: string; + /** only those about this machine */ + "machine"?: string; + /** only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module */ + "scope"?: string; + /** only urgent, or only warning */ + "severity"?: string; +} + +/** is what conditions answers. */ +export type ConditionsAnswer = Record; + +/** is what silence takes. */ +export interface SilenceInput { + /** the cause in a word (the condition's kind when absent) */ + "cause"?: string; + /** how long: 30m, 4h, 2d; at most 7d */ + "for": string; + /** the condition's key */ + "key": string; + /** why, required and recorded */ + "why": string; +} + +/** is what silence answers. */ +export type SilenceAnswer = Record; + +/** is what resync takes. */ +export interface ResyncInput { +} + +/** is what resync answers. */ +export type ResyncAnswer = Record; + +/** is the body of the event condition-raised. */ +export type ConditionRaisedEvent = ConditionEvent; + +/** is the body of the event condition-changed. */ +export type ConditionChangedEvent = ConditionEvent; + +/** is the body of the event condition-cleared. */ +export type ConditionClearedEvent = ConditionEvent; + +export interface ConditionsByKeyValueGapsItem { + "cleared"?: string; + "reopened"?: string; +} + +export interface ConditionsByKeyValueSilenced { + "by"?: string; + "since"?: string; + "until"?: string; + "why"?: string; +} + +export interface ConditionsByKeyValueTriedItem { + "at"?: string; + "by"?: string; + "outcome"?: string; + "what"?: string; +} + +/** is one value of the state conditions under the key . */ +export interface ConditionsByKeyValue { + "actions"?: Action[]; + "count"?: number; + "epoch"?: number; + /** newest first, at most 10 */ + "evidence"?: Evidence[]; + "explanation"?: string; + "first"?: string; + "gaps"?: ConditionsByKeyValueGapsItem[]; + "headline"?: string; + /** scope.id.token */ + "key": string; + "kind": string; + "last-observed": string; + "looks-unread"?: string; + "needs"?: string; + "observations"?: number; + "raised": string; + "resolved"?: string; + /** self, operator, agent or healer: */ + "resolver"?: string; + "severity": "urgent" | "warning"; + "silenced"?: ConditionsByKeyValueSilenced; + /** the row, probe or event that saw it */ + "source"?: string; + "subject": Subject; + /** one line */ + "summary"?: string; + "tried"?: ConditionsByKeyValueTriedItem[]; +} + +/** is one value of the state findings under the key . */ +export interface FindingsByKeyValue { + "actions"?: Action[]; + "at": string; + /** newest first, at most 10 */ + "evidence"?: Evidence[]; + "explanation"?: string; + "headline"?: string; + /** scope.id.token */ + "key": string; + "kind": string; + "needs"?: string; + /** the module that wrote it */ + "observer": string; + /** the key's last part when it is not the kind */ + "ref"?: string; + "resolved"?: string; + /** self, operator, agent or healer: */ + "resolver"?: string; + /** what of its own saw it last */ + "run"?: string; + "severity": "urgent" | "warning"; + /** the row, probe or event that saw it */ + "source": string; + "subject": Subject; + /** one line */ + "summary": string; + /** layout version, 1 */ + "v": number; +} + diff --git a/ts/seats/operatorchannel/tsconfig.json b/health-and-repair/ts/seats/meshconditions/tsconfig.json similarity index 100% rename from ts/seats/operatorchannel/tsconfig.json rename to health-and-repair/ts/seats/meshconditions/tsconfig.json diff --git a/health-and-repair/ts/seats/meshhandacts/package.json b/health-and-repair/ts/seats/meshhandacts/package.json new file mode 100644 index 0000000..c5da858 --- /dev/null +++ b/health-and-repair/ts/seats/meshhandacts/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-hand-acts, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-health-and-repair-contracts-mesh-hand-acts", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshhandacts/src/index.ts b/health-and-repair/ts/seats/meshhandacts/src/index.ts new file mode 100644 index 0000000..8568bb0 --- /dev/null +++ b/health-and-repair/ts/seats/meshhandacts/src/index.ts @@ -0,0 +1,160 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-hand-acts", defined whole (seats/mesh-hand-acts/seat.json): Acts done by hand, recorded with who, why and cause (ADR 0565): the log every module records through. + +export const seat = "mesh-hand-acts"; + +export const verbs = { + handAct: "hand-act", + handActs: "hand-acts", + warranted: "warranted", +} as const; + +export const events = { + actRecorded: "act-recorded", +} as const; + +export const family = { name: "mesh-hand-acts.events", maxAgeDays: 7 } as const; + +export const actRecordedSubject = "mesh.seat.mesh-hand-acts.event.act-recorded"; + +export const states = { + acts: "acts", + warrants: "warrants", +} as const; + +/** The key of one value of the state acts under the form "". */ +export function actsByIDKey(id: string): string { + return id; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseActsByIDKey(key: string): { id: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const id = rest; + rest = ""; + if (rest !== "") return undefined; + return { id }; +} + +/** The key of one value of the state warrants under the form ".". */ +export function warrantsByAskerAskKey(asker: string, ask: string): string { + return asker + "." + ask; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseWarrantsByAskerAskKey(key: string): { asker: string; ask: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const asker = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const ask = rest; + rest = ""; + if (rest !== "") return undefined; + return { asker, ask }; +} + +/** one act done by hand */ +export interface Act { + "arguments"?: string[]; + "ask"?: string; + "at": string; + /** the caller the runtime proved */ + "by": string; + "carried"?: string[]; + "cause": string; + "condition"?: string; + "id": string; + /** what the act was read to be from what it did */ + "kind"?: string; + /** which log holds it, set when it is read */ + "log"?: string; + "outcome"?: string; + "proofs"?: string[]; + "requested-by"?: string; + "verb": string; + "via"?: string; + "why": string; +} + +/** is what hand-act takes. */ +export interface HandActInput { + /** the cause in a word, or a condition's kind */ + "cause"?: string; + /** the key of the condition it addressed, if any (optional) */ + "condition"?: string; + /** the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own */ + "verb"?: string; + /** what was done, in a line */ + "what": string; + /** why it had to be done by hand */ + "why": string; +} + +/** is what hand-act answers. */ +export type HandActAnswer = Record; + +/** is what hand-acts takes. */ +export interface HandActsInput { + /** only this cause */ + "cause"?: string; + /** how many days back (default 14) */ + "days"?: string; + /** "true": every warrant recorded */ + "warrants"?: "true" | "false"; + /** only acts whose who contains this */ + "who"?: string; +} + +/** is what hand-acts answers. */ +export type HandActsAnswer = Record; + +/** is what warranted takes. */ +export interface WarrantedInput { + /** its ask's id */ + "ask": string; + /** the module that asked */ + "asker": string; +} + +/** is what warranted answers. */ +export type WarrantedAnswer = Record; + +/** is the body of the event act-recorded. */ +export type ActRecordedEvent = Act; + +/** is one value of the state acts under the key . */ +export interface ActsByIDValue { + "arguments"?: string[]; + "ask"?: string; + "at": string; + /** the caller the runtime proved */ + "by": string; + "carried"?: string[]; + "cause": string; + "condition"?: string; + "id": string; + /** what the act was read to be from what it did */ + "kind"?: string; + /** which log holds it, set when it is read */ + "log"?: string; + "outcome"?: string; + "proofs"?: string[]; + "requested-by"?: string; + "verb": string; + "via"?: string; + "why": string; +} + +/** is one value of the state warrants under the key .. */ +export interface WarrantsByAskerAskValue { + "ask": string; + "asker": string; + "at"?: string; + "by"?: string; + "option"?: string; +} + diff --git a/ts/tsconfig.json b/health-and-repair/ts/seats/meshhandacts/tsconfig.json similarity index 100% rename from ts/tsconfig.json rename to health-and-repair/ts/seats/meshhandacts/tsconfig.json diff --git a/ts/package.json b/health-and-repair/ts/seats/meshhealers/package.json similarity index 60% rename from ts/package.json rename to health-and-repair/ts/seats/meshhealers/package.json index cd5565a..6ffcf90 100644 --- a/ts/package.json +++ b/health-and-repair/ts/seats/meshhealers/package.json @@ -1,5 +1,5 @@ { - "description": "The mesh's seat set, generated from every seat's definition.", + "description": "The contract of the seat mesh-healers, generated from its definition.", "devDependencies": { "typescript": "^5.6.0" }, @@ -9,7 +9,7 @@ "files": [ "dist" ], - "name": "@novox/mesh-contracts", + "name": "@novox/mesh-health-and-repair-contracts-mesh-healers", "scripts": { "build": "tsc", "prepare": "tsc" diff --git a/health-and-repair/ts/seats/meshhealers/src/index.ts b/health-and-repair/ts/seats/meshhealers/src/index.ts new file mode 100644 index 0000000..3e0c695 --- /dev/null +++ b/health-and-repair/ts/seats/meshhealers/src/index.ts @@ -0,0 +1,127 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-healers", defined whole (seats/mesh-healers/seat.json): The healers (ADR 0564): each a registered response to one kind of condition, within a budget and the mesh-wide brake. + +export const seat = "mesh-healers"; + +export const verbs = { + healers: "healers", + heal: "heal", + drill: "drill", +} as const; + +export const events = { + healerActed: "healer-acted", +} as const; + +export const family = { name: "mesh-healers.events", maxAgeDays: 7 } as const; + +export const healerActedSubject = "mesh.seat.mesh-healers.event.healer-acted"; + +export const states = { + attempts: "attempts", + would: "would", +} as const; + +/** The key of one value of the state attempts under the form "". */ +export function attemptsByAttemptKey(attempt: string): string { + return attempt; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseAttemptsByAttemptKey(key: string): { attempt: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const attempt = rest; + rest = ""; + if (rest !== "") return undefined; + return { attempt }; +} + +/** The key of one value of the state would under the form "". */ +export function wouldByWouldKey(would: string): string { + return would; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseWouldByWouldKey(key: string): { would: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const would = rest; + rest = ""; + if (rest !== "") return undefined; + return { would }; +} + +/** is what healers takes. */ +export interface HealersInput { + /** how many days of acts back (default 7) */ + "days"?: string; +} + +/** is what healers answers. */ +export type HealersAnswer = Record; + +/** is what heal takes. */ +export interface HealInput { + /** the condition's key */ + "key": string; + /** why now, kept with the act */ + "why": string; +} + +/** is what heal answers. */ +export type HealAnswer = Record; + +/** is what drill takes. */ +export interface DrillInput { + /** the key of the condition the drill is meant to raise, if any (optional) */ + "condition"?: string; + /** what was done on purpose, in a line */ + "what": string; + /** what the drill tests */ + "why": string; +} + +/** is what drill answers. */ +export type DrillAnswer = Record; + +/** is the body of the event healer-acted. */ +export interface HealerActedEvent { + "act"?: string; + "at": string; + "budget"?: string; + "by"?: string; + "condition": string; + "event": string; + "healer": string; + "kind"?: string; + "outcome": "acting" | "acted" | "failed" | "escalated"; + "said"?: string; + "show"?: string; +} + +/** is one value of the state attempts under the key . */ +export interface AttemptsByAttemptValue { + "act": string; + "begun": string; + /** what the budget counts by: the condition's key unless the healer says another */ + "budget-key": string; + "condition": string; + "finished"?: string; + "healer": string; + "kind": string; + "outcome": "acting" | "acted" | "failed" | "escalated"; + "said"?: string; +} + +/** is one value of the state would under the key . */ +export interface WouldByWouldValue { + "act": string; + "at": string; + "condition": string; + "healer": string; + "kind"?: string; + "why"?: string; +} + diff --git a/health-and-repair/ts/seats/meshhealers/tsconfig.json b/health-and-repair/ts/seats/meshhealers/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/health-and-repair/ts/seats/meshhealers/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/health-and-repair/ts/seats/meshselfcheck/package.json b/health-and-repair/ts/seats/meshselfcheck/package.json new file mode 100644 index 0000000..9cd3661 --- /dev/null +++ b/health-and-repair/ts/seats/meshselfcheck/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-self-check, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-health-and-repair-contracts-mesh-self-check", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshselfcheck/src/index.ts b/health-and-repair/ts/seats/meshselfcheck/src/index.ts new file mode 100644 index 0000000..b76fc1d --- /dev/null +++ b/health-and-repair/ts/seats/meshselfcheck/src/index.ts @@ -0,0 +1,70 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-self-check", defined whole (seats/mesh-self-check/seat.json): The mesh checking itself (ADR 0564): probes run every five minutes, each failure a finding the seat's conditions raise. + +export const seat = "mesh-self-check"; + +export const verbs = { + doctor: "doctor", + probes: "probes", +} as const; + +export const events = { + checked: "checked", +} as const; + +export const family = { name: "mesh-self-check.events", maxAgeDays: 7 } as const; + +export const checkedSubject = "mesh.seat.mesh-self-check.event.checked"; + +/** one probe's verdict */ +export interface ProbeVerdict { + "error"?: string; + "found"?: string[]; + "found-count"?: number; + "id": string; + "more"?: string; + "took"?: string; + "unconfirmed"?: string[]; + "verdict": "passed" | "failed" | "failed-to-run" | "deferred"; +} + +/** is what doctor takes. */ +export interface DoctorInput { + /** one probe's id (D6): its verdict alone, with every finding */ + "probe"?: string; + /** "true": run every probe now and answer the verdict */ + "run"?: "true" | "false"; +} + +/** is what doctor answers. */ +export type DoctorAnswer = Record; + +/** is what probes takes. */ +export interface ProbesInput { +} + +/** is what probes answers. */ +export type ProbesAnswer = Record; + +export interface CheckedEventCounts { + "deferred"?: number; + "failed"?: number; + "failed-to-run"?: number; + "passed"?: number; +} + +/** is the body of the event checked. */ +export interface CheckedEvent { + "at": string; + "controller"?: string; + "counts"?: CheckedEventCounts; + "interval-seconds"?: number; + "probes": ProbeVerdict[]; + "run": string; + "started"?: string; + "took"?: string; + "unsaid"?: number; + "why"?: string; +} + diff --git a/health-and-repair/ts/seats/meshselfcheck/tsconfig.json b/health-and-repair/ts/seats/meshselfcheck/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/health-and-repair/ts/seats/meshselfcheck/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/health-and-repair/ts/seats/meshwatchdogs/package.json b/health-and-repair/ts/seats/meshwatchdogs/package.json new file mode 100644 index 0000000..f6d5493 --- /dev/null +++ b/health-and-repair/ts/seats/meshwatchdogs/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-watchdogs, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-health-and-repair-contracts-mesh-watchdogs", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshwatchdogs/src/index.ts b/health-and-repair/ts/seats/meshwatchdogs/src/index.ts new file mode 100644 index 0000000..ed74cd9 --- /dev/null +++ b/health-and-repair/ts/seats/meshwatchdogs/src/index.ts @@ -0,0 +1,182 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-watchdogs", defined whole (seats/mesh-watchdogs/seat.json): The watchman's seat (ADR 0564, ADR 0433): signals that expire unless renewed, what stopped moving, who is present, the dead letters. + +export const seat = "mesh-watchdogs"; + +export const verbs = { + signals: "signals", + stuck: "stuck", + presence: "presence", + deadLetters: "dead-letters", + resync: "resync", +} as const; + +export const states = { + presence: "presence", + pulse: "pulse", + stuck: "stuck", +} as const; + +/** The key of one value of the state presence under the form "". */ +export function presenceByNodeKey(node: string): string { + return node; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parsePresenceByNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state presence under the form "part..". */ +export function presenceByPartPartInstanceKey(part: string, instance: string): string { + return "part." + part + "." + instance; +} + +/** The parts of a key of the form "part..", or undefined for a key of another form. */ +export function parsePresenceByPartPartInstanceKey(key: string): { part: string; instance: string } | undefined { + let rest = key; + if (!rest.startsWith("part.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const part = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const instance = rest; + rest = ""; + if (rest !== "") return undefined; + return { part, instance }; +} + +/** The key of one value of the state pulse under the form ".". */ +export function pulseByBucketKeyKey(bucket: string, key: string): string { + return bucket + "." + key; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parsePulseByBucketKeyKey(key: string): { bucket: string; key: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const bucket = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { bucket, key }; +} + +/** The key of one value of the state stuck under the form ".". */ +export function stuckByBucketKeyKey(bucket: string, key: string): string { + return bucket + "." + key; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseStuckByBucketKeyKey(key: string): { bucket: string; key: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const bucket = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { bucket, key }; +} + +/** is what signals takes. */ +export interface SignalsInput { + /** one row, e.g. S1 */ + "row"?: string; +} + +/** is what signals answers. */ +export type SignalsAnswer = Record; + +/** is what stuck takes. */ +export interface StuckInput { + /** only those on this machine */ + "machine"?: string; +} + +/** is what stuck answers. */ +export type StuckAnswer = Record; + +/** is what presence takes. */ +export interface PresenceInput { + /** one machine */ + "machine"?: string; +} + +/** is what presence answers. */ +export type PresenceAnswer = Record; + +/** is what dead-letters takes. */ +export interface DeadLettersInput { + /** with deliver or drop: the cause in a word (dead-letter when absent) */ + "cause"?: string; + /** a consumer's name, or .: only what that one gave up on */ + "consumer"?: string; + /** a dead letter's id: deliver it again to the consumer that gave it up (needs why) */ + "deliver"?: string; + /** a dead letter's id: drop it for good (needs why) */ + "drop"?: string; + /** a dead letter's id, as the list gives it: that one whole, with its message */ + "id"?: string; + /** how many to list, newest first (default 50); only when listing */ + "limit"?: string; + /** with deliver or drop: why, required and recorded */ + "why"?: string; +} + +/** is what dead-letters answers. */ +export type DeadLettersAnswer = Record; + +/** is what resync takes. */ +export interface ResyncInput { +} + +/** is what resync answers. */ +export type ResyncAnswer = Record; + +/** is one value of the state presence under the key . */ +export interface PresenceByNodeValue { + "online": boolean; + "v"?: number; +} + +/** is one value of the state presence under the key part... */ +export interface PresenceByPartPartInstanceValue { + "online": boolean; + "v"?: number; +} + +/** is one value of the state pulse under the key .. */ +export interface PulseByBucketKeyValue { + "bucket": string; + "key": string; + "owes"?: string; + "revision": number; + "v"?: number; +} + +/** is one value of the state stuck under the key .. */ +export interface StuckByBucketKeyValue { + "bucket": string; + "key": string; + /** it has moved again */ + "moved"?: boolean; + "since": string; + "v"?: number; + /** what it waited for, in words */ + "waited"?: string; +} + diff --git a/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json b/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/go/clients/operatorchannelclient/ask.go b/operator-and-conversation/go/clients/operatorchannelclient/ask.go similarity index 99% rename from go/clients/operatorchannelclient/ask.go rename to operator-and-conversation/go/clients/operatorchannelclient/ask.go index ee93987..eb5a5ed 100644 --- a/go/clients/operatorchannelclient/ask.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/ask.go @@ -18,7 +18,7 @@ import ( "strings" "time" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" ) // The levels an option is answered at: how much proof its answer needs (novox/hq ADR 0234 §8, the glossary's diff --git a/go/clients/operatorchannelclient/ask_test.go b/operator-and-conversation/go/clients/operatorchannelclient/ask_test.go similarity index 99% rename from go/clients/operatorchannelclient/ask_test.go rename to operator-and-conversation/go/clients/operatorchannelclient/ask_test.go index 7166bbe..9d32152 100644 --- a/go/clients/operatorchannelclient/ask_test.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/ask_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" ) var now = time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC) diff --git a/go/clients/operatorchannelclient/client.go b/operator-and-conversation/go/clients/operatorchannelclient/client.go similarity index 98% rename from go/clients/operatorchannelclient/client.go rename to operator-and-conversation/go/clients/operatorchannelclient/client.go index f67025a..b809fe1 100644 --- a/go/clients/operatorchannelclient/client.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/client.go @@ -17,7 +17,7 @@ import ( "strings" "time" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" ) diff --git a/go/clients/operatorchannelclient/client_test.go b/operator-and-conversation/go/clients/operatorchannelclient/client_test.go similarity index 98% rename from go/clients/operatorchannelclient/client_test.go rename to operator-and-conversation/go/clients/operatorchannelclient/client_test.go index 3739b99..45c2b0a 100644 --- a/go/clients/operatorchannelclient/client_test.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/client_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/process" ) diff --git a/go/clients/operatorchannelclient/condition.go b/operator-and-conversation/go/clients/operatorchannelclient/condition.go similarity index 97% rename from go/clients/operatorchannelclient/condition.go rename to operator-and-conversation/go/clients/operatorchannelclient/condition.go index af9485b..0fa5c5a 100644 --- a/go/clients/operatorchannelclient/condition.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/condition.go @@ -7,7 +7,7 @@ import ( "reflect" "strings" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" ) // ConditionSubject is where a module says a condition to the operator: the seat's by-caller condition accept diff --git a/go/clients/operatorchannelclient/condition_test.go b/operator-and-conversation/go/clients/operatorchannelclient/condition_test.go similarity index 98% rename from go/clients/operatorchannelclient/condition_test.go rename to operator-and-conversation/go/clients/operatorchannelclient/condition_test.go index c9f32d1..2450b24 100644 --- a/go/clients/operatorchannelclient/condition_test.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/condition_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - seat "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel" + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" ) func TestConditionSubject(t *testing.T) { // 1202/AC1 diff --git a/go/clients/operatorchannelclient/go.mod b/operator-and-conversation/go/clients/operatorchannelclient/go.mod similarity index 59% rename from go/clients/operatorchannelclient/go.mod rename to operator-and-conversation/go/clients/operatorchannelclient/go.mod index 375590f..dedb474 100644 --- a/go/clients/operatorchannelclient/go.mod +++ b/operator-and-conversation/go/clients/operatorchannelclient/go.mod @@ -1,14 +1,13 @@ -module git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient +module git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a + git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef ) require ( - git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a // indirect github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect github.com/nats-io/nkeys v0.4.16 // indirect diff --git a/go/clients/operatorchannelclient/go.sum b/operator-and-conversation/go/clients/operatorchannelclient/go.sum similarity index 100% rename from go/clients/operatorchannelclient/go.sum rename to operator-and-conversation/go/clients/operatorchannelclient/go.sum diff --git a/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go similarity index 87% rename from go/seats/operatorchannel/contract.go rename to operator-and-conversation/go/seats/operatorchannel/contract.go index bb087ee..540d371 100644 --- a/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -3,6 +3,13 @@ // Package operatorchannel is the contract of the seat "operator-channel", defined whole (seats/operator-channel/seat.json): Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26). package operatorchannel +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + // Seat is the seat's name. const Seat = "operator-channel" @@ -75,9 +82,45 @@ func HolderSubjectOffered(holder string) string { return "mesh.stream." + holder func HolderSubjectClosed(holder string) string { return "mesh.stream." + holder + ".event.closed" } const ( - RecordAsks = "asks" + StateAsks = "asks" ) +// AsksByAskerIDKey is the key of one value of the state asks under the form ".". +func AsksByAskerIDKey(asker, id string) string { return asker + "." + id } + +// ParseAsksByAskerIDKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseAsksByAskerIDKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeAsksByAskerIDValue reads one value of the state asks under the form ".", refusing a field AsksByAskerIDValue does not know and any text after the value. +func DecodeAsksByAskerIDValue(raw []byte) (AsksByAskerIDValue, error) { + var v AsksByAskerIDValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // Ask is a type of the contract. type Ask struct { // what it is about: a newer ask about it replaces the older @@ -328,5 +371,5 @@ const ( ClosedHolderEventHowUnlinked = "unlinked" ) -// AsksRecord is one value of the record asks. -type AsksRecord = map[string]any +// AsksByAskerIDValue is one value of the state asks under the key .. +type AsksByAskerIDValue = map[string]any diff --git a/go/seats/operatorchannel/contract_test.go b/operator-and-conversation/go/seats/operatorchannel/contract_test.go similarity index 100% rename from go/seats/operatorchannel/contract_test.go rename to operator-and-conversation/go/seats/operatorchannel/contract_test.go diff --git a/operator-and-conversation/go/seats/operatorchannel/go.mod b/operator-and-conversation/go/seats/operatorchannel/go.mod new file mode 100644 index 0000000..691961a --- /dev/null +++ b/operator-and-conversation/go/seats/operatorchannel/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel + +go 1.26.0 diff --git a/operator-and-conversation/seats/index.json b/operator-and-conversation/seats/index.json new file mode 100644 index 0000000..62a0f68 --- /dev/null +++ b/operator-and-conversation/seats/index.json @@ -0,0 +1,6 @@ +{ + "format": 1, + "seats": [ + "operator-channel" + ] +} diff --git a/ts/seats/operatorchannel/package.json b/operator-and-conversation/ts/seats/operatorchannel/package.json similarity index 80% rename from ts/seats/operatorchannel/package.json rename to operator-and-conversation/ts/seats/operatorchannel/package.json index e1d9bcb..e2143b1 100644 --- a/ts/seats/operatorchannel/package.json +++ b/operator-and-conversation/ts/seats/operatorchannel/package.json @@ -9,7 +9,7 @@ "files": [ "dist" ], - "name": "@novox/mesh-contracts-operator-channel", + "name": "@novox/mesh-operator-and-conversation-contracts-operator-channel", "scripts": { "build": "tsc", "prepare": "tsc" diff --git a/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts similarity index 89% rename from ts/seats/operatorchannel/src/index.ts rename to operator-and-conversation/ts/seats/operatorchannel/src/index.ts index 16e5cb0..4b9e593 100644 --- a/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -58,6 +58,29 @@ export function closedHolderSubject(holder: string): string { return "mesh.stream." + holder + ".event.closed"; } +export const states = { + asks: "asks", +} as const; + +/** The key of one value of the state asks under the form ".". */ +export function asksByAskerIDKey(asker: string, id: string): string { + return asker + "." + id; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseAsksByAskerIDKey(key: string): { asker: string; id: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const asker = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const id = rest; + rest = ""; + if (rest !== "") return undefined; + return { asker, id }; +} + export interface Ask { /** what it is about: a newer ask about it replaces the older */ "about"?: string; @@ -277,6 +300,6 @@ export interface ClosedHolderEvent { "id": string; } -/** is one value of the record asks. */ -export type AsksRecord = Record; +/** is one value of the state asks under the key .. */ +export type AsksByAskerIDValue = Record; diff --git a/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json b/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/provisioning/go/seats/meshprovisioner/contract.go b/provisioning/go/seats/meshprovisioner/contract.go new file mode 100644 index 0000000..1bacedd --- /dev/null +++ b/provisioning/go/seats/meshprovisioner/contract.go @@ -0,0 +1,285 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshprovisioner is the contract of the seat "mesh-provisioner", defined whole (seats/mesh-provisioner/seat.json): The provisioner (ADR 0349): which domain answers which kind of placeholder, what each module needs, what is applied where. +package meshprovisioner + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-provisioner" + +const ( + VerbProvisioning = "provisioning" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbProvisioning} +} + +const ( + StatePlans = "plans" + StateProvisions = "provisions" +) + +// PlansByNodeModuleKey is the key of one value of the state plans under the form ".". +func PlansByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParsePlansByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParsePlansByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodePlansByNodeModuleValue reads one value of the state plans under the form ".", refusing a field PlansByNodeModuleValue does not know and any text after the value. +func DecodePlansByNodeModuleValue(raw []byte) (PlansByNodeModuleValue, error) { + var v PlansByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ProvisionsByNodeConsumerProvisionKey is the key of one value of the state provisions under the form "..". +func ProvisionsByNodeConsumerProvisionKey(node, consumer, provision string) string { + return node + "." + consumer + "." + provision +} + +// ParseProvisionsByNodeConsumerProvisionKey reads the parts of a key of the form "..", in the order of the form; the last result is false for a key of another form. +func ParseProvisionsByNodeConsumerProvisionKey(key string) (string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", false + } + rest = after2 + if rest == "" { + return "", "", "", false + } + v4 := rest + rest = "" + if rest != "" { + return "", "", "", false + } + return v0, v2, v4, true +} + +// DecodeProvisionsByNodeConsumerProvisionValue reads one value of the state provisions under the form "..", refusing a field ProvisionsByNodeConsumerProvisionValue does not know and any text after the value. +func DecodeProvisionsByNodeConsumerProvisionValue(raw []byte) (ProvisionsByNodeConsumerProvisionValue, error) { + var v ProvisionsByNodeConsumerProvisionValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ProvisionsByNodeConsumerProvisionLocalKey is the key of one value of the state provisions under the form "...". +func ProvisionsByNodeConsumerProvisionLocalKey(node, consumer, provision, local string) string { + return node + "." + consumer + "." + provision + "." + local +} + +// ParseProvisionsByNodeConsumerProvisionLocalKey reads the parts of a key of the form "...", in the order of the form; the last result is false for a key of another form. +func ParseProvisionsByNodeConsumerProvisionLocalKey(key string) (string, string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", "", false + } + rest = after2 + v4, after4, found4 := strings.Cut(rest, ".") + if !found4 || v4 == "" { + return "", "", "", "", false + } + rest = after4 + if rest == "" { + return "", "", "", "", false + } + v6 := rest + rest = "" + if rest != "" { + return "", "", "", "", false + } + return v0, v2, v4, v6, true +} + +// DecodeProvisionsByNodeConsumerProvisionLocalValue reads one value of the state provisions under the form "...", refusing a field ProvisionsByNodeConsumerProvisionLocalValue does not know and any text after the value. +func DecodeProvisionsByNodeConsumerProvisionLocalValue(raw []byte) (ProvisionsByNodeConsumerProvisionLocalValue, error) { + var v ProvisionsByNodeConsumerProvisionLocalValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ProvisioningInput is what provisioning takes. +type ProvisioningInput struct { + // for collect: a collection + Collection string `json:"collection,omitempty"` + // a module: for collect, a contributor + Module string `json:"module,omitempty"` + // a node + Node string `json:"node,omitempty"` + // registry (when not given), needs, applied or collect + What string `json:"what,omitempty"` +} + +// ProvisioningAnswer is what provisioning answers. +type ProvisioningAnswer = map[string]any + +// PlansByNodeModuleValue is one value of the state plans under the key .. +type PlansByNodeModuleValue struct { + Module string `json:"module"` + Node string `json:"node"` + // the module's plan + Plan map[string]any `json:"plan"` + V int64 `json:"v,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionValueConsumer is a type of the contract. +type ProvisionsByNodeConsumerProvisionValueConsumer struct { + Module string `json:"module,omitempty"` + Node string `json:"node,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionValueProvider is a type of the contract. +type ProvisionsByNodeConsumerProvisionValueProvider struct { + Module string `json:"module,omitempty"` + Node string `json:"node,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionValueReference is a type of the contract. +type ProvisionsByNodeConsumerProvisionValueReference struct { + Generation int64 `json:"generation,omitempty"` + ID string `json:"id,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionValueWarrant is a type of the contract. +type ProvisionsByNodeConsumerProvisionValueWarrant struct { + Ask string `json:"ask,omitempty"` + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionValue is one value of the state provisions under the key ... +type ProvisionsByNodeConsumerProvisionValue struct { + Ask string `json:"ask,omitempty"` + At string `json:"at"` + Consumer ProvisionsByNodeConsumerProvisionValueConsumer `json:"consumer"` + DependsOn []string `json:"depends-on,omitempty"` + Local string `json:"local,omitempty"` + // the plan's provision + Plan map[string]any `json:"plan,omitempty"` + Provider ProvisionsByNodeConsumerProvisionValueProvider `json:"provider"` + Provision string `json:"provision"` + Reference *ProvisionsByNodeConsumerProvisionValueReference `json:"reference,omitempty"` + State string `json:"state"` + V int64 `json:"v,omitempty"` + Warrant *ProvisionsByNodeConsumerProvisionValueWarrant `json:"warrant,omitempty"` + Why string `json:"why,omitempty"` +} + +// The values of ProvisionsByNodeConsumerProvisionValue's state. +const ( + ProvisionsByNodeConsumerProvisionValueStateAsked = "asked" + ProvisionsByNodeConsumerProvisionValueStateProvided = "provided" + ProvisionsByNodeConsumerProvisionValueStateWaits = "waits" + ProvisionsByNodeConsumerProvisionValueStateFailed = "failed" + ProvisionsByNodeConsumerProvisionValueStateRetiring = "retiring" +) + +// ProvisionsByNodeConsumerProvisionLocalValueConsumer is a type of the contract. +type ProvisionsByNodeConsumerProvisionLocalValueConsumer struct { + Module string `json:"module,omitempty"` + Node string `json:"node,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionLocalValueProvider is a type of the contract. +type ProvisionsByNodeConsumerProvisionLocalValueProvider struct { + Module string `json:"module,omitempty"` + Node string `json:"node,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionLocalValueReference is a type of the contract. +type ProvisionsByNodeConsumerProvisionLocalValueReference struct { + Generation int64 `json:"generation,omitempty"` + ID string `json:"id,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionLocalValueWarrant is a type of the contract. +type ProvisionsByNodeConsumerProvisionLocalValueWarrant struct { + Ask string `json:"ask,omitempty"` + At string `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` +} + +// ProvisionsByNodeConsumerProvisionLocalValue is one value of the state provisions under the key .... +type ProvisionsByNodeConsumerProvisionLocalValue struct { + Ask string `json:"ask,omitempty"` + At string `json:"at"` + Consumer ProvisionsByNodeConsumerProvisionLocalValueConsumer `json:"consumer"` + DependsOn []string `json:"depends-on,omitempty"` + Local string `json:"local,omitempty"` + // the plan's provision + Plan map[string]any `json:"plan,omitempty"` + Provider ProvisionsByNodeConsumerProvisionLocalValueProvider `json:"provider"` + Provision string `json:"provision"` + Reference *ProvisionsByNodeConsumerProvisionLocalValueReference `json:"reference,omitempty"` + State string `json:"state"` + V int64 `json:"v,omitempty"` + Warrant *ProvisionsByNodeConsumerProvisionLocalValueWarrant `json:"warrant,omitempty"` + Why string `json:"why,omitempty"` +} + +// The values of ProvisionsByNodeConsumerProvisionLocalValue's state. +const ( + ProvisionsByNodeConsumerProvisionLocalValueStateAsked = "asked" + ProvisionsByNodeConsumerProvisionLocalValueStateProvided = "provided" + ProvisionsByNodeConsumerProvisionLocalValueStateWaits = "waits" + ProvisionsByNodeConsumerProvisionLocalValueStateFailed = "failed" + ProvisionsByNodeConsumerProvisionLocalValueStateRetiring = "retiring" +) diff --git a/provisioning/go/seats/meshprovisioner/go.mod b/provisioning/go/seats/meshprovisioner/go.mod new file mode 100644 index 0000000..8604b33 --- /dev/null +++ b/provisioning/go/seats/meshprovisioner/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner + +go 1.26.0 diff --git a/provisioning/seats/index.json b/provisioning/seats/index.json new file mode 100644 index 0000000..185b64d --- /dev/null +++ b/provisioning/seats/index.json @@ -0,0 +1,9 @@ +{ + "format": 1, + "seats": [ + "mesh-provisioner", + "node-mounts", + "node-nfs-server", + "node-provisioner" + ] +} diff --git a/provisioning/ts/seats/meshprovisioner/package.json b/provisioning/ts/seats/meshprovisioner/package.json new file mode 100644 index 0000000..ff54abf --- /dev/null +++ b/provisioning/ts/seats/meshprovisioner/package.json @@ -0,0 +1,19 @@ +{ + "description": "The contract of the seat mesh-provisioner, generated from its definition.", + "devDependencies": { + "typescript": "^5.6.0" + }, + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "name": "@novox/mesh-provisioning-contracts-mesh-provisioner", + "scripts": { + "build": "tsc", + "prepare": "tsc" + }, + "type": "module", + "version": "0.1.0" +} diff --git a/provisioning/ts/seats/meshprovisioner/src/index.ts b/provisioning/ts/seats/meshprovisioner/src/index.ts new file mode 100644 index 0000000..ae048d9 --- /dev/null +++ b/provisioning/ts/seats/meshprovisioner/src/index.ts @@ -0,0 +1,188 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-provisioner", defined whole (seats/mesh-provisioner/seat.json): The provisioner (ADR 0349): which domain answers which kind of placeholder, what each module needs, what is applied where. + +export const seat = "mesh-provisioner"; + +export const verbs = { + provisioning: "provisioning", +} as const; + +export const states = { + plans: "plans", + provisions: "provisions", +} as const; + +/** The key of one value of the state plans under the form ".". */ +export function plansByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parsePlansByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state provisions under the form "..". */ +export function provisionsByNodeConsumerProvisionKey(node: string, consumer: string, provision: string): string { + return node + "." + consumer + "." + provision; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseProvisionsByNodeConsumerProvisionKey(key: string): { node: string; consumer: string; provision: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const consumer = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const provision = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, consumer, provision }; +} + +/** The key of one value of the state provisions under the form "...". */ +export function provisionsByNodeConsumerProvisionLocalKey(node: string, consumer: string, provision: string, local: string): string { + return node + "." + consumer + "." + provision + "." + local; +} + +/** The parts of a key of the form "...", or undefined for a key of another form. */ +export function parseProvisionsByNodeConsumerProvisionLocalKey(key: string): { node: string; consumer: string; provision: string; local: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const consumer = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + const at4 = rest.indexOf("."); + if (at4 <= 0) return undefined; + const provision = rest.slice(0, at4); + rest = rest.slice(at4 + 1); + if (rest === "") return undefined; + const local = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, consumer, provision, local }; +} + +/** is what provisioning takes. */ +export interface ProvisioningInput { + /** for collect: a collection */ + "collection"?: string; + /** a module: for collect, a contributor */ + "module"?: string; + /** a node */ + "node"?: string; + /** registry (when not given), needs, applied or collect */ + "what"?: string; +} + +/** is what provisioning answers. */ +export type ProvisioningAnswer = Record; + +/** is one value of the state plans under the key .. */ +export interface PlansByNodeModuleValue { + "module": string; + "node": string; + /** the module's plan */ + "plan": Record; + "v"?: number; +} + +export interface ProvisionsByNodeConsumerProvisionValueConsumer { + "module"?: string; + "node"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionValueProvider { + "module"?: string; + "node"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionValueReference { + "generation"?: number; + "id"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionValueWarrant { + "ask"?: string; + "at"?: string; + "by"?: string; + "option"?: string; +} + +/** is one value of the state provisions under the key ... */ +export interface ProvisionsByNodeConsumerProvisionValue { + "ask"?: string; + "at": string; + "consumer": ProvisionsByNodeConsumerProvisionValueConsumer; + "depends-on"?: string[]; + "local"?: string; + /** the plan's provision */ + "plan"?: Record; + "provider": ProvisionsByNodeConsumerProvisionValueProvider; + "provision": string; + "reference"?: ProvisionsByNodeConsumerProvisionValueReference; + "state": "asked" | "provided" | "waits" | "failed" | "retiring"; + "v"?: number; + "warrant"?: ProvisionsByNodeConsumerProvisionValueWarrant; + "why"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionLocalValueConsumer { + "module"?: string; + "node"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionLocalValueProvider { + "module"?: string; + "node"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionLocalValueReference { + "generation"?: number; + "id"?: string; +} + +export interface ProvisionsByNodeConsumerProvisionLocalValueWarrant { + "ask"?: string; + "at"?: string; + "by"?: string; + "option"?: string; +} + +/** is one value of the state provisions under the key .... */ +export interface ProvisionsByNodeConsumerProvisionLocalValue { + "ask"?: string; + "at": string; + "consumer": ProvisionsByNodeConsumerProvisionLocalValueConsumer; + "depends-on"?: string[]; + "local"?: string; + /** the plan's provision */ + "plan"?: Record; + "provider": ProvisionsByNodeConsumerProvisionLocalValueProvider; + "provision": string; + "reference"?: ProvisionsByNodeConsumerProvisionLocalValueReference; + "state": "asked" | "provided" | "waits" | "failed" | "retiring"; + "v"?: number; + "warrant"?: ProvisionsByNodeConsumerProvisionLocalValueWarrant; + "why"?: string; +} + diff --git a/provisioning/ts/seats/meshprovisioner/tsconfig.json b/provisioning/ts/seats/meshprovisioner/tsconfig.json new file mode 100644 index 0000000..c610421 --- /dev/null +++ b/provisioning/ts/seats/meshprovisioner/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/ts/seats/meshconfig/src/index.ts b/ts/seats/meshconfig/src/index.ts index 2c5bb50..e8fa023 100644 --- a/ts/seats/meshconfig/src/index.ts +++ b/ts/seats/meshconfig/src/index.ts @@ -32,6 +32,33 @@ export function retiredHolderSubject(holder: string): string { return "mesh.stream." + holder + ".event.retired"; } +export const states = { + resolved: "resolved", +} as const; + +/** The key of one value of the state resolved under the form "..". */ +export function resolvedByNodeModuleKeyKey(node: string, module: string, key: string): string { + return node + "." + module + "." + key; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseResolvedByNodeModuleKeyKey(key: string): { node: string; module: string; key: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const module = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module, key }; +} + /** is what config takes. */ export interface ConfigInput { /** list (when not given), set, clear or history */ @@ -117,3 +144,17 @@ export interface RetiredHolderEvent { "node"?: string; } +/** is one value of the state resolved under the key ... */ +export interface ResolvedByNodeModuleKeyValue { + "key": string; + "module": string; + "node": string; + /** the predefined key the value falls back to */ + "predefined"?: string; + /** an unset value is the operator's own to set */ + "required"?: boolean; + "source": string; + "trusted"?: boolean; + "value"?: unknown; +} + diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index d37717b..39feb40 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -43,6 +43,121 @@ export function manifestReadsChangedSubject(caller: string): string { return "mesh.seat.node-engine.event.manifest-reads-changed." + caller; } +export const states = { + have: "have", + composed: "composed", + data: "data", + packages: "packages", + health: "health", +} as const; + +/** The key of one value of the state have under the form ".". */ +export function haveByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseHaveByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state have under the form ".node". */ +export function haveByNodeNodeKey(node: string): string { + return node + ".node"; +} + +/** The parts of a key of the form ".node", or undefined for a key of another form. */ +export function parseHaveByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + const at0 = rest.indexOf(".node"); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 5); + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state composed under the form "..". */ +export function composedByNodeModuleFingerprintKey(node: string, module: string, fingerprint: string): string { + return node + "." + module + "." + fingerprint; +} + +/** The parts of a key of the form "..", or undefined for a key of another form. */ +export function parseComposedByNodeModuleFingerprintKey(key: string): { node: string; module: string; fingerprint: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const module = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + if (rest === "") return undefined; + const fingerprint = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module, fingerprint }; +} + +/** The key of one value of the state data under the form ".". */ +export function dataByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseDataByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state packages under the form "". */ +export function packagesByNodeKey(node: string): string { + return node; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parsePackagesByNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state health under the form "". */ +export function healthByNodeKey(node: string): string { + return node; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseHealthByNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + /** one act asked through the seat, as acts reads it (ADR 0563 §9) */ export interface Act { /** the act: hand-over, access-act, judge-directories or setuid-search */ @@ -505,3 +620,103 @@ export type RefusedEvent = ActSaid; /** is the body of the event manifest-reads-changed. */ export type ManifestReadsChangedEvent = ManifestReadsChanged; +/** is one value of the state have under the key .. */ +export interface HaveByNodeModuleValue { + "build"?: string; + "build-alone"?: boolean; + /** the composition's digest */ + "composed"?: string; + "detail"?: string; + /** the fingerprint held */ + "fp"?: string; + /** the accounts handed */ + "handed"?: Record; + /** how it is */ + "health": string; + "judgement"?: string; + "kept"?: string[]; + /** how far it has come */ + "phase": string; + /** a pull the engine waits for */ + "pull-wait"?: Record; + "sealed"?: string[]; + "sealed-at"?: Record; + /** the user list written */ + "user-list"?: Record; + "v": number; + /** what it waits for */ + "waits"?: string; +} + +/** is one value of the state have under the key .node. */ +export interface HaveByNodeNodeValue { + "busy"?: Record; + "candidates"?: Record>; + "composed"?: string; + /** own, or the controller's */ + "composes"?: string; + "composes-why"?: string; + "filters"?: Record; + "profile"?: Record; + /** the node's record: its tunnel and certificate */ + "record"?: Record; + "sealed"?: string[]; + "sealed-at"?: Record; + "strays"?: Record; + "v": number; + "windows"?: Record; +} + +/** is one value of the state composed under the key ... */ +export interface ComposedByNodeModuleFingerprintValue { + "Data"?: boolean; + "FP": string; + "Filled"?: string[]; + "Module": string; + "Node": string; + /** the node's own adoption, identities, groups, libraries and provisioner account */ + "Own"?: Record; + /** the module's plan */ + "Plan"?: Record; + /** why the composition was refused */ + "Refused"?: string; + "Resources"?: Record[]; + "V"?: number; +} + +/** is one value of the state data under the key .. */ +export interface DataByNodeModuleValue { + "digest": string; + "items": Record[]; + "module": string; + "node": string; + "v"?: number; +} + +export interface PackagesByNodeValuePackagesItem { + "absent"?: boolean; + "modules"?: string[]; + "package"?: string; +} + +/** is one value of the state packages under the key . */ +export interface PackagesByNodeValue { + "node": string; + "packages": PackagesByNodeValuePackagesItem[]; +} + +/** is one value of the state health under the key . */ +export interface HealthByNodeValue { + "at": string; + "contract"?: number; + "fallback"?: Record; + "found"?: Record[]; + "network"?: Record; + "node": string; + "peers"?: Record[]; + "relogin"?: Record[]; + "root"?: Record[]; + "units"?: Record; + "v": number; +} + diff --git a/ts/src/index.ts b/ts/src/index.ts deleted file mode 100644 index 6df2ae8..0000000 --- a/ts/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -export * as seats from "./seats.js"; diff --git a/ts/src/seats.ts b/ts/src/seats.ts deleted file mode 100644 index 6c51e36..0000000 --- a/ts/src/seats.ts +++ /dev/null @@ -1,8185 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The mesh's seat list, generated from every seat's definition (design 26). - -export interface Verb { - name: string; - description: string; - input?: Record; - output?: Record; - optional?: boolean; - replaces?: string[]; -} - -export interface Seat { - name: string; - scope: "mesh" | "node"; - grain: "whole" | "seat-list"; - decision: string; - delivers?: string; - replicated?: boolean; - accepts?: string[]; - emits?: string[]; - pulledByKind?: boolean; - yields?: boolean; - heldByTheEngine?: boolean; - serves?: Verb[]; -} - -export const names = { - meshStore: "mesh-store", - meshBroker: "mesh-broker", - meshVault: "mesh-vault", - meshConfig: "mesh-config", - meshProvisioner: "mesh-provisioner", - meshArtifactStore: "mesh-artifact-store", - meshCatalog: "mesh-catalog", - meshDelivery: "mesh-delivery", - meshPlacement: "mesh-placement", - meshDeliveryChecker: "mesh-delivery-checker", - meshConditions: "mesh-conditions", - meshWatchdogs: "mesh-watchdogs", - meshSelfCheck: "mesh-self-check", - meshHealers: "mesh-healers", - meshWants: "mesh-wants", - meshHandActs: "mesh-hand-acts", - meshEnrolment: "mesh-enrolment", - meshArtifactSweep: "mesh-artifact-sweep", - operatorChannel: "operator-channel", - npmPackageRegistry: "npm-package-registry", - goPackageRegistry: "go-package-registry", - containerImageRegistry: "container-image-registry", - ociArtifactRegistry: "oci-artifact-registry", - git: "git", - nodeBuildGo: "node-build-go", - nodeBuildNode: "node-build-node", - nodeBuildImage: "node-build-image", - meshDnsResolver: "mesh-dns-resolver", - nodeHostname: "node-hostname", - nodeIntrusionPrevention: "node-intrusion-prevention", - nodePacketFilter: "node-packet-filter", - nodeServiceManager: "node-service-manager", - nodePackageManager: "node-package-manager", - nodeContainerRuntime: "node-container-runtime", - nodeEnvironment: "node-environment", - nodeProvisioner: "node-provisioner", - nodeLoginShell: "node-login-shell", - nodePower: "node-power", - nodeHotkeys: "node-hotkeys", - nodeMessageBus: "node-message-bus", - nodeBackup: "node-backup", - thePrivateNetwork: "the-private-network", - nodeUplink: "node-uplink", - nodeResolver: "node-resolver", - nodeNfsServer: "node-nfs-server", - nodeMounts: "node-mounts", - nodeReads: "node-reads", - nodeFiles: "node-files", - nodeEngine: "node-engine", - nodeLoginManager: "node-login-manager", - nodeDisplayServer: "node-display-server", - nodeDisplaySession: "node-display-session", - nodeTerminalEmulator: "node-terminal-emulator", - nodeLauncher: "node-launcher", - nodeNotifier: "node-notifier", - nodeLockScreen: "node-lock-screen", - nodeClipboard: "node-clipboard", - nodeBar: "node-bar", - nodeCompositor: "node-compositor", - nodeSecretService: "node-secret-service", -} as const; - -export const list: readonly Seat[] = [ - { - "decision": "novox/hq ADR 0079", - "delivers": "postgres-database", - "grain": "seat-list", - "name": "mesh-store", - "scope": "mesh", - "serves": [ - { - "description": "Every database the store holds, with its on-disk size.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "databases", - "replaces": [ - "psql -l" - ] - }, - { - "description": "One read-only statement against one database the store holds.", - "input": { - "properties": { - "database": { - "description": "the database to query", - "type": "string" - }, - "sql": { - "description": "the read-only statement", - "type": "string" - } - }, - "required": [ - "database", - "sql" - ], - "type": "object" - }, - "name": "query", - "replaces": [ - "psql" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0079", - "delivers": "mesh-bus", - "emits": [ - "created", - "refused", - "drifted", - "drift-cleared", - "full", - "full-cleared", - "orphaned", - "recreated", - "dropped" - ], - "grain": "whole", - "name": "mesh-broker", - "scope": "mesh", - "serves": [ - { - "description": "Every entry the bus module created, its template, its settings and its fill against its cap.", - "input": { - "properties": { - "module": { - "description": "one module (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "list", - "optional": true, - "output": { - "properties": { - "entries": { - "description": "every entry", - "items": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "entries" - ], - "type": "object" - } - }, - { - "description": "One entry, its template, settings and fill.", - "input": { - "properties": { - "local-name": { - "description": "the entry's local name", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - } - }, - "required": [ - "module", - "local-name" - ], - "type": "object" - }, - "name": "show", - "optional": true, - "output": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - } - }, - { - "description": "Compare what the bus holds with what was asked, entry by entry; a difference that only grows is said as such.", - "input": { - "properties": { - "module": { - "description": "one module (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "check", - "optional": true, - "output": { - "properties": { - "differences": { - "description": "every difference", - "items": { - "properties": { - "asked": { - "description": "asked", - "type": "object" - }, - "grows": { - "description": "the difference only grows: apply may act on it", - "type": "boolean" - }, - "held": { - "description": "held", - "type": "object" - }, - "local-name": { - "description": "the entry", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - } - }, - "required": [ - "module", - "local-name" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "differences" - ], - "type": "object" - } - }, - { - "description": "Entries the bus holds that no manifest asks for; never one whose manifest the judge refused or outgrew, nor one of the foundation's old sections.", - "input": { - "description": "no arguments", - "properties": {}, - "type": "object" - }, - "name": "orphans", - "optional": true, - "output": { - "properties": { - "entries": { - "description": "every orphan", - "items": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "entries" - ], - "type": "object" - } - }, - { - "description": "Create one entry now.", - "input": { - "description": "one piece of bus state as a manifest asks it", - "properties": { - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type" - ], - "type": "object" - }, - "name": "create", - "optional": true, - "output": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - } - }, - { - "description": "Apply a difference that only grows: a larger cap, a longer age.", - "input": { - "properties": { - "local-name": { - "description": "the entry's local name", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - } - }, - "required": [ - "module", - "local-name" - ], - "type": "object" - }, - "name": "apply", - "optional": true, - "output": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - } - }, - { - "description": "Rebuild an entry whose settings differ in a way that cannot grow, from its data where the storage type allows, after a stream snapshot.", - "input": { - "properties": { - "local-name": { - "description": "the entry's local name", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "warrant": { - "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", - "type": "string" - }, - "why": { - "description": "why, kept in the record", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "warrant", - "why" - ], - "type": "object" - }, - "name": "recreate", - "optional": true, - "output": { - "description": "one entry as the bus holds it", - "properties": { - "bus-name": { - "description": "its name on the bus, _", - "type": "string" - }, - "configuration": { - "description": "its own configuration, where no template fits", - "type": "object" - }, - "fill": { - "properties": { - "bytes": { - "description": "bytes held", - "type": "integer" - }, - "cap": { - "description": "its cap in bytes", - "type": "integer" - }, - "count": { - "description": "messages, keys or objects held", - "type": "integer" - } - }, - "required": [ - "count", - "bytes", - "cap" - ], - "type": "object" - }, - "local-name": { - "description": "its local name in the module's manifest", - "type": "string" - }, - "module": { - "description": "the module asking it", - "type": "string" - }, - "options": { - "description": "the template's options the module set, within their bounds", - "type": "object" - }, - "settings": { - "description": "the settings the bus holds", - "type": "object" - }, - "state": { - "description": "the entry's state: held as asked, refused, waiting to be made, drifted from what is asked, full, or orphaned (no manifest asks it)", - "enum": [ - "held", - "refused", - "waiting", - "drifted", - "full", - "orphaned" - ], - "type": "string" - }, - "storage-type": { - "description": "how the bus keeps it", - "enum": [ - "stream", - "key-value", - "object-store", - "ledger" - ], - "type": "string" - }, - "template": { - "description": "its template; empty for a configuration of its own", - "enum": [ - "state", - "events", - "work-queue", - "log", - "objects", - "ledger", - "" - ], - "type": "string" - }, - "why": { - "description": "why it is in its state, in plain words; empty when held as asked", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "storage-type", - "bus-name" - ], - "type": "object" - } - }, - { - "description": "Remove one orphan, after a stream snapshot.", - "input": { - "properties": { - "local-name": { - "description": "the entry's local name", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "warrant": { - "description": "the operator's warrant, as the operator channel decided it (ADR 0259 §6)", - "type": "string" - }, - "why": { - "description": "why, kept in the record", - "type": "string" - } - }, - "required": [ - "module", - "local-name", - "warrant", - "why" - ], - "type": "object" - }, - "name": "drop", - "optional": true, - "output": { - "properties": { - "dropped": { - "description": "it is gone", - "type": "boolean" - }, - "snapshot": { - "description": "the snapshot taken first", - "type": "string" - } - }, - "required": [ - "dropped" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0161", - "delivers": "secret", - "grain": "whole", - "name": "mesh-vault", - "scope": "mesh", - "serves": [ - { - "description": "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its generation; answered only to an asker the vault allows, as the bus proved it. Never the value in the clear, and never to a private key.", - "input": { - "properties": { - "name": { - "description": "the secret's name", - "type": "string" - }, - "public_key": { - "description": "the asker's X25519 public key as SPKI PEM — never a private key", - "type": "string" - } - }, - "required": [ - "name", - "public_key" - ], - "type": "object" - }, - "name": "get", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Ask the operator for a module's own secret at a desk, from the controller or a caller the vault's manifest names, named as the bus proved it (novox/hq ADR 0563 §8): the ask is recorded in the vault's store and bounded before the prompt opens, and what is typed is sealed to a key the vault made for the call and opens there alone.", - "input": { - "properties": { - "asked_by": { - "description": "who asked, as the bus named its caller", - "type": "string" - }, - "call": { - "description": "the second step: the call the first step answered", - "type": "string" - }, - "desk": { - "description": "the machine whose prompt asks", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "name": { - "description": "the secret's name in the module", - "type": "string" - }, - "node": { - "description": "the machine the secret is for", - "type": "string" - }, - "outcome": { - "description": "instead of sealed: how the prompt ended", - "type": "string" - }, - "sealed": { - "description": "the second step: the value sealed to the call's key — never the value", - "type": "string" - } - }, - "type": "object" - }, - "name": "secret-ask", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Take the credential a provider's software issued for a provision, from the controller alone, which asks the provider's tool with a key the vault made for the call; kept as the provision's one credential, or, when the tool answers a value per holder named, each as that holder's own; the same value again writes nothing.", - "input": { - "properties": { - "call": { - "description": "the second step: the call the first step answered", - "type": "string" - }, - "holders": { - "description": "the provision's holders from that provider, each {node, module}: its tool may answer a value for each, kept as that holder's own and handed to it alone", - "items": { - "properties": { - "module": { - "description": "the consuming module", - "type": "string" - }, - "node": { - "description": "the holder's machine", - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "module": { - "description": "the provider's module", - "type": "string" - }, - "provider": { - "description": "the provider's machine", - "type": "string" - }, - "provision": { - "description": "the provision", - "type": "string" - }, - "sealed": { - "description": "the second step: the value sealed to the call's key — never the value", - "type": "string" - } - }, - "type": "object" - }, - "name": "issued", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Make a secret anew. Refused for a value a provider's software issued and for an accepted credential. Never answers the value.", - "input": { - "properties": { - "by": { - "description": "who asked, when the controller carries the act", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "name": { - "description": "the secret's name in the module", - "type": "string" - }, - "node": { - "description": "the machine, for a module's own secret", - "type": "string" - }, - "provider": { - "description": "the provider's machine", - "type": "string" - }, - "provision": { - "description": "the provision", - "type": "string" - } - }, - "type": "object" - }, - "name": "rotate", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Take one value of the controller's store in, as it is, from the node-engine of a machine it was sealed to and no other caller: name the row, its origin and generation and get a call and a key made for it, then send the call, the value sealed to that key and a key to read back to. Never replaces a value the vault holds; answers what it kept, read back sealed to that key.", - "input": { - "properties": { - "call": { - "description": "the second step: the call the first step answered", - "type": "string" - }, - "consumer": { - "description": "the consumer's machine", - "type": "string" - }, - "consumer_module": { - "description": "the consuming module", - "type": "string" - }, - "generation": { - "description": "the controller's generation for the value", - "type": "string" - }, - "local": { - "description": "the credential's local name, when it has one", - "type": "string" - }, - "made_at": { - "description": "when it was made (RFC 3339)", - "type": "string" - }, - "module": { - "description": "its module", - "type": "string" - }, - "name": { - "description": "its name in the module", - "type": "string" - }, - "node": { - "description": "a module's own secret: its machine", - "type": "string" - }, - "origin": { - "description": "made, accepted or issued, as the controller's store says", - "type": "string" - }, - "provider": { - "description": "a pair credential: the provider's machine", - "type": "string" - }, - "provision": { - "description": "the provision", - "type": "string" - }, - "read_back_to": { - "description": "the second step: a raw X25519 public key the read-back is sealed to", - "type": "string" - }, - "sealed": { - "description": "the second step: the value sealed to the call's key — never the value", - "type": "string" - } - }, - "type": "object" - }, - "name": "move-in", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "State this machine's sealing key to the vault, in its own name, from the node-engine of the machine alone: at its start and whenever the key changes. A first or changed key is recorded and the machine's core secrets are said changed; the same key again changes nothing.", - "input": { - "properties": { - "key": { - "description": "the machine's sealing key, a raw X25519 public key in base64", - "type": "string" - } - }, - "type": "object" - }, - "name": "node-key", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Answer the node-engine of one machine, about its own machine alone (as the bus proved the caller, never an argument), its modules' own secrets and the pair credentials it consumes or provides that it names, each sealed to the key it stated with node-key. A credential the vault does not hold is named missing; never a value in the clear.", - "input": { - "properties": { - "credentials": { - "description": "the pair credentials the machine's composition reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}", - "items": { - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - }, - "name": "own", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): only while the controller's lease is handed away, never a node's account or the controller's, never anew for one the vault holds a value or an adopted hash for. Never the value.", - "input": { - "properties": { - "user": { - "description": "the bus user, e.g. ace.mesh-build-go", - "type": "string" - } - }, - "required": [ - "user" - ], - "type": "object" - }, - "name": "issue-account", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", - "input": { - "properties": { - "kind": { - "description": "node-certificate or enrolment", - "type": "string" - }, - "node": { - "description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", - "type": "string" - }, - "reply": { - "description": "the redeem answer's bytes in base64, for an enrolment", - "type": "string" - }, - "serving_key": { - "description": "from mesh-enrolment: the serving key's public half the enrolling node presented", - "type": "string" - } - }, - "required": [ - "kind" - ], - "type": "object" - }, - "name": "sign", - "optional": true, - "output": { - "description": "for a node certificate the certificate as PEM and the authority that signed it, for an enrolment the signature in base64; always the signer's fingerprint and the digest; never a key's private half", - "properties": { - "authority": { - "description": "the authority's certificate that signed it, as PEM", - "type": "string" - }, - "certificate": { - "description": "the node certificate as PEM, for a node certificate", - "type": "string" - }, - "digest": { - "description": "sha256: of the bytes signed", - "type": "string" - }, - "kind": { - "description": "node-certificate or enrolment, as asked", - "type": "string" - }, - "node": { - "description": "the machine, for a node certificate", - "type": "string" - }, - "signature": { - "description": "the signature in base64, for an enrolment", - "type": "string" - }, - "signer": { - "description": "the signer's fingerprint", - "type": "string" - } - }, - "required": [ - "kind", - "signer", - "digest" - ], - "type": "object" - } - }, - { - "description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", - "input": { - "properties": { - "drop": { - "description": "\"true\": drop the node's token and its hash", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "expires": { - "description": "when the token expires (RFC 3339), as the enrolment's key says", - "type": "string" - }, - "node": { - "description": "the node the token is for", - "type": "string" - }, - "sealed": { - "description": "\"true\": answer the token sealed to the operator key", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "tunnel": { - "additionalProperties": false, - "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", - "properties": { - "address": { - "description": "the joining node's tunnel address", - "type": "string" - }, - "hub_endpoint": { - "description": "the hub's endpoint, host:port", - "type": "string" - }, - "hub_key": { - "description": "the hub's tunnel public key", - "type": "string" - }, - "key": { - "description": "the joining machine's own tunnel public key, as given at issue", - "type": "string" - }, - "range": { - "description": "the private network's range, a prefix", - "type": "string" - } - }, - "required": [ - "address", - "hub_endpoint", - "hub_key", - "key", - "range" - ], - "type": "object" - } - }, - "required": [ - "node" - ], - "type": "object" - }, - "name": "enrolment-token", - "optional": true, - "output": { - "description": "what the joining machine must trust, never the token: the bus's address, its certificate's fingerprint and the vault's public signing key; the token sealed to the operator key when asked sealed; or that a drop left neither the token nor its hash", - "properties": { - "broker": { - "description": "the bus's address the machine dials", - "type": "string" - }, - "dropped": { - "description": "a drop left neither the token nor its hash", - "type": "boolean" - }, - "fingerprint": { - "description": "the fingerprint of the bus's certificate", - "type": "string" - }, - "node": { - "description": "the node the token is for", - "type": "string" - }, - "sealed": { - "description": "the token sealed to the operator key, when asked sealed, relayed as it is", - "type": "string" - }, - "signer": { - "description": "the vault's public signing key in base64", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - } - }, - { - "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", - "input": { - "properties": { - "keys_digest": { - "description": "the digest of every public key the node presented (delivery.KeysDigest)", - "type": "string" - }, - "node": { - "description": "the joining node", - "type": "string" - }, - "retire": { - "description": "\"true\": retire the account minted for this node", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "sealing_key": { - "description": "the sealing key's public half the node presented, which the account is sealed to", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - }, - "name": "node-account", - "optional": true, - "output": { - "description": "the login minted and the account sealed to the sealing key the node presented, never in the clear; or that a retire left no account", - "properties": { - "account": { - "description": "the account sealed to the node's sealing key, in base64", - "type": "string" - }, - "login": { - "description": "the login minted", - "type": "string" - }, - "node": { - "description": "the joining node", - "type": "string" - }, - "retired": { - "description": "a retire left no account", - "type": "boolean" - } - }, - "required": [ - "node" - ], - "type": "object" - } - }, - { - "description": "Begin a node's account rotation to its other login: answered only for a call carrying a hand act recorded through mesh-hand-acts, which the vault reads back by its id. Makes the other login's value sealed to that node, writes it rotating in node-logins and says rotation-begun; says node-account-ready once the bus node's have shows a list holding both logins. One node at a time across the mesh, the bus node last: a call while a rotation runs waits until it is done.", - "input": { - "properties": { - "act": { - "description": "the id of the hand act recorded for it in mesh-hand-acts", - "type": "string" - }, - "node": { - "description": "the node whose account rotates", - "type": "string" - }, - "why": { - "description": "why, in a sentence", - "type": "string" - } - }, - "required": [ - "node", - "why", - "act" - ], - "type": "object" - }, - "name": "rotate-node", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Confirm a node's switch to its new login: asked by the node-engine dialled as that login, the runtime's proof of the caller being the confirmation. The old login's hash is then removed, and rotation-done said once the bus node's have shows the list without it.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "node-account-confirm", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Abandon a node's rotation before its old login is retired: the new login and its value dropped, the old one kept; a node that switched switches back first.", - "input": { - "properties": { - "node": { - "description": "the node whose rotation is abandoned", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - }, - "name": "abandon", - "optional": true, - "output": { - "description": "the holder's answer", - "type": "object" - } - }, - { - "description": "Retire one module's own secret, by node, module and name, on the operator's warrant bound to exactly those three (design 32 *A value no longer read*). Emits own-retired.", - "input": { - "properties": { - "ask": { - "description": "the id of the operator's ask the warrant answers", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "name": { - "description": "the own secret's name", - "type": "string" - }, - "node": { - "description": "the node", - "type": "string" - }, - "warrant": { - "description": "the operator's warrant for this node, module and name: the hand act's id", - "type": "string" - } - }, - "required": [ - "node", - "module", - "name", - "warrant", - "ask" - ], - "type": "object" - }, - "name": "retire", - "optional": true, - "output": { - "properties": { - "retired": { - "description": "the secret is gone", - "type": "boolean" - } - }, - "required": [ - "retired" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0348, ADR 0565", - "grain": "whole", - "name": "mesh-config", - "scope": "mesh", - "serves": [ - { - "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.", - "input": { - "properties": { - "action": { - "description": "list (when not given), set, clear or history", - "type": "string" - }, - "by": { - "description": "who gave the value at the controller's terminal", - "type": "string" - }, - "key": { - "description": "the config key", - "type": "string" - }, - "limit": { - "description": "for history: at most this many, newest", - "type": "number" - }, - "module": { - "description": "a module: its own value, with node", - "type": "string" - }, - "node": { - "description": "a node", - "type": "string" - }, - "value": { - "description": "for set: the value, of the key's type: a number, a string, a boolean, or for a trusted key an object or a list" - }, - "why": { - "description": "for set and clear: why", - "type": "string" - } - }, - "type": "object" - }, - "name": "config", - "optional": true, - "output": { - "description": "the values asked, or the one set or cleared, or its history", - "type": "object" - } - }, - { - "description": "The open renames of config values (design 32 *A config value renamed*): per rename the module, the old and new name, when it moved, and the nodes still holding the old name open.", - "input": { - "properties": { - "module": { - "description": "one module (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "renames", - "optional": true, - "output": { - "properties": { - "renames": { - "description": "every open rename", - "items": { - "properties": { - "at": { - "description": "when it moved, RFC 3339", - "type": "string" - }, - "from": { - "description": "the old name", - "type": "string" - }, - "module": { - "description": "the module", - "type": "string" - }, - "open": { - "description": "the nodes still holding the old name open", - "items": { - "type": "string" - }, - "type": "array" - }, - "to": { - "description": "the new name", - "type": "string" - } - }, - "required": [ - "module", - "from", - "to" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "renames" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0349", - "grain": "seat-list", - "name": "mesh-provisioner", - "scope": "mesh", - "serves": [ - { - "description": "Read what the provisioner keeps: the registry (which domain answers which kind of placeholder), the needs each module on each node recorded, what is applied where, or the items of every collection, narrowed by node and module. Reads only.", - "input": { - "properties": { - "collection": { - "description": "for collect: a collection", - "type": "string" - }, - "module": { - "description": "a module: for collect, a contributor", - "type": "string" - }, - "node": { - "description": "a node", - "type": "string" - }, - "what": { - "description": "registry (when not given), needs, applied or collect", - "type": "string" - } - }, - "type": "object" - }, - "name": "provisioning", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0075", - "grain": "seat-list", - "name": "mesh-artifact-store", - "scope": "mesh" - }, - { - "decision": "novox/hq ADR 0604", - "grain": "seat-list", - "name": "mesh-catalog", - "scope": "mesh", - "serves": [ - { - "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "modules", - "optional": true - }, - { - "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", - "input": { - "properties": { - "module": { - "description": "the module's name", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - }, - "name": "module", - "optional": true - }, - { - "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", - "input": { - "properties": { - "provision": { - "description": "the provision, such as postgres-database", - "type": "string" - } - }, - "required": [ - "provision" - ], - "type": "object" - }, - "name": "provides", - "optional": true - }, - { - "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", - "input": { - "properties": { - "module": { - "description": "the module's name", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - }, - "name": "dependents", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0239", - "grain": "seat-list", - "name": "mesh-delivery", - "scope": "mesh", - "serves": [ - { - "description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", - "input": { - "properties": { - "all": { - "description": "\"true\": the final ones of the last thirty days too", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "group": { - "description": "a group's id (its branch name)", - "type": "string" - }, - "repository": { - "description": "owner/repository", - "type": "string" - }, - "state": { - "description": "one state, e.g. delivering or held", - "type": "string" - } - }, - "type": "object" - }, - "name": "deliveries" - }, - { - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", - "input": { - "properties": {}, - "type": "object" - }, - "name": "times", - "optional": true - }, - { - "description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", - "input": { - "properties": { - "id": { - "description": "a delivery's id, or a group's", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - }, - "name": "show" - }, - { - "description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", - "input": { - "properties": { - "all": { - "description": "\"true\": the groups that ended too", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - }, - "name": "groups" - }, - { - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", - "input": { - "properties": {}, - "type": "object" - }, - "name": "what-if", - "optional": true - }, - { - "description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", - "input": { - "properties": { - "commit": { - "description": "a commit's sha, or the start of one, instead of a pull request", - "type": "string" - }, - "number": { - "description": "a pull request's number: its head is read", - "type": "string" - }, - "repository": { - "description": "owner/repository", - "type": "string" - } - }, - "required": [ - "repository" - ], - "type": "object" - }, - "name": "checks", - "optional": true - }, - { - "description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "table" - }, - { - "description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "stalled" - }, - { - "description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", - "input": { - "properties": { - "id": { - "description": "the delivery's id", - "type": "string" - }, - "why": { - "description": "why, kept with the transition", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - }, - "name": "recheck" - }, - { - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", - "input": { - "properties": {}, - "type": "object" - }, - "name": "release", - "optional": true - }, - { - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", - "input": { - "properties": {}, - "type": "object" - }, - "name": "stop", - "optional": true - }, - { - "description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", - "input": { - "properties": { - "id": { - "description": "the delivery's id", - "type": "string" - }, - "why": { - "description": "the condition it answers", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - }, - "name": "close" - }, - { - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", - "input": { - "properties": {}, - "type": "object" - }, - "name": "retire-history", - "optional": true - }, - { - "description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", - "input": { - "additionalProperties": false, - "properties": { - "ids": { - "description": "the held deliveries' ids (owner/repository@commit)", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "why": { - "description": "why they may be settled, shown to the operator", - "type": "string" - } - }, - "required": [ - "ids", - "why" - ], - "type": "object" - }, - "name": "settle", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0567", - "grain": "seat-list", - "name": "mesh-placement", - "scope": "mesh", - "serves": [ - { - "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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": { - "properties": { - "history": { - "description": "\"true\": the changes in order, from the stream and the state's revisions", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module: the nodes it is placed on", - "type": "string" - }, - "node": { - "description": "one node", - "type": "string" - }, - "seats": { - "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - }, - "name": "placement", - "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.", - "input": { - "properties": { - "id": { - "description": "one proposal's id", - "type": "string" - } - }, - "type": "object" - }, - "name": "proposals", - "optional": true - }, - { - "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", - "input": { - "additionalProperties": false, - "properties": { - "changes": { - "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", - "items": { - "required": [ - "op", - "node" - ], - "type": "object" - }, - "minItems": 1, - "type": "array" - }, - "warrant": { - "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", - "required": [ - "node", - "address", - "ask", - "warrant" - ], - "type": "object" - }, - "why": { - "description": "why, shown to the operator with the proposal", - "type": "string" - } - }, - "required": [ - "why", - "changes" - ], - "type": "object" - }, - "name": "propose", - "optional": true - }, - { - "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - }, - "why": { - "description": "why, kept in the event", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - }, - "name": "withdraw", - "optional": true - }, - { - "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - }, - "name": "again", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0547", - "grain": "whole", - "name": "mesh-delivery-checker", - "scope": "mesh", - "serves": [ - { - "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", - "input": { - "properties": { - "head": { - "description": "the full commit to try, 40 or 64 hex digits", - "type": "string" - }, - "level": { - "description": "unit or integration: only this level; both when absent", - "type": "string" - }, - "repository": { - "description": "/", - "type": "string" - }, - "tests": { - "description": "a go test -run pattern: only those tests run, and nothing is kept", - "type": "string" - }, - "trunk": { - "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", - "type": "string" - } - }, - "required": [ - "repository", - "head" - ], - "type": "object" - }, - "name": "try", - "optional": true, - "output": { - "properties": { - "id": { - "description": "the try's id", - "type": "string" - }, - "state": { - "description": "its state", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - }, - "replaces": [ - "sh merge-check.sh", - "docker run postgres" - ] - }, - { - "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", - "input": { - "properties": { - "id": { - "description": "the try's id, try-", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - }, - "name": "try-read", - "optional": true, - "output": { - "description": "the try: its state, its steps and its verdict when it has one", - "type": "object" - } - }, - { - "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", - "input": { - "properties": { - "id": { - "description": "the try's id, try-", - "type": "string" - }, - "why": { - "description": "why it is ended, in words", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - }, - "name": "try-end", - "optional": true, - "output": { - "properties": { - "ended": { - "description": "it is ended", - "type": "boolean" - }, - "id": { - "description": "the try", - "type": "string" - } - }, - "required": [ - "id", - "ended" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0564", - "emits": [ - "condition-raised", - "condition-changed", - "condition-cleared" - ], - "grain": "seat-list", - "name": "mesh-conditions", - "scope": "mesh", - "serves": [ - { - "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.", - "input": { - "properties": { - "days": { - "description": "with history: how many days back (default 7, at most 90)", - "type": "string" - }, - "history": { - "description": "\"true\": every raising, change, silence and clearing lately, oldest first", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "key": { - "description": "a condition's key: that one whole; with history, only its transitions", - "type": "string" - }, - "machine": { - "description": "only those about this machine", - "type": "string" - }, - "scope": { - "description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module", - "type": "string" - }, - "severity": { - "description": "only urgent, or only warning", - "type": "string" - } - }, - "type": "object" - }, - "name": "conditions" - }, - { - "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": { - "properties": { - "cause": { - "description": "the cause in a word (the condition's kind when absent)", - "type": "string" - }, - "for": { - "description": "how long: 30m, 4h, 2d; at most 7d", - "type": "string" - }, - "key": { - "description": "the condition's key", - "type": "string" - }, - "why": { - "description": "why, required and recorded", - "type": "string" - } - }, - "required": [ - "key", - "for", - "why" - ], - "type": "object" - }, - "name": "silence" - }, - { - "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": { - "properties": {}, - "type": "object" - }, - "name": "resync" - } - ] - }, - { - "decision": "novox/hq ADR 0564", - "grain": "seat-list", - "name": "mesh-watchdogs", - "scope": "mesh", - "serves": [ - { - "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": { - "properties": { - "row": { - "description": "one row, e.g. S1", - "type": "string" - } - }, - "type": "object" - }, - "name": "signals" - }, - { - "description": "Every want, build or step the watchman says stopped moving past its bound, with since when and the words of its finding.", - "input": { - "properties": { - "machine": { - "description": "only those on this machine", - "type": "string" - } - }, - "type": "object" - }, - "name": "stuck" - }, - { - "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": { - "properties": { - "machine": { - "description": "one machine", - "type": "string" - } - }, - "type": "object" - }, - "name": "presence" - }, - { - "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.", - "input": { - "properties": { - "cause": { - "description": "with deliver or drop: the cause in a word (dead-letter when absent)", - "type": "string" - }, - "consumer": { - "description": "a consumer's name, or .: only what that one gave up on", - "type": "string" - }, - "deliver": { - "description": "a dead letter's id: deliver it again to the consumer that gave it up (needs why)", - "type": "string" - }, - "drop": { - "description": "a dead letter's id: drop it for good (needs why)", - "type": "string" - }, - "id": { - "description": "a dead letter's id, as the list gives it: that one whole, with its message", - "type": "string" - }, - "limit": { - "description": "how many to list, newest first (default 50); only when listing", - "type": "string" - }, - "why": { - "description": "with deliver or drop: why, required and recorded", - "type": "string" - } - }, - "type": "object" - }, - "name": "dead-letters" - }, - { - "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": { - "properties": {}, - "type": "object" - }, - "name": "resync" - } - ] - }, - { - "decision": "novox/hq ADR 0564", - "emits": [ - "checked" - ], - "grain": "seat-list", - "name": "mesh-self-check", - "scope": "mesh", - "serves": [ - { - "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": { - "properties": { - "probe": { - "description": "one probe's id (D6): its verdict alone, with every finding", - "type": "string" - }, - "run": { - "description": "\"true\": run every probe now and answer the verdict", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - }, - "name": "doctor" - }, - { - "description": "The registry: what each probe asserts, where it reads its facts, and the finding it writes.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "probes" - } - ] - }, - { - "decision": "novox/hq ADR 0564", - "emits": [ - "healer-acted" - ], - "grain": "seat-list", - "name": "mesh-healers", - "scope": "mesh", - "serves": [ - { - "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, and the mesh-wide brake.", - "input": { - "properties": { - "days": { - "description": "how many days of acts back (default 7)", - "type": "string" - } - }, - "type": "object" - }, - "name": "healers" - }, - { - "description": "Take one healer's act for one open condition now, within its budget and the brake.", - "input": { - "properties": { - "key": { - "description": "the condition's key", - "type": "string" - }, - "why": { - "description": "why now, kept with the act", - "type": "string" - } - }, - "required": [ - "key", - "why" - ], - "type": "object" - }, - "name": "heal" - }, - { - "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": { - "properties": { - "condition": { - "description": "the key of the condition the drill is meant to raise, if any (optional)", - "type": "string" - }, - "what": { - "description": "what was done on purpose, in a line", - "type": "string" - }, - "why": { - "description": "what the drill tests", - "type": "string" - } - }, - "required": [ - "what", - "why" - ], - "type": "object" - }, - "name": "drill" - } - ] - }, - { - "decision": "novox/hq ADR 0565", - "emits": [ - "policy-changed" - ], - "grain": "seat-list", - "name": "mesh-wants", - "scope": "mesh", - "serves": [ - { - "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, each node's own composition and the assigner's findings.", - "input": { - "properties": { - "compare": { - "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module", - "type": "string" - }, - "policy": { - "description": "with module: roll, together, record, or default to take a person's choice back", - "type": "string" - }, - "why": { - "description": "with policy: why, required and kept", - "type": "string" - } - }, - "type": "object" - }, - "name": "upgrade" - }, - { - "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": { - "properties": {}, - "type": "object" - }, - "name": "bus" - } - ] - }, - { - "decision": "novox/hq ADR 0565", - "emits": [ - "act-recorded" - ], - "grain": "seat-list", - "name": "mesh-hand-acts", - "scope": "mesh", - "serves": [ - { - "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": { - "properties": { - "cause": { - "description": "the cause in a word, or a condition's kind", - "type": "string" - }, - "condition": { - "description": "the key of the condition it addressed, if any (optional)", - "type": "string" - }, - "verb": { - "description": "the act's verb in the log: a repair when absent, `hand-act drill` for a drill, or a recorder's own", - "type": "string" - }, - "what": { - "description": "what was done, in a line", - "type": "string" - }, - "why": { - "description": "why it had to be done by hand", - "type": "string" - } - }, - "required": [ - "what", - "why" - ], - "type": "object" - }, - "name": "hand-act" - }, - { - "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": { - "properties": { - "cause": { - "description": "only this cause", - "type": "string" - }, - "days": { - "description": "how many days back (default 14)", - "type": "string" - }, - "warrants": { - "description": "\"true\": every warrant recorded", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "who": { - "description": "only acts whose who contains this", - "type": "string" - } - }, - "type": "object" - }, - "name": "hand-acts" - }, - { - "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": { - "properties": { - "ask": { - "description": "its ask's id", - "type": "string" - }, - "asker": { - "description": "the module that asked", - "type": "string" - } - }, - "required": [ - "asker", - "ask" - ], - "type": "object" - }, - "name": "warranted" - } - ] - }, - { - "decision": "novox/hq ADR 0587", - "emits": [ - "enrolment-issued", - "node-enrolled", - "enrolment-refused", - "enrolment-revoked", - "enrolment-expired" - ], - "grain": "seat-list", - "name": "mesh-enrolment", - "scope": "mesh", - "serves": [ - { - "description": "Issue a one-time token for a new machine, on the operator's Approve: refused at once, naming every finding, for a name that is not a node name or that a node record, a placement key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise reserves the lowest free private-network address, asks the operator under the seat's own name and, on the warrant, adds the node to placement with its tunnel key, asks the vault for the token and says enrolment-issued. The token itself is read only through tokens.", - "input": { - "properties": { - "expires": { - "description": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", - "type": "string" - }, - "node": { - "description": "the new machine's name in the mesh", - "type": "string" - }, - "overlay_key": { - "description": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", - "type": "string" - }, - "why": { - "description": "why it joins, shown to the operator with the ask", - "type": "string" - } - }, - "required": [ - "node", - "why", - "overlay_key" - ], - "type": "object" - }, - "name": "issue" - }, - { - "description": "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", - "input": { - "additionalProperties": false, - "properties": { - "node": { - "description": "the node the token was issued for", - "type": "string" - }, - "overlay_key": { - "description": "the private network key's public half", - "type": "string" - }, - "profile": { - "description": "what the machine is, as its record says it", - "type": "object" - }, - "proof": { - "description": "the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64", - "type": "string" - }, - "public_key": { - "description": "the identity key's public half, base64", - "type": "string" - }, - "sealing_key": { - "description": "the sealing key's public half, which the account is sealed to", - "type": "string" - }, - "serving_key": { - "description": "the serving key's public half, which the certificate binds", - "type": "string" - }, - "tunnel": { - "description": "a tunnel found on the machine: everything but its private key", - "type": "object" - } - }, - "required": [ - "node", - "public_key", - "sealing_key", - "proof" - ], - "type": "object" - }, - "name": "redeem" - }, - { - "description": "Confirm an enrolment: asked by the node-engine dialled as its own node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the enrolment.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "confirm" - }, - { - "description": "End an enrolment at any stage, with why, without an Approve: withdraws an open ask, deletes the node's key and its address, has the vault retire an account it already minted for it, records the act as a hand act and says enrolment-revoked.", - "input": { - "properties": { - "node": { - "description": "the node", - "type": "string" - }, - "why": { - "description": "why, recorded with the hand act", - "type": "string" - } - }, - "required": [ - "node", - "why" - ], - "type": "object" - }, - "name": "revoke" - }, - { - "description": "Read every open enrolment, or one: its stage, expiry, ask and revision, the bucket's fill against its cap. For a node at live, its token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With history, the ended enrolments from the seat's events, as far back as the events are kept.", - "input": { - "properties": { - "history": { - "description": "\"true\": the ended enrolments too", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "node": { - "description": "one node", - "type": "string" - } - }, - "type": "object" - }, - "name": "tokens" - }, - { - "description": "Raise an issue's ask again when the operator's channel does not hold it, and nothing more.", - "input": { - "properties": { - "node": { - "description": "the node whose ask is raised again", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - }, - "name": "again" - } - ] - }, - { - "decision": "novox/hq ADR 0571", - "emits": [ - "artifacts-gone" - ], - "grain": "seat-list", - "name": "mesh-artifact-sweep", - "scope": "mesh", - "serves": [ - { - "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": { - "properties": { - "collected": { - "description": "\"true\": list the collected references too (counted always)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "repository": { - "description": "only this repository's references, as / (the counts stay whole)", - "type": "string" - } - }, - "type": "object" - }, - "name": "artifacts" - }, - { - "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": { - "properties": { - "references": { - "description": "the artifacts' references, artifact-store:////blobs/sha256:: a list, or one text separated by commas", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "references" - ], - "type": "object" - }, - "name": "held" - }, - { - "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": { - "properties": { - "confirm": { - "description": "\"true\": let go of what is eligible (needs why); without it nothing is done", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "most": { - "description": "let go of at most this many (500 by default, at most 5000)", - "type": "string" - }, - "why": { - "description": "with confirm: why, required and recorded", - "type": "string" - } - }, - "type": "object" - }, - "name": "collect" - }, - { - "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": { - "properties": {}, - "type": "object" - }, - "name": "mirrors" - }, - { - "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": { - "properties": { - "node": { - "description": "the machine's name", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - }, - "name": "images" - } - ] - }, - { - "decision": "novox/hq ADR 0109", - "delivers": "npm-package-registry", - "emits": [ - "published", - "refused", - "removed", - "kept" - ], - "grain": "whole", - "name": "npm-package-registry", - "scope": "mesh", - "serves": [ - { - "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", - "input": { - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "parts": { - "description": "every part's digest and size; none when the digest is already held", - "items": { - "description": "one part of a transfer", - "properties": { - "digest": { - "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", - "type": "string" - }, - "size": { - "description": "its size in bytes", - "type": "integer" - } - }, - "required": [ - "digest", - "size" - ], - "type": "object" - }, - "type": "array" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "transfer": { - "description": "the transfer's name, the caller's own; the same name opens the same transfer again", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "transfer", - "name", - "kind", - "digest" - ], - "type": "object" - }, - "name": "publish", - "optional": true, - "output": { - "properties": { - "held": { - "description": "the digest is held already: nothing to send; the outcome follows as an event", - "type": "boolean" - }, - "lacks": { - "description": "the digests of the parts the holder lacks, to send by publish-part", - "items": { - "type": "string" - }, - "type": "array" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer" - ], - "type": "object" - } - }, - { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", - "input": { - "properties": { - "data": { - "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", - "type": "string" - }, - "last": { - "description": "this chunk is the part's last", - "type": "boolean" - }, - "part": { - "description": "the part's digest", - "type": "string" - }, - "sequence": { - "description": "the chunk's place in its part, from 0", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part", - "sequence", - "last", - "data" - ], - "type": "object" - }, - "name": "publish-part", - "optional": true, - "output": { - "properties": { - "complete": { - "description": "the part is whole and its bytes match its digest", - "type": "boolean" - }, - "part": { - "description": "the part", - "type": "string" - }, - "received": { - "description": "the chunks of the part held so far", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part" - ], - "type": "object" - } - }, - { - "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", - "input": { - "properties": { - "kind": { - "description": "narrowed to one kind (optional)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "versions", - "optional": true, - "output": { - "properties": { - "name": { - "description": "the name", - "type": "string" - }, - "versions": { - "description": "newest first", - "items": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "name", - "versions" - ], - "type": "object" - } - }, - { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", - "input": { - "properties": { - "digest": { - "description": "the digest (for a bundle, archive or copied base)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it goes", - "enum": [ - "tag-gone", - "retention" - ], - "type": "string" - }, - "tag": { - "description": "the tag (for a tagged version)", - "type": "string" - }, - "version": { - "description": "the version", - "type": "string" - } - }, - "required": [ - "name", - "reason" - ], - "type": "object" - }, - "name": "remove", - "optional": true, - "output": { - "properties": { - "artifact": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "kept": { - "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", - "type": "boolean" - }, - "reason": { - "description": "why it was kept: shared-digest, another tag names its digest", - "enum": [ - "shared-digest" - ], - "type": "string" - }, - "removed": { - "description": "it is gone", - "type": "boolean" - }, - "tags": { - "description": "the other tags still naming its digest, when kept", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "removed" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0605", - "delivers": "go-package-registry", - "emits": [ - "published", - "refused", - "removed", - "kept" - ], - "grain": "whole", - "name": "go-package-registry", - "scope": "mesh", - "serves": [ - { - "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", - "input": { - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "parts": { - "description": "every part's digest and size; none when the digest is already held", - "items": { - "description": "one part of a transfer", - "properties": { - "digest": { - "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", - "type": "string" - }, - "size": { - "description": "its size in bytes", - "type": "integer" - } - }, - "required": [ - "digest", - "size" - ], - "type": "object" - }, - "type": "array" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "transfer": { - "description": "the transfer's name, the caller's own; the same name opens the same transfer again", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "transfer", - "name", - "kind", - "digest" - ], - "type": "object" - }, - "name": "publish", - "optional": true, - "output": { - "properties": { - "held": { - "description": "the digest is held already: nothing to send; the outcome follows as an event", - "type": "boolean" - }, - "lacks": { - "description": "the digests of the parts the holder lacks, to send by publish-part", - "items": { - "type": "string" - }, - "type": "array" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer" - ], - "type": "object" - } - }, - { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", - "input": { - "properties": { - "data": { - "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", - "type": "string" - }, - "last": { - "description": "this chunk is the part's last", - "type": "boolean" - }, - "part": { - "description": "the part's digest", - "type": "string" - }, - "sequence": { - "description": "the chunk's place in its part, from 0", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part", - "sequence", - "last", - "data" - ], - "type": "object" - }, - "name": "publish-part", - "optional": true, - "output": { - "properties": { - "complete": { - "description": "the part is whole and its bytes match its digest", - "type": "boolean" - }, - "part": { - "description": "the part", - "type": "string" - }, - "received": { - "description": "the chunks of the part held so far", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part" - ], - "type": "object" - } - }, - { - "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", - "input": { - "properties": { - "kind": { - "description": "narrowed to one kind (optional)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "versions", - "optional": true, - "output": { - "properties": { - "name": { - "description": "the name", - "type": "string" - }, - "versions": { - "description": "newest first", - "items": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "name", - "versions" - ], - "type": "object" - } - }, - { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", - "input": { - "properties": { - "digest": { - "description": "the digest (for a bundle, archive or copied base)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it goes", - "enum": [ - "tag-gone", - "retention" - ], - "type": "string" - }, - "tag": { - "description": "the tag (for a tagged version)", - "type": "string" - }, - "version": { - "description": "the version", - "type": "string" - } - }, - "required": [ - "name", - "reason" - ], - "type": "object" - }, - "name": "remove", - "optional": true, - "output": { - "properties": { - "artifact": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "kept": { - "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", - "type": "boolean" - }, - "reason": { - "description": "why it was kept: shared-digest, another tag names its digest", - "enum": [ - "shared-digest" - ], - "type": "string" - }, - "removed": { - "description": "it is gone", - "type": "boolean" - }, - "tags": { - "description": "the other tags still naming its digest, when kept", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "removed" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0605", - "emits": [ - "published", - "refused", - "removed", - "kept", - "held", - "released" - ], - "grain": "whole", - "name": "container-image-registry", - "scope": "mesh", - "serves": [ - { - "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", - "input": { - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "parts": { - "description": "every part's digest and size; none when the digest is already held", - "items": { - "description": "one part of a transfer", - "properties": { - "digest": { - "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", - "type": "string" - }, - "size": { - "description": "its size in bytes", - "type": "integer" - } - }, - "required": [ - "digest", - "size" - ], - "type": "object" - }, - "type": "array" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "transfer": { - "description": "the transfer's name, the caller's own; the same name opens the same transfer again", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "transfer", - "name", - "kind", - "digest" - ], - "type": "object" - }, - "name": "publish", - "optional": true, - "output": { - "properties": { - "held": { - "description": "the digest is held already: nothing to send; the outcome follows as an event", - "type": "boolean" - }, - "lacks": { - "description": "the digests of the parts the holder lacks, to send by publish-part", - "items": { - "type": "string" - }, - "type": "array" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer" - ], - "type": "object" - } - }, - { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", - "input": { - "properties": { - "data": { - "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", - "type": "string" - }, - "last": { - "description": "this chunk is the part's last", - "type": "boolean" - }, - "part": { - "description": "the part's digest", - "type": "string" - }, - "sequence": { - "description": "the chunk's place in its part, from 0", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part", - "sequence", - "last", - "data" - ], - "type": "object" - }, - "name": "publish-part", - "optional": true, - "output": { - "properties": { - "complete": { - "description": "the part is whole and its bytes match its digest", - "type": "boolean" - }, - "part": { - "description": "the part", - "type": "string" - }, - "received": { - "description": "the chunks of the part held so far", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part" - ], - "type": "object" - } - }, - { - "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", - "input": { - "properties": { - "kind": { - "description": "narrowed to one kind (optional)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "versions", - "optional": true, - "output": { - "properties": { - "name": { - "description": "the name", - "type": "string" - }, - "versions": { - "description": "newest first", - "items": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "name", - "versions" - ], - "type": "object" - } - }, - { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", - "input": { - "properties": { - "digest": { - "description": "the digest (for a bundle, archive or copied base)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it goes", - "enum": [ - "tag-gone", - "retention" - ], - "type": "string" - }, - "tag": { - "description": "the tag (for a tagged version)", - "type": "string" - }, - "version": { - "description": "the version", - "type": "string" - } - }, - "required": [ - "name", - "reason" - ], - "type": "object" - }, - "name": "remove", - "optional": true, - "output": { - "properties": { - "artifact": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "kept": { - "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", - "type": "boolean" - }, - "reason": { - "description": "why it was kept: shared-digest, another tag names its digest", - "enum": [ - "shared-digest" - ], - "type": "string" - }, - "removed": { - "description": "it is gone", - "type": "boolean" - }, - "tags": { - "description": "the other tags still naming its digest, when kept", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "removed" - ], - "type": "object" - } - }, - { - "description": "Copy an outside image into the store by its pinned digest; emits published or refused.", - "input": { - "properties": { - "digest": { - "description": "its pinned digest, sha256:", - "type": "string" - }, - "name": { - "description": "the name it is kept under", - "type": "string" - }, - "source": { - "description": "the outside image's reference", - "type": "string" - } - }, - "required": [ - "source", - "digest", - "name" - ], - "type": "object" - }, - "name": "mirror", - "optional": true, - "output": { - "properties": { - "digest": { - "description": "the digest stored", - "type": "string" - }, - "reference": { - "description": "the reference the seat answers", - "type": "string" - } - }, - "required": [ - "reference", - "digest" - ], - "type": "object" - } - }, - { - "description": "The tags of one image and the digest each names.", - "input": { - "properties": { - "name": { - "description": "the image", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "tags", - "optional": true, - "output": { - "properties": { - "name": { - "description": "the image", - "type": "string" - }, - "tags": { - "description": "every tag", - "items": { - "properties": { - "digest": { - "description": "its digest", - "type": "string" - }, - "tag": { - "description": "the tag", - "type": "string" - } - }, - "required": [ - "tag", - "digest" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "name", - "tags" - ], - "type": "object" - } - }, - { - "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", - "input": { - "properties": { - "digest": { - "description": "the digest", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "which of the sweep's rules keeps it", - "type": "string" - } - }, - "required": [ - "name", - "digest", - "reason" - ], - "type": "object" - }, - "name": "hold", - "optional": true, - "output": { - "properties": { - "held": { - "description": "it is held", - "type": "boolean" - }, - "since": { - "description": "when the hold began, RFC 3339", - "type": "string" - } - }, - "required": [ - "held" - ], - "type": "object" - } - }, - { - "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", - "input": { - "properties": { - "digest": { - "description": "the digest", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it is let go", - "type": "string" - } - }, - "required": [ - "name", - "digest", - "reason" - ], - "type": "object" - }, - "name": "release", - "optional": true, - "output": { - "properties": { - "released": { - "description": "a hold was let go; false when there was none", - "type": "boolean" - } - }, - "required": [ - "released" - ], - "type": "object" - } - }, - { - "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", - "input": { - "properties": { - "name": { - "description": "the repository, /; every repository of the seat when not given", - "type": "string" - } - }, - "type": "object" - }, - "name": "list", - "optional": true, - "output": { - "properties": { - "repositories": { - "description": "each repository named, in order of its name", - "items": { - "properties": { - "manifests": { - "description": "each manifest it holds, by digest", - "items": { - "properties": { - "artifact": { - "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", - "type": "string" - }, - "digest": { - "description": "the manifest's digest, sha256:", - "type": "string" - }, - "held": { - "description": "a hold keeps it from retention", - "type": "boolean" - }, - "index": { - "description": "it is an index, naming a manifest per platform", - "type": "boolean" - }, - "platforms": { - "description": "for an index, the digests of the manifests it names", - "items": { - "type": "string" - }, - "type": "array" - }, - "size": { - "description": "its bytes: its own content and every blob it names", - "type": "integer" - }, - "tags": { - "description": "the tags naming it, none for an untagged one", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "digest", - "index", - "size", - "held" - ], - "type": "object" - }, - "type": "array" - }, - "name": { - "description": "the repository", - "type": "string" - } - }, - "required": [ - "name", - "manifests" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "repositories" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0605", - "emits": [ - "published", - "refused", - "removed", - "kept", - "held", - "released" - ], - "grain": "whole", - "name": "oci-artifact-registry", - "scope": "mesh", - "serves": [ - { - "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", - "input": { - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "parts": { - "description": "every part's digest and size; none when the digest is already held", - "items": { - "description": "one part of a transfer", - "properties": { - "digest": { - "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob", - "type": "string" - }, - "size": { - "description": "its size in bytes", - "type": "integer" - } - }, - "required": [ - "digest", - "size" - ], - "type": "object" - }, - "type": "array" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "transfer": { - "description": "the transfer's name, the caller's own; the same name opens the same transfer again", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "transfer", - "name", - "kind", - "digest" - ], - "type": "object" - }, - "name": "publish", - "optional": true, - "output": { - "properties": { - "held": { - "description": "the digest is held already: nothing to send; the outcome follows as an event", - "type": "boolean" - }, - "lacks": { - "description": "the digests of the parts the holder lacks, to send by publish-part", - "items": { - "type": "string" - }, - "type": "array" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer" - ], - "type": "object" - } - }, - { - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", - "input": { - "properties": { - "data": { - "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)", - "type": "string" - }, - "last": { - "description": "this chunk is the part's last", - "type": "boolean" - }, - "part": { - "description": "the part's digest", - "type": "string" - }, - "sequence": { - "description": "the chunk's place in its part, from 0", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part", - "sequence", - "last", - "data" - ], - "type": "object" - }, - "name": "publish-part", - "optional": true, - "output": { - "properties": { - "complete": { - "description": "the part is whole and its bytes match its digest", - "type": "boolean" - }, - "part": { - "description": "the part", - "type": "string" - }, - "received": { - "description": "the chunks of the part held so far", - "type": "integer" - }, - "transfer": { - "description": "the transfer", - "type": "string" - } - }, - "required": [ - "transfer", - "part" - ], - "type": "object" - } - }, - { - "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", - "input": { - "properties": { - "kind": { - "description": "narrowed to one kind (optional)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "versions", - "optional": true, - "output": { - "properties": { - "name": { - "description": "the name", - "type": "string" - }, - "versions": { - "description": "newest first", - "items": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "name", - "versions" - ], - "type": "object" - } - }, - { - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", - "input": { - "properties": { - "digest": { - "description": "the digest (for a bundle, archive or copied base)", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it goes", - "enum": [ - "tag-gone", - "retention" - ], - "type": "string" - }, - "tag": { - "description": "the tag (for a tagged version)", - "type": "string" - }, - "version": { - "description": "the version", - "type": "string" - } - }, - "required": [ - "name", - "reason" - ], - "type": "object" - }, - "name": "remove", - "optional": true, - "output": { - "properties": { - "artifact": { - "description": "one published thing, as every registry event and answer names it", - "properties": { - "commit": { - "description": "the source commit it was built from", - "type": "string" - }, - "digest": { - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)", - "type": "string" - }, - "fingerprint": { - "description": "the build's fingerprint", - "type": "string" - }, - "kind": { - "description": "what it is", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "type": "string" - }, - "name": { - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)", - "type": "string" - }, - "tag": { - "description": "the tag it carries, for a tagged version", - "type": "string" - }, - "version": { - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone", - "type": "string" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "type": "object" - }, - "kept": { - "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)", - "type": "boolean" - }, - "reason": { - "description": "why it was kept: shared-digest, another tag names its digest", - "enum": [ - "shared-digest" - ], - "type": "string" - }, - "removed": { - "description": "it is gone", - "type": "boolean" - }, - "tags": { - "description": "the other tags still naming its digest, when kept", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "removed" - ], - "type": "object" - } - }, - { - "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", - "input": { - "properties": { - "digest": { - "description": "the digest", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "which of the sweep's rules keeps it", - "type": "string" - } - }, - "required": [ - "name", - "digest", - "reason" - ], - "type": "object" - }, - "name": "hold", - "optional": true, - "output": { - "properties": { - "held": { - "description": "it is held", - "type": "boolean" - }, - "since": { - "description": "when the hold began, RFC 3339", - "type": "string" - } - }, - "required": [ - "held" - ], - "type": "object" - } - }, - { - "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", - "input": { - "properties": { - "digest": { - "description": "the digest", - "type": "string" - }, - "name": { - "description": "the name", - "type": "string" - }, - "reason": { - "description": "why it is let go", - "type": "string" - } - }, - "required": [ - "name", - "digest", - "reason" - ], - "type": "object" - }, - "name": "release", - "optional": true, - "output": { - "properties": { - "released": { - "description": "a hold was let go; false when there was none", - "type": "boolean" - } - }, - "required": [ - "released" - ], - "type": "object" - } - }, - { - "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", - "input": { - "properties": { - "name": { - "description": "the repository, /; every repository of the seat when not given", - "type": "string" - } - }, - "type": "object" - }, - "name": "list", - "optional": true, - "output": { - "properties": { - "repositories": { - "description": "each repository named, in order of its name", - "items": { - "properties": { - "manifests": { - "description": "each manifest it holds, by digest", - "items": { - "properties": { - "artifact": { - "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by", - "type": "string" - }, - "digest": { - "description": "the manifest's digest, sha256:", - "type": "string" - }, - "held": { - "description": "a hold keeps it from retention", - "type": "boolean" - }, - "index": { - "description": "it is an index, naming a manifest per platform", - "type": "boolean" - }, - "platforms": { - "description": "for an index, the digests of the manifests it names", - "items": { - "type": "string" - }, - "type": "array" - }, - "size": { - "description": "its bytes: its own content and every blob it names", - "type": "integer" - }, - "tags": { - "description": "the tags naming it, none for an untagged one", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "digest", - "index", - "size", - "held" - ], - "type": "object" - }, - "type": "array" - }, - "name": { - "description": "the repository", - "type": "string" - } - }, - "required": [ - "name", - "manifests" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "repositories" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0111", - "delivers": "git", - "grain": "seat-list", - "name": "git", - "scope": "mesh" - }, - { - "accepts": [ - "delivery", - "check", - "recheck" - ], - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "grain": "seat-list", - "name": "node-build-go", - "pulledByKind": true, - "scope": "node", - "serves": [ - { - "description": "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "current" - }, - { - "description": "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "pause" - }, - { - "description": "Take Go asks again on this node.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "resume" - } - ], - "yields": true - }, - { - "accepts": [ - "delivery", - "check", - "recheck" - ], - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "grain": "seat-list", - "name": "node-build-node", - "pulledByKind": true, - "scope": "node", - "serves": [ - { - "description": "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "current" - }, - { - "description": "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "pause" - }, - { - "description": "Take Node asks again on this node.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "resume" - } - ], - "yields": true - }, - { - "accepts": [ - "delivery", - "check", - "recheck" - ], - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "grain": "seat-list", - "name": "node-build-image", - "pulledByKind": true, - "scope": "node", - "serves": [ - { - "description": "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "current" - }, - { - "description": "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "pause" - }, - { - "description": "Take image asks again on this node.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "resume" - } - ], - "yields": true - }, - { - "decision": "novox/hq ADR 0194, ADR 0223", - "delivers": "wildcard-resolution", - "grain": "seat-list", - "name": "mesh-dns-resolver", - "replicated": true, - "scope": "mesh" - }, - { - "decision": "novox/hq ADR 0199, ADR 0223", - "grain": "seat-list", - "name": "node-hostname", - "scope": "node", - "serves": [ - { - "description": "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "entries", - "replaces": [ - "cat /etc/hosts", - "getent hosts" - ] - }, - { - "description": "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", - "input": { - "properties": { - "address": { - "description": "the IPv4 or IPv6 address", - "type": "string" - }, - "names": { - "description": "the names for it, separated by spaces", - "type": "string" - } - }, - "required": [ - "address", - "names" - ], - "type": "object" - }, - "name": "add", - "replaces": [ - "edit /etc/hosts", - "HOSTALIASES" - ] - }, - { - "description": "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", - "input": { - "properties": { - "name": { - "description": "a host name, or an address to remove every line of", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "remove", - "replaces": [ - "sed -i /etc/hosts" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0121", - "grain": "seat-list", - "name": "node-intrusion-prevention", - "scope": "node", - "serves": [ - { - "description": "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", - "input": { - "properties": { - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "status", - "replaces": [ - "fail2ban-client status" - ] - }, - { - "description": "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", - "input": { - "properties": { - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "banned", - "replaces": [ - "fail2ban-client banned" - ] - }, - { - "description": "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", - "input": { - "properties": { - "ip": { - "description": "the address", - "type": "string" - }, - "jail": { - "description": "the jail to hold it", - "type": "string" - } - }, - "required": [ - "ip", - "jail" - ], - "type": "object" - }, - "name": "ban", - "replaces": [ - "fail2ban-client set banip" - ] - }, - { - "description": "Let one address go, from one jail or from every jail when none is named.", - "input": { - "properties": { - "ip": { - "description": "the address", - "type": "string" - }, - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "required": [ - "ip" - ], - "type": "object" - }, - "name": "unban", - "replaces": [ - "fail2ban-client unban", - "fail2ban-client set unbanip" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0121", - "grain": "seat-list", - "name": "node-packet-filter", - "scope": "node", - "serves": [ - { - "description": "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", - "input": { - "properties": { - "chain": { - "description": "one chain of that table (optional)", - "type": "string" - }, - "table": { - "description": "one nftables table, as `family name` (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "rules", - "replaces": [ - "nft list ruleset", - "iptables -L", - "iptables-save" - ] - }, - { - "description": "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "reload", - "replaces": [ - "nft -f" - ] - }, - { - "description": "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", - "input": { - "properties": { - "where": { - "description": "the rule set, as `node show` lists it", - "type": "string" - } - }, - "required": [ - "where" - ], - "type": "object" - }, - "name": "remove", - "replaces": [ - "nft delete", - "iptables -X" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0177", - "grain": "seat-list", - "name": "node-service-manager", - "scope": "node", - "serves": [ - { - "description": "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.", - "input": { - "properties": { - "pattern": { - "description": "a glob the unit's name must match (optional)", - "type": "string" - }, - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - } - }, - "type": "object" - }, - "name": "units", - "replaces": [ - "systemctl list-units" - ] - }, - { - "description": "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and whether the mesh declares it.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "status", - "replaces": [ - "systemctl status", - "systemctl is-active" - ] - }, - { - "description": "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at the next apply.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "start", - "replaces": [ - "systemctl start" - ] - }, - { - "description": "Stop one unit; for a mesh-declared unit the answer says the host will restore its declared state.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "stop", - "replaces": [ - "systemctl stop" - ] - }, - { - "description": "Restart one unit.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "restart", - "replaces": [ - "systemctl restart" - ] - }, - { - "description": "Make one unit start at boot (or at the account's login, in user scope).", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "enable", - "replaces": [ - "systemctl enable" - ] - }, - { - "description": "Stop one unit starting at boot (or at login, in user scope).", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "disable", - "replaces": [ - "systemctl disable" - ] - }, - { - "description": "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority and to lines holding a text when asked. A secret the unit printed is shown as [redacted: ].", - "input": { - "properties": { - "lines": { - "description": "how many lines from the end of what matches (default 100, at most 2000)", - "type": "string" - }, - "match": { - "description": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", - "type": "string" - }, - "priority": { - "description": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", - "type": "string" - }, - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "since": { - "description": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - }, - "until": { - "description": "the window's end, in the same forms (optional; now when absent)", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "journal", - "replaces": [ - "journalctl" - ] - }, - { - "description": "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, never as nothing failed.", - "input": { - "properties": { - "scope": { - "description": "\"system\" or \"user\": only that manager (both when absent)", - "type": "string" - } - }, - "type": "object" - }, - "name": "failed", - "optional": true, - "replaces": [ - "systemctl --failed" - ] - }, - { - "description": "Clear one unit's failed record in its manager — its failed state and its start-limit count — and answer its state after. It removes no file and starts nothing.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "reset-failed", - "replaces": [ - "systemctl reset-failed" - ] - }, - { - "description": "What wants, requires or triggers one unit, read-only: the manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), each with where it points and whether it dangles — the account's own directory included in user scope.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - } - }, - "required": [ - "unit" - ], - "type": "object" - }, - "name": "wanted-by", - "replaces": [ - "systemctl list-dependencies --reverse", - "systemctl show -p WantedBy", - "ls ~/.config/systemd/user/*.wants" - ] - }, - { - "description": "Remove the dangling enable links named for one unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the runtime placed, is left and said.", - "input": { - "properties": { - "scope": { - "description": "\"system\" (the default) or \"user\": the operator account's own manager", - "type": "string" - }, - "unit": { - "description": "the unit's name, as the service manager knows it", - "type": "string" - }, - "why": { - "description": "the person's reason for removing the links, kept in the record", - "type": "string" - } - }, - "required": [ - "unit", - "why" - ], - "type": "object" - }, - "name": "unlink-dangling", - "replaces": [ - "rm ~/.config/systemd/user/*.wants/", - "unlink /etc/systemd/system/*.wants/" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0207", - "grain": "seat-list", - "name": "node-package-manager", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0166, ADR 0207", - "grain": "seat-list", - "name": "node-container-runtime", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0203", - "grain": "seat-list", - "name": "node-environment", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0349", - "grain": "seat-list", - "name": "node-provisioner", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0204", - "grain": "seat-list", - "name": "node-login-shell", - "scope": "node", - "serves": [ - { - "description": "Run one command on this machine as the operator account, in a non-interactive login shell in its home; answers with what it printed and how it exited.", - "input": { - "properties": { - "command": { - "description": "the command line, as you would type it", - "type": "string" - }, - "timeout_seconds": { - "description": "give up after this long, at most 25 (default 20)", - "type": "string" - } - }, - "required": [ - "command" - ], - "type": "object" - }, - "name": "execute", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0211", - "grain": "seat-list", - "name": "node-power", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0212", - "grain": "seat-list", - "name": "node-hotkeys", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0215", - "grain": "seat-list", - "name": "node-message-bus", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0214", - "grain": "seat-list", - "name": "node-backup", - "scope": "node", - "serves": [ - { - "description": "What this machine backs up: each module, what it declared, its last good night, how many restore points are kept and the repository's size.", - "input": { - "properties": { - "module": { - "description": "one module (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "backed-up", - "replaces": [ - "restic snapshots" - ] - }, - { - "description": "Take a backup now, of one module or of every module on this machine — before a migration, a retirement or anything else that could go wrong.", - "input": { - "properties": { - "module": { - "description": "one module (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "now", - "replaces": [ - "restic backup" - ] - }, - { - "description": "Restore one module's data from a restore point BESIDE the live data, never over it: each directory as .restored-. Swapping it in is a person's act. Lists the restore points when none is named.", - "input": { - "properties": { - "module": { - "description": "the module", - "type": "string" - }, - "path": { - "description": "one of the module's directories (all of them when omitted)", - "type": "string" - }, - "snapshot": { - "description": "the restore point (from `backed-up`; the newest when omitted)", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - }, - "name": "restore", - "replaces": [ - "restic restore" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0110", - "grain": "seat-list", - "name": "the-private-network", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0117", - "grain": "seat-list", - "name": "node-uplink", - "scope": "node", - "serves": [ - { - "description": "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "resolvers", - "optional": true - }, - { - "description": "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "links", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0247", - "grain": "seat-list", - "name": "node-resolver", - "scope": "node", - "serves": [ - { - "description": "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "routes", - "replaces": [ - "resolvectl status", - "resolvectl dns", - "resolvectl domain" - ] - }, - { - "description": "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", - "input": { - "properties": { - "domains": { - "description": "the domains to route there, separated by spaces or commas", - "type": "string" - }, - "link": { - "description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", - "type": "string" - }, - "servers": { - "description": "the servers' addresses, separated by spaces or commas", - "type": "string" - } - }, - "required": [ - "link", - "domains", - "servers" - ], - "type": "object" - }, - "name": "route", - "replaces": [ - "resolvectl dns", - "resolvectl domain", - "resolvectl default-route" - ] - }, - { - "description": "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", - "input": { - "properties": { - "link": { - "description": "the network link, by name", - "type": "string" - } - }, - "required": [ - "link" - ], - "type": "object" - }, - "name": "unroute", - "replaces": [ - "resolvectl revert" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0263", - "grain": "seat-list", - "name": "node-nfs-server", - "scope": "node", - "serves": [ - { - "description": "Every share this machine exports: its name, its path, read-write or read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that node's private address and access (only the nodes the server grants it to and that ask for it), what is granted and not asked for or asked for and not granted, and whether the kernel holds it now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in /etc/exports.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "exports", - "replaces": [ - "exportfs -v", - "cat /etc/exports", - "zfs get sharenfs" - ] - }, - { - "description": "Which machines have mounted which share now, as the NFS server knows its clients.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "clients", - "replaces": [ - "ss -tn sport = :2049", - "cat /proc/fs/nfsd/clients/*/info" - ] - }, - { - "description": "Whether this machine exports one share for the mesh now, and whether its NFS service is up; with an address, also whether the share is exported to that address: a node's own private address, when the server grants it the share and the node asks for it. What a machine mounting the share asks before it mounts.", - "input": { - "properties": { - "address": { - "description": "a node's private address, to ask whether the share is exported to it (optional)", - "type": "string" - }, - "share": { - "description": "the share, by its name", - "type": "string" - } - }, - "required": [ - "share" - ], - "type": "object" - }, - "name": "test", - "replaces": [ - "showmount -e" - ] - }, - { - "description": "Have the kernel read this machine's export files again now (exportfs -ra) and answer the shares as it then holds them. The module's own process writes its export file; this is for after a change made outside it. Changes no shared path.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "reload", - "replaces": [ - "exportfs -ra" - ] - }, - { - "description": "Take over an export a person made by hand: clear a dataset's sharenfs property for a path the mesh's own export now serves — only when that export is live. Without confirm, says what it would do and changes nothing.", - "input": { - "properties": { - "confirm": { - "description": "\"true\": change it (needs why); anything else is a dry run", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "path": { - "description": "the shared path whose hand-made export is taken over", - "type": "string" - }, - "why": { - "description": "why, for the record", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "adopt", - "replaces": [ - "zfs set sharenfs=off" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0263", - "grain": "seat-list", - "name": "node-mounts", - "scope": "node", - "serves": [ - { - "description": "Every mount point on this machine: its fstab line, the mesh's mount and automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, and which settings asked for it. A password in a mount's options is never shown.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "list", - "replaces": [ - "cat /etc/fstab", - "findmnt", - "systemctl list-units --type=mount" - ] - }, - { - "description": "Whether one share's or occasional source's server answers from this machine now, without touching its mount point.", - "input": { - "properties": { - "name": { - "description": "the share or source, by its name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "test", - "replaces": [ - "showmount -e", - "ping" - ] - }, - { - "description": "Mount one share or occasional source now, rather than at its first access.", - "input": { - "properties": { - "name": { - "description": "the share or source, by its name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "mount", - "replaces": [ - "mount" - ] - }, - { - "description": "Release one share or occasional source now; its automount stays armed, so the next access mounts it again.", - "input": { - "properties": { - "name": { - "description": "the share or source, by its name", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "name": "unmount", - "replaces": [ - "umount" - ] - }, - { - "description": "Take over a mount a person wrote by hand: comment out the /etc/fstab line for a mount point the mesh's own units now serve, keeping a copy of the file — only when the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", - "input": { - "properties": { - "confirm": { - "description": "\"true\": change it (needs why); anything else is a dry run", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "mountpoint": { - "description": "the mount point whose fstab line is taken over", - "type": "string" - }, - "why": { - "description": "why, for the record", - "type": "string" - } - }, - "required": [ - "mountpoint" - ], - "type": "object" - }, - "name": "adopt", - "replaces": [ - "sed -i /etc/fstab" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0310", - "grain": "seat-list", - "name": "node-reads", - "scope": "node", - "serves": [ - { - "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". Read as the holder's own account, which holds no secret: a component it may not reach says so. At most 200 paths in one call.", - "input": { - "properties": { - "follow": { - "description": "\"true\": also say where the path leads once every link is followed", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "paths": { - "description": "one absolute path, or several separated by line breaks", - "type": "string" - } - }, - "required": [ - "paths" - ], - "type": "object" - }, - "name": "stat", - "replaces": [ - "stat", - "ls -ld", - "namei -l", - "findmnt -T" - ] - }, - { - "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists.", - "input": { - "properties": { - "path": { - "description": "an absolute path", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "readlink", - "replaces": [ - "readlink", - "readlink -f", - "realpath" - ] - }, - { - "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name.", - "input": { - "properties": { - "path": { - "description": "an absolute path to a directory", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "list", - "replaces": [ - "ls", - "ls -la" - ] - }, - { - "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may.", - "input": { - "properties": { - "bytes": { - "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", - "type": "string" - }, - "from": { - "description": "the first line to answer, counted from 1 (optional)", - "type": "string" - }, - "lines": { - "description": "how many lines to answer (optional)", - "type": "string" - }, - "path": { - "description": "an absolute path to a file", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "read", - "replaces": [ - "cat", - "head", - "tail", - "less" - ] - }, - { - "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met.", - "input": { - "properties": { - "case": { - "description": "\"false\": match whatever the case (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "depth": { - "description": "how many directories deep, up to 8 (optional)", - "type": "string" - }, - "matches": { - "description": "the most matches to answer, up to 500 (optional)", - "type": "string" - }, - "path": { - "description": "an absolute path to a file or a directory", - "type": "string" - }, - "text": { - "description": "the fixed string to find", - "type": "string" - } - }, - "required": [ - "text", - "path" - ], - "type": "object" - }, - "name": "search", - "replaces": [ - "grep", - "grep -r", - "grep -F" - ] - }, - { - "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field.", - "input": { - "properties": { - "group": { - "description": "a group's name or number (optional)", - "type": "string" - }, - "name": { - "description": "an account's name or number (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "account", - "replaces": [ - "getent passwd", - "getent group", - "id", - "groups" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0394", - "grain": "seat-list", - "name": "node-files", - "scope": "node", - "serves": [ - { - "description": "Each component of one path or several, from / down: its type, owner and group (name and number), mode, whether it is a link and to what, and whether it is a mount point and of which file system. A link is not followed unless follow is \"true\". At most 200 paths in one call. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "follow": { - "description": "\"true\": also say where the path leads once every link is followed", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "paths": { - "description": "one absolute path, or several separated by line breaks", - "type": "string" - } - }, - "required": [ - "paths" - ], - "type": "object" - }, - "name": "stat", - "replaces": [ - "stat", - "ls -ld", - "namei -l", - "findmnt -T" - ] - }, - { - "description": "What one link points to, as written, and where the path leads once every link is followed, and whether that exists. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "path": { - "description": "an absolute path", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "readlink", - "replaces": [ - "readlink", - "readlink -f", - "realpath" - ] - }, - { - "description": "Every entry of one directory with its type, owner, group, mode, size and, for a link, its target. At most 2000 entries; a longer directory is cut and says so. A place a secret is kept is refused by name. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "path": { - "description": "an absolute path to a directory", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "list", - "replaces": [ - "ls", - "ls -la" - ] - }, - { - "description": "One text file, or a part of it by line: at most 256 KiB in one answer (64 KiB unless more is asked), cut at a line and said so. A binary file is refused; a place a secret is kept, or a known credential file, is refused by name; a line shaped like a secret is withheld and its number said. /proc and /sys are read as far as the holder's account may. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "bytes": { - "description": "the most bytes to answer, up to 262144 (optional, 65536 unless given)", - "type": "string" - }, - "from": { - "description": "the first line to answer, counted from 1 (optional)", - "type": "string" - }, - "lines": { - "description": "how many lines to answer (optional)", - "type": "string" - }, - "path": { - "description": "an absolute path to a file", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "read", - "replaces": [ - "cat", - "head", - "tail", - "less" - ] - }, - { - "description": "Find a fixed string (never a pattern) in one file or under one directory: every matching line with its file and number. Bounded: at most 500 matches (100 unless more is asked), 8 directories deep (4 unless more is asked), 5000 files and 4 MiB per file; links are not followed and binary files are skipped. A place a secret is kept is skipped and named; a matching line shaped like a secret is withheld. The answer says which bound it met. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "case": { - "description": "\"false\": match whatever the case (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "depth": { - "description": "how many directories deep, up to 8 (optional)", - "type": "string" - }, - "matches": { - "description": "the most matches to answer, up to 500 (optional)", - "type": "string" - }, - "path": { - "description": "an absolute path to a file or a directory", - "type": "string" - }, - "text": { - "description": "the fixed string to find", - "type": "string" - } - }, - "required": [ - "text", - "path" - ], - "type": "object" - }, - "name": "search", - "replaces": [ - "grep", - "grep -r", - "grep -F" - ] - }, - { - "description": "One account or group by name or number, as this node's account files say: an account's number, group, home, shell and every group it is in; a group's number and members. With neither, every account and group. Never a password field. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "group": { - "description": "a group's name or number (optional)", - "type": "string" - }, - "name": { - "description": "an account's name or number (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "account", - "replaces": [ - "getent passwd", - "getent group", - "id", - "groups" - ] - }, - { - "description": "Write one text file whole, replacing what it holds; the old content goes to the node's trash. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "content": { - "description": "the text to write", - "type": "string" - }, - "mode": { - "description": "the file's mode, such as 0644 (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - } - }, - "required": [ - "path", - "content", - "reason" - ], - "type": "object" - }, - "name": "write", - "replaces": [ - "tee", - ">" - ] - }, - { - "description": "Copy a file or directory to a path on this node. A target on another node (:/path) is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "from": { - "description": "an absolute path", - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "to": { - "description": "an absolute path on this node", - "type": "string" - } - }, - "required": [ - "from", - "to", - "reason" - ], - "type": "object" - }, - "name": "copy", - "replaces": [ - "cp", - "cp -a", - "ssh cp" - ] - }, - { - "description": "Move or rename a file or directory. A target on another node is refused: copying between nodes comes in step 2 (issue 903). An existing target is overwritten, the old one kept in the node's trash for 7 days. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "from": { - "description": "an absolute path", - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "to": { - "description": "an absolute path", - "type": "string" - } - }, - "required": [ - "from", - "to", - "reason" - ], - "type": "object" - }, - "name": "move", - "replaces": [ - "mv" - ] - }, - { - "description": "Delete a file or directory into the node's trash, where restore can bring it back. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "recursive": { - "description": "\"true\": delete a directory with what it holds", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "required": [ - "path", - "reason" - ], - "type": "object" - }, - "name": "delete", - "replaces": [ - "rm", - "rm -r" - ] - }, - { - "description": "Make a directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "parents": { - "description": "\"true\": make the missing parents too", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - } - }, - "required": [ - "path", - "reason" - ], - "type": "object" - }, - "name": "mkdir", - "replaces": [ - "mkdir", - "mkdir -p" - ] - }, - { - "description": "Change the mode of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "mode": { - "description": "the mode, such as 0644 or u+x", - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "recursive": { - "description": "\"true\": also everything under a directory", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "required": [ - "path", - "mode", - "reason" - ], - "type": "object" - }, - "name": "chmod", - "replaces": [ - "chmod" - ] - }, - { - "description": "Change the owner and group of a file or directory. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "group": { - "description": "a group's name or number (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "owner": { - "description": "an account's name or number", - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "recursive": { - "description": "\"true\": also everything under a directory", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "required": [ - "path", - "owner", - "reason" - ], - "type": "object" - }, - "name": "chown", - "replaces": [ - "chown", - "chgrp" - ] - }, - { - "description": "Send one file of this node into a bucket of the object store, over a short signed link and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, before anything is sent. Read as the node's operator account, through sudo -n where its rights do not reach; a place a secret is kept, or a known credential file, is refused by name.", - "input": { - "properties": { - "bucket": { - "description": "the bucket the file goes into", - "type": "string" - }, - "key": { - "description": "the object's key (optional; the file's SHA-256 when not given)", - "type": "string" - }, - "path": { - "description": "an absolute path to a file", - "type": "string" - } - }, - "required": [ - "path", - "bucket" - ], - "type": "object" - }, - "name": "upload" - }, - { - "description": "Write one object of a bucket to a file of this node, over a short signed link and never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "bucket": { - "description": "the bucket", - "type": "string" - }, - "key": { - "description": "the object's key", - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "type": "string" - }, - "path": { - "description": "an absolute path", - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - }, - "sha256": { - "description": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)", - "type": "string" - } - }, - "required": [ - "bucket", - "key", - "path", - "reason" - ], - "type": "object" - }, - "name": "download" - }, - { - "description": "What the node's trash holds: each deleted or replaced item with its id, original path and when it went.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "trash", - "replaces": [ - "ls ~/.local/share/Trash" - ] - }, - { - "description": "Put an item of the node's trash back where it was; refuses when something is there now. Refuses a path a module owns unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.", - "input": { - "properties": { - "id": { - "description": "an id from trash", - "type": "string" - }, - "override": { - "description": "a reason, to change a path a module owns anyway (optional)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "reason": { - "description": "why the file is changed, for the audit trail", - "type": "string" - } - }, - "required": [ - "id", - "reason" - ], - "type": "object" - }, - "name": "restore", - "replaces": [ - "trash-restore" - ] - } - ] - }, - { - "decision": "novox/hq ADR 0563", - "emits": [ - "asked", - "done", - "refused", - "manifest-reads-changed" - ], - "grain": "whole", - "heldByTheEngine": true, - "name": "node-engine", - "scope": "node", - "serves": [ - { - "description": "Hand a directory this machine uses as found to the mesh (novox/hq ADR 0563 §3, §5): the engine judges the path as it does every hand-over (absolute, as it states it, used as found, reached through no link) and refuses in words, or asks the operator on their channel at the level approve, binding the owner, group and mode it will set. Nothing changes on the call: only the operator's yes hands it over. One ask is open per path; acts shows where it stands.", - "input": { - "properties": { - "path": { - "description": "the directory's absolute path, as the engine states it", - "type": "string" - }, - "why": { - "description": "why it is asked, in words the operator reads", - "type": "string" - } - }, - "required": [ - "path" - ], - "type": "object" - }, - "name": "hand-over", - "output": { - "description": "the answer of an act the operator is asked", - "properties": { - "asked": { - "description": "the ask's id: acts shows where it stands", - "type": "string" - }, - "node": { - "description": "the node", - "type": "string" - }, - "says": { - "description": "what happens next, in words", - "type": "string" - } - }, - "required": [ - "asked", - "node", - "says" - ], - "type": "object" - } - }, - { - "description": "Ask the operator for the take act on a library whose root holds data outside its group (novox/hq ADR 0328 §7, ADR 0563 §3, §4), on the library's home engine: the access as declared and its root as last declared, judged and refused in words or asked at the level approve. Only the operator's yes sets the library's group, group write and setgid under its root. One ask is open per access.", - "input": { - "properties": { - "access": { - "description": "the access, library:", - "type": "string" - }, - "act": { - "description": "the act: take", - "type": "string" - }, - "why": { - "description": "why it is asked, in words the operator reads", - "type": "string" - } - }, - "required": [ - "act", - "access" - ], - "type": "object" - }, - "name": "access-act", - "output": { - "description": "the answer of an act the operator is asked", - "properties": { - "asked": { - "description": "the ask's id: acts shows where it stands", - "type": "string" - }, - "node": { - "description": "the node", - "type": "string" - }, - "says": { - "description": "what happens next, in words", - "type": "string" - } - }, - "required": [ - "asked", - "node", - "says" - ], - "type": "object" - } - }, - { - "description": "Which of this machine's declared directories the engine would refuse to make or keep, one line each (novox/hq ADR 0563 §2). Read-only: changes no owner or mode.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "judge-directories", - "output": { - "description": "the answer of a read-only judgement", - "properties": { - "node": { - "description": "the node", - "type": "string" - }, - "said": { - "description": "the engine's judgement or search, one line per directory or program", - "type": "string" - } - }, - "required": [ - "node", - "said" - ], - "type": "object" - } - }, - { - "description": "Throw away the engine's last search for setuid programs and start a full one (novox/hq ADR 0563 §2); until it completes root-free answers that the machine is not judged yet. Refused while one runs, and when one was started within the hour, naming when the next may start.", - "input": { - "properties": { - "why": { - "description": "why it is asked, in words", - "type": "string" - } - }, - "type": "object" - }, - "name": "setuid-search", - "output": { - "description": "the answer of a read-only judgement", - "properties": { - "node": { - "description": "the node", - "type": "string" - }, - "said": { - "description": "the engine's judgement or search, one line per directory or program", - "type": "string" - } - }, - "required": [ - "node", - "said" - ], - "type": "object" - } - }, - { - "description": "Whether this machine is root-free (novox/hq ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most fifteen minutes old, that the account agents run as cannot become root without a person, and the login shell's execute not served here. Anything else is no pass, and says why. Answered by this machine's engine alone.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "root-free", - "output": { - "properties": { - "machines": { - "description": "this machine's verdict", - "items": { - "description": "the engine's root-free verdict (ADR 0563 §6)", - "properties": { - "free": { - "description": "the machine is root-free", - "type": "boolean" - }, - "judged": { - "description": "when the engine judged it, RFC 3339", - "type": "string" - }, - "machine": { - "description": "the machine", - "type": "string" - }, - "why": { - "description": "why, in words", - "type": "string" - } - }, - "required": [ - "machine", - "free", - "why", - "judged" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "machines" - ], - "type": "object" - } - }, - { - "description": "Every act this machine's engine was asked through this seat (novox/hq ADR 0563 §9): 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": { - "properties": { - "id": { - "description": "one act, by its id", - "type": "string" - }, - "limit": { - "description": "at most this many, newest", - "type": "string" - } - }, - "type": "object" - }, - "name": "acts", - "output": { - "description": "the acts kept", - "properties": { - "acts": { - "description": "the acts, newest first", - "items": { - "description": "one act asked through the seat, as acts reads it (ADR 0563 §9)", - "properties": { - "act": { - "description": "the act: hand-over, access-act, judge-directories or setuid-search", - "type": "string" - }, - "ask": { - "description": "the operator's ask of a hand-over or an access act", - "type": "string" - }, - "at": { - "description": "when it was asked, RFC 3339", - "type": "string" - }, - "auto": { - "description": "the engine asked it itself, on finding a library's root holding data outside its group (ADR 0563 §4)", - "type": "boolean" - }, - "by": { - "description": "who answered, as the operator's warrant names them", - "type": "string" - }, - "caller": { - "description": "the caller as the runtime proved it", - "type": "string" - }, - "disk": { - "description": "what the engine read on disk when it judged", - "type": "string" - }, - "ended": { - "description": "when it ended, RFC 3339", - "type": "string" - }, - "expires": { - "description": "when the ask ends unanswered, RFC 3339", - "type": "string" - }, - "id": { - "description": "the act's id", - "type": "string" - }, - "path": { - "description": "the directory the act changes", - "type": "string" - }, - "proofs": { - "description": "the warrant's proofs", - "items": { - "type": "string" - }, - "type": "array" - }, - "said": { - "description": "what the engine said of it: its outcome or why it was refused", - "type": "string" - }, - "sets": { - "description": "the owner, group and mode it sets, in words", - "type": "string" - }, - "state": { - "description": "where it stands", - "enum": [ - "asked", - "done", - "refused", - "answered" - ], - "type": "string" - }, - "subject": { - "description": "the path of a hand-over, or the access of an access act", - "type": "string" - }, - "via": { - "description": "the channel the warrant came on", - "type": "string" - }, - "why": { - "description": "why it was asked, in words", - "type": "string" - } - }, - "required": [ - "id", - "act", - "caller", - "at", - "state" - ], - "type": "object" - }, - "type": "array" - }, - "node": { - "description": "the node", - "type": "string" - } - }, - "required": [ - "node", - "acts" - ], - "type": "object" - } - }, - { - "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": { - "properties": { - "write": { - "description": "\"true\": write this machine's key again from what the engine composed", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - }, - "name": "packages", - "optional": true, - "output": { - "properties": { - "key": { - "description": "the node's key as the bus holds it; absent when the engine wrote none", - "properties": { - "node": { - "description": "the node", - "type": "string" - }, - "packages": { - "description": "its packages sorted by name, absent after present", - "items": { - "properties": { - "absent": { - "description": "declared absent", - "type": "boolean" - }, - "modules": { - "description": "the modules that declare it", - "items": { - "type": "string" - }, - "type": "array" - }, - "package": { - "description": "the package", - "type": "string" - } - }, - "required": [ - "package", - "modules", - "absent" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "node", - "packages" - ], - "type": "object" - }, - "key_error": { - "description": "why the key could not be read", - "type": "string" - }, - "node": { - "description": "the node", - "type": "string" - }, - "note": { - "description": "a note, such as that no key is written yet", - "type": "string" - }, - "source": { - "description": "where the key is read: the engine's state packages", - "type": "string" - }, - "wrote": { - "description": "with write: the key was written again (false: it already held what the engine composed)", - "type": "boolean" - } - }, - "required": [ - "node", - "source" - ], - "type": "object" - } - }, - { - "description": "One module's composition on this machine beside what runs of it, read-only: the composition its want's build has in the bucket composed- (the latest revision, or the one whose digest is given, from the revisions the bucket keeps), the digest of each kept revision, what runs (applied, its have, or the kept declaration), its want and have (phase running, failed, held or waiting-yes, and the digest that waits for a yes), and the move's judgement with its words: why a record module waits for a yes. With diff, every resource added, removed or changed, a provider's received file naming the consumers that join or leave.", - "input": { - "properties": { - "diff": { - "description": "\"true\": list the resources that differ from what runs", - "type": "string" - }, - "digest": { - "description": "a composition's digest (sha256:…), to read that revision rather than the latest", - "type": "string" - }, - "module": { - "description": "the module, as placed on this machine; node for the node's own", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - }, - "name": "composition", - "optional": true, - "output": { - "properties": { - "attempt": { - "description": "the module's last apply that did not end running; absent when none", - "properties": { - "answered_revision": { - "description": "the want's revision the hold took", - "type": "integer" - }, - "composed": { - "description": "the digest tried", - "type": "string" - }, - "composition_revision": { - "description": "the composition's revision it was tried at", - "type": "integer" - }, - "fp": { - "description": "the fingerprint tried", - "type": "string" - }, - "held": { - "description": "held for a failed backup", - "type": "boolean" - }, - "transient": { - "description": "it failed because the bus did not answer for a moment", - "type": "boolean" - }, - "want_revision": { - "description": "the want's revision it was tried at", - "type": "integer" - } - }, - "required": [ - "fp", - "want_revision", - "composition_revision" - ], - "type": "object" - }, - "composition": { - "description": "one revision of a composition key", - "properties": { - "digest": { - "description": "its digest", - "type": "string" - }, - "key": { - "description": "the composition's key", - "type": "string" - }, - "latest": { - "description": "the latest revision", - "type": "boolean" - }, - "refused": { - "description": "why it cannot be read or trusted", - "type": "string" - }, - "resources": { - "description": "how many resources it names", - "type": "integer" - }, - "revision": { - "description": "its revision", - "type": "integer" - } - }, - "required": [ - "key" - ], - "type": "object" - }, - "diff": { - "description": "every resource that differs from what runs, when asked", - "items": { - "description": "one resource added, removed or changed, as the engine's judge says it", - "type": "object" - }, - "type": "array" - }, - "have": { - "description": "what the module's have key says now; absent when it says nothing", - "properties": { - "composed": { - "description": "digest", - "type": "string" - }, - "detail": { - "description": "detail", - "type": "string" - }, - "fp": { - "description": "fingerprint", - "type": "string" - }, - "judgement": { - "description": "judgement", - "type": "string" - }, - "phase": { - "description": "running, failed, held or waiting-yes", - "type": "string" - }, - "waits": { - "description": "the digest that waits for a yes", - "type": "string" - } - }, - "required": [ - "phase" - ], - "type": "object" - }, - "judged": { - "description": "the move from what runs to the composition read", - "properties": { - "judgement": { - "description": "the judgement", - "type": "string" - }, - "waits_for_yes": { - "description": "the agent would wait for a person's yes before applying it", - "type": "boolean" - }, - "words": { - "description": "its words", - "type": "string" - } - }, - "required": [ - "judgement", - "words", - "waits_for_yes" - ], - "type": "object" - }, - "module": { - "description": "the module", - "type": "string" - }, - "node": { - "description": "the node", - "type": "string" - }, - "own": { - "description": "of the node's own composition, its own fields beside what runs names", - "properties": { - "adoption": { - "description": "the revision's adoption; absent for a converged node", - "type": "object" - }, - "groups": { - "description": "groups", - "type": "integer" - }, - "held": { - "additionalProperties": { - "type": "string" - }, - "description": "each module the node's composer held in its last pass, with why", - "type": "object" - }, - "identities": { - "description": "identities the revision names", - "type": "integer" - }, - "libraries": { - "description": "libraries", - "type": "integer" - }, - "provisioner_account": { - "description": "the account the provisioner's client runs as, as the revision names it", - "type": "string" - }, - "runs_provisioner_account": { - "description": "as what runs names it", - "type": "string" - } - }, - "required": [ - "provisioner_account" - ], - "type": "object" - }, - "revisions": { - "description": "the revisions of its key the bucket keeps, oldest first", - "items": { - "description": "one revision of a composition key", - "properties": { - "digest": { - "description": "its digest", - "type": "string" - }, - "key": { - "description": "the composition's key", - "type": "string" - }, - "latest": { - "description": "the latest revision", - "type": "boolean" - }, - "refused": { - "description": "why it cannot be read or trusted", - "type": "string" - }, - "resources": { - "description": "how many resources it names", - "type": "integer" - }, - "revision": { - "description": "its revision", - "type": "integer" - } - }, - "required": [ - "key" - ], - "type": "object" - }, - "type": "array" - }, - "runs": { - "description": "what runs of the module; absent when nothing runs", - "properties": { - "composed": { - "description": "digest", - "type": "string" - }, - "fp": { - "description": "fingerprint", - "type": "string" - }, - "from": { - "description": "applied, have or kept", - "type": "string" - }, - "phase": { - "description": "phase", - "type": "string" - }, - "resources": { - "description": "how many resources run", - "type": "integer" - } - }, - "required": [ - "from", - "resources" - ], - "type": "object" - }, - "started": { - "description": "the agent has looked at the module once since it started", - "type": "boolean" - }, - "want": { - "description": "the module's want as the agent holds it; absent when it holds none", - "properties": { - "backup": { - "description": "the operator's answer to a failed backup, again or skip", - "type": "string" - }, - "composed": { - "description": "the composition's digest it names", - "type": "string" - }, - "fp": { - "description": "its build's fingerprint", - "type": "string" - }, - "removed": { - "description": "the module is removed", - "type": "boolean" - }, - "revision": { - "description": "the revision of the want's key read last", - "type": "integer" - }, - "self": { - "description": "the node composes the module itself (ADR 0547 §2)", - "type": "boolean" - }, - "words": { - "description": "its words", - "type": "string" - }, - "yes": { - "description": "the digest a person said yes to", - "type": "string" - } - }, - "required": [ - "fp", - "revision" - ], - "type": "object" - } - }, - "required": [ - "node", - "module", - "started", - "composition", - "judged" - ], - "type": "object" - } - }, - { - "description": "Compose this machine's modules now and say how it went, so a composition that failed, or a module held, recovers without a restart: what the pass refused or held, each with why, and when the engine composes again on its own after a seat that did not answer (a deadline, no responders: retried with a growing wait; a refusal never). With module, that module's apply is also tried again. Changes only what a pass the engine makes on its own would change.", - "input": { - "properties": { - "module": { - "description": "a module placed on this machine whose apply is also tried again (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "compose", - "optional": true, - "output": { - "properties": { - "pass": { - "description": "how the pass went", - "properties": { - "again": { - "description": "when the node composes again on its own, RFC 3339", - "type": "string" - }, - "at": { - "description": "when the pass ended, RFC 3339", - "type": "string" - }, - "earlier": { - "description": "the answer is the last pass, made moments ago", - "type": "boolean" - }, - "handed": { - "description": "how many compositions the pass handed the agent that differ from before", - "type": "integer" - }, - "held": { - "additionalProperties": { - "type": "string" - }, - "description": "each module refused alone, with why", - "type": "object" - }, - "refused": { - "description": "why the node composed none of its modules", - "type": "string" - } - }, - "required": [ - "at", - "handed" - ], - "type": "object" - }, - "retried": { - "description": "the module whose apply was tried again", - "type": "string" - } - }, - "required": [ - "pass" - ], - "type": "object" - } - }, - { - "description": "Compose a check's candidate on this machine in shadow, with its own sealed values and the indexer's manifest verdicts, and say per module placed here whether it composes (with its digest and whether the candidate changes it), is refused by the candidate or is held without it, each with why, and the verdict of the machine's own composition (novox/hq ADR 0593 §7). Read-only: applies nothing and writes nothing. Answered by this machine's engine alone, within the asker's bound.", - "input": { - "properties": { - "candidate": { - "description": "the key of the indexer's candidate, .", - "type": "string" - }, - "input": { - "description": "also answer the composition input the candidate was composed from, for the sandbox path", - "type": "boolean" - } - }, - "required": [ - "candidate" - ], - "type": "object" - }, - "name": "compose-candidate", - "optional": true, - "output": { - "properties": { - "candidate": { - "description": "the candidate's key", - "type": "string" - }, - "input": { - "description": "when asked: the composition input it composed from", - "type": "object" - }, - "modules": { - "description": "every module placed on the node, sorted by name", - "items": { - "description": "one module's composition with the candidate", - "properties": { - "changed": { - "description": "the candidate changes it from the one composed without it", - "type": "boolean" - }, - "digest": { - "description": "the composition's digest when composed", - "type": "string" - }, - "fp": { - "description": "the fingerprint it was composed at", - "type": "string" - }, - "module": { - "description": "the module, node for the node's own composition", - "type": "string" - }, - "state": { - "description": "composed (digest set), refused by the candidate, or held without it too (words set)", - "enum": [ - "composed", - "refused", - "held" - ], - "type": "string" - }, - "words": { - "description": "why, for a refused or held one", - "type": "string" - } - }, - "required": [ - "module", - "state" - ], - "type": "object" - }, - "type": "array" - }, - "node": { - "description": "the node", - "type": "string" - }, - "own": { - "description": "one module's composition with the candidate", - "properties": { - "changed": { - "description": "the candidate changes it from the one composed without it", - "type": "boolean" - }, - "digest": { - "description": "the composition's digest when composed", - "type": "string" - }, - "fp": { - "description": "the fingerprint it was composed at", - "type": "string" - }, - "module": { - "description": "the module, node for the node's own composition", - "type": "string" - }, - "state": { - "description": "composed (digest set), refused by the candidate, or held without it too (words set)", - "enum": [ - "composed", - "refused", - "held" - ], - "type": "string" - }, - "words": { - "description": "why, for a refused or held one", - "type": "string" - } - }, - "required": [ - "module", - "state" - ], - "type": "object" - }, - "refused": { - "description": "why the node could not compose with the candidate at all", - "type": "string" - }, - "took": { - "description": "how long the engine took, in milliseconds", - "type": "integer" - }, - "v": { - "description": "the layout's version", - "type": "integer" - }, - "verdict": { - "description": "the node's verdict (ADR 0593 §7, §9)", - "enum": [ - "composed", - "refused", - "unready", - "not-judged" - ], - "type": "string" - } - }, - "required": [ - "v", - "node", - "candidate", - "verdict", - "modules", - "own", - "took" - ], - "type": "object" - } - }, - { - "description": "The reader capabilities this machine's engine states (manifest words, forms and seat names it reads, novox/hq ADR 0618 §3) and when it last stated them. An engine built before it said them states none, which a reader takes as the baseline. Read-only. Answered by this machine's engine alone.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "manifest-reads", - "optional": true, - "output": { - "description": "what this machine's engine reads of a manifest (ADR 0618 §3)", - "properties": { - "manifest-reads": { - "description": "the reader capabilities its engine states: the manifest words, forms and seat names it reads; empty for an engine built before it said them, read as the baseline", - "items": { - "minLength": 1, - "type": "string" - }, - "type": "array" - }, - "node": { - "description": "the node", - "type": "string" - }, - "stated-at": { - "description": "when the engine last stated them, RFC 3339", - "type": "string" - }, - "v": { - "description": "the layout's version", - "type": "integer" - } - }, - "required": [ - "v", - "node", - "stated-at" - ], - "type": "object" - } - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-login-manager", - "scope": "node", - "serves": [ - { - "description": "The sessions the login manager offers on this machine, and which one the operator account starts by default.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "sessions" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-display-server", - "scope": "node", - "serves": [ - { - "description": "The monitors connected now, each with its identity, its modes and where it is placed; and the layout profile in force, if one matches.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "displays" - }, - { - "description": "The monitor layout profiles, keyed by the connected monitors' identities: list them, save the current arrangement under a name, or apply one.", - "input": { - "properties": { - "action": { - "description": "list, save or apply", - "enum": [ - "list", - "save", - "apply" - ], - "type": "string" - }, - "name": { - "description": "the profile to save or apply (save and apply only)", - "type": "string" - } - }, - "required": [ - "action" - ], - "type": "object" - }, - "name": "layout" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-display-session", - "scope": "node", - "serves": [ - { - "description": "Reload the session's configuration in place, keeping its windows.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "reload" - }, - { - "description": "The session's workspaces: each one's name, output, and whether it is visible or focused.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "workspaces" - }, - { - "description": "The session's windows: each one's title, class, workspace and whether it has focus; narrowed to one workspace when named.", - "input": { - "properties": { - "workspace": { - "description": "one workspace (optional)", - "type": "string" - } - }, - "type": "object" - }, - "name": "windows" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-terminal-emulator", - "scope": "node", - "serves": [ - { - "description": "Open a terminal window in the operator's session, running a command or the login shell, in a directory or the account's home.", - "input": { - "properties": { - "command": { - "description": "what to run in it (optional; the login shell when absent)", - "type": "string" - }, - "directory": { - "description": "where it starts (optional; the account's home when absent)", - "type": "string" - } - }, - "type": "object" - }, - "name": "open" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-launcher", - "scope": "node", - "serves": [ - { - "description": "Put a menu of choices in front of the operator and answer with the one chosen, or nothing when the menu was dismissed — the dmenu-compatible contract.", - "input": { - "properties": { - "choices": { - "description": "the lines to choose between, in order", - "items": { - "type": "string" - }, - "type": "array" - }, - "prompt": { - "description": "what the menu asks (optional)", - "type": "string" - } - }, - "required": [ - "choices" - ], - "type": "object" - }, - "name": "menu" - }, - { - "description": "Ask the operator for a value in a prompt that does not show what is typed, and answer it sealed to the key the asker gives — never in the clear — or cancelled when the prompt was dismissed or not answered in time.", - "input": { - "properties": { - "module": { - "description": "the module whose own secret is asked for", - "type": "string" - }, - "node": { - "description": "the machine the module runs on", - "type": "string" - }, - "seal_to": { - "description": "the asker's public sealing key: the answer is sealed to it", - "type": "string" - }, - "secret": { - "description": "the own secret's name", - "type": "string" - }, - "timeout_seconds": { - "description": "give up after this long (optional)", - "type": "string" - } - }, - "required": [ - "module", - "secret", - "node", - "seal_to" - ], - "type": "object" - }, - "name": "secret", - "optional": true - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-notifier", - "scope": "node", - "serves": [ - { - "description": "Show the operator a notification.", - "input": { - "properties": { - "body": { - "description": "its text (optional)", - "type": "string" - }, - "title": { - "description": "the notification's summary", - "type": "string" - }, - "urgency": { - "description": "low, normal (the default) or critical", - "enum": [ - "low", - "normal", - "critical" - ], - "type": "string" - } - }, - "required": [ - "title" - ], - "type": "object" - }, - "name": "send" - }, - { - "description": "The notifications shown lately, newest first.", - "input": { - "properties": { - "limit": { - "description": "how many (optional, default 20)", - "type": "string" - } - }, - "type": "object" - }, - "name": "history" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-lock-screen", - "scope": "node", - "serves": [ - { - "description": "Lock the operator's session now.", - "input": { - "properties": {}, - "type": "object" - }, - "name": "lock" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-clipboard", - "scope": "node", - "serves": [ - { - "description": "What the clipboard held lately, newest first.", - "input": { - "properties": { - "limit": { - "description": "how many (optional, default 20)", - "type": "string" - } - }, - "type": "object" - }, - "name": "history" - }, - { - "description": "Put text on the operator's clipboard.", - "input": { - "properties": { - "text": { - "description": "the text", - "type": "string" - } - }, - "required": [ - "text" - ], - "type": "object" - }, - "name": "copy" - } - ] - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-bar", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-compositor", - "scope": "node" - }, - { - "decision": "novox/hq ADR 0208", - "grain": "seat-list", - "name": "node-secret-service", - "scope": "node" - } -]; -- 2.54.0 From f7cc1ba92043fa1fa0c1fcd3b38a14a7e22b57d5 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 13:24:11 +0100 Subject: [PATCH 47/94] Connectivity seat-list files join their folder; root module.json drops the seat-set and operator-channel packages (ADR 0634, ADR 0636; issue 1966) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../seats}/mesh-dns-resolver/seat.json | 0 .../seats}/node-hostname/seat.json | 0 .../node-intrusion-prevention/seat.json | 0 .../seats}/node-uplink/seat.json | 0 .../go/clients/meshconditionsclient/go.mod | 2 +- .../go/clients/meshhandactsclient/go.mod | 2 +- module.json | 253 ++++++++++++++++-- seats/index.json | 4 - 8 files changed, 227 insertions(+), 34 deletions(-) rename {seats => connectivity/seats}/mesh-dns-resolver/seat.json (100%) rename {seats => connectivity/seats}/node-hostname/seat.json (100%) rename {seats => connectivity/seats}/node-intrusion-prevention/seat.json (100%) rename {seats => connectivity/seats}/node-uplink/seat.json (100%) diff --git a/seats/mesh-dns-resolver/seat.json b/connectivity/seats/mesh-dns-resolver/seat.json similarity index 100% rename from seats/mesh-dns-resolver/seat.json rename to connectivity/seats/mesh-dns-resolver/seat.json diff --git a/seats/node-hostname/seat.json b/connectivity/seats/node-hostname/seat.json similarity index 100% rename from seats/node-hostname/seat.json rename to connectivity/seats/node-hostname/seat.json diff --git a/seats/node-intrusion-prevention/seat.json b/connectivity/seats/node-intrusion-prevention/seat.json similarity index 100% rename from seats/node-intrusion-prevention/seat.json rename to connectivity/seats/node-intrusion-prevention/seat.json diff --git a/seats/node-uplink/seat.json b/connectivity/seats/node-uplink/seat.json similarity index 100% rename from seats/node-uplink/seat.json rename to connectivity/seats/node-uplink/seat.json diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index 8ee6c8e..c29f214 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondit go 1.26.0 -require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0 +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod index 575a706..75a57a4 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.mod +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandac go 1.26.0 -require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0 +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-20261105000000-000000000000 diff --git a/module.json b/module.json index 849e5c4..c850c06 100644 --- a/module.json +++ b/module.json @@ -4,34 +4,231 @@ "slug": "contracts", "build": { "artifacts": [ - { "name": "go", "kind": "package", "language": "go", "paths": ["go"], "series": "0.1", "generated-from": "." }, - { "name": "go-containerimageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/containerimageregistryclient"], "series": "0.1" }, - { "name": "go-gopackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/gopackageregistryclient"], "series": "0.1" }, - { "name": "go-nodeengineclient", "kind": "package", "language": "go", "paths": ["go/clients/nodeengineclient"], "series": "0.1" }, - { "name": "go-npmpackageregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/npmpackageregistryclient"], "series": "0.1" }, - { "name": "go-ociartifactregistryclient", "kind": "package", "language": "go", "paths": ["go/clients/ociartifactregistryclient"], "series": "0.1" }, - { "name": "go-operatorchannelclient", "kind": "package", "language": "go", "paths": ["go/clients/operatorchannelclient"], "series": "0.1" }, - { "name": "go-containerimageregistry", "kind": "package", "language": "go", "paths": ["go/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "go-gopackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "go-meshbroker", "kind": "package", "language": "go", "paths": ["go/seats/meshbroker"], "series": "0.1", "generated-from": "." }, - { "name": "go-meshconfig", "kind": "package", "language": "go", "paths": ["go/seats/meshconfig"], "series": "0.1", "generated-from": "." }, - { "name": "go-meshdeliverychecker", "kind": "package", "language": "go", "paths": ["go/seats/meshdeliverychecker"], "series": "0.1", "generated-from": "." }, - { "name": "go-meshvault", "kind": "package", "language": "go", "paths": ["go/seats/meshvault"], "series": "0.1", "generated-from": "." }, - { "name": "go-nodeengine", "kind": "package", "language": "go", "paths": ["go/seats/nodeengine"], "series": "0.1", "generated-from": "." }, - { "name": "go-npmpackageregistry", "kind": "package", "language": "go", "paths": ["go/seats/npmpackageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "go-ociartifactregistry", "kind": "package", "language": "go", "paths": ["go/seats/ociartifactregistry"], "series": "0.1", "generated-from": "." }, - { "name": "go-operatorchannel", "kind": "package", "language": "go", "paths": ["go/seats/operatorchannel"], "series": "0.1", "generated-from": "." }, - { "name": "lib", "kind": "package", "language": "typescript", "paths": ["ts"], "series": "0.1", "generated-from": "." }, - { "name": "ts-containerimageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/containerimageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "ts-gopackageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/gopackageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "ts-meshbroker", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshbroker"], "series": "0.1", "generated-from": "." }, - { "name": "ts-meshconfig", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshconfig"], "series": "0.1", "generated-from": "." }, - { "name": "ts-meshdeliverychecker", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshdeliverychecker"], "series": "0.1", "generated-from": "." }, - { "name": "ts-meshvault", "kind": "package", "language": "typescript", "paths": ["ts/seats/meshvault"], "series": "0.1", "generated-from": "." }, - { "name": "ts-nodeengine", "kind": "package", "language": "typescript", "paths": ["ts/seats/nodeengine"], "series": "0.1", "generated-from": "." }, - { "name": "ts-npmpackageregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/npmpackageregistry"], "series": "0.1", "generated-from": "." }, - { "name": "ts-ociartifactregistry", "kind": "package", "language": "typescript", "paths": ["ts/seats/ociartifactregistry"], "series": "0.1", "generated-from": "." }, - { "name": "ts-operatorchannel", "kind": "package", "language": "typescript", "paths": ["ts/seats/operatorchannel"], "series": "0.1", "generated-from": "." } + { + "name": "go-containerimageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/containerimageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-gopackageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/gopackageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-nodeengineclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/nodeengineclient" + ], + "series": "0.1" + }, + { + "name": "go-npmpackageregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/npmpackageregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-ociartifactregistryclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/ociartifactregistryclient" + ], + "series": "0.1" + }, + { + "name": "go-containerimageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/containerimageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-gopackageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/gopackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshbroker", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshbroker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshconfig", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshconfig" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshdeliverychecker", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshdeliverychecker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshvault", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-nodeengine", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/nodeengine" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-npmpackageregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/npmpackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-ociartifactregistry", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/ociartifactregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-containerimageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/containerimageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-gopackageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/gopackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshbroker", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshbroker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshconfig", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshconfig" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshdeliverychecker", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshdeliverychecker" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshvault", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-nodeengine", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/nodeengine" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-npmpackageregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/npmpackageregistry" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-ociartifactregistry", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/ociartifactregistry" + ], + "series": "0.1", + "generated-from": "." + } ] }, "resources": [] diff --git a/seats/index.json b/seats/index.json index d027d6f..eed4efb 100644 --- a/seats/index.json +++ b/seats/index.json @@ -21,9 +21,6 @@ "node-build-go", "node-build-node", "node-build-image", - "mesh-dns-resolver", - "node-hostname", - "node-intrusion-prevention", "node-service-manager", "node-package-manager", "node-container-runtime", @@ -33,7 +30,6 @@ "node-hotkeys", "node-message-bus", "node-backup", - "node-uplink", "node-reads", "node-files", "node-engine", -- 2.54.0 From a3106a4b642b7e2f9e8c4fd37364b1e82fd31418 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:09:06 +0100 Subject: [PATCH 48/94] Provisioner client: the key and plan layouts, the provider and its harness, moved from mesh-sdk; the provider's 31 tests are 6 tables, the plan stays typed in the client (ADR 0634, ADR 0635; issue 1966) --- .../clients/meshprovisionerclient/client.go | 310 ++++++ .../meshprovisionerclient/client_test.go | 121 +++ .../go/clients/meshprovisionerclient/go.mod | 5 + .../provider/consumers.go | 72 ++ .../provider/harness/ctx_test.go | 5 + .../provider/harness/early_test.go | 22 + .../provider/harness/grants_moved_test.go | 124 +++ .../provider/harness/harness.go | 572 +++++++++++ .../provider/harness/harness_test.go | 154 +++ .../provider/harness/moved.go | 312 ++++++ .../provider/harness/moved_test.go | 464 +++++++++ .../provider/harness/retirement.go | 626 ++++++++++++ .../provider/harness/retirement_test.go | 575 +++++++++++ .../provider/harness/standing_test.go | 193 ++++ .../provider/harness/tables_test.go | 114 +++ .../provider/provider.go | 858 +++++++++++++++++ .../provider/provider_test.go | 294 ++++++ .../provider/statebus_test.go | 54 ++ .../provider/tables_test.go | 909 ++++++++++++++++++ 19 files changed, 5784 insertions(+) create mode 100644 provisioning/go/clients/meshprovisionerclient/client.go create mode 100644 provisioning/go/clients/meshprovisionerclient/client_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/go.mod create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/consumers.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/ctx_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/early_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/grants_moved_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/harness.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/harness_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/retirement.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/retirement_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/standing_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/provider.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/provider_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/statebus_test.go create mode 100644 provisioning/go/clients/meshprovisionerclient/provider/tables_test.go diff --git a/provisioning/go/clients/meshprovisionerclient/client.go b/provisioning/go/clients/meshprovisionerclient/client.go new file mode 100644 index 0000000..32a0ed6 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/client.go @@ -0,0 +1,310 @@ +// Package meshprovisionerclient is the contract of the seat mesh-provisioner's records (novox/hq ADR 0570 clauses 1, 2 and 5; issue +// 1954): every provision one key of the state `provisions`, holding a reference and never a value, and every handed +// module's provisioning plan one key of the state `plans`. The module mesh-provisioner alone writes both; the vault, +// each node's provisioner client and every provider read them through `reads`. +package meshprovisionerclient + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "sort" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// The seat, its holder and its two states, and the names a reader declares in its `reads`. +const ( + Seat = "mesh-provisioner" + Module = "mesh-provisioner" + ProvisionsState = "provisions" + PlansState = "plans" + ProvisionsRead = Module + "." + ProvisionsState + PlansRead = Module + "." + PlansState + // ProvisionsBucket and PlansBucket are the states' buckets. + ProvisionsBucket = Module + "_" + ProvisionsState + PlansBucket = Module + "_" + PlansState +) + +// The states of a provision (clause 1). +const ( + // Asked is a provision its consumer needs that its provider has not answered yet, or whose dependencies are not + // all provided (clause 3). + Asked = "asked" + // Provided is a provision the provider said it made: set only from the provider's outcome (clause 3). + Provided = "provided" + // Waits is a provision only a person can give, naming what is awaited (clause 7). + Waits = "waits" + // Failed is a provision the provider could not make, with why. + Failed = "failed" + // Retiring is a provision no want needs, asked of the operator before its credential goes (clause 5). + Retiring = "retiring" +) + +// States is every state a provision may be in. +var States = []string{Asked, Provided, Waits, Failed, Retiring} + +// Version is the version of the records' layout. +const Version = 1 + +// token is one part of a key: what a bucket's key takes, no dot. +var token = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +// Key is a provision's key: `..`, and `.` where the consumer keeps several values of +// one provision (ADR 0094). The same key the provider's own entry for that consumer has (provider.Entry.Key). +func Key(node, consumer, provision, local string) (string, error) { + parts := []string{node, consumer, provision} + if local != "" { + parts = append(parts, local) + } + for _, p := range parts { + if !token.MatchString(p) { + return "", fmt.Errorf("provisions: %q cannot be part of a provision's key", p) + } + } + return strings.Join(parts, "."), nil +} + +// ParseKey is the inverse of Key. +func ParseKey(key string) (node, consumer, provision, local string, err error) { + parts := strings.Split(key, ".") + if len(parts) < 3 || len(parts) > 4 { + return "", "", "", "", fmt.Errorf("provisions: %q is not ..[.]", key) + } + for _, p := range parts { + if !token.MatchString(p) { + return "", "", "", "", fmt.Errorf("provisions: %q is not ..[.]", key) + } + } + if len(parts) == 4 { + local = parts[3] + } + return parts[0], parts[1], parts[2], local, nil +} + +// PlanKey is a plan's key in `plans`: `.` (clause 2). +func PlanKey(node, module string) (string, error) { + if !token.MatchString(node) || !token.MatchString(module) { + return "", fmt.Errorf("provisions: %s.%s is not a plan's key, .", node, module) + } + return node + "." + module, nil +} + +// The forms of a provision's credential, as a plan's entry names them (catalogue.PlanProvision.CredentialForm): the +// catalogue's one list (catalogue.CredentialForms, issue 774), under the names this package has used. +const ( + FormPair = catalogue.CredentialFormPair + FormShared = catalogue.CredentialFormShared + FormIssued = catalogue.CredentialFormIssued + FormGiven = catalogue.CredentialFormGiven + // FormNone is an offer whose consumers present no credential (catalogue.CredentialFormNone): the vault mints nothing, + // and the provision depends on no credential. + FormNone = catalogue.CredentialFormNone +) + +// VaultName is the vault's name of a provision's credential in form, as its verb `own` names it and the provider asks +// it: `pair////[/]` for a login of the consumer's own (ADR 0158 +// §11), `provision//` for the provider's one shared value, and `provision////` for a holder's own issued or given value. Empty for a form with no +// credential. +func VaultName(form string, p Provision) string { + switch form { + case FormPair: + n := "pair/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module + if p.Local != "" { + n += "/" + p.Local + } + return n + case FormShared: + return "provision/" + p.Provider.Node + "/" + p.Provision + case FormIssued, FormGiven: + return "provision/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module + } + return "" +} + +// VaultModule is the provider of every provision's credential (novox/hq ADR 0570 clauses 1, 6). +const VaultModule = "mesh-vault" + +// CredentialKey is the key of a provision's credential, itself a provision whose provider is the vault (clause 6; the +// key form the lead settled on 2026-10-29): `..secret-[.]` for a value of the +// consumer's own (pair, issued, given), and `..secret-` for the provider's one +// shared value. False for a form with no vault value, and for a provision the vault itself provides. +func CredentialKey(form string, consumer, provider End, provision, local string) (string, bool) { + if provider.Module == VaultModule { + return "", false // the vault's own offer is the value: it has no credential beside it (the lead, 2026-10-29) + } + name := catalogue.CredentialProvisionPrefix + provision + switch form { + case FormPair, FormIssued, FormGiven: + k, err := Key(consumer.Node, consumer.Module, name, local) + return k, err == nil + case FormShared: + k, err := Key(provider.Node, provider.Module, name, "") + return k, err == nil + } + return "", false +} + +// DependsOn is what a provision waits for before its provider makes it (clause 3): its credential's key, where its form +// has a vault value, and the key of every provision its provider module itself requires on its own node, read from +// the provider's plan there (nil when the provider is not handed and has none). Sorted, none twice. +func DependsOn(consumer End, entry catalogue.PlanProvision, providerPlan *catalogue.Plan) []string { + seen := map[string]bool{} + var out []string + add := func(k string) { + if k != "" && !seen[k] { + seen[k] = true + out = append(out, k) + } + } + provider := End{Module: entry.ProviderModule, Node: entry.Provider} + if k, ok := CredentialKey(entry.CredentialForm, consumer, provider, entry.Provision, entry.Local); ok { + add(k) + } + if providerPlan != nil { + for _, own := range providerPlan.Provisions { + if k, err := Key(provider.Node, provider.Module, own.Provision, own.Local); err == nil { + add(k) + } + } + } + sort.Strings(out) + return out +} + +// End is one end of a provision: a module on a node. +type End struct { + Module string `json:"module"` + Node string `json:"node"` +} + +// Reference is a value the vault keeps, by its id and the generation of the value: never the value, sealed or not +// (ADR 0349 §7). The holder writes the generation the vault says in its own entry (clause 6). ID is the vault's name of +// the credential exactly as its verb `own` names it (VaultName): the consuming node's engine checks both the name and +// the generation of what it is handed. +type Reference struct { + ID string `json:"id"` + Generation int `json:"generation"` +} + +// Warrant is the operator's answer to a retiring provision's ask, as the holder writes it into the key (clause 5). +type Warrant struct { + Ask string `json:"ask"` + Option string `json:"option"` + By string `json:"by"` + At time.Time `json:"at"` +} + +// Provision is one key of `provisions`. +type Provision struct { + V int `json:"v"` + Provision string `json:"provision"` + Local string `json:"local,omitempty"` + Consumer End `json:"consumer"` + Provider End `json:"provider"` + // DependsOn are the keys of the provisions this one waits for (clause 3): a database user whose password is the + // vault's. + DependsOn []string `json:"depends-on,omitempty"` + State string `json:"state"` + // Why is the reason for waits, failed and retiring, and for asked when a dependency holds it. + Why string `json:"why,omitempty"` + Reference *Reference `json:"reference,omitempty"` + // Ask is a retiring provision's ask of the operator; Warrant the answer, once given. + Ask string `json:"ask,omitempty"` + Warrant *Warrant `json:"warrant,omitempty"` + // Plan is the plan entry the key was composed from, at the consumer's wanted build or its newest (issue 1126): the + // provider binds the consumer from it, so a requirement the newest build adds is provided before the want that + // waits for it moves, though the plan the seat keeps at the wanted build names it not yet. + Plan *catalogue.PlanProvision `json:"plan,omitempty"` + At time.Time `json:"at"` +} + +// Key is the provision's key. +func (p Provision) Key() (string, error) { + return Key(p.Consumer.Node, p.Consumer.Module, p.Provision, p.Local) +} + +func (p Provision) check() error { + if _, err := p.Key(); err != nil { + return err + } + if p.Provider.Module == "" || p.Provider.Node == "" { + return fmt.Errorf("provisions: %s names no provider", p.Provision) + } + known := false + for _, s := range States { + known = known || s == p.State + } + switch { + case !known: + return fmt.Errorf("provisions: a provision's state is one of %s, not %q", strings.Join(States, ", "), p.State) + case (p.State == Waits || p.State == Failed || p.State == Retiring) && strings.TrimSpace(p.Why) == "": + return fmt.Errorf("provisions: a %s provision says why", p.State) + case p.Warrant != nil && p.State != Retiring: + return fmt.Errorf("provisions: only a retiring provision carries the operator's warrant") + } + for _, d := range p.DependsOn { + if _, _, _, _, err := ParseKey(d); err != nil { + return err + } + } + return nil +} + +// Encode is p as JSON at Version; it refuses what a holder may not write. +func Encode(p Provision) ([]byte, error) { + p.V = Version + if err := p.check(); err != nil { + return nil, err + } + return json.Marshal(p) +} + +// Decode reads a key's value. **A field the layout does not name is read past and dropped** (issue 1126 review 1): a +// reader on an older mesh-sdk must not refuse a key a newer writer adds a field to. No value rides in a provision all +// the same (clause 1, ADR 0201 §13): Encode writes the layout alone, so what a reader does not know is never kept. +func Decode(data []byte) (Provision, error) { + var p Provision + dec := json.NewDecoder(bytes.NewReader(data)) + if err := dec.Decode(&p); err != nil { + return Provision{}, fmt.Errorf("provisions: a provision is not readable: %w", err) + } + if p.V != Version { + return Provision{}, fmt.Errorf("provisions: a provision of version %d, and this reads %d", p.V, Version) + } + return p, p.check() +} + +// Plan is one key of `plans`: a module's provisioning plan on a node, never a value (clause 2). +type Plan struct { + V int `json:"v"` + Node string `json:"node"` + Module string `json:"module"` + Plan catalogue.Plan `json:"plan"` +} + +// EncodePlan is p as JSON at Version. +func EncodePlan(p Plan) ([]byte, error) { + p.V = Version + if _, err := PlanKey(p.Node, p.Module); err != nil { + return nil, err + } + return json.Marshal(p) +} + +// DecodePlan reads a plan's key. +func DecodePlan(data []byte) (Plan, error) { + var p Plan + if err := json.Unmarshal(data, &p); err != nil { + return Plan{}, fmt.Errorf("provisions: a plan is not readable: %w", err) + } + if p.V != Version { + return Plan{}, fmt.Errorf("provisions: a plan of version %d, and this reads %d", p.V, Version) + } + _, err := PlanKey(p.Node, p.Module) + return p, err +} diff --git a/provisioning/go/clients/meshprovisionerclient/client_test.go b/provisioning/go/clients/meshprovisionerclient/client_test.go new file mode 100644 index 0000000..8461091 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/client_test.go @@ -0,0 +1,121 @@ +package meshprovisionerclient + +import ( + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// ADR 0570 clauses 1, 2 and 5 (issue 1954 L6-P2): a provision's key is its consumer's node and module, the provision and +// its local name, the provider's entry's key; its value round-trips, names its state and why, and refuses any field the +// layout does not name, so no value rides in it; a plan's key is its node and module. +func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { + at := time.Date(2026, 10, 29, 22, 0, 0, 0, time.UTC) + db := Provision{Provision: "postgres-database", Consumer: End{Module: "keycloak", Node: "novox"}, + Provider: End{Module: "postgres", Node: "novox"}, DependsOn: []string{"novox.keycloak.secret.db"}, State: Asked, + Reference: &Reference{ID: "provision/postgres/postgres-database", Generation: 2}, At: at} + for _, c := range []struct { + name string + edit func(*Provision) + key string + refused string + }{ + {name: "asked, depending on the vault's", key: "novox.keycloak.postgres-database"}, + {name: "under a local name", edit: func(p *Provision) { p.Local = "replica" }, key: "novox.keycloak.postgres-database.replica"}, + {name: "retiring with the operator's warrant", edit: func(p *Provision) { + p.State, p.Why, p.Ask = Retiring, "keycloak left novox", "a-1" + p.Warrant = &Warrant{Ask: "a-1", Option: "yes", By: "the operator", At: at} + }, key: "novox.keycloak.postgres-database"}, + {name: "an unknown state", edit: func(p *Provision) { p.State = "done" }, refused: "one of"}, + {name: "failed without why", edit: func(p *Provision) { p.State = Failed }, refused: "says why"}, + {name: "a warrant on a provided one", edit: func(p *Provision) { p.State, p.Warrant = Provided, &Warrant{Ask: "a"} }, refused: "only a retiring"}, + {name: "no provider", edit: func(p *Provision) { p.Provider = End{} }, refused: "no provider"}, + {name: "a dotted consumer", edit: func(p *Provision) { p.Consumer.Module = "a.b" }, refused: "cannot be part"}, + {name: "a dependency that is no key", edit: func(p *Provision) { p.DependsOn = []string{"x"} }, refused: "is not"}, + } { + p := db + if c.edit != nil { + c.edit(&p) + } + raw, err := Encode(p) + if c.refused != "" { + if err == nil || !strings.Contains(err.Error(), c.refused) { + t.Errorf("%s: %v, want refused saying %q", c.name, err, c.refused) + } + continue + } + back, err := Decode(raw) + key, kerr := back.Key() + if err != nil || kerr != nil || key != c.key || back.V != Version || back.Reference.Generation != 2 { + t.Errorf("%s: read back as %+v under %q, %v %v", c.name, back, key, err, kerr) + } + if node, consumer, provision, local, err := ParseKey(key); err != nil || node != "novox" || consumer != "keycloak" || + provision != "postgres-database" || local != p.Local { + t.Errorf("%s: %s parses as %s %s %s %s, %v", c.name, key, node, consumer, provision, local, err) + } + } + // A field a later layout adds is read past, never kept (issue 1126 review 1: a reader on an older mesh-sdk must not + // refuse a key a newer writer adds a field to). A value still never rides in a provision: Encode writes the layout + // alone, so what a reader does not know is dropped, and encoding it again carries none of it. + later, err := Decode([]byte(`{"v":1,"provision":"p","consumer":{"module":"c","node":"n"},"provider":{"module":"p","node":"n"},` + + `"state":"provided","at":"2026-10-29T22:00:00Z","a-later-field":{"x":1},"password":"hunter2"}`)) + if err != nil || later.Provision != "p" || later.State != Provided { + t.Errorf("a provision with a field of a later layout: %+v, %v", later, err) + } + if again, _ := Encode(later); strings.Contains(string(again), "hunter2") || strings.Contains(string(again), "a-later-field") { + t.Errorf("a field the layout does not name was kept: %s", again) + } + plan := Plan{Node: "novox", Module: "restic", Plan: catalogue.Plan{}} + raw, err := EncodePlan(plan) + if err != nil { + t.Fatal(err) + } + if back, err := DecodePlan(raw); err != nil || back.Node != "novox" || back.Module != "restic" { + t.Errorf("a plan read back as %+v, %v", back, err) + } + if k, err := PlanKey("novox", "restic"); k != "novox.restic" || err != nil { + t.Errorf("a plan's key is %q, %v", k, err) + } + // A reference's id is the vault's name as its verb own names it, in each form (the host agent, 2026-10-29). + for form, want := range map[string]string{FormPair: "pair/novox/postgres-database/novox/keycloak", FormShared: "provision/novox/postgres-database", + FormIssued: "provision/novox/postgres-database/novox/keycloak", FormNone: ""} { + if got := VaultName(form, db); got != want { + t.Errorf("the %s name is %q", form, got) + } + } + if got := VaultName(FormPair, Provision{Provision: "p", Local: "r", Provider: End{Node: "a"}, Consumer: End{Node: "b", Module: "m"}}); got != "pair/a/p/b/m/r" { + t.Errorf("a local name's pair is %q", got) + } + // A provision waits for its credential, the vault's provision `secret-`, and for what its provider itself + // requires on its node (clause 3; the key form the lead settled on 2026-10-29). + keycloak, postgres := End{Module: "keycloak", Node: "novox"}, End{Module: "postgres", Node: "ace"} + providerPlan := &catalogue.Plan{Provisions: []catalogue.PlanProvision{{Provision: "backup-target", Provider: "ace", ProviderModule: "restic"}}} + for _, c := range []struct { + form string + plan *catalogue.Plan + want string + }{ + {FormPair, nil, "novox.keycloak.secret-postgres-database"}, + {FormShared, nil, "ace.postgres.secret-postgres-database"}, + {FormPair, providerPlan, "ace.postgres.backup-target,novox.keycloak.secret-postgres-database"}, + {"", nil, ""}, + // An offer with no credential (the lead, 2026-10-29): nothing minted, nothing to wait for. + {FormNone, nil, ""}, + } { + entry := catalogue.PlanProvision{Provision: "postgres-database", Provider: postgres.Node, ProviderModule: postgres.Module, + CredentialForm: c.form} + if got := strings.Join(DependsOn(keycloak, entry, c.plan), ","); got != c.want { + t.Errorf("%q depends on %q, want %q", c.form, got, c.want) + } + } + // The vault's own offer is the value: no secret-secret beside it. + vaultEntry := catalogue.PlanProvision{Provision: "secret", Provider: "novox", ProviderModule: VaultModule, CredentialForm: FormPair} + if got := DependsOn(keycloak, vaultEntry, nil); len(got) != 0 { + t.Errorf("the vault's secret depends on %v", got) + } + if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { + t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/go.mod b/provisioning/go/clients/meshprovisionerclient/go.mod new file mode 100644 index 0000000..b6fdcd2 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef diff --git a/provisioning/go/clients/meshprovisionerclient/provider/consumers.go b/provisioning/go/clients/meshprovisionerclient/provider/consumers.go new file mode 100644 index 0000000..326d0a9 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/consumers.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" +) + +// The provider's consumers' credentials, from the file its machine's provisioner client fills (novox/hq ADR 0373, +// issue 679). +// +// **The provider never asks the vault.** A provider in the tool runner has no account of its own, and the vault +// hands a secret only to a node's provisioner client or to a module under an account of its own (ADR 0349 clause +// 11, ADR 0370 clause 2). So the provider's template names `${consumers:}`, and its machine's client +// fills that file with one entry per consumer bound to it for that provision, each credential asked of the vault in +// the client's own name; the file is the provider's alone (mode 0600, owned). ConsumersFile is that file as the +// helper's Vault: it reads the file on each ask, so a consumer that binds or a credential the vault remakes reaches +// the provider at its next pass. + +// ConsumerCredential is one entry of the file: the consumer, the vault's name of its credential, and its value. +type ConsumerCredential struct { + // Consumer and Module are the consumer's machine and module, Local its local name for the provision, if any. + Consumer string `json:"consumer"` + Module string `json:"module"` + Local string `json:"local,omitempty"` + // Credential is the vault's name of the consumer's credential, PairName's. + Credential string `json:"credential"` + // Value is the credential itself. + Value string `json:"value"` +} + +// ConsumersFile answers a credential from the files the provider machine's client fills: one per provision whose +// template names `${consumers:}`, each a JSON list of ConsumerCredential. +type ConsumersFile struct { + Paths []string +} + +// Value is the credential named name, from the first file carrying it. A file not there yet is passed over; one not +// in the client's form is an error, never read as no consumers. +func (f ConsumersFile) Value(_ context.Context, name string) ([]byte, error) { + var missing []string + for _, at := range f.Paths { + raw, err := os.ReadFile(at) + if errors.Is(err, fs.ErrNotExist) { + missing = append(missing, at) + continue + } + if err != nil { + return nil, fmt.Errorf("the consumers' file %s: %w", at, err) + } + var entries []ConsumerCredential + if err := json.Unmarshal(raw, &entries); err != nil { + return nil, fmt.Errorf("the consumers' file %s is not the provisioner client's list of consumers", at) + } + for _, e := range entries { + if e.Credential == name { + if e.Value == "" { + return nil, fmt.Errorf("the consumers' file %s carries %s with no value", at, name) + } + return []byte(e.Value), nil + } + } + } + if len(missing) == len(f.Paths) { + return nil, fmt.Errorf("%s: no consumers' file is filled yet (%v)", name, f.Paths) + } + return nil, fmt.Errorf("%s is in no consumers' file: the vault holds none for it yet, or its machine's client "+ + "has not filled it", name) +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/ctx_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/ctx_test.go new file mode 100644 index 0000000..736ef25 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/ctx_test.go @@ -0,0 +1,5 @@ +package harness + +import "context" + +var ctx = context.Background() diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/early_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/early_test.go new file mode 100644 index 0000000..4598603 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/early_test.go @@ -0,0 +1,22 @@ +package harness + +// The moved path started before its runtime answers asks again until it does (issue 943). + +import ( + "context" + "errors" +) + +// earlyBus refuses its first watches as the runtime does before Serve runs, then answers as the bucket bus does. +type earlyBus struct { + *bucketBus + refuse int +} + +func (b *earlyBus) Watch(ctx context.Context, bucket string, handle func(string, []byte)) error { + if b.refuse > 0 { + b.refuse-- + return errors.New("before Serve: a bundle reaches the bus through the runtime that launched it") + } + return b.bucketBus.Watch(ctx, bucket, handle) +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/grants_moved_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/grants_moved_test.go new file mode 100644 index 0000000..d8206dd --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/grants_moved_test.go @@ -0,0 +1,124 @@ +package harness + +// postgres handed to the provisioner still serves the consumers the controller's grant file gives (issue 937): a +// consumer not moved has no plan naming postgres, only its grant, until R1 (issue 682). + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + "time" +) + +// granting gives the moved world a grant file, as the controller writes it for the consumers not moved. +func (w *movedWorld) granting(given ...map[string]any) { + if w.h.Receives == "" { + w.h.Receives = filepath.Join(w.t.TempDir(), "mesh.json") + } + dir := filepath.Dir(w.h.Receives) + for _, g := range given { + secret := filepath.Join(dir, g["as"].(string)+".secret") + if err := os.WriteFile(secret, []byte("pw-"+g["as"].(string)+"\n"), 0o600); err != nil { + w.t.Fatal(err) + } + g["secret"] = secret + } + if given == nil { + given = []map[string]any{} + } + raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": given}) + if err := os.WriteFile(w.h.Receives, raw, 0o600); err != nil { + w.t.Fatal(err) + } +} + +// settled is enough passes and time for a retirement to be stable. +func (w *movedWorld) settled() { + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(15 * time.Minute) + w.pass() + w.pass() +} + +// The logins the grant file gives are asked for: made as the grant path makes them, never offered for retirement, +// beside a moved consumer the helper makes from its plan. +func testAConsumerOnlyTheGrantFileGivesIsAskedForAndMade(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.a.inv.Active = []string{"mesh_ace_baserow", "mesh_ace_letta", logins[0]} + w.bus.bind("ace", "app1", nil) + w.granting( + map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}, + map[string]any{"as": "mesh_ace_letta", "node": "ace", + "values": map[string]any{"name": "letta", "extensions": []any{"vector"}}}, + ) + w.settled() + if len(w.a.removed) != 0 || w.h.r.waiting != nil { + t.Fatalf("retired %v, waiting %+v: the grant file's logins were counted unasked", w.a.removed, w.h.r.waiting) + } + made := map[string]Provision{} + for _, c := range w.a.created { + made[c.As] = c + } + letta := made["mesh_ace_letta"] + if letta.Password != "pw-mesh_ace_letta" || letta.Consumer != "ace" || + !reflect.DeepEqual(letta.Values, map[string]any{"name": "letta", "extensions": []any{"vector"}}) { + t.Fatalf("made %+v for letta, as its grant gives it", letta) + } + if _, ok := made["mesh_ace_baserow"]; !ok { + t.Fatalf("baserow not made: %+v", w.a.created) + } + if _, ok := made[logins[0]]; !ok { + t.Fatalf("the moved consumer not made: %+v", w.a.created) + } +} + +// A login both a plan and the grant file name is the plan's: made once, with the plan's credential. +func testALoginAPlanNamesIsNotMadeFromTheGrantFile(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.granting(map[string]any{"as": logins[0], "node": "ace", "values": map[string]any{"name": "app1"}}) + w.pass() + w.pass() + for _, c := range w.a.created { + if c.As == logins[0] && c.Password != "pw-app1" { + t.Fatalf("made %s with the grant's password %q, not the plan's", c.As, c.Password) + } + } +} + +// An unreadable grant file is not nobody asking (issue 241): nothing is retired while it cannot be read. +func testAnUnreadableGrantFileRetiresNothing(t *testing.T) { + bound, logins := bindings(1) + w := newMovedWorld(t, bound) + w.a.inv.Active = []string{"mesh_ace_baserow", logins[0]} + w.bus.bind("ace", "app1", nil) + w.h.Receives = filepath.Join(t.TempDir(), "absent.json") + w.settled() + if len(w.a.removed) != 0 || w.h.r.waiting != nil { + t.Fatalf("retired %v, waiting %+v with the grant file unreadable", w.a.removed, w.h.r.waiting) + } +} + +// A login the grant file stops giving is not retired by the provider itself (novox/hq ADR 0570 clause 5): every +// retirement waits for the operator's yes on a provision's key. +func testALoginTheGrantFileStopsGivingIsRetired(t *testing.T) { + bound, _ := bindings(1) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.granting( + map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}, + map[string]any{"as": "mesh_ace_letta", "node": "ace", "values": map[string]any{"name": "letta"}}, + ) + w.pass() + w.granting(map[string]any{"as": "mesh_ace_baserow", "node": "ace", "values": map[string]any{"name": "baserow"}}) + w.settled() + if len(w.a.removed) != 0 || len(w.a.deleted) != 0 { + t.Fatalf("retired %v, deleted %v, by itself", w.a.removed, w.a.deleted) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/harness.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/harness.go new file mode 100644 index 0000000..9e9dedc --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/harness.go @@ -0,0 +1,572 @@ +// Package harness is the reconcile loop and the retirement of a provider that keeps a resource per consumer; the +// module supplies an Adapter. +package harness + +// The reconcile loop every provider shares, as the TypeScript SDK's runProvisioner runs it +// (@novox/mesh-sdk/provisioner, 0.1.12), line for line in behaviour. It moved here from the postgres and keycloak +// providers, which each carried an identical copy (novox/hq ADR 0039: the loop is the SDK's, the adapter is the +// module's; issue 849). +// +// Read the contributions the mesh delivered; bring each consumer's resource into being through the +// adapter, under the login and password the mesh minted; retire what the mesh no longer asks for. +// **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0048).** +// +// **A provider that keeps failing a consumer says so on the bus (novox/hq ADR 0224).** A consumer +// whose create, check or secret has failed without one success in between for FailingAfter is +// announced as `provisioner.failing` — naming the consumer, its machine and the class of error — and +// again every SayAgainEvery while it lasts; the first success after that is `provisioner.recovered`. +// The controller keeps the newest per provider and consumer and `status` names it. On 2026-10-05 the +// identity provider failed every consumer 31,000 times in a day and said so only in its journal +// (novox/hq issue 179). +// +// **A consumer the mesh stops asking for is retired, not withdrawn, and deleted only by a person +// (novox/hq ADR 0230, the operator's model of 2026-10-06).** Retiring disables its access — reversibly — +// and marks its login and data "to delete" with when and why; nothing is deleted. Asked for again, the +// ordinary create re-enables it as it was. It is retired only once the same set has gone unasked in +// StablePasses consecutive passes and for StableFor, and a set larger than the bound waits for a +// person. retirement.go. + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "strings" + "sync" + "time" +) + +// Provision is one consumer's resource to bring into being — everything the mesh derived and delivered. +type Provision struct { + // As is the login the mesh derived and gave the consumer to present. + As string + // Password is the one the mesh minted, read from the file the host unsealed. + Password string + // Values are what the consumer contributed (e.g. {"name": "letta", "extensions": ["vector"]}). + Values map[string]any + // Derived is what this provider's own definition derives for the consumer (novox/hq ADR 0201). + Derived map[string]any + // At is where the consumer is; Consumer is its node. + At string + Consumer string +} + +// Adapter is the per-service half. +type Adapter interface { + // Create brings the consumer into being under the mesh's login and password — and, for one that + // was retired, re-enables it as it was and clears its mark to delete. + Create(ctx context.Context, p Provision) error + // Retire disables the consumer's access, reversibly, and marks its login and data to delete with + // when and why. It deletes nothing (novox/hq ADR 0230). derived is what the mesh last derived, + // remembered here; nil for a consumer this process did not make. + Retire(ctx context.Context, as string, derived map[string]any, why string, at time.Time) error + // Holds says whether the backend still holds the consumer exactly as p says. Read-only. + Holds(ctx context.Context, p Provision) (bool, error) + // Inventory is what the backend holds that the mesh made: consumers active and retired, read from + // the backend itself so a restart forgets nothing. Read-only. + Inventory(ctx context.Context) (Inventory, error) + // Delete removes one retired consumer — its login and its data — for good. Only ever asked by a + // person, through `cleanup delete`; the harness has checked it is retired and not asked for. + Delete(ctx context.Context, r Retired) (freedBytes int64, err error) +} + +// Harness is the loop's settings and memory. +type Harness struct { + Resource string + Receives string + Adapter Adapter + Every time.Duration // 5s + VerifyEvery time.Duration // 60s + HoldsTimeout time.Duration // 30s + Log func(format string, args ...any) + Now func() time.Time + // Announce publishes one of the provider's standing events; nil announces nothing. Node is the + // machine this provider runs on, said in each. + Announce func(event string, body map[string]any) + Node string + // FailingAfter is how long a consumer fails without a success before it is announced (5m); + // SayAgainEvery is how often it is announced again while it lasts (15m), so a controller that + // missed the first hears the next, and a standing nobody repeats can be told from one that holds. + FailingAfter time.Duration + SayAgainEvery time.Duration + // StablePasses (5) is how many consecutive passes must see the same set no longer asked for, and + // StableFor (10m) how long it must have held since the first of them, before it is retired. RetireAtOnce (3) and RetireFraction (0.5) bound what is retired without a person: + // more consumers than RetireAtOnce, or — where more than one is held — a larger share of those held + // than RetireFraction, waits for `retire approve` (novox/hq ADR 0230). + StablePasses int + StableFor time.Duration + RetireAtOnce int + RetireFraction float64 + + // mu is held by a pass and by every tool a person asks, so the two never interleave. + mu sync.Mutex + verifiedAt time.Time + r retirement + applied map[string]appliedEntry + lost map[string]brake + waiting map[string]int + failing map[string]failure + trouble map[string]*standing + cleared map[string]bool + lastWarning string +} + +// standing is one consumer's unbroken run of failures: since when, how often, and the last error. +type standing struct { + node string + since time.Time + attempts int + class string + text string + saidAt time.Time +} + +// The events a provider's standing is announced as (novox/hq ADR 0224). The controller derives the +// permission to emit them for every module that receives contributions; no manifest lists them. +const ( + EventFailing = "provisioner.failing" + EventRecovered = "provisioner.recovered" +) + +// The classes of error a standing is announced with: what a person reading `status` needs to know +// before reading the journal. An adapter may say better (Classifier). +const ( + ClassCredentials = "credentials-rejected" + ClassUnreachable = "unreachable" + ClassSecret = "secret-unreadable" + ClassRefused = "refused" +) + +// Classifier is an adapter that can say what class an error of its own is. +type Classifier interface { + Class(err error) string +} + +type appliedEntry struct { + hash string + derived map[string]any + node string +} + +type brake struct { + times int + nextAt time.Time +} + +type failure struct { + text string + times int +} + +// The longest a consumer whose create keeps failing to satisfy holds waits between checks. +const maxBackoff = time.Hour + +// How many passes a secret may be unreadable before it stops being called a race (issue 225), and +// once said loudly, how often it is repeated. The same cadence quiets a create that keeps failing +// the same way. +const ( + patiently = 12 + loudlyEvery = 240 +) + +type contribution struct { + As string `json:"as"` + Secret string `json:"secret"` + Node string `json:"node"` + At string `json:"at"` + Values map[string]any `json:"values"` + Derived map[string]any `json:"derived"` +} + +func (h *Harness) init() { + if h.Every == 0 { + h.Every = 5 * time.Second + } + if h.VerifyEvery == 0 { + h.VerifyEvery = time.Minute + } + if h.HoldsTimeout == 0 { + h.HoldsTimeout = 30 * time.Second + } + if h.Now == nil { + h.Now = time.Now + } + if h.FailingAfter == 0 { + h.FailingAfter = 5 * time.Minute + } + if h.SayAgainEvery == 0 { + h.SayAgainEvery = 15 * time.Minute + } + if h.StablePasses == 0 { + h.StablePasses = 5 + } + if h.StableFor == 0 { + h.StableFor = StableFor + } + if h.RetireAtOnce == 0 { + h.RetireAtOnce = 3 + } + if h.RetireFraction == 0 { + h.RetireFraction = 0.5 + } + if h.Log == nil { + h.Log = func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) } + } + if h.applied == nil { + h.applied = map[string]appliedEntry{} + h.lost = map[string]brake{} + h.waiting = map[string]int{} + h.failing = map[string]failure{} + h.trouble = map[string]*standing{} + h.cleared = map[string]bool{} + h.r.retired = map[string]retiredEntry{} + } +} + +// Run reconciles until ctx ends. One consumer's failure never stops the others'. +func (h *Harness) Run(ctx context.Context) { + h.init() + for { + h.Reconcile(ctx) + select { + case <-ctx.Done(): + return + case <-time.After(h.Every): + } + } +} + +func (h *Harness) say(format string, args ...any) { + h.Log("[provisioner:"+h.Resource+"] "+format, args...) +} + +func (h *Harness) warn(why string) { + if why == h.lastWarning { + return + } + if why != "" { + h.say("%s; nothing applied or removed until it can be read", why) + } else { + h.say("contributions file readable again") + } + h.lastWarning = why +} + +// readContributions answers the consumers asked for, or nil when the file says nothing usable. +// **Nothing read is not nobody asking** (novox/hq issue 241): only a file that was read can retire anything. +func (h *Harness) readContributions() []contribution { + raw, err := os.ReadFile(h.Receives) + if err != nil { + h.warn(fmt.Sprintf("contributions file unreadable (%s): %v", h.Receives, err)) + return nil + } + var doc struct { + Requirement string `json:"requirement"` + Given json.RawMessage `json:"given"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + h.warn(fmt.Sprintf("contributions file is not JSON (%s): %v", h.Receives, err)) + return nil + } + if doc.Requirement != "" && doc.Requirement != h.Resource { + h.warn(fmt.Sprintf("%s is for %s, not %s", h.Receives, doc.Requirement, h.Resource)) + return nil + } + var given []contribution + if len(doc.Given) == 0 || string(doc.Given) == "null" || json.Unmarshal(doc.Given, &given) != nil { + h.warn(fmt.Sprintf("%s has no given list", h.Receives)) + return nil + } + h.warn("") + out := []contribution{} + for _, g := range given { + // No `as` is not a credential grant: nothing to create for it. + if g.As != "" && g.Secret != "" { + out = append(out, g) + } + } + return out +} + +func hashOf(as, password string, values, derived map[string]any) string { + // Derived is in the hash: a provider that renames what it derives gave a different resource. + b, _ := json.Marshal([]any{as, password, orEmpty(values), orEmpty(derived)}) + return string(b) +} + +func orEmpty(m map[string]any) map[string]any { + if m == nil { + return map[string]any{} + } + return m +} + +// Reconcile is one pass. +func (h *Harness) Reconcile(ctx context.Context) { + h.mu.Lock() + defer h.mu.Unlock() + h.init() + given := h.readContributions() + if given == nil { + // Not a result: the passes that must agree before anything is retired start again. + h.r.key, h.r.count = "", 0 + return + } + want := map[string]bool{} + for _, g := range given { + want[g.As] = true + } + h.seed(ctx, want) + h.apply(ctx, given) + + // Retire what the mesh has stably stopped asking for — within the bound, or with a person. + h.retireUnasked(ctx, want) + h.r.lastWant = want + for as := range h.failing { + if !want[as] { + delete(h.failing, as) + } + } + // A consumer the mesh stopped asking for that this provider holds nothing for is no longer failed by + // anyone: said, so a standing the controller keeps for it is cleared rather than left naming a + // consumer that is gone. One still held is said recovered when it is retired. + for as := range h.trouble { + if _, held := h.applied[as]; !want[as] && !held { + h.recovered(as, "no longer asked for") + } + } +} + +// apply makes each consumer the grant file gives, and checks every VerifyEvery that the backend still holds it. The +// caller holds h.mu. One consumer's failure never stops the others'. +func (h *Harness) apply(ctx context.Context, given []contribution) { + verifying := h.Now().Sub(h.verifiedAt) >= h.VerifyEvery + if verifying { + h.verifiedAt = h.Now() + } + + for _, g := range given { + raw, err := os.ReadFile(g.Secret) + if err != nil { + // A secret the host has not written yet is a race on the first pass; past a minute it is + // a person's to look at, and said so (novox/hq issue 225). + n := h.waiting[g.As] + 1 + h.waiting[g.As] = n + if n <= patiently { + h.say("%s: secret not readable yet (%s): %v", g.As, g.Secret, err) + } else if n == patiently+1 || n%loudlyEvery == 0 { + h.say("%s: CANNOT READ the secret after %d attempts (%s): %v. This is not a race any more — "+ + "nothing has been provisioned for this consumer and nothing will be until somebody looks. "+ + "Check who owns the file and who this process runs as (novox/hq issue 225)", g.As, n, g.Secret, err) + } + h.failed(g.As, g.Node, ClassSecret, fmt.Sprintf("secret not readable (%s): %v", g.Secret, err)) + continue + } + delete(h.waiting, g.As) + password := strings.TrimSuffix(string(raw), "\n") + p := Provision{As: g.As, Password: password, Values: orEmpty(g.Values), Derived: orEmpty(g.Derived), At: g.At, Consumer: g.Node} + hash := hashOf(g.As, password, g.Values, g.Derived) + + reapplying := 0 + if was, ok := h.applied[g.As]; ok && was.hash == hash { + if !verifying { + continue + } + b, braked := h.lost[g.As] + if braked && h.Now().Before(b.nextAt) { + continue + } + hctx, cancel := context.WithTimeout(ctx, h.HoldsTimeout) + held, err := h.Adapter.Holds(hctx, p) + timedOut := errors.Is(hctx.Err(), context.DeadlineExceeded) + cancel() + if err != nil { + // Unable to ask is not evidence of loss. A backend that timed out will time out for + // the next consumer too, so the rest of this pass is not asked. + text := scrub(err, password) + h.say("%s: could not check the backend, will ask again: %s", g.As, text) + h.failed(g.As, g.Node, h.classOf(err, text), text) + if timedOut { + verifying = false + } + continue + } + if held { + delete(h.lost, g.As) + h.succeeded(g.As) + continue + } + reapplying = b.times + 1 + if reapplying == 1 { + h.say("%s: the backend no longer holds it; applying again", g.As) + } else { + h.say("%s: still not held after being applied again (%d times in a row) — create does not "+ + "produce what holds checks; applying again", g.As, reapplying) + } + } + if err := h.Adapter.Create(ctx, p); err != nil { + text := scrub(err, password) + f := h.failing[g.As] + if f.text != text { + f = failure{text: text} + } + f.times++ + h.failing[g.As] = f + // Said each time it changes, and while it stays the same, as rarely as a lost secret. + if f.times == 1 || f.times%loudlyEvery == 0 { + h.say("%s: create failed, will retry: %s", g.As, text) + } + h.failed(g.As, g.Node, h.classOf(err, text), text) + if reapplying > 0 { + h.lost[g.As] = brake{times: reapplying - 1} + } + continue + } + if f, was := h.failing[g.As]; was { + h.say("%s: created, after %d failed attempt(s)", g.As, f.times) + delete(h.failing, g.As) + } + h.succeeded(g.As) + h.applied[g.As] = appliedEntry{hash: hash, derived: p.Derived, node: g.Node} + h.reenabled(g.As, g.Node) + if reapplying == 0 { + delete(h.lost, g.As) + } else { + wait := h.VerifyEvery << (reapplying - 1) + if wait > maxBackoff || wait <= 0 { + wait = maxBackoff + } + h.lost[g.As] = brake{times: reapplying, nextAt: h.Now().Add(wait)} + if reapplying > 1 { + h.say("%s: next check in %s", g.As, wait.Round(time.Second)) + } + } + } +} + +// failed counts one more failure in a consumer's unbroken run, and announces the run once it has +// lasted FailingAfter — then again every SayAgainEvery while it lasts. +func (h *Harness) failed(as, node, class, text string) { + now := h.Now() + s := h.trouble[as] + if s == nil { + s = &standing{since: now} + h.trouble[as] = s + } + s.node, s.class, s.text = node, class, text + s.attempts++ + if now.Sub(s.since) < h.FailingAfter { + return + } + if !s.saidAt.IsZero() && now.Sub(s.saidAt) < h.SayAgainEvery { + return + } + first := s.saidAt.IsZero() + s.saidAt = now + if first { + h.say("%s: FAILING for %s (%d attempts, %s): %s. Announced as %s; `status` names it until it "+ + "succeeds (novox/hq ADR 0224)", as, now.Sub(s.since).Round(time.Second), s.attempts, class, text, EventFailing) + } + h.announce(EventFailing, map[string]any{ + "provider": h.Resource, "provider-node": h.Node, + "consumer": as, "node": node, + "class": class, "error": clip(text), + "since": s.since.UTC().Format(time.RFC3339), "attempts": s.attempts, + }) +} + +// succeeded ends a consumer's run of failures; one that was announced is announced recovered. +// +// **And the first success for a consumer since this process started is announced too**, failing or +// not: a provider that announced a failure and was restarted has forgotten it, and without this the +// controller would name the consumer failing for ever after it recovered unheard. +func (h *Harness) succeeded(as string) { + if h.trouble[as] == nil && !h.cleared[as] { + h.cleared[as] = true + h.announce(EventRecovered, map[string]any{ + "provider": h.Resource, "provider-node": h.Node, "consumer": as, "why": "first-success", + }) + return + } + h.cleared[as] = true + h.recovered(as, "") +} + +func (h *Harness) recovered(as, why string) { + s := h.trouble[as] + if s == nil { + return + } + delete(h.trouble, as) + if s.saidAt.IsZero() { + return // never announced, so there is nothing to take back + } + if why == "" { + h.say("%s: recovered after %s and %d failed attempt(s)", as, h.Now().Sub(s.since).Round(time.Second), s.attempts) + } + body := map[string]any{ + "provider": h.Resource, "provider-node": h.Node, "consumer": as, "node": s.node, + "since": s.since.UTC().Format(time.RFC3339), "attempts": s.attempts, + } + if why != "" { + body["why"] = why + } + h.announce(EventRecovered, body) +} + +func (h *Harness) announce(event string, body map[string]any) { + if h.Announce != nil { + h.Announce(event, body) + } +} + +// classOf is an error's class: the adapter's word when it has one, else read from the text. +func (h *Harness) classOf(err error, text string) string { + if c, ok := h.Adapter.(Classifier); ok { + if class := c.Class(err); class != "" { + return class + } + } + return ClassOf(text) +} + +// ClassOf reads an error's class from its text — the words the backends the mesh runs use. +func ClassOf(text string) string { + t := strings.ToLower(text) + for _, w := range []string{"invalid_grant", "invalid user credentials", "password authentication failed", + "authentication failed", "unauthorized", " 401"} { + if strings.Contains(t, w) { + return ClassCredentials + } + } + for _, w := range []string{"connection refused", "no such host", "i/o timeout", "deadline exceeded", + "connection reset", "network is unreachable", "no route to host", "eof"} { + if strings.Contains(t, w) { + return ClassUnreachable + } + } + return ClassRefused +} + +// clip keeps an announced error to what belongs in a status line. +func clip(text string) string { + const most = 300 + if len(text) <= most { + return text + } + return text[:most] + "…" +} + +// scrub is an error's text with the consumer's password removed, raw and URL-encoded. +func scrub(err error, password string) string { + text := err.Error() + if password == "" { + return text + } + for _, form := range []string{password, url.QueryEscape(password), url.PathEscape(password)} { + text = strings.ReplaceAll(text, form, "***") + } + return text +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/harness_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/harness_test.go new file mode 100644 index 0000000..d06976a --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/harness_test.go @@ -0,0 +1,154 @@ +package harness + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +type world struct { + t *testing.T + dir string + receives string + now time.Time + h *Harness + a *recorder + said []string +} + +func newWorld(t *testing.T) *world { + w := &world{t: t, dir: t.TempDir(), now: time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC), a: &recorder{held: true}} + w.receives = filepath.Join(w.dir, "mesh.json") + w.h = &Harness{Resource: "postgres-database", Receives: w.receives, Adapter: w.a, + Now: func() time.Time { return w.now }, + Log: func(f string, a ...any) { w.said = append(w.said, f) }} + return w +} + +func (w *world) give(given ...map[string]any) { + for _, g := range given { + secret := filepath.Join(w.dir, g["as"].(string)+".secret") + if err := os.WriteFile(secret, []byte("pw-"+g["as"].(string)+"\n"), 0o600); err != nil { + w.t.Fatal(err) + } + g["secret"] = secret + } + if given == nil { + given = []map[string]any{} + } + raw, _ := json.Marshal(map[string]any{"requirement": "postgres-database", "given": given}) + if err := os.WriteFile(w.receives, raw, 0o600); err != nil { + w.t.Fatal(err) + } +} + +func testAConsumerIsCreatedOnceUnderTheMeshsLoginAndPassword(t *testing.T) { + w := newWorld(t) + w.give(map[string]any{"as": "mesh_ace_letta", "node": "ace", "values": map[string]any{"name": "letta"}}) + w.h.Reconcile(ctx) + w.h.Reconcile(ctx) + if len(w.a.created) != 1 { + t.Fatalf("created %d times", len(w.a.created)) + } + p := w.a.created[0] + if p.As != "mesh_ace_letta" || p.Password != "pw-mesh_ace_letta" || p.Consumer != "ace" { + t.Fatalf("%+v", p) + } +} + +func testAConsumerNoLongerAskedForIsRetiredOnceStable(t *testing.T) { + w := newWorld(t) + w.give(map[string]any{"as": "a"}, map[string]any{"as": "b"}) + w.h.Reconcile(ctx) + w.give(map[string]any{"as": "a"}) + w.settle() // five passes and ten minutes + if strings.Join(w.a.removed, ",") != "b" { + t.Fatal(w.a.removed) + } + // Only a file that says nobody asks retires the last one. + w.give() + w.settle() + if strings.Join(w.a.removed, ",") != "b,a" { + t.Fatal(w.a.removed) + } +} + +func testNothingReadIsNotNobodyAsking(t *testing.T) { + for name, content := range map[string]string{ + "unreadable": "", + "not JSON": "{", + "no given": `{"requirement": "postgres-database"}`, + "another": `{"requirement": "mssql-database", "given": []}`, + } { + t.Run(name, func(t *testing.T) { + w := newWorld(t) + w.give(map[string]any{"as": "a"}) + w.h.Reconcile(ctx) + if content == "" { + os.Remove(w.receives) + } else { + os.WriteFile(w.receives, []byte(content), 0o600) + } + w.h.Reconcile(ctx) + if len(w.a.removed) != 0 { + t.Fatalf("retired %v on a file it could not use", w.a.removed) + } + }) + } +} + +func testALostConsumerIsAppliedAgainAndBraked(t *testing.T) { + w := newWorld(t) + w.give(map[string]any{"as": "a"}) + w.h.Reconcile(ctx) + w.a.held = false + w.now = w.now.Add(2 * time.Minute) + w.h.Reconcile(ctx) + if len(w.a.created) != 2 { + t.Fatalf("created %d times", len(w.a.created)) + } + // Still not held a minute later: applied again, then braked for two minutes. + w.now = w.now.Add(61 * time.Second) + w.h.Reconcile(ctx) + w.now = w.now.Add(61 * time.Second) + w.h.Reconcile(ctx) + if len(w.a.created) != 3 { + t.Fatalf("not braked: created %d times", len(w.a.created)) + } +} + +func testAFailingCreateIsRetriedAndSaidOnce(t *testing.T) { + w := newWorld(t) + w.a.failing = &pgErr{"extension \"nope\" refused, password pw-a"} + w.give(map[string]any{"as": "a"}) + for i := 0; i < 5; i++ { + w.h.Reconcile(ctx) + } + n := 0 + for _, s := range w.said { + if strings.Contains(s, "create failed") { + n++ + } + } + if n != 1 { + t.Fatalf("said %d times", n) + } + w.a.failing = nil + w.h.Reconcile(ctx) + if len(w.a.created) != 1 { + t.Fatal("not retried") + } +} + +type pgErr struct{ s string } + +func (e *pgErr) Error() string { return e.s } + +func testScrubRemovesThePassword(t *testing.T) { + if got := scrub(&pgErr{"bad pw a/b c and a%2Fb+c"}, "a/b c"); strings.Contains(got, "a/b c") || strings.Contains(got, "a%2Fb+c") { + t.Fatal(got) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go new file mode 100644 index 0000000..d0d6e48 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go @@ -0,0 +1,312 @@ +package harness + +// A provider handed to the provisioner (novox/hq ADR 0373, ADR 0349 clause 12, issues 780 and 805): where its +// consumers file is set, it runs on the provider helper (package provider) instead of the harness reading the +// controller's grant files. Moved is that path, shared by every provider that keeps a resource per consumer; a module +// says only what differs, in a MovedSpec. +// +// The helper learns who binds the provider from the plans, composes each consumer's entry and reads its credential +// through ConsumersFile, the file this machine's provisioner client fills. It leaves the rest to the two hooks +// here: Make brings the consumer's resource into being exactly as the harness's adapter does, and Gone leaves +// retirement to the harness's guards (retirement.go): the resource is disabled and marked to delete, never removed. +// +// **A consumer not moved is still the grant file's** (issue 937, ADR 0349 clause 17), for a provider whose spec says +// WithGrants: only a moved consumer's plan names its provider, so the controller still gives every other consumer +// through the grant file (MESH_RECEIVES) until R1 (issue 682). Each login it gives is asked for, and one no plan +// names is made and checked by the harness's grant pass exactly as on the grant path; a login a plan names is the +// helper's alone. A grant file that cannot be read is not nobody asking (issue 241): nothing is retired until it is +// read again. + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" +) + +// MovedSpec is what differs per provider on the moved path. +type MovedSpec struct { + // Module and Provision are the provider module's name and what it provides, as its manifest says. + Module, Provision string + // Offer is what it serves: its credential form and served values. + Offer provider.Offer + // Login is the identity a consumer is made under at the provider's end (postgres: the entry's User; keycloak: its + // As); an entry with none is not made. + Login func(e provider.Entry) string + // Values are the values the adapter is handed for a consumer; the entry's contribution when not set. + Values func(e provider.Entry) map[string]any + // Noun names what Login is in words ("login", "client id"), Made what is made ("login", "client"), and Backend + // the service in "no longer holds it" ("the backend", "Keycloak"). + Noun, Made, Backend string + // WithGrants makes the logins the grant file gives, that no plan names, asked for and made by the harness's grant + // pass (issue 937). + WithGrants bool +} + +// Moved is a provider on the provider helper, with the harness's own memory and retirement behind it: what is held, +// what is counted unasked, what waits for a person (retirement.go). The harness is never Run here; the helper's Make +// and Gone and the loop below drive the same state, so the guards are the harness's, not a copy. +type Moved struct { + h *Harness + spec MovedSpec + + // FirstRetry is the first wait before a watch asked too early is asked again (issue 943); a second when zero. + FirstRetry time.Duration + + mu sync.Mutex // guards plans and loaded + plans map[string][]byte + loaded bool + verified map[string]time.Time +} + +func newMoved(h *Harness, spec MovedSpec) *Moved { + h.mu.Lock() + h.init() + h.mu.Unlock() + return &Moved{h: h, spec: spec, plans: map[string][]byte{}, verified: map[string]time.Time{}} +} + +// Make brings one consumer's resource into being under its login and the vault's credential, as the harness does: +// at once when it is new or changed, and otherwise only when the backend no longer holds it, which is asked every +// VerifyEvery. +func (m *Moved) Make(ctx context.Context, c provider.Consumer) error { + h, e := m.h, c.Entry + as := m.spec.Login(e) + if as == "" { + return fmt.Errorf("%s has no %s to make", e.Key(), m.spec.Noun) + } + if len(c.Credential) == 0 { + return fmt.Errorf("%s has no credential to make its %s with", e.Key(), m.spec.Made) + } + h.mu.Lock() + defer h.mu.Unlock() + password := string(c.Credential) + values := orEmpty(e.Contributes) + if m.spec.Values != nil { + values = m.spec.Values(e) + } + p := Provision{As: as, Password: password, Values: values, Consumer: e.Consumer} + hash := hashOf(as, password, p.Values, nil) + if was, ok := h.applied[as]; ok && was.hash == hash { + if h.Now().Sub(m.verified[as]) < h.VerifyEvery { + return nil + } + m.verified[as] = h.Now() + hctx, cancel := context.WithTimeout(ctx, h.HoldsTimeout) + held, err := h.Adapter.Holds(hctx, p) + cancel() + if err != nil { + text := scrub(err, password) + h.failed(as, e.Consumer, h.classOf(err, text), text) + return errors.New(text) + } + if held { + h.succeeded(as) + return nil + } + h.say("%s: %s no longer holds it; applying again", as, m.spec.Backend) + } + if err := h.Adapter.Create(ctx, p); err != nil { + text := scrub(err, password) + h.failed(as, e.Consumer, h.classOf(err, text), text) + return errors.New(text) + } + h.succeeded(as) + h.applied[as] = appliedEntry{hash: hash, node: e.Consumer} + m.verified[as] = h.Now() + h.reenabled(as, e.Consumer) + return nil +} + +// Gone is the helper letting a consumer go, which it does only on a retiring provision the operator said yes to (novox/hq +// ADR 0570 clause 5): its login is retired at once, through the adapter, its resource and data kept and marked to +// delete, deleted only by a person's `cleanup delete`. A consumer another plan still binds under the same login loses +// nothing. +func (m *Moved) Gone(ctx context.Context, e provider.Entry) error { + h := m.h + as := m.spec.Login(e) + if as == "" { + return nil // refused: nothing was made for it + } + want, _ := m.asked(h.Node) + h.mu.Lock() + defer h.mu.Unlock() + if want[as] { + return nil // another holder of the same login still binds: nothing to retire + } + if _, held := h.applied[as]; !held { + return nil + } + if done := h.retire(ctx, []string{as}, "the operator approved its retirement ("+e.Key()+")", nil); len(done) != 1 { + return fmt.Errorf("%s was not retired; it is tried again at the next pass", as) + } + return nil +} + +// asked is the logins the seat's plans bind the provider for (novox/hq ADR 0570 clause 2), and whether they could all be +// read: a plan that cannot be read, or no read yet, is not an answer (novox/hq issue 241). +func (m *Moved) asked(node string) (map[string]bool, bool) { + m.mu.Lock() + defer m.mu.Unlock() + if !m.loaded || len(m.plans) == 0 { + return nil, false + } + want := map[string]bool{} + for key, raw := range m.plans { + pl, err := provisions.DecodePlan(raw) + if err != nil || pl.Node+"."+pl.Module != key { + return nil, false + } + for _, b := range pl.Plan.Provisions { + if b.Provider == node && b.ProviderModule == m.spec.Module && b.Provision == m.spec.Provision { + as := b.As + if as == "" { + as = provider.ConsumerIdentity(pl.Node, pl.Module) // a plan that names none + } + want[as] = true + } + } + } + return want, true +} + +// granted is what the grant file gives that no plan names (issue 937), and every login it gives; and whether it +// could be read. The caller holds h.mu. With no grant file named there is nothing given, and that is a read. +func (m *Moved) granted(planned map[string]bool) ([]contribution, map[string]bool, bool) { + h := m.h + if h.Receives == "" { + return nil, map[string]bool{}, true + } + given := h.readContributions() + if given == nil { + return nil, nil, false + } + var own []contribution + logins := map[string]bool{} + for _, g := range given { + logins[g.As] = true + if !planned[g.As] { + own = append(own, g) + } + } + return own, logins, true +} + +// retire is one pass over what the plans bind and the grant file gives now: the logins only the grant file gives are +// made and checked, as the grant path does. It retires nothing (ADR 0570 clause 5). +func (m *Moved) retire(ctx context.Context) { + h := m.h + want, ok := m.asked(h.Node) + h.mu.Lock() + defer h.mu.Unlock() + var given []contribution + if ok && m.spec.WithGrants { + var granted map[string]bool + given, granted, ok = m.granted(want) + for as := range granted { + want[as] = true + } + } + if !ok { + // Not a result: the passes that must agree before anything is retired start again. + h.r.key, h.r.count = "", 0 + return + } + h.seed(ctx, want) + h.apply(ctx, given) + // No login is retired for going unasked (novox/hq ADR 0570 clause 5): only Gone, on the operator's yes. + h.r.lastWant = want + for as := range h.failing { + if !want[as] { + delete(h.failing, as) + } + } + for as := range h.trouble { + if _, held := h.applied[as]; !want[as] && !held { + h.recovered(as, "no longer asked for") + } + } +} + +// watch keeps the plans as the bus holds them, from the first read on. +func (m *Moved) watch(ctx context.Context, bus provider.Bus) error { + err := bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + m.mu.Lock() + defer m.mu.Unlock() + if value == nil { + delete(m.plans, key) + } else { + m.plans[key] = append([]byte(nil), value...) + } + }) + if err == nil { + m.mu.Lock() + m.loaded = true + m.mu.Unlock() + } + return err +} + +// Run answers the consumers through the helper and retires through the harness's rules, until ctx ends. +// +// The bus is reached through the runtime that launched this bundle, which answers only once Serve runs, and Run starts +// beside Serve: a watch asked too early is asked again, after a wait that doubles from FirstRetry up to a minute, +// until it is answered or ctx ends, so a start that loses the race never leaves the consumers unanswered (issue 943). +func (m *Moved) Run(ctx context.Context, p *provider.Provider, bus provider.Bus) error { + wait := m.FirstRetry + if wait <= 0 { + wait = time.Second + } + for tries := 1; ; tries++ { + err := m.watch(ctx, bus) + if err == nil { + break + } + if tries == 4 || tries%20 == 0 { + m.h.say("the plans cannot be watched yet (%d tries), asking again: %v", tries, err) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(wait): + } + wait = min(wait*2, time.Minute) + } + go func() { + for { + m.retire(ctx) + select { + case <-ctx.Done(): + return + case <-time.After(m.h.Every): + } + } + }() + return p.Run(ctx) +} + +// NewMoved is a provider handed to the provisioner: the helper answering its consumers, and the harness (never run) +// whose retirement and tools it shares. consumersFile is where its machine's client fills its consumers. Run the +// result with Moved.Run, passing the provider and the bus returned with it. +func NewMoved(consumersFile string, h *Harness, spec MovedSpec) (*Moved, *provider.Provider, provider.Bus, error) { + if consumersFile == "" { + return nil, nil, nil, errors.New("no consumers' file") + } + run := newMoved(h, spec) + bus := provider.StateBus(spec.Module) + return run, provider.New(provider.Options{ + Node: h.Node, + Module: spec.Module, + Offers: map[string]provider.Offer{spec.Provision: spec.Offer}, + Bus: bus, + Vault: provider.ConsumersFile{Paths: []string{consumersFile}}, + Make: run.Make, + Gone: run.Gone, + Every: h.Every, + Logf: func(f string, a ...any) { h.say(f, a...) }, + }), bus, nil +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go new file mode 100644 index 0000000..ffa5a0c --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go @@ -0,0 +1,464 @@ +package harness + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "reflect" + "sync" + "testing" + "time" + + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +const ( + ProvisionName = "postgres-database" + servedPort = 5432 +) + +// postgresSpec is what a postgres-like provider says: a login per consumer, and the grant file's logins. +var postgresSpec = MovedSpec{Module: "postgres", Provision: ProvisionName, + Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"port": servedPort}}, + Login: func(e provider.Entry) string { return e.User }, + Noun: "login", Made: "login", Backend: "the backend", WithGrants: true} + +// bucketBus is the helper's bus over in-memory buckets. +type bucketBus struct { + mu sync.Mutex + buckets map[string]map[string][]byte +} + +func (b *bucketBus) Watch(_ context.Context, bucket string, handle func(string, []byte)) error { + b.mu.Lock() + defer b.mu.Unlock() + for k, v := range b.buckets[bucket] { + handle(k, v) + } + return nil +} + +func (b *bucketBus) Put(_ context.Context, bucket, key string, value []byte) error { + b.mu.Lock() + defer b.mu.Unlock() + if b.buckets[bucket] == nil { + b.buckets[bucket] = map[string][]byte{} + } + b.buckets[bucket][key] = append([]byte(nil), value...) + return nil +} + +func (b *bucketBus) Delete(_ context.Context, bucket, key string) error { + b.mu.Lock() + defer b.mu.Unlock() + delete(b.buckets[bucket], key) + return nil +} + +// putPlan is a consumer's plan on the seat mesh-provisioner, of these plan entries, each provision's key provided (novox/hq +// ADR 0570 clauses 1, 2): what the helper binds by. +func (b *bucketBus) putPlan(node, module string, entries ...map[string]any) { + raw, _ := json.Marshal(entries) + var ps []catalogue.PlanProvision + if err := json.Unmarshal(raw, &ps); err != nil { + panic(err) + } + for _, pp := range ps { + pr := provisions.Provision{Provision: pp.Provision, Local: pp.Local, Consumer: provisions.End{Module: module, Node: node}, + Provider: provisions.End{Module: pp.ProviderModule, Node: pp.Provider}, State: provisions.Provided} + key, _ := pr.Key() + value, err := provisions.Encode(pr) + if err != nil { + panic(err) + } + _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) + } + plan, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) + if err != nil { + panic(err) + } + _ = b.Put(context.Background(), provisions.PlansBucket, node+"."+module, plan) +} + +// retireOn is a consumer's provision retiring with the operator's yes (ADR 0570 clause 5), its plan gone: the one way +// a provider lets it go. +func (b *bucketBus) retireOn(node, module, provides string) { + pr := provisions.Provision{Provision: ProvisionName, Consumer: provisions.End{Module: module, Node: node}, + Provider: provisions.End{Module: provides, Node: "novox"}, State: provisions.Retiring, Why: module + " left", + Warrant: &provisions.Warrant{Ask: "a-1", Option: "yes", By: "the operator"}} + key, _ := pr.Key() + value, _ := provisions.Encode(pr) + _ = b.Delete(context.Background(), provisions.PlansBucket, node+"."+module) + _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) +} + +// bind is one consumer's assignment, whose plan names this machine's postgres as the provider and carries what the +// consumer contributes (issue 776). +func (b *bucketBus) bind(node, module string, contributes map[string]any) { + b.bindNamed("postgres", node, module, contributes, nil) +} + +// bindNamed is bind to a provider module, with the names the controller composed for the consumer (issue 792). +func (b *bucketBus) bindNamed(provides, node, module string, contributes map[string]any, names map[string]string) { + b.putPlan(node, module, map[string]any{"provision": ProvisionName, "provider": "novox", "provider-module": provides, + "contributes": contributes, "names": names}) +} + +// consumersFile writes the file the machine's client fills: one credential per consumer. +func consumersFile(t *testing.T, bound map[[2]string]string) string { + var list []provider.ConsumerCredential + for k, password := range bound { + list = append(list, provider.ConsumerCredential{Consumer: k[0], Module: k[1], + Credential: provider.PairName("novox", ProvisionName, k[0], k[1], ""), Value: password}) + } + raw, _ := json.Marshal(list) + at := filepath.Join(t.TempDir(), "consumers.json") + if err := os.WriteFile(at, raw, 0o600); err != nil { + t.Fatal(err) + } + return at +} + +// movedWorld is postgres on the helper over an in-memory bus, with a recorder for a server and a clock to move. +type movedWorld struct { + t *testing.T + a *recorder + bus *bucketBus + h *Harness + spec MovedSpec + run *Moved + p *provider.Provider + now time.Time + file string +} + +func newMovedWorld(t *testing.T, bound map[[2]string]string) *movedWorld { + return newMovedWorldOf(t, bound, postgresSpec) +} + +func newMovedWorldOf(t *testing.T, bound map[[2]string]string, spec MovedSpec) *movedWorld { + w := &movedWorld{t: t, spec: spec, a: &recorder{}, bus: &bucketBus{buckets: map[string]map[string][]byte{}}, + now: time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC)} + w.file = consumersFile(t, bound) + w.h = &Harness{Resource: ProvisionName, Adapter: w.a, Node: "novox", Now: func() time.Time { return w.now }, + Log: func(string, ...any) {}} + var bus provider.Bus + w.run, w.p, bus, _ = NewMoved(w.file, w.h, spec) + _ = bus + if err := w.run.watch(ctx, w.bus); err != nil { + t.Fatal(err) + } + return w +} + +// pass is one helper pass and one retirement pass, five seconds after the last. +func (w *movedWorld) pass() []provider.Outcome { + w.now = w.now.Add(5 * time.Second) + // the plans as the bus holds them now, as the watch would hand them + w.run.plans = map[string][]byte{} + for k, v := range w.bus.buckets[provisions.PlansBucket] { + w.run.plans[k] = v + } + // the helper reads its buckets from the in-memory bus + w.p = provider.New(provider.Options{Node: "novox", Module: w.spec.Module, Bus: w.bus, + Vault: provider.ConsumersFile{Paths: []string{w.file}}, + Offers: map[string]provider.Offer{ProvisionName: w.spec.Offer}, + Make: w.run.Make, Gone: w.run.Gone, Logf: func(string, ...any) {}}) + out, err := w.p.Pass(ctx) + if err != nil { + w.t.Fatal(err) + } + w.run.retire(ctx) + return out +} + +func bindings(n int) (map[[2]string]string, []string) { + bound := map[[2]string]string{} + var logins []string + for i := 1; i <= n; i++ { + module := fmt.Sprintf("app%d", i) + bound[[2]string{"ace", module}] = "pw-" + module + logins = append(logins, provider.ConsumerIdentity("ace", module)) + } + return bound, logins +} + +// The consumers' file makes the same calls on the adapter as the equivalent grant files do (the harness). +func testTheConsumersFileMakesWhatTheGrantFilesMake(t *testing.T) { + letta := provider.ConsumerIdentity("ace", "letta") + plain := provider.ConsumerIdentity("ace", "plain") + + // Today: the grant files, through the harness. + g := newWorld(t) + g.give( + map[string]any{"as": letta, "node": "ace", "values": map[string]any{"name": "letta", "extensions": []any{"vector"}}}, + map[string]any{"as": plain, "node": "ace", "values": map[string]any{"name": "plain"}}, + ) + g.h.Reconcile(ctx) + + // The consumers' file, through the provider helper. + w := newMovedWorld(t, map[[2]string]string{{"ace", "letta"}: "pw-" + letta, {"ace", "plain"}: "pw-" + plain}) + w.bus.bind("ace", "letta", map[string]any{"name": "letta", "extensions": []any{"vector"}}) + w.bus.bind("ace", "plain", map[string]any{"name": "plain"}) + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + byAs := map[string]Provision{} + for _, c := range w.a.created { + byAs[c.As] = c + } + if len(w.a.created) != 2 { + t.Fatalf("created %d, want 2", len(w.a.created)) + } + for _, want := range g.a.created { + c := byAs[want.As] + if c.As != want.As || c.Password != want.Password || c.Consumer != want.Consumer || + !reflect.DeepEqual(orEmpty(c.Values), orEmpty(want.Values)) { + t.Fatalf("made %+v, the grant files make %+v", c, want) + } + } + // Unchanged, and held: nothing is made again, within a minute or after it. + w.a.held = true + w.pass() + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 2 { + t.Fatalf("an unchanged pass created again: %d", len(w.a.created)) + } + // No longer held: made again. + w.a.held = false + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 4 { + t.Fatalf("not made again: %d", len(w.a.created)) + } +} + +// A consumer whose plan goes retires nothing by itself, however long (novox/hq ADR 0570 clause 5): its login is retired, +// never dropped, and its entry removed only once its provision is retiring with the operator's yes. +func testAConsumerMissingFromOneReadRetiresNothing(t *testing.T) { + bound, logins := bindings(2) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.bus.bind("ace", "app2", nil) + w.pass() + _ = w.bus.Delete(ctx, provisions.PlansBucket, "ace.app2") + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(30 * time.Minute) + w.pass() + if len(w.a.removed) != 0 || w.h.r.waiting != nil { + t.Fatalf("retired %v, waiting %v, with no warrant", w.a.removed, w.h.r.waiting) + } + if _, kept := w.bus.buckets[provider.BucketOf("postgres")]["ace.app2.postgres-database"]; !kept { + t.Fatal("the entry went with no warrant") + } + w.bus.retireOn("ace", "app2", "postgres") + w.pass() + if !reflect.DeepEqual(w.a.removed, []string{logins[1]}) || len(w.a.deleted) != 0 { + t.Fatalf("retired %v, deleted %v", w.a.removed, w.a.deleted) + } + if _, kept := w.bus.buckets[provider.BucketOf("postgres")]["ace.app2.postgres-database"]; kept { + t.Fatal("the entry stayed after the login was retired") + } +} + +// A plan list that was never read is not nobody asking. +func testNoPlansReadRetiresNothing(t *testing.T) { + bound, _ := bindings(1) + w := newMovedWorld(t, bound) + w.bus.bind("ace", "app1", nil) + w.pass() + w.bus.Delete(ctx, provisions.PlansBucket, "ace.app1") + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(20 * time.Minute) + w.pass() + if len(w.a.removed) != 0 { + t.Fatalf("retired %v with no plans read", w.a.removed) + } +} + +// A consumer whose credential is not in the file yet is waited for: nothing is made for it. +func testAConsumerWithoutACredentialIsWaitedFor(t *testing.T) { + w := newMovedWorld(t, map[[2]string]string{}) + w.bus.bind("ace", "letta", map[string]any{"name": "letta"}) + outcomes := w.pass() + if len(outcomes) != 1 || outcomes[0].Status != provider.Waiting || len(w.a.created) != 0 { + t.Fatalf("%+v, created %v", outcomes, w.a.created) + } +} + +// Where the file is not there, postgres takes the grant directory, as before; where it is, the helper. +// The grant path restarts itself when the consumers file is first filled. +// A consumer whose module has a slug keeps the login its plan names, the slug first (issue 788): the adapter makes it +// under that login, and the retirement counts it as asked for, so a pass retires nothing. +func testASluggedConsumerKeepsTheLoginItsPlanNames(t *testing.T) { + agent := provider.ConsumerIdentity("ace", "agent") + w := newMovedWorld(t, map[[2]string]string{{"ace", "claude-code"}: "pw-agent"}) + w.bus.putPlan("ace", "claude-code", map[string]any{"provision": ProvisionName, "provider": "novox", + "provider-module": "postgres", "as": agent}) + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + if len(w.a.created) != 1 || w.a.created[0].As != agent { + t.Fatalf("made %+v, want one under %s", w.a.created, agent) + } + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(15 * time.Minute) + w.pass() + w.pass() + if len(w.a.removed) != 0 { + t.Fatalf("retired %v: the plan's login was counted unasked", w.a.removed) + } +} + +// A provider whose spec names the plan's `as` as the login and no grant file (the keycloak shape) makes the consumer +// under that name with the values its spec composes, and a grant file that cannot be read does not hold up its +// retirement pass. +func testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing.T) { + a := &recorder{} + bus := &bucketBus{buckets: map[string]map[string][]byte{}} + now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) + h := &Harness{Resource: ProvisionName, Adapter: a, Node: "novox", Now: func() time.Time { return now }, + Receives: filepath.Join(t.TempDir(), "unreadable-or-missing.json"), Log: func(string, ...any) {}} + spec := MovedSpec{Module: "keycloak", Provision: ProvisionName, + Offer: provider.Offer{Credential: provider.PerConsumer}, + Login: func(e provider.Entry) string { return e.As }, Noun: "client id", Made: "client", Backend: "Keycloak", + Values: func(e provider.Entry) map[string]any { return map[string]any{"label": e.Contributes["label"]} }} + file := consumersFile(t, map[[2]string]string{{"ace", "wiki"}: "pw-wiki"}) + run, _, _, err := NewMoved(file, h, spec) + if err != nil { + t.Fatal(err) + } + bus.putPlan("ace", "wiki", map[string]any{"provision": ProvisionName, "provider": "novox", + "provider-module": "keycloak", "as": "wiki-client", "contributes": map[string]any{"label": "Wiki"}}) + if err := run.watch(ctx, bus); err != nil { + t.Fatal(err) + } + p := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus, + Vault: provider.ConsumersFile{Paths: []string{file}}, + Offers: map[string]provider.Offer{ProvisionName: spec.Offer}, Make: run.Make, Gone: run.Gone, + Logf: func(string, ...any) {}}) + out, err := p.Pass(ctx) + if err != nil || len(out) != 1 || out[0].Status != provider.Answered { + t.Fatalf("%v %+v", err, out) + } + run.retire(ctx) + if len(a.created) != 1 || a.created[0].As != "wiki-client" || a.created[0].Password != "pw-wiki" || + a.created[0].Values["label"] != "Wiki" { + t.Fatalf("made %+v", a.created) + } +} + +const testIssuer = "https://id.example.org/realms/Novox" + +var ( + grafanaContributes = map[string]any{"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth"} + grafanaNames = map[string]string{"name": "grafana.example.org", "internal-name": "grafana.home.internal"} +) + +// keycloakSpec is what a keycloak-like provider says: the client id is the plan's `as`, the values are what the +// consumer contributes with the controller's names over them, and no grant file is consulted. +var keycloakSpec = MovedSpec{Module: "keycloak", Provision: ProvisionName, + Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"issuer": testIssuer, + "token-path": "/protocol/openid-connect/token"}}, + Login: func(e provider.Entry) string { return e.As }, + Values: func(e provider.Entry) map[string]any { + values := map[string]any{} + for k, v := range e.Contributes { + values[k] = v + } + for k, name := range e.Names { + values[k] = name + } + return values + }, + Noun: "client id", Made: "client", Backend: "Keycloak"} + +// The consumers' file makes the same call on the adapter as the equivalent grant file does (the harness), for a +// provider of the keycloak shape: the same client id, secret and values; and the entry tells the consumer what the +// provider serves. +func testTheConsumersFileMakesWhatTheGrantFileMakesForTheKeycloakShape(t *testing.T) { + grafana := provider.ConsumerIdentity("home", "grafana") + values := map[string]any{"name": grafanaNames["name"], "internal-name": grafanaNames["internal-name"]} + for k, v := range grafanaContributes { + values[k] = v + } + g := newWorld(t) + g.give(map[string]any{"as": grafana, "node": "home", "values": values}) + g.h.Reconcile(ctx) + + w := newMovedWorldOf(t, map[[2]string]string{{"home", "grafana"}: "pw-" + grafana}, keycloakSpec) + w.bus.bindNamed("keycloak", "home", "grafana", grafanaContributes, grafanaNames) + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + if len(w.a.created) != 1 || len(g.a.created) != 1 { + t.Fatalf("created %d through the helper, %d through the harness, want 1 each", len(w.a.created), len(g.a.created)) + } + c, want := w.a.created[0], g.a.created[0] + if c.As != want.As || c.Password != want.Password || c.Consumer != want.Consumer || + !reflect.DeepEqual(orEmpty(c.Values), orEmpty(want.Values)) { + t.Fatalf("made %+v, the grant file makes %+v", c, want) + } + var entry provider.Entry + if err := json.Unmarshal(w.bus.buckets[provider.BucketOf("keycloak")]["home.grafana."+ProvisionName], &entry); err != nil { + t.Fatal(err) + } + if entry.Values["issuer"] != testIssuer || entry.Values["token-path"] != "/protocol/openid-connect/token" { + t.Fatalf("served %v", entry.Values) + } + w.a.held = true + w.pass() + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 1 { + t.Fatalf("an unchanged pass created again: %d", len(w.a.created)) + } + w.a.held = false + w.now = w.now.Add(2 * time.Minute) + w.pass() + if len(w.a.created) != 2 { + t.Fatalf("not made again: %d", len(w.a.created)) + } +} + +// A consumer whose plan names its client id (a slugged module) keeps it, and the retirement counts it as asked for. +func testAConsumerNamedByItsPlanKeepsItsClientId(t *testing.T) { + agent := provider.ConsumerIdentity("home", "dashboard") + w := newMovedWorldOf(t, map[[2]string]string{{"home", "grafana-2"}: "pw"}, keycloakSpec) + w.bus.putPlan("home", "grafana-2", map[string]any{"provision": ProvisionName, "provider": "novox", "provider-module": "keycloak", + "as": agent, "contributes": grafanaContributes, "names": grafanaNames}) + for _, o := range w.pass() { + if o.Status != provider.Answered { + t.Fatalf("%+v", o) + } + } + if len(w.a.created) != 1 || w.a.created[0].As != agent { + t.Fatalf("made %+v, want one under %s", w.a.created, agent) + } + for i := 0; i < 8; i++ { + w.pass() + } + w.now = w.now.Add(15 * time.Minute) + w.pass() + w.pass() + if len(w.a.removed) != 0 { + t.Fatalf("retired %v: the plan's client id was counted unasked", w.a.removed) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement.go new file mode 100644 index 0000000..180213a --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement.go @@ -0,0 +1,626 @@ +package harness + +// What becomes of a consumer the mesh no longer asks for (novox/hq ADR 0230 — the operator's model, +// decided 2026-10-06; it replaces ADR 0229's withdrawal brake, which let one consumer go every hour). +// +// A consumer — a login, a client, a key, and the data behind it — is in one of three states: +// +// 1. ACTIVE. +// 2. RETIRED. The mesh stopped asking for it: its access is disabled, reversibly, and its login and +// data are marked "to delete" with when and why. Nothing is deleted. Asked for again, the ordinary +// create re-enables it at once, as it was. +// 3. DELETED, only through `cleanup delete`, a person's act, which this provider executes because it +// owns its backend. +// +// **Stable removals.** A consumer is retired only once the same set of consumers has gone unasked BOTH +// in StablePasses (5) consecutive passes that read the file AND for at least StableFor (10 minutes) +// since the first pass that saw it. Five passes alone are twenty-five seconds — shorter than a +// controller restart, a store reconnecting or a file half written. A pass that could not read the file +// is not a result and starts both again; so does a different set. Additions and changes are never +// delayed. +// +// **Too many is a person.** A stable set of more than RetireAtOnce (3), or — where more than one is +// held — of more than RetireFraction (half) of those held, retires nothing: it WAITS, said in the +// journal and announced `provisioner.retirement` `waiting` (again every SayAgainEvery), which the +// controller raises as an urgent condition, until `retire approve ` or `retire reject`. +// An unassignment the controller made itself is no exception: many changes at once means a person is +// at work, and a person confirms once. On 2026-10-04 one misread file withdrew seven consumers at once +// (issue 241); under this rule that is a question, not an act. +// +// **The backend remembers, not this process.** What is retired, since when and why is read from the +// backend's own mark (Adapter.Inventory), so a restart forgets nothing; a consumer the backend holds +// active that the mesh no longer asks for is retired by the same rules after a restart as before one. +// A consumer found disabled without the mark — withdrawn before this record — is ADOPTED as retired: +// marked, said, announced `adopted`, and its clock starts then. +// +// **A rejection lives as long as this process.** Rejected, the set is kept active and not asked about +// again while it stays the same set; a restart asks again — loudly, never silently. + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// EventRetirement is the one event a provider says about retirement, its `change` saying what +// happened. The controller derives the permission to emit it for every module that receives +// contributions, as it does the standing events (novox/hq ADR 0224, 0230). +const EventRetirement = "provisioner.retirement" + +// The changes EventRetirement carries. +const ( + ChangeWaiting = "waiting" // a stable set over the bound waits for a person + ChangeSettled = "settled" // a waiting or rejected set ended without being retired + ChangeApproved = "approved" // a person approved the waiting (or rejected) set + ChangeRejected = "rejected" // a person kept the waiting set active + ChangeRetired = "retired" // consumers disabled and marked to delete + ChangeReenabled = "reenabled" // a retired consumer asked for again, enabled as it was + ChangeDeleted = "deleted" // a retired consumer deleted, by a person + ChangeAdopted = "adopted" // found disabled without the mark, now retired on record +) + +// StableFor is the least time the same unasked set must hold before it is retired (novox/hq ADR 0230): +// longer than a controller restart, a store reconnecting or a file half written. +const StableFor = 10 * time.Minute + +// KindConsumer is a retired consumer. A backend may list other things set aside for deletion under a +// word of its own (postgres: a database renamed aside). +const KindConsumer = "consumer" + +// Retired is one thing a provider holds retired, as its backend's mark says. +type Retired struct { + Consumer string + // Node is the consumer's machine, where the mark records it. + Node string + // RetiredAt is when it was retired; zero for one found disabled without the mesh's mark. + RetiredAt time.Time + Why string + // SizeBytes is what deleting it would free; -1 when the backend cannot say. + SizeBytes int64 + Kind string +} + +// Inventory is what a backend holds that the mesh made. +type Inventory struct { + Active []string + Retired []Retired + // Sizes is what each active consumer keeps on the backend, in bytes, where the backend can say: what + // the controller tells an empty replacement of a consumer's data by (novox/hq ADR 0233). + Sizes map[string]int64 +} + +// retirement is the loop's memory of the sets it is counting, waiting on and was told to keep. +type retirement struct { + key string // the unasked set, joined + count int // consecutive passes that saw it + firstSeen time.Time + waiting *waitingSet + rejected *rejectedSet + retired map[string]retiredEntry + lastWant map[string]bool + seeded bool + seedSaid string + // noneSaid is whether this process has said that nothing waits: an earlier process may have left a set + // waiting that this one never knew (issue 937). + noneSaid bool +} + +type waitingSet struct { + set []string + since time.Time + saidAt time.Time + held int +} + +type rejectedSet struct { + set []string + by, why string + at time.Time +} + +type retiredEntry struct { + node string + at time.Time + why string +} + +// seed reads what the backend holds, once per process: an active consumer the mesh no longer asks +// for is held, so it is retired by the same rules as one this process made; one found disabled +// without the mark is adopted as retired. +func (h *Harness) seed(ctx context.Context, want map[string]bool) { + if h.r.seeded { + return + } + inv, err := h.Adapter.Inventory(ctx) + if err != nil { + if said := err.Error(); said != h.r.seedSaid { + h.say("cannot list what the backend holds (%v); a consumer the mesh stopped asking for while this "+ + "provider was down is not retired until it can", err) + h.r.seedSaid = said + } + return + } + h.r.seeded = true + for _, as := range inv.Active { + if _, held := h.applied[as]; !held && !want[as] { + // No hash: asked for again, it is applied again, which is harmless and marks it. + h.applied[as] = appliedEntry{} + h.say("%s: held by the backend and no longer asked for; retired once that holds for %d passes "+ + "and %s (novox/hq ADR 0230)", as, h.StablePasses, h.StableFor) + } + } + now := h.Now() + for _, r := range inv.Retired { + if r.Kind != "" && r.Kind != KindConsumer { + continue + } + if !r.RetiredAt.IsZero() { + h.r.retired[r.Consumer] = retiredEntry{node: r.Node, at: r.RetiredAt, why: r.Why} + continue + } + if want[r.Consumer] { + continue // asked for: this pass's create enables it + } + why := "found disabled without the mesh's mark — withdrawn before retirement was recorded " + + "(novox/hq ADR 0230); retired as found" + if err := h.Adapter.Retire(ctx, r.Consumer, nil, why, now); err != nil { + h.say("%s: found disabled and could not be marked retired, will try at the next start: %v", r.Consumer, err) + continue + } + h.r.retired[r.Consumer] = retiredEntry{node: r.Node, at: now, why: why} + h.say("%s: ADOPTED as retired — %s", r.Consumer, why) + h.announce(EventRetirement, h.retirementBody(ChangeAdopted, []map[string]any{ + {"consumer": r.Consumer, "node": r.Node, "retired_at": stamp(now), "why": why, "size_bytes": r.SizeBytes}, + }, map[string]any{"why": why})) + } +} + +// retireUnasked counts the passes that saw the same set no longer asked for and, once it is stable, +// retires it — or, past the bound, waits for a person. +func (h *Harness) retireUnasked(ctx context.Context, want map[string]bool) { + var unasked []string + for as := range h.applied { + if !want[as] { + unasked = append(unasked, as) + } + } + sort.Strings(unasked) + key := strings.Join(unasked, "\x00") + now := h.Now() + switch { + case len(unasked) == 0: + h.settle("every consumer held is asked for again") + h.r.key, h.r.count = "", 0 + return + case key != h.r.key: + if h.r.waiting != nil || h.r.rejected != nil { + h.settle("the set the mesh no longer asks for changed") + } + h.r.key, h.r.count, h.r.firstSeen = key, 1, now + h.say("no longer asked for: %s; retired once the same set holds for %d passes and %s", + strings.Join(unasked, ", "), h.StablePasses, h.StableFor) + default: + h.r.count++ + } + if h.r.rejected != nil || h.r.count < h.StablePasses || now.Sub(h.r.firstSeen) < h.StableFor { + return + } + if h.overTheBound(len(unasked), len(h.applied)) { + h.wait(unasked) + return + } + why := fmt.Sprintf("the mesh stopped asking for it: the same result in %d consecutive passes over %s, from %s", + h.r.count, now.Sub(h.r.firstSeen).Round(time.Second), stamp(h.r.firstSeen)) + h.retire(ctx, unasked, why, nil) +} + +// overTheBound says retiring n of the held consumers at once needs a person. +func (h *Harness) overTheBound(n, held int) bool { + if n == 0 { + return false + } + return n > h.RetireAtOnce || (held > 1 && float64(n) > h.RetireFraction*float64(held)) +} + +func (h *Harness) bound(held int) string { + return fmt.Sprintf("more than %d at once, or more than %.0f%% of the %d held", h.RetireAtOnce, + h.RetireFraction*100, held) +} + +// wait holds a stable set over the bound for a person, said once and announced again every +// SayAgainEvery so a controller that missed the first hears the next. +func (h *Harness) wait(set []string) { + now := h.Now() + w := h.r.waiting + if w == nil { + w = &waitingSet{set: set, since: now, held: len(h.applied)} + h.r.waiting = w + h.say("RETIREMENT WAITS FOR A PERSON: the mesh no longer asks for %d of the %d consumer(s) this provider "+ + "holds (%s), %s. Nothing is retired; each stays active until `retire approve %s ` or "+ + "`retire reject` (novox/hq ADR 0230)", len(set), w.held, strings.Join(set, ", "), h.bound(w.held), h.Node) + } else if now.Sub(w.saidAt) < h.SayAgainEvery { + return + } + w.saidAt = now + h.announce(EventRetirement, h.retirementBody(ChangeWaiting, h.named(set), map[string]any{ + "held": w.held, "bound": h.bound(w.held), "since": stamp(w.since), + })) +} + +// settle ends a waiting or rejected set that the mesh's own answer made moot. +func (h *Harness) settle(why string) { + var set []string + switch { + case h.r.waiting != nil: + set = h.r.waiting.set + case h.r.rejected != nil: + set = h.r.rejected.set + case h.r.noneSaid: + return + default: + // A restart forgets a set an earlier process waited on, and so would never say it ended: the + // controller's condition would ask a person to approve what nobody waits on any more (issue 937). + // The first time this process finds every consumer held asked for, it says so once, with no set. + h.r.noneSaid = true + h.say("nothing waits for a person: %s", why) + h.announce(EventRetirement, h.retirementBody(ChangeSettled, []map[string]any{}, map[string]any{"why": why})) + return + } + h.r.waiting, h.r.rejected = nil, nil + h.say("retirement of %s settled without retiring: %s", strings.Join(set, ", "), why) + h.announce(EventRetirement, h.retirementBody(ChangeSettled, h.named(set), map[string]any{"why": why})) +} + +// retire disables and marks each consumer of set; one that fails stays held and is tried again once +// its set is stable again. +func (h *Harness) retire(ctx context.Context, set []string, why string, extra map[string]any) []string { + now := h.Now() + held := len(h.applied) + var done []string + var said []map[string]any + for _, as := range set { + was, ok := h.applied[as] + if !ok { + continue + } + if err := h.Adapter.Retire(ctx, as, was.derived, why, now); err != nil { + h.say("%s: retiring failed, will try again: %v", as, err) + continue + } + delete(h.applied, as) + delete(h.lost, as) + delete(h.failing, as) + h.r.retired[as] = retiredEntry{node: was.node, at: now, why: why} + h.recovered(as, "retired") + h.say("%s: RETIRED — its access disabled and its data kept, marked to delete (%s). Asked for again "+ + "it is re-enabled as it was; it is deleted only by `cleanup delete` (novox/hq ADR 0230)", as, why) + done = append(done, as) + said = append(said, map[string]any{"consumer": as, "node": was.node, "retired_at": stamp(now), "why": why}) + } + h.r.key, h.r.count, h.r.waiting, h.r.rejected = "", 0, nil, nil + if len(done) > 0 { + body := map[string]any{"held": held, "why": why} + for k, v := range extra { + body[k] = v + } + h.announce(EventRetirement, h.retirementBody(ChangeRetired, said, body)) + } + return done +} + +// reenabled says a retired consumer was asked for again and its create enabled it. +func (h *Harness) reenabled(as, node string) { + e, was := h.r.retired[as] + if !was { + return + } + delete(h.r.retired, as) + h.say("%s: asked for again — re-enabled as it was, its mark to delete cleared (retired %s: %s)", as, + stamp(e.at), e.why) + h.announce(EventRetirement, h.retirementBody(ChangeReenabled, []map[string]any{ + {"consumer": as, "node": node, "retired_at": stamp(e.at), "why": e.why}, + }, nil)) +} + +// Approve retires exactly the set waiting (or the set rejected) — a person's confirmation. +func (h *Harness) Approve(ctx context.Context, consumers []string, why, by, via string) ([]string, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.init() + if strings.TrimSpace(why) == "" { + return nil, errors.New("approve: say why") + } + set := sorted(consumers) + var held []string + switch { + case h.r.waiting != nil && same(set, h.r.waiting.set): + held = h.r.waiting.set + case h.r.rejected != nil && same(set, h.r.rejected.set): + held = h.r.rejected.set + default: + return nil, fmt.Errorf("approve: %s is not the set waiting here — %s", orNone(set), h.waitingWords()) + } + h.say("retirement of %s APPROVED by %s: %s", strings.Join(held, ", "), orSomebody(by), why) + h.announce(EventRetirement, h.retirementBody(ChangeApproved, h.named(held), + map[string]any{"why": why, "by": by, "via": via})) + reason := fmt.Sprintf("the mesh stopped asking for it; approved by %s: %s", orSomebody(by), why) + return h.retire(ctx, held, reason, map[string]any{"by": by, "via": via}), nil +} + +// Reject keeps the waiting set active; it is not asked about again while it stays the same set. +func (h *Harness) Reject(consumers []string, why, by, via string) ([]string, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.init() + if strings.TrimSpace(why) == "" { + return nil, errors.New("reject: say why") + } + set := sorted(consumers) + if h.r.waiting == nil || !same(set, h.r.waiting.set) { + return nil, fmt.Errorf("reject: %s is not the set waiting here — %s", orNone(set), h.waitingWords()) + } + h.r.rejected = &rejectedSet{set: h.r.waiting.set, by: by, why: why, at: h.Now()} + h.r.waiting = nil + h.say("retirement of %s REJECTED by %s: %s. Kept active, and not asked about again while the mesh goes on "+ + "not asking for exactly these (until this provider restarts)", strings.Join(set, ", "), orSomebody(by), why) + h.announce(EventRetirement, h.retirementBody(ChangeRejected, h.named(set), + map[string]any{"why": why, "by": by, "via": via})) + return set, nil +} + +// DeleteRetired deletes one retired consumer, by a person's word: never an active one, never one the +// mesh asks for. +func (h *Harness) DeleteRetired(ctx context.Context, consumer, why, by, via string) (int64, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.init() + if strings.TrimSpace(why) == "" { + return 0, errors.New("delete: say why") + } + if h.r.lastWant[consumer] { + return 0, fmt.Errorf("delete: the mesh asks for %s — it is not retired", consumer) + } + if _, held := h.applied[consumer]; held { + return 0, fmt.Errorf("delete: %s is active here — only a retired consumer is deleted", consumer) + } + inv, err := h.Adapter.Inventory(ctx) + if err != nil { + return 0, fmt.Errorf("delete: what the backend holds cannot be read, so nothing is deleted: %w", err) + } + var found *Retired + for i := range inv.Retired { + if inv.Retired[i].Consumer == consumer { + found = &inv.Retired[i] + } + } + if found == nil { + return 0, fmt.Errorf("delete: %s is not retired here — only a retired consumer is deleted", consumer) + } + freed, err := h.Adapter.Delete(ctx, *found) + if err != nil { + return 0, err + } + delete(h.r.retired, consumer) + h.say("%s: DELETED by %s: %s (retired %s: %s; %d bytes freed)", consumer, orSomebody(by), why, + stampOr(found.RetiredAt), found.Why, freed) + h.announce(EventRetirement, h.retirementBody(ChangeDeleted, []map[string]any{{ + "consumer": consumer, "node": found.Node, "retired_at": stampOr(found.RetiredAt), "why": found.Why, + "size_bytes": found.SizeBytes, "kind": kindOf(*found), + }}, map[string]any{"why": why, "by": by, "via": via, "freed_bytes": freed})) + return freed, nil +} + +// Retirement is what a person (and the controller's `cleanup list` and its probe) asks: what is +// held, waiting, rejected and retired here. +func (h *Harness) Retirement(ctx context.Context) (map[string]any, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.init() + inv, err := h.Adapter.Inventory(ctx) + if err != nil { + return nil, err + } + var held []string + for as := range h.applied { + held = append(held, as) + } + sort.Strings(held) + retired := []map[string]any{} + for _, r := range inv.Retired { + retired = append(retired, map[string]any{"consumer": r.Consumer, "node": r.Node, + "retired_at": stampOr(r.RetiredAt), "why": r.Why, "size_bytes": r.SizeBytes, "kind": kindOf(r)}) + } + sizes := map[string]int64{} + for _, as := range held { + if size, ok := inv.Sizes[as]; ok && size >= 0 { + sizes[as] = size + } + } + out := map[string]any{"resource": h.Resource, "node": h.Node, "held": orEmptyList(held), "held_sizes": sizes, + "stable_passes": h.StablePasses, "stable_for_seconds": int(h.StableFor.Seconds()), "bound": h.bound(len(h.applied)), "waiting": nil, "rejected": nil, + "retired": retired} + if w := h.r.waiting; w != nil { + out["waiting"] = map[string]any{"consumers": h.named(w.set), "since": stamp(w.since), "held": w.held} + } + if r := h.r.rejected; r != nil { + out["rejected"] = map[string]any{"consumers": h.named(r.set), "by": r.by, "why": r.why, "at": stamp(r.at)} + } + return out, nil +} + +func (h *Harness) waitingWords() string { + switch { + case h.r.waiting != nil: + return "waiting: " + strings.Join(h.r.waiting.set, ", ") + case h.r.rejected != nil: + return "nothing waits; rejected and kept: " + strings.Join(h.r.rejected.set, ", ") + } + return "nothing waits for a person here" +} + +// named is a set with each consumer's machine, as the events and the tools say it. +func (h *Harness) named(set []string) []map[string]any { + out := make([]map[string]any, 0, len(set)) + for _, as := range set { + out = append(out, map[string]any{"consumer": as, "node": h.applied[as].node}) + } + return out +} + +func (h *Harness) retirementBody(change string, consumers []map[string]any, extra map[string]any) map[string]any { + body := map[string]any{"provider": h.Resource, "provider-node": h.Node, "change": change, + "consumers": consumers, "at": stamp(h.Now())} + for k, v := range extra { + body[k] = v + } + return body +} + +// RetirementTools are the four tools every provider serves for retirement, the same names in every +// module: a caller asks them on the provider's machine, with the operator's answer named in why and by. +func RetirementTools(h *Harness) []stdio.Tool { + if h == nil { + return nil + } + ask := func() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 2*time.Minute) + } + who := map[string]any{ + "why": map[string]any{"type": "string", "description": "why — required, kept in the hand-act log"}, + "by": map[string]any{"type": "string", "description": "who decided"}, + "via": map[string]any{"type": "string", "description": "the part or session that asks, when not the caller itself"}, + } + withSet := map[string]any{"consumers": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, + "description": "the set, exactly as provisioner_retirement names it"}} + for k, v := range who { + withSet[k] = v + } + withOne := map[string]any{ + "consumer": map[string]any{"type": "string", "description": "the retired consumer to delete"}, + "confirm": map[string]any{"type": "string", "description": "the consumer's name again, to say this is meant"}, + } + for k, v := range who { + withOne[k] = v + } + return []stdio.Tool{ + {Name: "provisioner_retirement", + Description: "What this provider holds, what waits for a person to approve its retirement, what was rejected, " + + "and every retired consumer with when, why and its size (novox/hq ADR 0230).", + Run: func(map[string]any) (any, error) { + ctx, cancel := ask() + defer cancel() + return h.Retirement(ctx) + }}, + {Name: "provisioner_retire_approve", + Description: "Retire exactly the set waiting for a person (or the set rejected earlier): access disabled, data " + + "kept and marked to delete. It asks nobody: the operator's answer on the operator channel to the provisioner's " + + "ask (novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.", + Input: withSet, + Run: func(args map[string]any) (any, error) { + ctx, cancel := ask() + defer cancel() + done, err := h.Approve(ctx, strs(args["consumers"]), argString(args, "why"), argString(args, "by"), argString(args, "via")) + if err != nil { + return nil, err + } + return map[string]any{"retired": orEmptyList(done)}, nil + }}, + {Name: "provisioner_retire_reject", + Description: "Keep the set waiting for a person active. It asks nobody: the operator's answer on the operator channel " + + "(novox/hq ADR 0570 §5) is named in why and by, which the hand-act log keeps.", + Input: withSet, + Run: func(args map[string]any) (any, error) { + kept, err := h.Reject(strs(args["consumers"]), argString(args, "why"), argString(args, "by"), argString(args, "via")) + if err != nil { + return nil, err + } + return map[string]any{"kept": kept}, nil + }}, + {Name: "provisioner_delete", + Description: "Delete one RETIRED consumer — its login and its data — for good. Refused for anything active or " + + "asked for. It asks nobody: ask the operator on the operator channel first, and name that answer in why and " + + "by, which the hand-act log keeps (novox/hq ADR 0604 §8).", + Input: withOne, + Run: func(args map[string]any) (any, error) { + consumer := argString(args, "consumer") + if consumer == "" || argString(args, "confirm") != consumer { + return nil, errors.New("delete: name the consumer, and repeat it in confirm") + } + ctx, cancel := ask() + defer cancel() + freed, err := h.DeleteRetired(ctx, consumer, argString(args, "why"), argString(args, "by"), argString(args, "via")) + if err != nil { + return nil, err + } + return map[string]any{"deleted": consumer, "freed_bytes": freed}, nil + }}, + } +} + +func strs(v any) []string { + list, _ := v.([]any) + out := []string{} + for _, x := range list { + if s, ok := x.(string); ok && s != "" { + out = append(out, s) + } + } + return out +} + +func sorted(list []string) []string { + out := append([]string(nil), list...) + sort.Strings(out) + return out +} + +func same(a, b []string) bool { + return strings.Join(sorted(a), "\x00") == strings.Join(sorted(b), "\x00") +} + +func orNone(set []string) string { + if len(set) == 0 { + return "an empty set" + } + return strings.Join(set, ", ") +} + +func orSomebody(by string) string { + if by == "" { + return "a person" + } + return by +} + +func orEmptyList(list []string) []string { + if list == nil { + return []string{} + } + return list +} + +func kindOf(r Retired) string { + if r.Kind == "" { + return KindConsumer + } + return r.Kind +} + +func stamp(t time.Time) string { return t.UTC().Format(time.RFC3339) } + +func stampOr(t time.Time) string { + if t.IsZero() { + return "" + } + return stamp(t) +} + +func argString(args map[string]any, key string) string { + s, _ := args[key].(string) + return s +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement_test.go new file mode 100644 index 0000000..4408e1f --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/retirement_test.go @@ -0,0 +1,575 @@ +package harness + +// Retirement (novox/hq ADR 0230), as the shared loop does it: a consumer the mesh stops asking for is +// retired only after the same result in five consecutive passes, too many at once wait for a person, +// asked for again it is re-enabled, and only a person deletes. The same file in every Go provider. + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + "time" +) + +// recorder is a backend that remembers what it holds, active and retired, as a real one's mark does. +type recorder struct { + created []Provision + removed []string // retired, in order + deleted []string + held bool + failing error + holdsErr error + retErr error + inv Inventory + invErr error +} + +func (r *recorder) Create(_ context.Context, p Provision) error { + if r.failing != nil { + return r.failing + } + r.created = append(r.created, p) + r.inv.Retired = withoutRetired(r.inv.Retired, p.As) + if !listHas(r.inv.Active, p.As) { + r.inv.Active = append(r.inv.Active, p.As) + } + return nil +} + +func (r *recorder) Retire(_ context.Context, as string, _ map[string]any, why string, at time.Time) error { + if r.retErr != nil { + return r.retErr + } + r.removed = append(r.removed, as) + r.inv.Active = without(r.inv.Active, as) + r.inv.Retired = append(withoutRetired(r.inv.Retired, as), + Retired{Consumer: as, RetiredAt: at, Why: why, SizeBytes: 42, Kind: KindConsumer}) + return nil +} + +func (r *recorder) Holds(context.Context, Provision) (bool, error) { + if r.holdsErr != nil { + return false, r.holdsErr + } + return r.held, nil +} + +func (r *recorder) Inventory(context.Context) (Inventory, error) { return r.inv, r.invErr } + +func (r *recorder) Delete(_ context.Context, x Retired) (int64, error) { + r.deleted = append(r.deleted, x.Consumer) + r.inv.Retired = withoutRetired(r.inv.Retired, x.Consumer) + return x.SizeBytes, nil +} + +func listHas(list []string, s string) bool { + for _, x := range list { + if x == s { + return true + } + } + return false +} + +func without(list []string, s string) []string { + var out []string + for _, x := range list { + if x != s { + out = append(out, x) + } + } + return out +} + +func withoutRetired(list []Retired, s string) []Retired { + var out []Retired + for _, x := range list { + if x.Consumer != s { + out = append(out, x) + } + } + return out +} + +// holding gives the provider n consumers c1…cn and applies them. +func holding(w *world, n int) []map[string]any { + var given []map[string]any + for i := 1; i <= n; i++ { + given = append(given, map[string]any{"as": fmt.Sprintf("c%d", i), "node": "anchor"}) + } + w.give(given...) + w.h.Reconcile(ctx) + return given +} + +// pass is one reconcile five seconds after the last. +func (w *world) pass() { + w.now = w.now.Add(5 * time.Second) + w.h.Reconcile(ctx) +} + +func retirements(said []announced) []string { + var out []string + for _, a := range said { + if a.event == EventRetirement { + out = append(out, a.body["change"].(string)) + } + } + return out +} + +func noneWaits(a announced) bool { + c, _ := a.body["consumers"].([]map[string]any) + return a.body["change"] == ChangeSettled && len(c) == 0 +} + +func lastRetirement(t *testing.T, said []announced) map[string]any { + t.Helper() + for i := len(said) - 1; i >= 0; i-- { + if said[i].event == EventRetirement { + return said[i].body + } + } + t.Fatal("nothing about retirement was announced") + return nil +} + +func namesOf(body map[string]any) string { + var out []string + for _, c := range body["consumers"].([]map[string]any) { + out = append(out, c["consumer"].(string)) + } + return strings.Join(out, ",") +} + +// settle passes every five seconds until the same answer has held for StableFor, and one pass more. +func (w *world) settle() { w.passes(StableFor + 5*time.Second) } + +func testATransientEmptyListForFourPassesRetiresNothing(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 1) + w.give() + for i := 0; i < 4; i++ { + w.pass() + } + w.give(given...) + w.pass() + if len(w.a.removed) != 0 || len(retirements(*said)) != 0 { + t.Fatalf("four passes retired %v and said %v", w.a.removed, retirements(*said)) + } +} + +// Five identical passes are twenty-five seconds — shorter than a controller restart. Both must hold: +// five passes AND ten minutes of the same answer (novox/hq ADR 0230). +func testFivePassesInTwentyFiveSecondsRetireNothingTenMinutesDo(t *testing.T) { + w, said := standingWorld(t) + holding(w, 1) + w.give() + for i := 0; i < 5; i++ { + w.pass() + } + if len(w.a.removed) != 0 || len(retirements(*said)) != 0 { + t.Fatalf("five passes in 25 s retired %v", w.a.removed) + } + w.passes(StableFor - 30*time.Second) + if len(w.a.removed) != 0 { + t.Fatalf("retired before the set held %s: %v", StableFor, w.a.removed) + } + w.passes(time.Minute) + if strings.Join(w.a.removed, ",") != "c1" { + t.Fatalf("the same set held %s and was not retired: %v", StableFor, w.a.removed) + } + // Ten minutes in one slow pass are not five passes. + w2 := newWorld(t) + holding(w2, 1) + w2.give() + w2.pass() + w2.now = w2.now.Add(StableFor) + w2.pass() + if len(w2.a.removed) != 0 { + t.Fatalf("two passes ten minutes apart retired %v", w2.a.removed) + } + w2.passes(15 * time.Second) + if len(w2.a.removed) != 1 { + t.Fatalf("five passes over ten minutes did not retire: %v", w2.a.removed) + } +} + +func testAStableSetIsRetiredAndAskedAgainReEnables(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 3) + w.give(given[:2]...) + w.settle() + if strings.Join(w.a.removed, ",") != "c3" { + t.Fatalf("retired %v", w.a.removed) + } + body := lastRetirement(t, *said) + if body["change"] != ChangeRetired || namesOf(body) != "c3" || body["held"] != 3 || body["provider-node"] != "anchor" || + !strings.Contains(body["why"].(string), "consecutive passes over 10m") { + t.Fatalf("%v", body) + } + if len(w.a.inv.Retired) != 1 || w.a.inv.Retired[0].Consumer != "c3" { + t.Fatalf("the backend does not hold it retired: %+v", w.a.inv) + } + // Asked for again: the ordinary create, on the first pass, and said. + created := len(w.a.created) + w.give(given...) + w.pass() + if len(w.a.created) != created+1 || w.a.created[created].As != "c3" { + t.Fatalf("not created again: %v", w.a.created) + } + if body := lastRetirement(t, *said); body["change"] != ChangeReenabled || namesOf(body) != "c3" { + t.Fatalf("%v", body) + } + if len(w.a.inv.Retired) != 0 { + t.Fatalf("still marked retired: %+v", w.a.inv.Retired) + } +} + +func testAnUnreadablePassStartsTheCountAgain(t *testing.T) { + w := newWorld(t) + holding(w, 1) + w.give() + w.passes(StableFor - time.Minute) + os.WriteFile(w.receives, []byte("{"), 0o600) + w.pass() + w.give() + w.passes(StableFor - time.Minute) + if len(w.a.removed) != 0 { + t.Fatalf("an unreadable pass counted: %v", w.a.removed) + } + w.passes(2 * time.Minute) + if len(w.a.removed) != 1 { + t.Fatalf("not retired after ten minutes of readable passes: %v", w.a.removed) + } +} + +func testADifferentSetStartsTheCountAgain(t *testing.T) { + w := newWorld(t) + given := holding(w, 5) + w.give(given[:4]...) + w.passes(StableFor - time.Minute) + w.give(given[:3]...) + w.passes(StableFor - time.Minute) + if len(w.a.removed) != 0 { + t.Fatalf("a changed set kept its count: %v", w.a.removed) + } + w.passes(2 * time.Minute) + if strings.Join(w.a.removed, ",") != "c4,c5" { + t.Fatalf("%v", w.a.removed) + } +} + +func testAdditionsAndChangesAreNeverDelayed(t *testing.T) { + w := newWorld(t) + holding(w, 1) + w.give(map[string]any{"as": "c1", "node": "anchor"}, map[string]any{"as": "c2"}) + w.pass() + if len(w.a.created) != 2 || w.a.created[1].As != "c2" { + t.Fatalf("an addition waited: %v", w.a.created) + } + w.give(map[string]any{"as": "c1", "node": "anchor", "values": map[string]any{"name": "rotated"}}, map[string]any{"as": "c2"}) + w.pass() + if len(w.a.created) != 3 || w.a.created[2].As != "c1" { + t.Fatalf("a change waited: %v", w.a.created) + } +} + +func testTooManyWaitsForAPersonAndApproveRetiresExactlyThatSet(t *testing.T) { + w, said := standingWorld(t) + holding(w, 4) + w.give() + w.passes(15 * time.Minute) + if len(w.a.removed) != 0 { + t.Fatalf("four of four were retired without a person: %v", w.a.removed) + } + if got := strings.Join(retirements(*said), ","); got != ChangeWaiting { + t.Fatalf("said %q, want one waiting", got) + } + body := lastRetirement(t, *said) + if namesOf(body) != "c1,c2,c3,c4" || body["held"] != 4 || body["bound"] == "" { + t.Fatalf("%v", body) + } + if !strings.Contains(strings.Join(w.said, "\n"), "RETIREMENT WAITS FOR A PERSON") { + t.Fatal("the wait was not said") + } + // Said again every quarter of an hour while it waits. + w.passes(11 * time.Minute) + if got := strings.Join(retirements(*said), ","); got != "waiting,waiting" { + t.Fatalf("%q", got) + } + + if _, err := w.h.Approve(ctx, []string{"c1", "c2"}, "tidy", "operator", "mesh-controller"); err == nil { + t.Fatal("approved a set that is not the one waiting") + } + if _, err := w.h.Approve(ctx, []string{"c4", "c3", "c2", "c1"}, "", "operator", ""); err == nil { + t.Fatal("approved without a why") + } + done, err := w.h.Approve(ctx, []string{"c4", "c3", "c2", "c1"}, "the old machine is gone", "operator", "mesh-controller") + if err != nil || strings.Join(done, ",") != "c1,c2,c3,c4" || strings.Join(w.a.removed, ",") != "c1,c2,c3,c4" { + t.Fatal(done, err, w.a.removed) + } + changes := retirements(*said) + if strings.Join(changes[len(changes)-2:], ",") != "approved,retired" { + t.Fatalf("%v", changes) + } + if b := lastRetirement(t, *said); b["by"] != "operator" || b["via"] != "mesh-controller" || + !strings.Contains(b["why"].(string), "the old machine is gone") { + t.Fatalf("%v", b) + } + // Nothing more waits. + w.passes(time.Minute) + if r, _ := w.h.Retirement(ctx); r["waiting"] != nil || len(r["retired"].([]map[string]any)) != 4 { + t.Fatalf("%v", r) + } +} + +func testRejectedIsKeptAndNotAskedAgainUntilTheSetChanges(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 4) + w.give() + w.settle() + if _, err := w.h.Reject([]string{"c1"}, "no", "operator", ""); err == nil { + t.Fatal("rejected a set that is not the one waiting") + } + kept, err := w.h.Reject([]string{"c1", "c2", "c3", "c4"}, "a person is moving them", "operator", "mesh-controller") + if err != nil || len(kept) != 4 { + t.Fatal(kept, err) + } + w.passes(time.Hour) + if len(w.a.removed) != 0 { + t.Fatalf("a rejected set was retired: %v", w.a.removed) + } + if got := strings.Join(retirements(*said), ","); got != "waiting,rejected" { + t.Fatalf("asked again after a rejection: %q", got) + } + r, _ := w.h.Retirement(ctx) + if r["rejected"] == nil || r["waiting"] != nil { + t.Fatalf("%v", r) + } + // One of them asked for again: a different answer, so the rejection is settled and counting + // starts over — three of four is over the bound again, and waits again. + w.give(given[0]) + w.pass() + if got := strings.Join(retirements(*said), ","); got != "waiting,rejected,settled" { + t.Fatalf("%q", got) + } + w.settle() + if got := strings.Join(retirements(*said), ","); got != "waiting,rejected,settled,waiting" { + t.Fatalf("%q", got) + } + // A rejected set can still be approved later. + w2, _ := standingWorld(t) + holding(w2, 4) + w2.give() + w2.settle() + w2.h.Reject([]string{"c1", "c2", "c3", "c4"}, "wait", "operator", "") + if done, err := w2.h.Approve(ctx, []string{"c1", "c2", "c3", "c4"}, "now", "operator", ""); err != nil || len(done) != 4 { + t.Fatal(done, err) + } +} + +func testAWaitingSetAskedForAgainSettles(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 4) + w.give() + w.settle() + w.give(given...) + w.pass() + if got := strings.Join(retirements(*said), ","); got != "waiting,settled" || len(w.a.removed) != 0 { + t.Fatalf("%q %v", got, w.a.removed) + } + if _, err := w.h.Approve(ctx, []string{"c1", "c2", "c3", "c4"}, "late", "operator", ""); err == nil { + t.Fatal("approved a set that no longer waits") + } +} + +func testDeleteRemovesOnlyThatRetiredConsumer(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 5) + w.give(given[:3]...) + w.settle() + if strings.Join(w.a.removed, ",") != "c4,c5" { + t.Fatalf("%v", w.a.removed) + } + if _, err := w.h.DeleteRetired(ctx, "c1", "tidy", "operator", ""); err == nil { + t.Fatal("deleted an active consumer") + } + if _, err := w.h.DeleteRetired(ctx, "c5", "", "operator", ""); err == nil { + t.Fatal("deleted without a why") + } + if _, err := w.h.DeleteRetired(ctx, "nobody", "tidy", "operator", ""); err == nil { + t.Fatal("deleted something not retired") + } + freed, err := w.h.DeleteRetired(ctx, "c5", "the experiment is over", "operator", "mesh-controller") + if err != nil || freed != 42 || strings.Join(w.a.deleted, ",") != "c5" { + t.Fatal(freed, err, w.a.deleted) + } + if b := lastRetirement(t, *said); b["change"] != ChangeDeleted || namesOf(b) != "c5" || b["freed_bytes"] != int64(42) { + t.Fatalf("%v", b) + } + r, _ := w.h.Retirement(ctx) + if left := r["retired"].([]map[string]any); len(left) != 1 || left[0]["consumer"] != "c4" { + t.Fatalf("%v", r) + } + // Retired and asked for again before anybody deleted it: refused, it is the mesh's again. + w.give(given[:4]...) + w.pass() + if _, err := w.h.DeleteRetired(ctx, "c4", "tidy", "operator", ""); err == nil { + t.Fatal("deleted a consumer the mesh asks for") + } +} + +func testTheBound(t *testing.T) { + h := &Harness{} + h.init() + for _, c := range []struct{ n, held int }{{1, 1}, {1, 2}, {1, 3}, {3, 7}, {3, 6}, {2, 5}} { + if h.overTheBound(c.n, c.held) { + t.Errorf("%d of %d waits for a person", c.n, c.held) + } + } + for _, c := range []struct{ n, held int }{{2, 2}, {2, 3}, {4, 7}, {4, 10}, {7, 7}} { + if !h.overTheBound(c.n, c.held) { + t.Errorf("%d of %d is retired without a person", c.n, c.held) + } + } +} + +func testARestartRetiresWhatTheBackendHoldsUnaskedAndAdoptsWhatItFindsDisabled(t *testing.T) { + w, said := standingWorld(t) + w.a.inv = Inventory{Active: []string{"kept", "orphan"}, Retired: []Retired{ + {Consumer: "locked-before", SizeBytes: 7, Kind: KindConsumer}, + {Consumer: "old_deleted_20261005", RetiredAt: time.Date(2026, 10, 5, 0, 0, 0, 0, time.UTC), Kind: "set-aside-database"}, + }} + w.give(map[string]any{"as": "kept"}) + w.h.Reconcile(ctx) + if b := lastRetirement(t, *said); b["change"] != ChangeAdopted || namesOf(b) != "locked-before" { + t.Fatalf("%v", b) + } + if strings.Join(w.a.removed, ",") != "locked-before" { + t.Fatalf("adopting did not mark it: %v", w.a.removed) + } + w.settle() + if strings.Join(w.a.removed, ",") != "locked-before,orphan" { + t.Fatalf("an orphan the backend holds was not retired: %v", w.a.removed) + } +} + +func testABackendThatCannotBeListedIsSaidAndAskedAgain(t *testing.T) { + w := newWorld(t) + w.a.invErr = errors.New("connection refused") + holding(w, 1) + if !strings.Contains(strings.Join(w.said, "\n"), "cannot list what the backend holds") { + t.Fatal(w.said) + } + w.a.invErr = nil + w.a.inv = Inventory{Active: []string{"c1", "orphan"}} + w.pass() + w.settle() + if strings.Join(w.a.removed, ",") != "orphan" { + t.Fatalf("%v", w.a.removed) + } +} + +func testTheToolsAnswer(t *testing.T) { + w, _ := standingWorld(t) + holding(w, 4) + w.give() + w.settle() + tools := map[string]func(map[string]any) (any, error){} + for _, tool := range RetirementTools(w.h) { + tools[tool.Name] = tool.Run + // L3: the delete, approve and reject name no removed controller verb, and say where the operator answers (ADR 0604 §8). + if tool.Name != "provisioner_retirement" && (strings.Contains(tool.Description, "controller") || !strings.Contains(tool.Description, "operator channel")) { + t.Errorf("%s says %q", tool.Name, tool.Description) + } + } + if len(tools) != 4 { + t.Fatalf("%d tools", len(tools)) + } + got, err := tools["provisioner_retirement"](nil) + if err != nil || got.(map[string]any)["waiting"] == nil { + t.Fatal(got, err) + } + set := []any{"c1", "c2", "c3", "c4"} + if _, err := tools["provisioner_retire_approve"](map[string]any{"consumers": set}); err == nil { + t.Fatal("approved without a why") + } + if _, err := tools["provisioner_retire_approve"](map[string]any{"consumers": set, "why": "gone", "by": "operator"}); err != nil { + t.Fatal(err) + } + if _, err := tools["provisioner_delete"](map[string]any{"consumer": "c1", "why": "gone"}); err == nil { + t.Fatal("deleted without confirm") + } + if _, err := tools["provisioner_delete"](map[string]any{"consumer": "c1", "confirm": "c1", "why": "gone"}); err != nil { + t.Fatal(err) + } + if strings.Join(w.a.deleted, ",") != "c1" { + t.Fatal(w.a.deleted) + } +} + +func testAFailingConsumerRetiredIsSaidRecovered(t *testing.T) { + w, said := standingWorld(t) + given := holding(w, 2) + w.a.held = false + w.a.failing = errors.New("boom") + w.passes(7 * time.Minute) + w.give(given[0]) + w.settle() + recovered := false + for _, a := range *said { + if a.event == EventRecovered && a.body["consumer"] == "c2" && a.body["why"] == "retired" { + recovered = true + } + } + if !recovered { + t.Fatalf("%v", *said) + } +} + +// everyWord is a world that keeps every word the provider announces. +func everyWord(t *testing.T) (*world, *[]announced) { + w := newWorld(t) + var said []announced + w.h.Announce = func(e string, b map[string]any) { said = append(said, announced{e, b}) } + w.h.Node = "anchor" + return w, &said +} + +// A restarted provider that finds every consumer it holds asked for says once that nothing waits, so the controller +// clears a waiting condition an earlier process raised and this one never knew (issue 937); it says it no more. +func testARestartedProviderSaysOnceThatNothingWaits(t *testing.T) { + w, said := everyWord(t) + holding(w, 2) + w.pass() + w.pass() + var words []announced + for _, a := range *said { + if a.event == EventRetirement { + words = append(words, a) + } + } + if len(words) != 1 || !noneWaits(words[0]) || words[0].body["provider-node"] != w.h.Node { + t.Fatalf("said %+v, want one settled with no set", words) + } +} + +// A provider whose own set waits does not say that nothing waits. +func testAProviderThatWaitsDoesNotSayNothingWaits(t *testing.T) { + w, said := everyWord(t) + w.a.inv.Active = []string{"c1", "c2", "c3", "c4", "c5"} + w.give(map[string]any{"as": "c1", "node": "anchor"}) + w.settle() + for _, a := range *said { + if a.event == EventRetirement && noneWaits(a) { + t.Fatalf("said nothing waits while %v waited", w.h.r.waiting) + } + } + if w.h.r.waiting == nil { + t.Fatal("nothing waited: the test does not hold the case") + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/standing_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/standing_test.go new file mode 100644 index 0000000..52fd137 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/standing_test.go @@ -0,0 +1,193 @@ +package harness + +// A provider that keeps failing a consumer says so on the bus (novox/hq ADR 0224): not on the first +// failure, which may be a restart; after FailingAfter of failures with no success between; again +// every SayAgainEvery while it lasts; and recovered on the first success, or when the consumer goes. + +import ( + "errors" + "os" + "strings" + "testing" + "time" +) + +type announced struct { + event string + body map[string]any +} + +func standingWorld(t *testing.T) (*world, *[]announced) { + w := newWorld(t) + var said []announced + w.h.Announce = func(e string, b map[string]any) { + // The first success since start, and the first word that nothing waits (issue 937), are their own + // tests'; every other test reads past them. + if c, _ := b["consumers"].([]map[string]any); b["why"] != "first-success" && + !(e == EventRetirement && b["change"] == ChangeSettled && len(c) == 0) { + said = append(said, announced{e, b}) + } + } + w.h.Node = "anchor" + return w, &said +} + +// passes reconciles every five seconds for d, as Run would. +func (w *world) passes(d time.Duration) { + for end := w.now.Add(d); w.now.Before(end); w.now = w.now.Add(5 * time.Second) { + w.h.Reconcile(ctx) + } +} + +func events(said []announced) string { + var out []string + for _, a := range said { + out = append(out, a.event) + } + return strings.Join(out, ",") +} + +func testAConsumerFailedForMinutesIsAnnouncedNamingItAndTheClass(t *testing.T) { + w, said := standingWorld(t) + w.a.failing = errors.New(`token request failed: 401 {"error":"invalid_grant","error_description":"Invalid user credentials"}`) + w.give(map[string]any{"as": "mesh_home_grafana", "node": "home-server"}) + + w.passes(4 * time.Minute) + if len(*said) != 0 { + t.Fatalf("announced before FailingAfter: %v", events(*said)) + } + w.passes(2 * time.Minute) + if events(*said) != EventFailing { + t.Fatalf("want one %s, got %q", EventFailing, events(*said)) + } + b := (*said)[0].body + if b["consumer"] != "mesh_home_grafana" || b["node"] != "home-server" || b["class"] != ClassCredentials || + b["provider"] != "postgres-database" || b["provider-node"] != "anchor" || b["attempts"].(int) < 60 { + t.Fatalf("%v", b) + } + + // Said again while it lasts, not every pass. + w.passes(14 * time.Minute) + if events(*said) != EventFailing { + t.Fatalf("repeated too soon: %q", events(*said)) + } + w.passes(2 * time.Minute) + if events(*said) != EventFailing+","+EventFailing { + t.Fatalf("not repeated: %q", events(*said)) + } + + // The first success takes it back. + w.a.failing = nil + w.passes(5 * time.Second) + if events(*said) != EventFailing+","+EventFailing+","+EventRecovered { + t.Fatalf("no recovery: %q", events(*said)) + } + if (*said)[2].body["consumer"] != "mesh_home_grafana" { + t.Fatal((*said)[2].body) + } +} + +func testOneSuccessBetweenFailuresStartsTheRunAgain(t *testing.T) { + w, said := standingWorld(t) + w.a.failing = errors.New("connection refused") + w.give(map[string]any{"as": "a"}) + w.passes(4 * time.Minute) + w.a.failing = nil + w.passes(5 * time.Second) + w.give(map[string]any{"as": "a", "values": map[string]any{"name": "changed"}}) + w.a.failing = errors.New("connection refused") + w.passes(4 * time.Minute) + if len(*said) != 0 { + t.Fatalf("two runs of four minutes are not one of eight: %q", events(*said)) + } +} + +// The check that failed for a day on 2026-10-05: clients already made, every minute's check refused +// at the token. A check that cannot be asked is a failure too. +func testACheckThatKeepsFailingIsAFailureToo(t *testing.T) { + w, said := standingWorld(t) + w.give(map[string]any{"as": "a"}) + w.h.Reconcile(ctx) + w.a.holdsErr = errors.New("401 invalid_grant") + w.passes(7 * time.Minute) + if events(*said) != EventFailing || (*said)[0].body["class"] != ClassCredentials { + t.Fatalf("%q %v", events(*said), *said) + } + w.a.holdsErr = nil + w.passes(time.Minute + 5*time.Second) + if events(*said) != EventFailing+","+EventRecovered { + t.Fatalf("%q", events(*said)) + } +} + +func testAnUnreadableSecretIsAnnouncedAsSuch(t *testing.T) { + w, said := standingWorld(t) + w.give(map[string]any{"as": "a"}) + os.Remove(w.dir + "/a.secret") + w.passes(6 * time.Minute) + if events(*said) != EventFailing || (*said)[0].body["class"] != ClassSecret { + t.Fatalf("%q %v", events(*said), *said) + } +} + +func testAConsumerNoLongerAskedForIsNoLongerFailing(t *testing.T) { + w, said := standingWorld(t) + w.a.failing = errors.New("boom") + w.give(map[string]any{"as": "a"}, map[string]any{"as": "b"}) + w.passes(6 * time.Minute) + if events(*said) != EventFailing+","+EventFailing { + t.Fatalf("%q", events(*said)) + } + w.give(map[string]any{"as": "a"}) + w.passes(5 * time.Second) + last := (*said)[len(*said)-1] + if last.event != EventRecovered || last.body["consumer"] != "b" || last.body["why"] != "no longer asked for" { + t.Fatalf("%v", *said) + } +} + +func testAnErrorIsClassedByItsWords(t *testing.T) { + for text, want := range map[string]string{ + `Keycloak token request failed: 401 {"error":"invalid_grant"}`: ClassCredentials, + `FATAL: password authentication failed for user "postgres"`: ClassCredentials, + `dial tcp 127.0.0.1:5432: connect: connection refused`: ClassUnreachable, + `context deadline exceeded`: ClassUnreachable, + `extension "nope" is not available`: ClassRefused, + } { + if got := ClassOf(text); got != want { + t.Errorf("%s: %s, want %s", text, got, want) + } + } +} + +func testAnAdapterThatClassesItsOwnErrorsIsBelieved(t *testing.T) { + w, said := standingWorld(t) + w.h.Adapter = classing{w.a} + w.a.failing = errors.New("anything") + w.give(map[string]any{"as": "a"}) + w.passes(6 * time.Minute) + if (*said)[0].body["class"] != "its-own" { + t.Fatal((*said)[0].body) + } +} + +type classing struct{ *recorder } + +func (classing) Class(error) string { return "its-own" } + +// A provider restarted after announcing a failure has forgotten it; its first success for each +// consumer is announced, so the controller clears what it kept rather than naming it for ever. +func testTheFirstSuccessSinceStartIsAnnouncedOnce(t *testing.T) { + w := newWorld(t) + var said []announced + w.h.Announce = func(e string, b map[string]any) { + if e != EventRetirement { + said = append(said, announced{e, b}) + } + } + w.give(map[string]any{"as": "a"}) + w.passes(3 * time.Minute) + if events(said) != EventRecovered || said[0].body["why"] != "first-success" || said[0].body["consumer"] != "a" { + t.Fatalf("%v", said) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go new file mode 100644 index 0000000..988396f --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go @@ -0,0 +1,114 @@ +package harness + +import ( + "context" + "testing" + "time" + + "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" +) + +// TestHarnessRuns groups the cases below, each one a subtest. +func TestHarnessRuns(t *testing.T) { + // TestHarness holds this file's cases, each a subtest of its own name (mesh-sdk's test budget: at most 500 unit tests). + t.Run("Harness", func(t *testing.T) { + t.Run("AConsumerIsCreatedOnceUnderTheMeshsLoginAndPassword", testAConsumerIsCreatedOnceUnderTheMeshsLoginAndPassword) + t.Run("AConsumerNoLongerAskedForIsRetiredOnceStable", testAConsumerNoLongerAskedForIsRetiredOnceStable) + t.Run("NothingReadIsNotNobodyAsking", testNothingReadIsNotNobodyAsking) + t.Run("ALostConsumerIsAppliedAgainAndBraked", testALostConsumerIsAppliedAgainAndBraked) + t.Run("AFailingCreateIsRetriedAndSaidOnce", testAFailingCreateIsRetriedAndSaidOnce) + t.Run("ScrubRemovesThePassword", testScrubRemovesThePassword) + }) + + t.Run("AMovedPathStartedTooEarlyAsksAgainUntilTheRuntimeAnswers", func(t *testing.T) { + w := newMovedWorld(t, map[[2]string]string{}) + w.run.FirstRetry = time.Millisecond + w.run.loaded = false + bus := &earlyBus{bucketBus: w.bus, refuse: 3} + p := provider.New(provider.Options{Node: "novox", Module: "postgres", Bus: w.bus, + Vault: provider.ConsumersFile{Paths: []string{w.file}}, Make: w.run.Make, Gone: w.run.Gone, + Every: time.Hour, Logf: func(string, ...any) {}}) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- w.run.Run(ctx, p, bus) }() + deadline := time.After(time.Second) + for { + w.run.mu.Lock() + loaded := w.run.loaded + w.run.mu.Unlock() + if loaded { + break + } + select { + case err := <-done: + cancel() + t.Fatalf("Run stopped before the runtime answered: %v", err) + case <-deadline: + cancel() + t.Fatal("the plans were never watched") + case <-time.After(time.Millisecond): + } + } + cancel() + <-done + if bus.refuse != 0 { + t.Fatalf("%d refusals left", bus.refuse) + } + }) + + // TestStanding holds this file's cases, each a subtest of its own name (mesh-sdk's test budget: at most 500 unit tests). + t.Run("Standing", func(t *testing.T) { + t.Run("AConsumerFailedForMinutesIsAnnouncedNamingItAndTheClass", testAConsumerFailedForMinutesIsAnnouncedNamingItAndTheClass) + t.Run("OneSuccessBetweenFailuresStartsTheRunAgain", testOneSuccessBetweenFailuresStartsTheRunAgain) + t.Run("ACheckThatKeepsFailingIsAFailureToo", testACheckThatKeepsFailingIsAFailureToo) + t.Run("AnUnreadableSecretIsAnnouncedAsSuch", testAnUnreadableSecretIsAnnouncedAsSuch) + t.Run("AConsumerNoLongerAskedForIsNoLongerFailing", testAConsumerNoLongerAskedForIsNoLongerFailing) + t.Run("AnErrorIsClassedByItsWords", testAnErrorIsClassedByItsWords) + t.Run("AnAdapterThatClassesItsOwnErrorsIsBelieved", testAnAdapterThatClassesItsOwnErrorsIsBelieved) + t.Run("TheFirstSuccessSinceStartIsAnnouncedOnce", testTheFirstSuccessSinceStartIsAnnouncedOnce) + }) +} + +// TestHarnessMoves groups the cases below, each one a subtest. +func TestHarnessMoves(t *testing.T) { + // TestMoved holds this file's cases, each a subtest of its own name (mesh-sdk's test budget: at most 500 unit tests). + t.Run("Moved", func(t *testing.T) { + t.Run("TheConsumersFileMakesWhatTheGrantFilesMake", testTheConsumersFileMakesWhatTheGrantFilesMake) + t.Run("AConsumerMissingFromOneReadRetiresNothing", testAConsumerMissingFromOneReadRetiresNothing) + t.Run("NoPlansReadRetiresNothing", testNoPlansReadRetiresNothing) + t.Run("AConsumerWithoutACredentialIsWaitedFor", testAConsumerWithoutACredentialIsWaitedFor) + t.Run("ASluggedConsumerKeepsTheLoginItsPlanNames", testASluggedConsumerKeepsTheLoginItsPlanNames) + t.Run("ASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile", testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile) + t.Run("TheConsumersFileMakesWhatTheGrantFileMakesForTheKeycloakShape", testTheConsumersFileMakesWhatTheGrantFileMakesForTheKeycloakShape) + t.Run("AConsumerNamedByItsPlanKeepsItsClientId", testAConsumerNamedByItsPlanKeepsItsClientId) + }) + + // TestGrantsMoved holds this file's cases, each a subtest of its own name (mesh-sdk's test budget: at most 500 unit tests). + t.Run("GrantsMoved", func(t *testing.T) { + t.Run("AConsumerOnlyTheGrantFileGivesIsAskedForAndMade", testAConsumerOnlyTheGrantFileGivesIsAskedForAndMade) + t.Run("ALoginAPlanNamesIsNotMadeFromTheGrantFile", testALoginAPlanNamesIsNotMadeFromTheGrantFile) + t.Run("AnUnreadableGrantFileRetiresNothing", testAnUnreadableGrantFileRetiresNothing) + t.Run("ALoginTheGrantFileStopsGivingIsRetired", testALoginTheGrantFileStopsGivingIsRetired) + }) + + // TestRetirement holds this file's cases, each a subtest of its own name (mesh-sdk's test budget: at most 500 unit tests). + t.Run("Retirement", func(t *testing.T) { + t.Run("ATransientEmptyListForFourPassesRetiresNothing", testATransientEmptyListForFourPassesRetiresNothing) + t.Run("FivePassesInTwentyFiveSecondsRetireNothingTenMinutesDo", testFivePassesInTwentyFiveSecondsRetireNothingTenMinutesDo) + t.Run("AStableSetIsRetiredAndAskedAgainReEnables", testAStableSetIsRetiredAndAskedAgainReEnables) + t.Run("AnUnreadablePassStartsTheCountAgain", testAnUnreadablePassStartsTheCountAgain) + t.Run("ADifferentSetStartsTheCountAgain", testADifferentSetStartsTheCountAgain) + t.Run("AdditionsAndChangesAreNeverDelayed", testAdditionsAndChangesAreNeverDelayed) + t.Run("TooManyWaitsForAPersonAndApproveRetiresExactlyThatSet", testTooManyWaitsForAPersonAndApproveRetiresExactlyThatSet) + t.Run("RejectedIsKeptAndNotAskedAgainUntilTheSetChanges", testRejectedIsKeptAndNotAskedAgainUntilTheSetChanges) + t.Run("AWaitingSetAskedForAgainSettles", testAWaitingSetAskedForAgainSettles) + t.Run("DeleteRemovesOnlyThatRetiredConsumer", testDeleteRemovesOnlyThatRetiredConsumer) + t.Run("TheBound", testTheBound) + t.Run("ARestartRetiresWhatTheBackendHoldsUnaskedAndAdoptsWhatItFindsDisabled", testARestartRetiresWhatTheBackendHoldsUnaskedAndAdoptsWhatItFindsDisabled) + t.Run("ABackendThatCannotBeListedIsSaidAndAskedAgain", testABackendThatCannotBeListedIsSaidAndAskedAgain) + t.Run("TheToolsAnswer", testTheToolsAnswer) + t.Run("AFailingConsumerRetiredIsSaidRecovered", testAFailingConsumerRetiredIsSaidRecovered) + t.Run("ARestartedProviderSaysOnceThatNothingWaits", testARestartedProviderSaysOnceThatNothingWaits) + t.Run("AProviderThatWaitsDoesNotSayNothingWaits", testAProviderThatWaitsDoesNotSayNothingWaits) + }) +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider.go b/provisioning/go/clients/meshprovisionerclient/provider/provider.go new file mode 100644 index 0000000..d3c0e82 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider.go @@ -0,0 +1,858 @@ +// Package provider is a providing module's half of a binding: it answers the provider's consumers itself, over the +// bus (novox/hq ADR 0349 clause 12, issue 678): the provider module, not the controller, makes each bound +// consumer's entry. +// +// **Who binds the provider.** Each node's provisioner client records the needs of the modules handed to it from +// their plans, and the controller names in each plan, per required provision and local name, the provider that +// resolution chose: `provisioning.provisions` in `.` of the assignments mirror (issue 760). A recorded +// need names no provider (issue 677's note 7), so the plan's entry is where a bind is seen, as the vault sees it +// (mesh-vault pair.go). A consumer binds this provider when an entry names this machine and this module, and +// unbinds when the entry, or the assignment, goes. A consumer still filled the old way has no plan and is not seen +// here; the controller answers it as before (ADR 0349 clause 17). +// +// **One entry per consumer, in the provider's own bucket.** For each bind the helper composes the consumer's +// served values from the provider's offer (its config already composed into it, ADR 0202 §7) and the consumer's +// identity (`${consumer:as}`, `${consumer:as:dns}`, ADR 0202 §1), and writes one entry, `.. +// [.]` of `_bound`: the one statement both ends read. It never carries a +// secret. It names the credential the vault keeps for the pair, which the consumer's client asks the vault for. +// +// **The credential is the vault's** (ADR 0349 clause 8). The helper mints nothing and reads nothing of the +// provider's own secrets: it reads the value from the vault and hands it to the provider's adapter, which makes the +// login its software keeps. An offer whose software holds one credential (ADR 0158) is the vault's one value per +// provider and provision, `provision//`, with the provider's one user name; an offer with +// a login per consumer (ADR 0048, ADR 0158 §11, the default) is each consumer's own value, +// `pair////[/]`, with the consumer's identity as its login (issue +// 677's notes 12 and 13). A credential the vault does not hold yet is waited for: nothing is made and no entry is +// written, so a consumer is never told of a login its provider has not made. A provider in the tool runner never asks +// the vault itself: its Vault is ConsumersFile, the file its machine's provisioner client fills (ADR 0373). +// +// **A multi-holder consumer gets no derived value** (ADR 0202 §10 as ADR 0349 clause 12 changes it): a consumer +// bound under a local name keeps several holders of the provision, each with its own login, while what the +// provider derives names one; its entry refuses it, naming both ends, and its client reports it. +// +// The helper speaks to the bus and the vault through the two interfaces below, so a provider wires it to its +// runtime and a test to a stand-in. Standard library only. +package provider + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "strings" + "sync" + "time" + + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// BucketOf is a providing module's own bucket of entries, one per consumer: `_bound`, the module's state +// `bound`. +func BucketOf(module string) string { return module + "_bound" } + +// Bus is what the helper needs of the bus: a key-value watch and the provider's writes to its own bucket. +type Bus interface { + // Watch hands handle every current key and value of bucket, then each change as it comes, a removed key with a + // nil value; it returns once the current values are handed, and the watch lasts until ctx ends. + Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error + Put(ctx context.Context, bucket, key string, value []byte) error + Delete(ctx context.Context, bucket, key string) error +} + +// ConditionalBus is a Bus that can also delete a key only while it still holds what the caller read. The helper uses +// it when the Bus offers it, so a pass that lets a consumer go never removes an entry another instance of the +// provider wrote after the pass read it; a Bus without it deletes unconditionally. DeleteIf returns an error that +// is stdio.ErrChanged when the key holds something else now. +type ConditionalBus interface { + Bus + DeleteIf(ctx context.Context, bucket, key string, held []byte) error +} + +// Vault is how the provider reads a credential the vault keeps, by its name, opened. +type Vault interface { + Value(ctx context.Context, name string) ([]byte, error) +} + +// CredentialForm is which credential an offer's consumers present. +type CredentialForm int + +const ( + // PerConsumer is a login of each consumer's own (ADR 0048, ADR 0158 §11): the default. + PerConsumer CredentialForm = iota + // Shared is the one credential the provider's software holds, shared with every consumer (ADR 0158). + Shared + // NoCredential is an offer whose consumers present none. + NoCredential +) + +// Offer is what the provider serves one provision with. +type Offer struct { + // Serves are the values served to each consumer, the provider's config already composed into them; a string + // may name the consumer's identity, `${consumer:as}` or `${consumer:as:dns}`. + Serves map[string]any + // Credential is the form of the credential its consumers present. A consumer's plan entry that names a + // credential form of its own wins over it (the plan's `credential-form`). + Credential CredentialForm + // User is the provider's one user name, for a Shared credential. + User string +} + +// Entry is one consumer's entry in the provider's bucket: the one statement both ends read. Never a secret. +type Entry struct { + Provider string `json:"provider"` + ProviderModule string `json:"provider-module"` + Provision string `json:"provision"` + Consumer string `json:"consumer"` + Module string `json:"module"` + Local string `json:"local,omitempty"` + // As is the consumer's identity, the mesh's (ADR 0202 §1). + As string `json:"as,omitempty"` + // User is the login the consumer presents: its identity, or the provider's one user name for a shared credential. + User string `json:"user,omitempty"` + // Values are the served values, composed for this consumer: what `${bound::}` reads. + Values map[string]any `json:"values,omitempty"` + // VaultName is the name under which the vault keeps this consumer's credential, which the consumer's client asks + // the vault for: a name, never the credential. Its key is not "credential" because the runtime's state guard + // refuses any key ending in that word, whatever the value (issue 945). + VaultName string `json:"vault-name,omitempty"` + // Refused, when set, is why the consumer is not served, naming both ends. + Refused string `json:"refused,omitempty"` + // State is the provider's outcome for the consumer, which the holder of mesh-provisioner reads to set the + // provision's key (novox/hq ADR 0570 clause 3): provided, failed with Why, or waits with what is awaited. Empty from + // a provider before it. + State string `json:"state,omitempty"` + Why string `json:"why,omitempty"` + // Contributes is what the consumer's module tells its provider about this provision (a database's name, its + // extensions), from its plan's entry (issue 776). Handed to Make and Gone; never written to the bucket. + Contributes map[string]any `json:"-"` + // At and From are where the consumer's plan says the provider is and which machine it is (issue 788): the values + // today's binding file gives that consumer. Written into Values as `at` and `from`; never alone to the bucket. + At, From string `json:"-"` + // Names are the names the controller composed for this consumer on this provision (issue 792): its own name, its + // internal name, and one of each per local name it contributes under, keyed as its binding file names them. + // Written into Values over anything the provider serves under the key; never alone to the bucket. + Names map[string]string `json:"-"` + // CredentialForm and Name are from the consumer's plan entry (issue 825): the form of the credential the + // controller chose for this bind (`shared`, `pair`, `issued` or `given`) and the holder's name. When set, they + // win over the offer's form. Never written to the bucket. + CredentialForm, Name string `json:"-"` +} + +// Key is the entry's key in the provider's bucket: `..`, and `.` for a bind under a +// local name. +func (e Entry) Key() string { + k := e.Consumer + "." + e.Module + "." + e.Provision + if e.Local != "" { + k += "." + e.Local + } + return k +} + +// Consumer is what the provider's adapter makes: the consumer's entry and the credential the vault keeps for it. +type Consumer struct { + Entry Entry + Credential []byte +} + +// Options are the provider's. +type Options struct { + // Node and Module are this provider: the machine it runs on and the module serving. + Node, Module string + // Offers are the provisions it serves, by name. + Offers map[string]Offer + Bus Bus + Vault Vault + // Make brings a consumer into being in the provider's software under the vault's credential. Asked on every + // pass for every consumer answered, so it must leave one that is already as asked alone; a rotated credential + // reaches the software this way. + Make func(ctx context.Context, c Consumer) error + // Gone, when set, hears that a consumer no longer binds the provider, before its entry is removed; an error + // keeps the entry, and it is heard again on the next pass. Retiring what was kept for it is the provider's + // (ADR 0230). + Gone func(ctx context.Context, e Entry) error + // Identity is the consumer's identity for an entry whose plan names none; ConsumerIdentity of the node and the + // module when not set. + Identity func(node, module string) string + // Every is how often a pass runs without a change watched; a minute when not set. + Every time.Duration + // Logf says what a pass did that differs from the last; the standard log when not set. + Logf func(format string, args ...any) +} + +// Provider answers its consumers. +type Provider struct { + opts Options + + mu sync.Mutex + entries map[string][]byte + // plans and provisions are the seat mesh-provisioner's states (ADR 0570 clauses 1, 2), by key. + plans map[string][]byte + provisions map[string][]byte + said map[string]string + poke chan struct{} +} + +// New is a provider answering its consumers as opts say. +func New(opts Options) *Provider { + if opts.Identity == nil { + opts.Identity = ConsumerIdentity + } + if opts.Every <= 0 { + opts.Every = time.Minute + } + if opts.Logf == nil { + opts.Logf = func(f string, a ...any) { fmt.Printf("[provider] "+f+"\n", a...) } + } + return &Provider{opts: opts, entries: map[string][]byte{}, + plans: map[string][]byte{}, provisions: map[string][]byte{}, said: map[string]string{}, poke: make(chan struct{}, 1)} +} + +// ErrNotReady is what a Make returns, wrapped or alone, when the provider cannot make a consumer's provision yet but +// will without anyone's hand (its keys not read since a restart): the entry waits and keeps serving what it served. +// Any other error is a failure, and its entry serves nothing. +var ErrNotReady = errors.New("not ready yet") + +// What a pass did for one bind. +const ( + Answered = "answered" + Refused = "refused" + Waiting = "waiting" + Failed = "failed" + Removed = "removed" + // Superseded is an entry no bind asks for any more that another instance of the provider wrote after this pass + // read it: it is left standing. + Superseded = "superseded" +) + +// Outcome is what a pass did for one consumer's entry. +type Outcome struct { + Key string `json:"key"` + Status string `json:"status"` + Why string `json:"why,omitempty"` +} + +// Run watches the binds and the provider's own entries, and passes once at start, on every change and every +// Every, until ctx ends. +func (p *Provider) Run(ctx context.Context) error { + if err := p.watch(ctx, BucketOf(p.opts.Module), p.entries); err != nil { + return fmt.Errorf("watching %s: %w", BucketOf(p.opts.Module), err) + } + for _, bucket := range []string{provisions.PlansBucket, provisions.ProvisionsBucket} { + into := p.plans + if bucket == provisions.ProvisionsBucket { + into = p.provisions + } + if err := p.watch(ctx, bucket, into); err != nil { + return fmt.Errorf("watching %s: %w", bucket, err) + } + } + tick := time.NewTicker(p.opts.Every) + defer tick.Stop() + for { + p.mu.Lock() + in := Inputs{Entries: clone(p.entries), Plans: clone(p.plans), Provisions: clone(p.provisions)} + p.mu.Unlock() + if _, err := p.passIn(ctx, in); err != nil { + p.say("pass", err.Error()) + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-p.poke: + case <-tick.C: + } + } +} + +func (p *Provider) watch(ctx context.Context, bucket string, into map[string][]byte) error { + return p.opts.Bus.Watch(ctx, bucket, func(key string, value []byte) { + p.mu.Lock() + if value == nil { + delete(into, key) + } else { + into[key] = append([]byte(nil), value...) + } + p.mu.Unlock() + if bucket != BucketOf(p.opts.Module) { + select { + case p.poke <- struct{}{}: + default: + } + } + }) +} + +// Pass reads the binds and the provider's entries from the bus and answers once: each bind of this provider gets +// its entry, and each entry of this provider no bind asks for is removed. An assignment that cannot be read, or that +// is not the one its key names, is skipped and said (an outcome of status Failed under the assignment's key, and a +// line of the log), and the entries of its consumer are left as they are, since a bind missed would read as an +// unbind; the pass goes on for the rest. Run passes by itself; Pass is for a provider that passes when it chooses. +// +// Over StateBus each call to Pass leaves two watches open for the life of the process, silenced once the call has +// ended (see StateBus): a provider that passes often should use Run, which watches once. +func (p *Provider) Pass(ctx context.Context) ([]Outcome, error) { + watchCtx, cancel := context.WithCancel(ctx) + defer cancel() + var mu sync.Mutex + read := func(bucket string) (map[string][]byte, error) { + got := map[string][]byte{} + err := p.opts.Bus.Watch(watchCtx, bucket, func(key string, value []byte) { + mu.Lock() + defer mu.Unlock() + if value == nil { + delete(got, key) + } else { + got[key] = append([]byte(nil), value...) + } + }) + mu.Lock() + defer mu.Unlock() + return clone(got), err + } + entries, err := read(BucketOf(p.opts.Module)) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", BucketOf(p.opts.Module), err) + } + plans, err := read(provisions.PlansBucket) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", provisions.PlansBucket, err) + } + provs, err := read(provisions.ProvisionsBucket) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", provisions.ProvisionsBucket, err) + } + return p.passIn(ctx, Inputs{Entries: entries, Plans: plans, Provisions: provs}) +} + +// Inputs are what one pass reads: the provider's own entries, and the seat mesh-provisioner's plans and provisions +// (novox/hq ADR 0570 clauses 1, 2), each by key. The controller's assignments are read no more (clause 2). +type Inputs struct { + Entries, Plans, Provisions map[string][]byte +} + +func (p *Provider) passIn(ctx context.Context, in Inputs) ([]Outcome, error) { + entries := in.Entries + var binds []Entry + var out []Outcome + unknown := map[string]bool{} // consumers whose assignment was skipped: their entries are not judged + // Every consumer is bound by its plan and its provision keys (ADR 0570 clauses 2, 3), and an entry no plan names is + // never let go by the provider itself (clause 5): only a retiring key with the operator's yes retires it. + seat := map[string]bool{} + gated := map[string]provisions.Provision{} + for _, key := range sortedKeys(in.Plans) { + plan, err := provisions.DecodePlan(in.Plans[key]) + if err != nil || plan.Node+"."+plan.Module != key { + o := Outcome{Key: key, Status: Failed, Why: fmt.Sprintf("the plan %s is not readable, so who it binds is not "+ + "known; it is skipped, and its entries are left as they are", key)} + out = append(out, o) + p.say(key, o.Status+" "+o.Why) + unknown[key], seat[key] = true, true + continue + } + seat[key] = true + for _, b := range plan.Plan.Provisions { + if b.Provider != p.opts.Node || b.ProviderModule != p.opts.Module { + continue + } + e := Entry{Provider: p.opts.Node, ProviderModule: p.opts.Module, Provision: b.Provision, Consumer: plan.Node, + Module: plan.Module, Local: b.Local, Contributes: b.Contributes, At: b.At, From: b.From, As: b.As, + Names: b.Names, CredentialForm: b.CredentialForm, Name: b.Name} + binds = append(binds, e) + } + } + for _, key := range sortedKeys(in.Provisions) { + if pr, err := provisions.Decode(in.Provisions[key]); err == nil && pr.Provider.Node == p.opts.Node && + pr.Provider.Module == p.opts.Module { + gated[key] = pr + } + } + // **A key no plan names is bound from the entry it carries** (issue 1126): the seat keys a consumer's provisions + // from its wanted and its newest builds, and keeps the plan at its wanted build alone, so a requirement the newest + // build adds has a key before any plan names it, and the want that waits for it waits on this provider. The plan + // wins where both say it. A key with no entry is said, never left without a word. + planned := map[string]bool{} + for _, b := range binds { + planned[b.Key()] = true + } + for _, key := range sortedKeys(gated) { + pr := gated[key] + if planned[key] || pr.State == provisions.Retiring || unknown[consumerOf(key)] { + continue + } + if pr.Plan == nil { + o := Outcome{Key: key, Status: Waiting, Why: "asked, but no plan names it and its key carries no plan entry, " + + "so whom it binds is not known; mesh-provisioner composes the entry"} + out = append(out, o) + p.say(key, o.Status+" "+o.Why) + continue + } + b := *pr.Plan + binds = append(binds, Entry{Provider: p.opts.Node, ProviderModule: p.opts.Module, Provision: pr.Provision, + Consumer: pr.Consumer.Node, Module: pr.Consumer.Module, Local: pr.Local, Contributes: b.Contributes, At: b.At, + From: b.From, As: b.As, Names: b.Names, CredentialForm: b.CredentialForm, Name: b.Name}) + } + want := map[string]bool{} + for _, b := range binds { + key := b.Key() + want[key] = true + if why, ok := p.ready(key, gated, in.Provisions); !ok { + o := Outcome{Key: key, Status: Waiting, Why: why} + out = append(out, o) + p.say(key, o.Status+" "+o.Why) + continue + } + o := p.answer(ctx, b, entries[key]) + out = append(out, o) + p.say(key, o.Status+" "+o.Why) + } + // ADR 0570 clause 5: a retiring key the operator said yes to is let go; one asked and not answered, or declined, + // keeps its entry. + for _, key := range sortedKeys(gated) { + if pr := gated[key]; pr.State == provisions.Retiring && pr.Warrant != nil && pr.Warrant.Option == "yes" { + want[key] = false + if _, held := entries[key]; held { + out = append(out, p.letGo(ctx, key, entries[key])...) + } + } + } + return out, nil +} + +// consumerOf is the `.` an entry's key starts with: its first two parts. +func consumerOf(key string) string { + parts := strings.SplitN(key, ".", 3) + if len(parts) < 3 { + return key + } + return parts[0] + "." + parts[1] +} + +// remove deletes an entry this instance read as held, and only while it is still as read when the Bus can say so. +func (p *Provider) remove(ctx context.Context, key string, held []byte) error { + if c, ok := p.opts.Bus.(ConditionalBus); ok { + return c.DeleteIf(ctx, BucketOf(p.opts.Module), key, held) + } + return p.opts.Bus.Delete(ctx, BucketOf(p.opts.Module), key) +} + +// answer makes one bind's entry and writes it when it differs from what the bucket holds. +func (p *Provider) answer(ctx context.Context, e Entry, have []byte) Outcome { + key := e.Key() + offer, offered := p.opts.Offers[e.Provision] + switch { + case !offered: + e.Refused = fmt.Sprintf("%s on %s is bound to %s on %s for %s, which %s does not serve", e.Module, + e.Consumer, e.ProviderModule, e.Provider, e.Provision, e.ProviderModule) + case e.Local != "" && derives(offer.Serves): + e.Refused = fmt.Sprintf("%s on %s keeps several holders of %s (this one is %q), and %s on %s derives %s for "+ + "each consumer from its login: each holder has a login of its own and a consumer is told one value per "+ + "requirement, so the two ends would name different things (novox/hq ADR 0202 §10, ADR 0349 clause 12)", + e.Module, e.Consumer, e.Provision, e.Local, e.ProviderModule, e.Provider, + strings.Join(derivedKeys(offer.Serves), ", ")) + } + if e.Refused != "" { + e.State, e.Why = provisions.Failed, e.Refused + return p.write(ctx, e, have, Outcome{Key: key, Status: Refused, Why: e.Refused}) + } + + // The plan's identity is the controller's, slug first; Identity only answers for an entry without one. + if e.As == "" { + e.As = p.opts.Identity(e.Consumer, e.Module) + } + values, err := ServedTo(offer.Serves, e.As) + if err != nil { + e.Refused = fmt.Sprintf("%s on %s serving %s to %s on %s: %v", e.ProviderModule, e.Provider, e.Provision, + e.Module, e.Consumer, err) + e.As = "" + e.State, e.Why = provisions.Failed, e.Refused + return p.write(ctx, e, have, Outcome{Key: key, Status: Refused, Why: e.Refused}) + } + if e.At != "" || e.From != "" { + if values == nil { + values = map[string]any{} + } + if e.At != "" { + values["at"] = e.At + } + if e.From != "" { + values["from"] = e.From + } + } + if len(e.Names) > 0 { + if values == nil { + values = map[string]any{} + } + for key, name := range e.Names { + values[key] = name + } + } + e.Values = values + var credential []byte + // The plan's form wins; the offer's is the default. + // An offer that declares no credential has none whatever the plan says: the controller writes "pair" for it. + form := e.CredentialForm + if offer.Credential == NoCredential { + form = "" + } else if form == "" { + form = map[CredentialForm]string{Shared: "shared", PerConsumer: "pair"}[offer.Credential] + } + switch form { + case "shared": + e.User = offer.User + e.VaultName = SharedName(e.Provider, e.Provision) + case "pair": + e.User = e.As + e.VaultName = PairName(e.Provider, e.Provision, e.Consumer, e.Module, e.Local) + case "issued", "given": + // A per-holder value, as mesh-vault names it (perholder.go). + e.User = e.As + e.VaultName = HolderName(e.Provider, e.Provision, e.Consumer, e.Module) + } + if e.VaultName != "" { + credential, err = p.opts.Vault.Value(ctx, e.VaultName) + if err != nil { + return Outcome{Key: key, Status: Waiting, Why: fmt.Sprintf("the vault's %s: %v", e.VaultName, err)} + } + } + if p.opts.Make != nil { + if err := p.opts.Make(ctx, Consumer{Entry: e, Credential: credential}); errors.Is(err, ErrNotReady) { + // **Not ready is no failure** (the regression of 2026-10-30: gitea after a restart, its keys not read yet, + // stopped serving every consumer): the entry says waits with why, and keeps what the last entry served, so a + // consumer goes on with it until the provider can make it again. + why := "the provider is not ready to make it yet: " + redact(err.Error(), credential) + waiting := e + var prev Entry + if len(have) > 0 && json.Unmarshal(have, &prev) == nil && prev.State == provisions.Provided { + waiting.Values, waiting.VaultName, waiting.User = prev.Values, prev.VaultName, prev.User + } else { + waiting.Values, waiting.VaultName, waiting.User = nil, "", "" + } + waiting.State, waiting.Why = provisions.Waits, why + p.write(ctx, waiting, have, Outcome{Key: key, Status: Waiting, Why: why}) + return Outcome{Key: key, Status: Waiting, Why: why} + } else if err != nil { + why := "the provider could not make it: " + redact(err.Error(), credential) + // Said in its entry, which the holder of mesh-provisioner reads (ADR 0570 clause 3), with nothing served. + failed := e + failed.Values, failed.VaultName, failed.User, failed.State, failed.Why = nil, "", "", provisions.Failed, why + p.write(ctx, failed, have, Outcome{Key: key, Status: Failed, Why: why}) + return Outcome{Key: key, Status: Failed, Why: why} + } + } + e.State = provisions.Provided + return p.write(ctx, e, have, Outcome{Key: key, Status: Answered}) +} + +// redact takes a credential out of text in the forms it can come back in: as it is, in base64 (standard and URL, +// padded and not) and as a JSON string says it (escapes included). +func redact(text string, credential []byte) string { + if len(credential) == 0 { + return text + } + quoted, _ := json.Marshal(string(credential)) + forms := []string{string(credential), base64.StdEncoding.EncodeToString(credential), + base64.RawStdEncoding.EncodeToString(credential), base64.URLEncoding.EncodeToString(credential), + base64.RawURLEncoding.EncodeToString(credential), string(quoted[1 : len(quoted)-1])} + sort.SliceStable(forms, func(i, j int) bool { return len(forms[i]) > len(forms[j]) }) + for _, f := range forms { + text = strings.ReplaceAll(text, f, "") + } + return text +} + +func (p *Provider) write(ctx context.Context, e Entry, have []byte, o Outcome) Outcome { + body, err := json.Marshal(e) + if err != nil { + return Outcome{Key: o.Key, Status: Failed, Why: err.Error()} + } + if sameJSON(have, body) { + return o + } + if err := p.opts.Bus.Put(ctx, BucketOf(p.opts.Module), o.Key, body); err != nil { + return Outcome{Key: o.Key, Status: Failed, Why: "its entry could not be written: " + err.Error()} + } + return o +} + +// say logs what a key's pass said when it differs from the last. +func (p *Provider) say(key, what string) { + p.mu.Lock() + same := p.said[key] == what + p.said[key] = what + p.mu.Unlock() + if !same { + p.opts.Logf("%s: %s", key, strings.TrimSpace(what)) + } +} + +// SharedName is the vault's one value per provider and provision (ADR 0158, issue 677). +func SharedName(provider, provision string) string { + return provisions.VaultName(provisions.FormShared, provisions.Provision{Provision: provision, Provider: provisions.End{Node: provider}}) +} + +// HolderName is the vault's value of one holder's own credential, issued or given for it (mesh-vault perholder.go). +func HolderName(provider, provision, node, module string) string { + return provisions.VaultName(provisions.FormIssued, provisions.Provision{Provision: provision, + Provider: provisions.End{Node: provider}, Consumer: provisions.End{Node: node, Module: module}}) +} + +// PairName is the vault's value of one consumer's own login (ADR 0048, ADR 0158 §11, issue 677's note 12). +func PairName(provider, provision, node, module, local string) string { + return provisions.VaultName(provisions.FormPair, provisions.Provision{Provision: provision, Local: local, + Provider: provisions.End{Node: provider}, Consumer: provisions.End{Node: node, Module: module}}) +} + +// identityUnusable is every character a consumer's identity does not hold (mesh-controller internal/catalogue +// identity.go, the one derivation). +var identityUnusable = regexp.MustCompile(`[^a-z0-9_]+`) + +// ConsumerIdentity is what one module on one machine is called wherever it authenticates: `mesh__`, +// lower case, anything but letters, digits and `_` written `_` (mesh-controller ConsumerIdentity). module is the +// identity source: the module's slug when it declares one, otherwise its name (ADR 0049). +func ConsumerIdentity(node, module string) string { + clean := func(s string) string { + return strings.Trim(identityUnusable.ReplaceAllString(strings.ToLower(s), "_"), "_") + } + return "mesh_" + clean(node) + "_" + clean(module) +} + +// consumerFact is `${consumer:}` or `${consumer::}`. +var consumerFact = regexp.MustCompile(`\$\{consumer:([a-z][a-z0-9-]*)(?::([a-z][a-z0-9-]*))?\}`) + +// ServedTo fills an offer's served values for one consumer whose identity is as (ADR 0202 §1): `${consumer:as}` as +// it is, `${consumer:as:dns}` with `-` for `_`. Only strings carry placeholders; any other fact or alphabet is +// refused, never left standing. +func ServedTo(serves map[string]any, as string) (map[string]any, error) { + if serves == nil { + return nil, nil + } + out := make(map[string]any, len(serves)) + for _, key := range sortedKeys(serves) { + text, ok := serves[key].(string) + if !ok || !strings.Contains(text, "${consumer:") { + out[key] = serves[key] + continue + } + var failed error + out[key] = consumerFact.ReplaceAllStringFunc(text, func(match string) string { + parts := consumerFact.FindStringSubmatch(match) + switch { + case parts[1] == "as" && parts[2] == "": + return as + case parts[1] == "as" && parts[2] == "dns": + return strings.ReplaceAll(as, "_", "-") + } + if failed == nil { + failed = fmt.Errorf("the value served as %q says %s, and the mesh states only ${consumer:as} and "+ + "${consumer:as:dns}", key, match) + } + return match + }) + if failed != nil { + return nil, failed + } + } + return out, nil +} + +func derives(serves map[string]any) bool { return len(derivedKeys(serves)) > 0 } + +func derivedKeys(serves map[string]any) []string { + var out []string + for _, k := range sortedKeys(serves) { + if text, ok := serves[k].(string); ok && strings.Contains(text, "${consumer:") { + out = append(out, k) + } + } + return out +} + +func clone(m map[string][]byte) map[string][]byte { + out := make(map[string][]byte, len(m)) + for k, v := range m { + out[k] = v + } + return out +} + +func sameJSON(a, b []byte) bool { + if a == nil || b == nil { + return false + } + var x, y any + if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil { + return false + } + ja, _ := json.Marshal(x) + jb, _ := json.Marshal(y) + return string(ja) == string(jb) +} + +func sortedKeys[V any](m map[string]V) []string { + if len(m) == 0 { + return nil + } + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} + +// StateBus is the Bus of a provider that runs as a module bundle over the runtime's stdio: the provider's own bucket, +// BucketOf(module), is the module's state `bound`, and provisions.PlansBucket and provisions.ProvisionsBucket are the +// states it reads, `mesh-provisioner.plans` and `mesh-provisioner.provisions` (each declared in the module's manifest). +// Any other bucket is refused. +// +// A stdio state watch lasts as long as the process and cannot be ended (stdio.KeptState.Watch), so Watch honours the +// Bus contract by silencing instead: once ctx has ended the handler is never called again, though the runtime keeps +// sending changes that are dropped. A Pass, which watches twice per call, is therefore correct, only the dropped +// changes of its two spent watches remain. +func StateBus(module string) Bus { return stateBus{module: module} } + +type stateBus struct{ module string } + +func (b stateBus) state(bucket string) (stdio.KeptState, error) { + switch bucket { + case BucketOf(b.module): + return stdio.State("bound"), nil + case provisions.PlansBucket: + return stdio.State(provisions.PlansRead), nil + case provisions.ProvisionsBucket: + return stdio.State(provisions.ProvisionsRead), nil + } + return stdio.KeptState{}, fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket) +} + +func (b stateBus) Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error { + s, err := b.state(bucket) + if err != nil { + return err + } + return s.Watch("", func(ch stdio.StateChange) error { + if ctx.Err() != nil { + return nil + } + if ch.Op == "delete" { + handle(ch.Key, nil) + } else { + handle(ch.Key, ch.Value) + } + return nil + }) +} + +func (b stateBus) Put(_ context.Context, bucket, key string, value []byte) error { + s, err := b.state(bucket) + if err != nil { + return err + } + _, err = s.Put(key, json.RawMessage(value)) + return err +} + +// DeleteIf removes the key only while it still holds what the caller read; it reads the key first, so another +// writer's change in that short while can still be lost. +func (b stateBus) DeleteIf(_ context.Context, bucket, key string, held []byte) error { + s, err := b.state(bucket) + if err != nil { + return err + } + cur, err := s.Get(key) + if err != nil { + return err + } + if cur == nil { + return nil + } + if !sameJSON(cur.Value, held) { + return fmt.Errorf("%s is not as it was read: %w", key, stdio.ErrChanged) + } + return s.Delete(key) +} + +func (b stateBus) Delete(_ context.Context, bucket, key string) error { + s, err := b.state(bucket) + if err != nil { + return err + } + return s.Delete(key) +} + +// Standing is whether an assignment's value is a standing assignment: false when `ended` is set (the module was +// taken off its node, novox/hq ADR 0408 clause 4) or `state` is not empty (a pending one). Such a value is read +// exactly as a removed key. Every other field, known or not, changes nothing (issue 1098). +func Standing(value []byte) bool { + var v struct { + Ended json.RawMessage `json:"ended"` + State json.RawMessage `json:"state"` + } + if json.Unmarshal(value, &v) != nil { + return true // not decided here: the parse that follows refuses it + } + return !fieldSet(v.Ended) && !fieldSet(v.State) +} + +// fieldSet is whether a field carries something: not absent, null or empty. +func fieldSet(f json.RawMessage) bool { + s := strings.TrimSpace(string(f)) + return s != "" && s != "null" && s != `""` +} + +// letGo lets a consumer's entry go: the provider hears it gone (Gone, which retires what it kept for the consumer), and +// the entry is removed while it is still as read. +func (p *Provider) letGo(ctx context.Context, key string, held []byte) []Outcome { + var e Entry + if err := json.Unmarshal(held, &e); err != nil || e.Provider != p.opts.Node || e.ProviderModule != p.opts.Module { + return nil // another machine's instance of this provider wrote it, or nothing of this helper's + } + said := func(o Outcome) []Outcome { + p.say(key, o.Status+" "+o.Why) + return []Outcome{o} + } + if p.opts.Gone != nil { + if err := p.opts.Gone(ctx, e); err != nil { + return said(Outcome{Key: key, Status: Failed, Why: "no longer bound, and the provider could not let it go: " + err.Error()}) + } + } + if err := p.remove(ctx, key, held); err != nil { + if errors.Is(err, stdio.ErrChanged) { + return said(Outcome{Key: key, Status: Superseded, Why: "another instance of the provider wrote its entry " + + "since this pass read it, so it stands"}) + } + return said(Outcome{Key: key, Status: Failed, Why: "no longer bound, and its entry could not be removed: " + err.Error()}) + } + p.say(key, Removed) + p.mu.Lock() + delete(p.said, key) + p.mu.Unlock() + return []Outcome{{Key: key, Status: Removed}} +} + +// ready says whether the provision under key may be made now (novox/hq ADR 0570 clause 3): its key is on the seat, it is +// asked or provided, and every provision it depends on is provided. Otherwise why it waits, naming the key or the +// dependency. +func (p *Provider) ready(key string, mine map[string]provisions.Provision, all map[string][]byte) (string, bool) { + pr, ok := mine[key] + if !ok { + return "mesh-provisioner holds no key " + key + " naming this provider yet", false + } + if pr.State != provisions.Asked && pr.State != provisions.Provided { + return "its key is " + pr.State + ": " + pr.Why, false + } + for _, d := range pr.DependsOn { + dep, err := provisions.Decode(all[d]) + if err != nil { + return "it depends on " + d + ", which mesh-provisioner holds no readable key of", false + } + if dep.State != provisions.Provided { + return "it depends on " + d + ", which is " + dep.State, false + } + } + return "", true +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go new file mode 100644 index 0000000..594052a --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go @@ -0,0 +1,294 @@ +package provider + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "slices" + "strings" + "sync" + "testing" + "time" + + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// standIn is a stand-in bus: buckets of keys and values, and watches that hand the current values first and then +// every change, as the bus's key-value watch does. +type standIn struct { + mu sync.Mutex + buckets map[string]map[string][]byte + watchers map[string][]func(string, []byte) +} + +func newStandIn() *standIn { + return &standIn{buckets: map[string]map[string][]byte{}, watchers: map[string][]func(string, []byte){}} +} + +func (s *standIn) Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error { + s.mu.Lock() + current := map[string][]byte{} + for k, v := range s.buckets[bucket] { + current[k] = v + } + s.watchers[bucket] = append(s.watchers[bucket], handle) + s.mu.Unlock() + for _, k := range sortedKeys(current) { + handle(k, current[k]) + } + return nil +} + +func (s *standIn) Put(_ context.Context, bucket, key string, value []byte) error { + s.mu.Lock() + if s.buckets[bucket] == nil { + s.buckets[bucket] = map[string][]byte{} + } + s.buckets[bucket][key] = append([]byte(nil), value...) + ws := slices.Clone(s.watchers[bucket]) + s.mu.Unlock() + for _, w := range ws { + w(key, value) + } + return nil +} + +func (s *standIn) Delete(_ context.Context, bucket, key string) error { + s.mu.Lock() + delete(s.buckets[bucket], key) + ws := slices.Clone(s.watchers[bucket]) + s.mu.Unlock() + for _, w := range ws { + w(key, nil) + } + return nil +} + +// DeleteIf removes the key only while it still holds what the caller read. +func (s *standIn) DeleteIf(ctx context.Context, bucket, key string, held []byte) error { + s.mu.Lock() + cur := s.buckets[bucket][key] + s.mu.Unlock() + if !bytes.Equal(cur, held) { + return fmt.Errorf("%s: %w", key, stdio.ErrChanged) + } + return s.Delete(ctx, bucket, key) +} + +func (s *standIn) get(bucket, key string) []byte { + s.mu.Lock() + defer s.mu.Unlock() + return s.buckets[bucket][key] +} + +func (s *standIn) keys(bucket string) []string { + s.mu.Lock() + defer s.mu.Unlock() + return sortedKeys(s.buckets[bucket]) +} + +// everything is every value the bus holds, for a test that no secret is in a bucket. +func (s *standIn) everything() []byte { + s.mu.Lock() + defer s.mu.Unlock() + var all []byte + for _, b := range s.buckets { + for k, v := range b { + all = append(all, k...) + all = append(all, v...) + } + } + return all +} + +// vault is a stand-in vault: a value per name, or nothing yet. +type vault struct { + mu sync.Mutex + values map[string]string + asked []string +} + +func (v *vault) Value(_ context.Context, name string) ([]byte, error) { + v.mu.Lock() + defer v.mu.Unlock() + v.asked = append(v.asked, name) + val, ok := v.values[name] + if !ok { + return nil, fmt.Errorf("the vault holds no %s yet", name) + } + return []byte(val), nil +} + +// maker records what the provider's adapter was asked to make, with the credential it was handed. +type maker struct { + mu sync.Mutex + made map[string]string + // contributed is what each consumer's entry carried of its plan's contribution. + contributed map[string]map[string]any + // as is the identity each consumer's entry was made under. + as map[string]string + gone []string + fail error + calls int +} + +func (m *maker) Make(_ context.Context, c Consumer) error { + m.mu.Lock() + defer m.mu.Unlock() + m.calls++ + if m.fail != nil { + return m.fail + } + if m.made == nil { + m.made = map[string]string{} + } + m.made[c.Entry.Key()] = string(c.Credential) + if m.contributed == nil { + m.contributed = map[string]map[string]any{} + } + m.contributed[c.Entry.Key()] = c.Entry.Contributes + if m.as == nil { + m.as = map[string]string{} + } + m.as[c.Entry.Key()] = c.Entry.As + return nil +} + +func (m *maker) Gone(_ context.Context, e Entry) error { + m.mu.Lock() + defer m.mu.Unlock() + m.gone = append(m.gone, e.Key()) + return nil +} + +// assign puts a consumer's plan with its provisions on the seat mesh-provisioner, each provision's key provided, as its +// holder writes them (novox/hq ADR 0570 clauses 1, 2): what a provider binds by. +func assign(t *testing.T, bus *standIn, node, module string, binds ...map[string]string) { + t.Helper() + var ps []catalogue.PlanProvision + for _, b := range binds { + raw, _ := json.Marshal(b) + var pp catalogue.PlanProvision + if err := json.Unmarshal(raw, &pp); err != nil { + t.Fatal(err) + } + ps = append(ps, pp) + } + planned(t, bus, node, module, ps...) +} + +// planned puts a consumer's plan of these provisions on the seat, each provision's key provided. +func planned(t *testing.T, bus *standIn, node, module string, ps ...catalogue.PlanProvision) { + t.Helper() + for _, pp := range ps { + pr := provisions.Provision{Provision: pp.Provision, Local: pp.Local, Consumer: provisions.End{Module: module, Node: node}, + Provider: provisions.End{Module: pp.ProviderModule, Node: pp.Provider}, State: provisions.Provided} + key, err := pr.Key() + if err != nil { + t.Fatal(err) + } + value, err := provisions.Encode(pr) + if err != nil { + t.Fatal(err) + } + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, value) + } + raw, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) + if err != nil { + t.Fatal(err) + } + if err := bus.Put(context.Background(), provisions.PlansBucket, node+"."+module, raw); err != nil { + t.Fatal(err) + } +} + +// retire marks a consumer's provision retiring, with the operator's yes (novox/hq ADR 0570 clause 5): the one way a +// provider lets a consumer go. +func retire(t *testing.T, bus *standIn, node, module, provision, provider, providerModule string) { + t.Helper() + pr := provisions.Provision{Provision: provision, Consumer: provisions.End{Module: module, Node: node}, + Provider: provisions.End{Module: providerModule, Node: provider}, State: provisions.Retiring, Why: module + " left " + node, + Warrant: &provisions.Warrant{Ask: "a-1", Option: "yes", By: "the operator"}} + key, _ := pr.Key() + raw, err := provisions.Encode(pr) + if err != nil { + t.Fatal(err) + } + _ = bus.Delete(context.Background(), provisions.PlansBucket, node+"."+module) + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, raw) +} + +func bind(provision, local, provider, providerModule string) map[string]string { + b := map[string]string{"provision": provision, "provider": provider, "provider-module": providerModule} + if local != "" { + b["local"] = local + } + return b +} + +func entryOf(t *testing.T, bus *standIn, key string) Entry { + t.Helper() + raw := bus.get(BucketOf("postgres"), key) + if raw == nil { + t.Fatalf("no entry %s in %s; it holds %v", key, BucketOf("postgres"), bus.keys(BucketOf("postgres"))) + } + var e Entry + if err := json.Unmarshal(raw, &e); err != nil { + t.Fatalf("entry %s: %v", key, err) + } + return e +} + +// postgres is a provider of postgres-database on novox with a login per consumer (ADR 0048, ADR 0158 §11), its +// served values derived from each consumer's identity (ADR 0202 §1). +func postgres(bus *standIn, v *vault, m *maker) *Provider { + return New(Options{ + Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Gone: m.Gone, + Offers: map[string]Offer{ + "postgres-database": {Credential: PerConsumer, Serves: map[string]any{ + "host": "postgres.novox.mesh", "port": 5432, "database": "${consumer:as}"}}, + }, + Logf: func(string, ...any) {}, + }) +} + +func waitFor(t *testing.T, ok func() bool) { + t.Helper() + deadline := time.Now().Add(3 * time.Second) + for !ok() { + if time.Now().After(deadline) { + t.Fatal("not within 3s") + } + time.Sleep(5 * time.Millisecond) + } +} + +// refusedByTheStateGuard mirrors the key rule of the runtime's state guard (mesh-tools node-tools internal/bus +// state.go): a JSON key whose lowercased name, with `-`, `_` and spaces taken out, ends in one of these is refused. +func refusedByTheStateGuard(key string) bool { + k := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(key)) + for _, end := range []string{"password", "passwd", "secret", "token", "credential", "credentials", + "authorization", "apikey", "privatekey", "accesskey", "cookie"} { + if strings.HasSuffix(k, end) { + return true + } + } + return false +} + +func keysOf(v any, into *[]string) { + switch x := v.(type) { + case map[string]any: + for k, c := range x { + *into = append(*into, k) + keysOf(c, into) + } + case []any: + for _, c := range x { + keysOf(c, into) + } + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/statebus_test.go b/provisioning/go/clients/meshprovisionerclient/provider/statebus_test.go new file mode 100644 index 0000000..9475411 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/statebus_test.go @@ -0,0 +1,54 @@ +package provider + +import ( + "bufio" + "encoding/json" + "io" + "testing" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" +) + +// runtime drives a bundle over pipes as the runtime does: it reads what the bundle says and sends what the runtime would. +type runtime struct { + t *testing.T + lines *bufio.Scanner + in io.Writer +} + +func (r *runtime) next() map[string]any { + done := make(chan map[string]any, 1) + go func() { + if r.lines.Scan() { + var m map[string]any + _ = json.Unmarshal(r.lines.Bytes(), &m) + done <- m + } + }() + select { + case m := <-done: + return m + case <-time.After(3 * time.Second): + r.t.Fatal("the bundle said nothing") + return nil + } +} + +func (r *runtime) send(m map[string]any) { + b, _ := json.Marshal(m) + _, _ = r.in.Write(append(b, '\n')) +} + +func (r *runtime) call(id int, tool string, args map[string]any) { + r.send(map[string]any{"jsonrpc": "2.0", "id": id, "method": "tools/call", "params": map[string]any{"name": tool, "arguments": args}}) +} + +func serve(t *testing.T, tools []stdio.Tool) *runtime { + t.Setenv(stdio.ServedAs, "keeper") + inR, inW := io.Pipe() + outR, outW := io.Pipe() + go func() { _ = stdio.ServeOn("", tools, inR, outW) }() + t.Cleanup(func() { _ = inW.Close() }) + return &runtime{t: t, lines: bufio.NewScanner(outR), in: inW} +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go new file mode 100644 index 0000000..2dbf2d6 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go @@ -0,0 +1,909 @@ +package provider + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + "time" + + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// TestEntries groups the cases below, each one a subtest. +func TestEntries(t *testing.T) { + // A provider answers each consumer that binds it with an entry of its own in its own bucket: composed from its + // served values and the consumer's identity, naming the credential the vault keeps for that consumer, and never + // the credential itself; its adapter is handed each consumer's own value read from the vault. + t.Run("EachConsumerGetsAnEntryOfItsOwnAndTheVaultsValue", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{ + "pair/novox/postgres-database/ace/letta": "letta-secret-1", + "pair/novox/postgres-database/g14/umami": "umami-secret-2", + }} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) + p := postgres(bus, v, m) + out, err := p.Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"ace.letta.postgres-database", + "g14.umami.postgres-database"}) { + t.Fatalf("entries %v", got) + } + e := entryOf(t, bus, "ace.letta.postgres-database") + if e.Provider != "novox" || e.ProviderModule != "postgres" || e.Consumer != "ace" || e.Module != "letta" || + e.As != "mesh_ace_letta" || e.User != "mesh_ace_letta" || + e.VaultName != "pair/novox/postgres-database/ace/letta" || e.Refused != "" { + t.Fatalf("entry %+v", e) + } + if e.Values["database"] != "mesh_ace_letta" || e.Values["host"] != "postgres.novox.mesh" || + e.Values["port"] != float64(5432) { + t.Fatalf("values %v", e.Values) + } + if m.made["ace.letta.postgres-database"] != "letta-secret-1" || + m.made["g14.umami.postgres-database"] != "umami-secret-2" { + t.Fatalf("made %v", m.made) + } + if bytes.Contains(bus.everything(), []byte("secret-")) { + t.Fatal("a credential is in a bucket") + } + for _, o := range out { + if o.Status != Answered { + t.Fatalf("outcome %+v", o) + } + } + }) + + // A provider whose offer shares one credential (ADR 0158) names the vault's one value for the provision, and its + // one user name, to every consumer. + t.Run("ASharedCredentialIsTheVaultsOneValueAndTheProvidersOneUser", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"provision/novox/sonarr-api": "one-value"}} + assign(t, bus, "ace", "overseerr", bind("sonarr-api", "", "novox", "sonarr")) + assign(t, bus, "g14", "homepage", bind("sonarr-api", "", "novox", "sonarr")) + p := New(Options{Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"sonarr-api": {Credential: Shared, User: "sonarr", + Serves: map[string]any{"url": "http://sonarr.novox.mesh:8989"}}}}) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + for _, key := range []string{"ace.overseerr.sonarr-api", "g14.homepage.sonarr-api"} { + var e Entry + if err := json.Unmarshal(bus.get(BucketOf("sonarr"), key), &e); err != nil { + t.Fatalf("%s: %v", key, err) + } + if e.VaultName != "provision/novox/sonarr-api" || e.User != "sonarr" || e.Values["url"] == nil { + t.Fatalf("%s: %+v", key, e) + } + if m.made[key] != "one-value" { + t.Fatalf("%s made with %q", key, m.made[key]) + } + } + if bytes.Contains(bus.everything(), []byte("one-value")) { + t.Fatal("the shared credential is in a bucket") + } + }) + + // A consumer that keeps several holders of one provision is served no derived value: its entry refuses it, naming + // both ends, so its client can report it (ADR 0349 clause 12, ADR 0202 §10); nothing is made for it. + t.Run("AMultiHolderConsumerIsRefusedADerivedValueNamingBothEnds", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta/archive": "x-secret"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "archive", "novox", "postgres")) + out, err := postgres(bus, v, m).Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + e := entryOf(t, bus, "ace.letta.postgres-database.archive") + for _, says := range []string{"letta on ace", "postgres on novox", "postgres-database", "archive"} { + if !strings.Contains(e.Refused, says) { + t.Fatalf("the refusal does not name %q: %s", says, e.Refused) + } + } + if e.Values != nil || e.VaultName != "" { + t.Fatalf("a refused entry carries values: %+v", e) + } + if m.calls != 0 { + t.Fatal("the adapter made a refused consumer") + } + if len(out) != 1 || out[0].Status != Refused { + t.Fatalf("outcomes %+v", out) + } + }) + + // A consumer bound under a local name is served when the offer derives nothing from it: its credential is its own + // pair row under that local name. + t.Run("ALocalNameIsServedWhenNothingIsDerived", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/redis-cache/ace/letta/queue": "q-secret"}} + assign(t, bus, "ace", "letta", bind("redis-cache", "queue", "novox", "redis")) + p := New(Options{Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"redis-cache": {Credential: PerConsumer, Serves: map[string]any{"port": 6379}}}}) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + var e Entry + if err := json.Unmarshal(bus.get(BucketOf("redis"), "ace.letta.redis-cache.queue"), &e); err != nil { + t.Fatal(err) + } + if e.Refused != "" || e.VaultName != "pair/novox/redis-cache/ace/letta/queue" || e.Local != "queue" { + t.Fatalf("entry %+v", e) + } + if m.made["ace.letta.redis-cache.queue"] != "q-secret" { + t.Fatalf("made %v", m.made) + } + }) + + // A consumer's plan entry may carry what its module contributes to the provider (issue 776): the helper hands it to + // the provider's adapter with the consumer, and does not write it to the bucket. + t.Run("AConsumersContributionIsHandedToTheProviderPerConsumer", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{ + "pair/novox/postgres-database/ace/nextcloud": "a", "pair/novox/postgres-database/g14/umami": "b"}} + var pp catalogue.PlanProvision + if err := json.Unmarshal([]byte(`{"provision":"postgres-database","provider":"novox","provider-module":"postgres",`+ + `"contributes":{"name":"nextcloud","extensions":["pg_trgm"]}}`), &pp); err != nil { + t.Fatal(err) + } + planned(t, bus, "ace", "nextcloud", pp) + assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) + if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil { + t.Fatal(err) + } + want := map[string]any{"name": "nextcloud", "extensions": []any{"pg_trgm"}} + if got := m.contributed["ace.nextcloud.postgres-database"]; !reflect.DeepEqual(got, want) { + t.Fatalf("the adapter was handed the contribution %v, want %v", got, want) + } + if got := m.contributed["g14.umami.postgres-database"]; got != nil { + t.Fatalf("a consumer contributing nothing was handed %v", got) + } + if bytes.Contains(bus.get(BucketOf("postgres"), "ace.nextcloud.postgres-database"), []byte("pg_trgm")) { + t.Fatal("a contribution is in the provider's bucket") + } + }) + + // A consumer's plan entry carries where its provider is, which machine, and who the consumer is (issue 788): the + // entry's values carry `at` and `from` as the binding file gives them, and its `as` is the plan's, so a module with a + // slug keeps the login it has today. An entry of a plan without them is as it was. + t.Run("AConsumersEntryCarriesAtFromAndTheAsOfItsPlan", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{ + "pair/novox/postgres-database/ace/claude-code": "a", "pair/novox/postgres-database/g14/umami": "b"}} + var pp catalogue.PlanProvision + if err := json.Unmarshal([]byte(`{"provision":"postgres-database","provider":"novox","provider-module":"postgres",`+ + `"at":"10.7.0.1","from":"novox","as":"mesh_ace_agent"}`), &pp); err != nil { + t.Fatal(err) + } + planned(t, bus, "ace", "claude-code", pp) + assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) + if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil { + t.Fatal(err) + } + var e Entry + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.claude-code.postgres-database"), &e); err != nil { + t.Fatal(err) + } + if e.Values["at"] != "10.7.0.1" || e.Values["from"] != "novox" || e.Values["port"] == nil { + t.Fatalf("the entry's values are %v", e.Values) + } + if e.As != "mesh_ace_agent" || e.User != "mesh_ace_agent" || m.as["ace.claude-code.postgres-database"] != "mesh_ace_agent" { + t.Fatalf("the entry is %+v, made as %q", e, m.as["ace.claude-code.postgres-database"]) + } + var plain Entry + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "g14.umami.postgres-database"), &plain); err != nil { + t.Fatal(err) + } + if _, has := plain.Values["at"]; has || plain.As != "mesh_g14_umami" { + t.Fatalf("an entry of a plan without them changed: %+v", plain) + } + }) + + // A consumer's plan entry may carry the names the controller composed for it (issue 792): each is copied into the + // entry's values under its key, over anything the provider serves under it. An entry without them is as it was. + t.Run("AConsumersEntryCarriesTheNamesOfItsPlan", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{ + "pair/novox/postgres-database/ace/gitea": "a", "pair/novox/postgres-database/g14/umami": "b"}} + var pp catalogue.PlanProvision + if err := json.Unmarshal([]byte(`{"provision":"postgres-database","provider":"novox","provider-module":"postgres",`+ + `"names":{"name":"git.example.org","internal-name":"git.mesh","name-web":"web.example.org"}}`), &pp); err != nil { + t.Fatal(err) + } + planned(t, bus, "ace", "gitea", pp) + assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) + if _, err := postgres(bus, v, m).Pass(context.Background()); err != nil { + t.Fatal(err) + } + var e Entry + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.gitea.postgres-database"), &e); err != nil { + t.Fatal(err) + } + if e.Values["name"] != "git.example.org" || e.Values["internal-name"] != "git.mesh" || + e.Values["name-web"] != "web.example.org" || e.Values["port"] == nil { + t.Fatalf("the entry's values are %v", e.Values) + } + var plain Entry + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "g14.umami.postgres-database"), &plain); err != nil { + t.Fatal(err) + } + if _, has := plain.Values["name"]; has { + t.Fatalf("an entry of a plan without names changed: %+v", plain) + } + }) + + // The plan's credential form and holder name win over the offer's; the offer's form is the default (issue 825, + // finding 4). + t.Run("ThePlansCredentialFormWinsOverTheOffers", func(t *testing.T) { + run := func(offer CredentialForm, plan map[string]string) Entry { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "p", + "provision/novox/postgres-database": "s", "provision/novox/postgres-database/ace/letta": "h"}} + b := bind("postgres-database", "", "novox", "postgres") + for k, val := range plan { + b[k] = val + } + assign(t, bus, "ace", "letta", b) + p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"postgres-database": {Credential: offer, User: "shared-user"}}}) + out, err := p.Pass(context.Background()) + if err != nil || len(out) != 1 || out[0].Status != Answered { + t.Fatalf("%v %+v", err, out) + } + return entryOf(t, bus, "ace.letta.postgres-database") + } + if e := run(PerConsumer, map[string]string{"credential-form": "shared"}); e.VaultName != + "provision/novox/postgres-database" || e.User != "shared-user" { + t.Fatalf("shared over pair: %+v", e) + } + if e := run(Shared, map[string]string{"credential-form": "pair"}); e.VaultName != + "pair/novox/postgres-database/ace/letta" || e.User != "mesh_ace_letta" { + t.Fatalf("pair over shared: %+v", e) + } + for _, form := range []string{"issued", "given"} { + if e := run(PerConsumer, map[string]string{"credential-form": form, "name": "main"}); e.VaultName != + "provision/novox/postgres-database/ace/letta" || e.User != "mesh_ace_letta" { + t.Fatalf("%s holder: %+v", form, e) + } + } + if e := run(Shared, nil); e.VaultName != "provision/novox/postgres-database" { + t.Fatalf("the offer's form is the default: %+v", e) + } + { + // An offer that declares no credential is answered with none whatever form the plan says (the controller writes + // "pair" for such an offer): no vault name, and the Vault is never asked, so a provider with no Vault works (issue 825, + // finding 4; issue 923). + bus := newStandIn() + b := bind("route", "", "novox", "route-proxy") + b["credential-form"] = "pair" + assign(t, bus, "ace", "letta", b) + p := New(Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"route": {Credential: NoCredential}}}) + out, err := p.Pass(context.Background()) + if err != nil || len(out) != 1 || out[0].Status != Answered { + t.Fatalf("%v %+v", err, out) + } + var e Entry + if err := json.Unmarshal(bus.get(BucketOf("route-proxy"), "ace.letta.route"), &e); err != nil || e.VaultName != "" || + e.User != "" { + t.Fatalf("%v %+v", err, e) + } + } + }) + + // Issue 1126 review 1 (mcp-endpoint asked on ace and shanks, 2026-11-02 11:56): a consumer's key composed from its newest + // build carries the plan entry it was composed from, though its wanted build's plan names no such provision; the + // provider binds it from the key and provides it, so the want that waits for it moves. A key of this provider that + // carries no entry and no plan names is said, never bound silently or left without a word. + t.Run("AKeyNoPlanNamesIsBoundFromTheEntryItCarries", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "letta-secret-1"}} + // letta's wanted plan names nothing of postgres; its key, from its newest build, carries the entry. + planned(t, bus, "ace", "letta") + entry := catalogue.PlanProvision{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres", As: "mesh_ace_letta"} + pr := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "letta", Node: "ace"}, + Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked, Plan: &entry} + raw, err := provisions.Encode(pr) + if err != nil { + t.Fatal(err) + } + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "ace.letta.postgres-database", raw) + // And umami's key carries none: asked, but no plan names it. + bare := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "umami", Node: "g14"}, + Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked} + raw, _ = provisions.Encode(bare) + _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "g14.umami.postgres-database", raw) + + out, err := postgres(bus, v, m).Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if e := entryOf(t, bus, "ace.letta.postgres-database"); e.Consumer != "ace" || e.Module != "letta" || e.Refused != "" { + t.Errorf("the entry %+v", e) + } + said := map[string]Outcome{} + for _, o := range out { + said[o.Key] = o + } + if said["ace.letta.postgres-database"].Status != Answered { + t.Errorf("letta: %+v", said["ace.letta.postgres-database"]) + } + if o := said["g14.umami.postgres-database"]; o.Status != Waiting || !strings.Contains(o.Why, "no plan names it") { + t.Errorf("umami: %+v", o) + } + if bus.get(BucketOf("postgres"), "g14.umami.postgres-database") != nil { + t.Error("a key with no entry and no plan was bound") + } + }) +} + +// TestBinds groups the cases below, each one a subtest. +func TestBinds(t *testing.T) { + // A consumer that unbinds loses its entry and the adapter hears it is gone; an entry another machine's instance of + // the same provider wrote is not this one's to remove. + t.Run("AnUnbindRemovesTheEntryAndLeavesAnotherProvidersAlone", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + other, _ := json.Marshal(Entry{Provider: "shanks", ProviderModule: "postgres", Provision: "postgres-database", + Consumer: "g14", Module: "gitea"}) + _ = bus.Put(context.Background(), BucketOf("postgres"), "g14.gitea.postgres-database", other) + p := postgres(bus, v, m) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + retire(t, bus, "ace", "letta", "postgres-database", "novox", "postgres") + out, err := p.Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"g14.gitea.postgres-database"}) { + t.Fatalf("entries left %v", got) + } + if !slices.Equal(m.gone, []string{"ace.letta.postgres-database"}) { + t.Fatalf("gone %v", m.gone) + } + if len(out) != 1 || out[0].Status != Removed { + t.Fatalf("outcomes %+v", out) + } + }) + + // Only the binds naming this provider, on this machine, are its own; a bind naming it for a provision it does not + // offer is refused, naming both ends. + t.Run("OnlyBindsNamingThisProviderAreAnswered", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "shanks", "postgres"), + bind("mongodb-database", "", "novox", "postgres")) + assign(t, bus, "ace", "other", bind("postgres-database", "", "novox", "mssql")) + out, err := postgres(bus, v, m).Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if got := bus.keys(BucketOf("postgres")); !slices.Equal(got, []string{"ace.letta.mongodb-database"}) { + t.Fatalf("entries %v", got) + } + e := entryOf(t, bus, "ace.letta.mongodb-database") + if !strings.Contains(e.Refused, "postgres on novox") || !strings.Contains(e.Refused, "letta on ace") { + t.Fatalf("refusal %q", e.Refused) + } + if len(out) != 1 || m.calls != 0 { + t.Fatalf("outcomes %+v, %d made", out, m.calls) + } + }) + + // A plan that cannot be read is skipped and said, by its key and why; the rest of the pass goes on (issue 825, + // finding 3). Its consumer's own entry is left alone, since a bind missed would read as an unbind. + t.Run("AnUnreadableAssignmentIsSkippedAndSaidAndTheRestGoesOn", func(t *testing.T) { + for name, bad := range map[string]string{"not json": "{not json", + "another key": `{"v":1,"node":"g14","module":"umami","plan":{"templates":{"source":"","digest":""},"files":null}}`} { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s", + "pair/novox/postgres-database/g14/umami": "u"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + p := postgres(bus, v, m) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + _ = bus.Put(context.Background(), provisions.PlansBucket, "ace.letta", []byte(bad)) + assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) + var logged []string + p.opts.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } + out, err := p.Pass(context.Background()) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if bus.get(BucketOf("postgres"), "g14.umami.postgres-database") == nil { + t.Fatalf("%s: the good consumer was not answered", name) + } + if bus.get(BucketOf("postgres"), "ace.letta.postgres-database") == nil || len(m.gone) != 0 { + t.Fatalf("%s: the entry of the unreadable assignment's consumer was removed", name) + } + said := false + for _, o := range out { + if o.Key == "ace.letta" && o.Status == Failed && o.Why != "" { + said = true + } + } + if !said || !strings.Contains(strings.Join(logged, "\n"), "ace.letta") { + t.Fatalf("%s: the skipped assignment is not said: %+v, %q", name, out, logged) + } + } + }) + + // Run watches the binds on the stand-in bus: a consumer that binds is answered, and one that unbinds is removed, + // without a pass asked by hand. + t.Run("RunAnswersBindsAndUnbindsAsTheyAreWatched", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}} + p := postgres(bus, v, m) + p.opts.Every = time.Hour + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { done <- p.Run(ctx) }() + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + waitFor(t, func() bool { return bus.get(BucketOf("postgres"), "ace.letta.postgres-database") != nil }) + retire(t, bus, "ace", "letta", "postgres-database", "novox", "postgres") + waitFor(t, func() bool { return bus.get(BucketOf("postgres"), "ace.letta.postgres-database") == nil }) + cancel() + if err := <-done; err != nil && !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + }) + + // An instance deletes only an entry it wrote, and only while the entry is as it read it: one that another instance of + // the provider wrote after the consumer moved stands (issue 825, finding 5). + t.Run("AnEntryAnotherInstanceWroteAfterTheMoveStands", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + a := postgres(bus, v, m) + if _, err := a.Pass(context.Background()); err != nil { + t.Fatal(err) + } + // The consumer moves to the instance on shanks: A's pass reads its own entry and the unbind, and while it lets + // the consumer go, B writes the consumer's entry. + retire(t, bus, "ace", "letta", "postgres-database", "novox", "postgres") + theirs, _ := json.Marshal(Entry{Provider: "shanks", ProviderModule: "postgres", Provision: "postgres-database", + Consumer: "ace", Module: "letta", VaultName: "pair/shanks/postgres-database/ace/letta"}) + a.opts.Gone = func(ctx context.Context, e Entry) error { + return bus.Put(ctx, BucketOf("postgres"), e.Key(), theirs) + } + out, err := a.Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if got := bus.get(BucketOf("postgres"), "ace.letta.postgres-database"); !bytes.Equal(got, theirs) { + t.Fatalf("the other instance's entry was removed: %s", got) + } + for _, o := range out { + if o.Status == Removed { + t.Fatalf("outcomes %+v", out) + } + } + }) +} + +// TestCredentialsAndKeys groups the cases below, each one a subtest. +func TestCredentialsAndKeys(t *testing.T) { + // A bind whose credential the vault does not hold yet is waited for: nothing is made and no entry is written, so a + // consumer is never told of a login its provider has not made; once the vault holds it, it is answered. + t.Run("ACredentialTheVaultDoesNotHoldYetIsWaitedFor", func(t *testing.T) { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + p := postgres(bus, v, m) + out, err := p.Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(out) != 1 || out[0].Status != Waiting || !strings.Contains(out[0].Why, "pair/novox/postgres-database/ace/letta") { + t.Fatalf("outcomes %+v", out) + } + if bus.keys(BucketOf("postgres")) != nil || m.calls != 0 { + t.Fatal("a consumer was answered before its credential was there") + } + v.values["pair/novox/postgres-database/ace/letta"] = "late" + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + if entryOf(t, bus, "ace.letta.postgres-database").Refused != "" || m.made["ace.letta.postgres-database"] != "late" { + t.Fatal("not answered once the vault held the value") + } + }) + + // An adapter that fails is said with the credential taken out of its words, and its entry says it failed, serving + // nothing (novox/hq ADR 0570 clause 3: the provider's outcome is its entry, which the holder of mesh-provisioner reads). + t.Run("AFailedMakeIsSaidWithoutTheCredential", func(t *testing.T) { + bus, m := newStandIn(), &maker{fail: errors.New(`password "hunter2-secret" rejected`)} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "hunter2-secret"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + var logged []string + p := postgres(bus, v, m) + p.opts.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } + out, err := p.Pass(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(out) != 1 || out[0].Status != Failed || strings.Contains(out[0].Why, "hunter2") { + t.Fatalf("outcomes %+v", out) + } + if strings.Contains(strings.Join(logged, "\n"), "hunter2") || len(logged) == 0 { + t.Fatalf("logged %q", logged) + } + var e Entry + raw := bus.get(BucketOf("postgres"), "ace.letta.postgres-database") + if err := json.Unmarshal(raw, &e); err != nil || e.State != "failed" || e.Values != nil || e.VaultName != "" || + strings.Contains(string(raw), "hunter2") { + t.Fatalf("the failed entry is %s", raw) + } + + // A provider not ready yet (gitea after a restart, its keys not read; the regression of 2026-10-30) is no failure: + // a first entry waits serving nothing, and once made, a later not ready keeps serving what it served. + m.fail = fmt.Errorf("keys not read yet: %w", ErrNotReady) + _ = bus.Delete(context.Background(), BucketOf("postgres"), "ace.letta.postgres-database") + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Waiting { + t.Fatalf("not ready at first: %+v", out) + } + raw = bus.get(BucketOf("postgres"), "ace.letta.postgres-database") + if err := json.Unmarshal(raw, &e); err != nil || e.State != "waits" || !strings.Contains(e.Why, "keys not read yet") || e.Values != nil { + t.Fatalf("the first entry not ready is %s", raw) + } + m.fail = nil + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Answered { + t.Fatalf("made: %+v", out) + } + var made Entry + _ = json.Unmarshal(bus.get(BucketOf("postgres"), "ace.letta.postgres-database"), &made) + m.fail = ErrNotReady + if out, _ := p.Pass(context.Background()); len(out) != 1 || out[0].Status != Waiting { + t.Fatalf("not ready after a restart: %+v", out) + } + raw = bus.get(BucketOf("postgres"), "ace.letta.postgres-database") + if err := json.Unmarshal(raw, &e); err != nil || e.State != "waits" || e.VaultName != made.VaultName || e.User != made.User || + fmt.Sprint(e.Values) != fmt.Sprint(made.Values) || len(made.Values) == 0 { + t.Fatalf("not ready after it served, the entry is %s, it served %+v", raw, made) + } + }) + + // A failed Make is said with the credential taken out in the forms it can come back in: as it is, in base64 (standard + // and URL, with and without padding) and as a JSON string says it (issue 825, finding 6). + t.Run("AFailedMakeIsSaidWithoutTheCredentialInAnyOfItsForms", func(t *testing.T) { + cred := "pa/ss+w\"rd?>>x" + b := []byte(cred) + jsonQuoted, _ := json.Marshal(cred) + forms := map[string]string{ + "raw": cred, "std": base64.StdEncoding.EncodeToString(b), "std unpadded": base64.RawStdEncoding.EncodeToString(b), + "url": base64.URLEncoding.EncodeToString(b), "url unpadded": base64.RawURLEncoding.EncodeToString(b), + "json": string(jsonQuoted[1 : len(jsonQuoted)-1]), + } + if forms["std"] == forms["url"] || forms["json"] == forms["raw"] { + t.Fatal("the test credential does not tell the forms apart") + } + for name, form := range forms { + bus, m := newStandIn(), &maker{fail: errors.New("login failed for " + form + " at the server")} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": cred}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + out, err := postgres(bus, v, m).Pass(context.Background()) + if err != nil || len(out) != 1 { + t.Fatalf("%v %+v", err, out) + } + if strings.Contains(out[0].Why, form) || !strings.Contains(out[0].Why, "") { + t.Fatalf("%s form: %q", name, out[0].Why) + } + } + }) + + // The consumer's identity is the mesh's (ADR 0202 §1): mesh__, lower case, `_` for anything else, + // and its DNS form writes `-` for `_`. + t.Run("TheIdentityIsTheMeshsAndItsDNSForm", func(t *testing.T) { + if got := ConsumerIdentity("Home.Server", "my-app"); got != "mesh_home_server_my_app" { + t.Fatal(got) + } + got, err := ServedTo(map[string]any{"bucket": "b-${consumer:as:dns}", "n": 3}, "mesh_ace_letta") + if err != nil || got["bucket"] != "b-mesh-ace-letta" || got["n"] != 3 { + t.Fatalf("%v %v", got, err) + } + if _, err := ServedTo(map[string]any{"x": "${consumer:node}"}, "mesh_ace_letta"); err == nil { + t.Fatal("a fact the mesh does not state was filled") + } + }) + + // Every entry the helper writes passes the state guard's key rule (issue 945), and names where the vault keeps the + // credential under `vault-name`, for a login per consumer and for a shared one. + t.Run("AnEntryHasNoKeyTheStateGuardRefuses", func(t *testing.T) { + for name, form := range map[string]CredentialForm{"pair": PerConsumer, "shared": Shared} { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1", + "provision/novox/postgres-database": "s2"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"postgres-database": {Credential: form, User: "shared-user"}}}) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + var doc map[string]any + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.letta.postgres-database"), &doc); err != nil { + t.Fatal(err) + } + var keys []string + keysOf(doc, &keys) + for _, k := range keys { + if refusedByTheStateGuard(k) { + t.Fatalf("%s: the entry carries the key %q, which the state guard refuses", name, k) + } + } + want := map[string]string{"pair": "pair/novox/postgres-database/ace/letta", + "shared": "provision/novox/postgres-database"}[name] + if doc["vault-name"] != want { + t.Fatalf("%s: vault-name is %v, want %s", name, doc["vault-name"], want) + } + } + }) + + // novox/hq ADR 0570 clauses 3 and 5 (issue 1954 L6-P3, P4): a consumer the seat mesh-provisioner plans is bound by its + // plan and its provision key, never by its assignment. Its entry is made only once its key is asked and every provision + // it depends on is provided, and says provided; a plan gone retires nothing by itself; a retiring key is let go only on + // the operator's yes. + t.Run("TheSeatsKeysSayWhenAProviderActsAndWhenItLetsGo", func(t *testing.T) { + ctx := context.Background() + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1"}} + // The same consumer's assignment names another provider: the seat's plan wins. + assign(t, bus, "ace", "letta", bind("postgres-database", "", "shanks", "postgres")) + plan, _ := provisions.EncodePlan(provisions.Plan{Node: "ace", Module: "letta", Plan: catalogue.Plan{ + Provisions: []catalogue.PlanProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}}}) + _ = bus.Put(ctx, provisions.PlansBucket, "ace.letta", plan) + key := "ace.letta.postgres-database" + put := func(k string, state string, depends []string, warrant *provisions.Warrant) { + pr := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "letta", Node: "ace"}, + Provider: provisions.End{Module: "postgres", Node: "novox"}, DependsOn: depends, State: state, Warrant: warrant} + if k != key { + _, _, pr.Provision, _, _ = provisions.ParseKey(k) + pr.Provider = provisions.End{Module: "mesh-vault", Node: "novox"} + } + if state != provisions.Asked && state != provisions.Provided { + pr.Why = "said" + } + raw, err := provisions.Encode(pr) + if err != nil { + t.Fatal(err) + } + _ = bus.Put(ctx, provisions.ProvisionsBucket, k, raw) + } + p := postgres(bus, v, m) + steps := []struct { + name string + before func() + status string + says string + entry bool + }{ + {name: "no key yet", status: Waiting, says: "holds no key"}, + {name: "a dependency asked", before: func() { + put("ace.letta.secret", provisions.Asked, nil, nil) + put(key, provisions.Asked, []string{"ace.letta.secret"}, nil) + }, status: Waiting, says: "depends on ace.letta.secret, which is asked"}, + {name: "the dependency provided", before: func() { put("ace.letta.secret", provisions.Provided, nil, nil) }, + status: Answered, entry: true}, + {name: "its plan gone: nothing let go by itself", before: func() { _ = bus.Delete(ctx, provisions.PlansBucket, "ace.letta") }, + entry: true}, + {name: "retiring, not yet answered", before: func() { put(key, provisions.Retiring, nil, nil) }, entry: true}, + {name: "retiring, the operator's yes", before: func() { + put(key, provisions.Retiring, nil, &provisions.Warrant{Ask: "a-1", Option: "yes", By: "the operator"}) + }, status: Removed}, + } + for _, s := range steps { + if s.before != nil { + s.before() + } + out, err := p.Pass(ctx) + if err != nil { + t.Fatalf("%s: %v", s.name, err) + } + var mine []Outcome + for _, o := range out { + if o.Key == key { + mine = append(mine, o) + } + } + if s.status != "" && (len(mine) != 1 || mine[0].Status != s.status || !strings.Contains(mine[0].Why, s.says)) { + t.Errorf("%s: %+v", s.name, mine) + } + if held := bus.get(BucketOf("postgres"), key) != nil; held != s.entry { + t.Errorf("%s: an entry held %v", s.name, held) + } + if s.entry && entryOf(t, bus, key).State != provisions.Provided { + t.Errorf("%s: the entry does not say provided", s.name) + } + } + if !slices.Equal(m.gone, []string{key}) || m.made[key] != "s1" { + t.Errorf("gone %v, made %v: let go once, on the warrant, and made from the vault's value", m.gone, m.made) + } + }) +} + +// TestConsumersFileAndStateBus groups the cases below, each one a subtest. +func TestConsumersFileAndStateBus(t *testing.T) { + // The provider reads its consumers' credentials only from the file its machine's provisioner client fills (novox/hq + // ADR 0373 clause 3, issue 679): one entry per consumer, naming the consumer and the vault's name for its credential, + // and carrying the value. The helper is wired to it as its Vault and asks nothing of the vault itself. + t.Run("TheConsumersFileAnswersEachConsumersCredential", func(t *testing.T) { + dir := t.TempDir() + at := filepath.Join(dir, "consumers.json") + file := `[{"consumer":"ace","module":"letta","credential":"pair/novox/postgres-database/ace/letta","value":"s3cret"}, + {"consumer":"g14","module":"gitea","local":"cache","credential":"pair/novox/postgres-database/g14/gitea/cache","value":"other"}]` + if err := os.WriteFile(at, []byte(file), 0o600); err != nil { + t.Fatal(err) + } + v := ConsumersFile{Paths: []string{filepath.Join(dir, "absent.json"), at}} + got, err := v.Value(context.Background(), PairName("novox", "postgres-database", "ace", "letta", "")) + if err != nil || string(got) != "s3cret" { + t.Fatalf("ace's letta: %q, %v; want s3cret", got, err) + } + got, err = v.Value(context.Background(), PairName("novox", "postgres-database", "g14", "gitea", "cache")) + if err != nil || string(got) != "other" { + t.Fatalf("g14's gitea under cache: %q, %v; want other", got, err) + } + // A consumer the file does not carry is waited for, and never told of a login its provider has not made. + _, err = v.Value(context.Background(), PairName("novox", "postgres-database", "shanks", "photos", "")) + if err == nil || !strings.Contains(err.Error(), "pair/novox/postgres-database/shanks/photos") { + t.Fatalf("a consumer the file does not carry: %v", err) + } + if strings.Contains(err.Error(), "s3cret") || strings.Contains(err.Error(), "other") { + t.Fatalf("an error names a credential: %v", err) + } + + // Wired as the helper's Vault, the file is what makes each consumer's login. + bus := newStandIn() + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + var made []string + p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, + Offers: map[string]Offer{"postgres-database": {Serves: map[string]any{"port": 5432}}}, + Make: func(_ context.Context, c Consumer) error { + made = append(made, c.Entry.User+"="+string(c.Credential)) + return nil + }, Logf: func(string, ...any) {}}) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + if len(made) != 1 || made[0] != "mesh_ace_letta=s3cret" { + t.Fatalf("made %v, want ace's letta under the file's credential", made) + } + }) + + // A file that is not the client's form is said, never read as no consumers. + t.Run("AConsumersFileNotInItsFormIsRefused", func(t *testing.T) { + at := filepath.Join(t.TempDir(), "consumers.json") + if err := os.WriteFile(at, []byte(`{"not": "a list"}`), 0o600); err != nil { + t.Fatal(err) + } + _, err := ConsumersFile{Paths: []string{at}}.Value(context.Background(), "pair/novox/p/ace/m") + if err == nil || !strings.Contains(err.Error(), at) { + t.Fatalf("a file not in the client's form: %v", err) + } + }) + + t.Run("StandingIsFalseOnlyForAnEndedOrPendingValue", func(t *testing.T) { + for raw, want := range map[string]bool{ + `{"node":"a","module":"b"}`: true, + `{"node":"a","module":"b","ended":"","state":"","x":{"y":1}}`: true, + `{"node":"a","module":"b","ended":null}`: true, + `{"node":"a","module":"b","build":"b1","commit":"c","rollout":"r","since":"t","artifact":{"name":"n"}}`: true, + `{"node":"a","module":"b","ended":"2026-10-18T01:00:00Z"}`: false, + `{"node":"a","module":"b","state":"pending"}`: false, + } { + if got := Standing([]byte(raw)); got != want { + t.Errorf("%s: %v, want %v", raw, got, want) + } + } + }) + + // The bus of a moved provider maps its own bucket to the state `bound` and the seat's plans to the state + // `mesh-provisioner.plans` (novox/hq ADR 0570 clause 2), writes only the own bucket's state, and refuses any other + // bucket, the controller's assignments among them. + t.Run("StateBusMapsTheProvidersBucketsToTheRuntimesStates", func(t *testing.T) { + bus := StateBus("postgres") + rt := serve(t, []stdio.Tool{ + {Name: "watch", Run: func(a map[string]any) (any, error) { + return nil, bus.Watch(context.Background(), a["bucket"].(string), func(string, []byte) {}) + }}, + {Name: "put", Run: func(map[string]any) (any, error) { + return "put", bus.Put(context.Background(), BucketOf("postgres"), "k", []byte(`{"a":1}`)) + }}, + {Name: "delete", Run: func(map[string]any) (any, error) { + return "deleted", bus.Delete(context.Background(), BucketOf("postgres"), "k") + }}, + }) + for i, c := range []struct{ bucket, state string }{ + {BucketOf("postgres"), "bound"}, + {provisions.PlansBucket, "mesh-provisioner.plans"}, + } { + rt.call(i+1, "watch", map[string]any{"bucket": c.bucket}) + w := rt.next() + if w["method"] != "mesh/state.watch" || w["params"].(map[string]any)["state"] != c.state { + t.Fatalf("bucket %s asked %v, want the state %s", c.bucket, w, c.state) + } + rt.send(map[string]any{"jsonrpc": "2.0", "id": w["id"], "result": map[string]any{}}) + rt.next() + } + rt.call(3, "put", nil) + p := rt.next() + if p["method"] != "mesh/state.put" || p["params"].(map[string]any)["state"] != "bound" || p["params"].(map[string]any)["key"] != "k" { + t.Fatalf("put asked %v", p) + } + rt.send(map[string]any{"jsonrpc": "2.0", "id": p["id"], "result": map[string]any{"revision": 1}}) + rt.next() + rt.call(4, "delete", nil) + d := rt.next() + if d["method"] != "mesh/state.delete" || d["params"].(map[string]any)["state"] != "bound" { + t.Fatalf("delete asked %v", d) + } + rt.send(map[string]any{"jsonrpc": "2.0", "id": d["id"], "result": map[string]any{}}) + rt.next() + + for _, bucket := range []string{"other_bound", "elsewhere", "mesh-controller_assignments"} { + if err := bus.Watch(context.Background(), bucket, func(string, []byte) {}); err == nil || !strings.Contains(err.Error(), "not a bucket") { + t.Fatalf("a watch of %s gave %v", bucket, err) + } + if err := bus.Put(context.Background(), bucket, "k", nil); err == nil { + t.Fatalf("a put to %s was allowed", bucket) + } + if err := bus.Delete(context.Background(), bucket, "k"); err == nil { + t.Fatalf("a delete in %s was allowed", bucket) + } + } + }) + + // A watch whose ctx has ended calls its handler no more, though the runtime keeps sending changes: the Bus contract + // says a watch lasts until ctx ends, and a stdio state watch lasts as long as the process. + t.Run("StateBusWatchCallsItsHandlerNoMoreOnceItsContextEnded", func(t *testing.T) { + bus := StateBus("postgres") + ctx, cancel := context.WithCancel(context.Background()) + handled := make(chan string, 8) + rt := serve(t, []stdio.Tool{ + {Name: "start", Run: func(map[string]any) (any, error) { + return "started", bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + handled <- key + "=" + string(value) + }) + }}, + {Name: "stop", Run: func(map[string]any) (any, error) { cancel(); return "stopped", nil }}, + }) + change := func(id int, watch any, key, op string) { + rt.send(map[string]any{"jsonrpc": "2.0", "id": id, "method": "mesh/state", "params": map[string]any{"watch": watch, + "change": map[string]any{"state": "mesh-controller.assignments", "key": key, "op": op, "value": "v"}}}) + if r := rt.next(); r["id"] != float64(id) || r["error"] != nil { + t.Fatalf("a change was answered %v", r) + } + } + rt.call(1, "start", nil) + w := rt.next() + rt.send(map[string]any{"jsonrpc": "2.0", "id": w["id"], "result": map[string]any{}}) + rt.next() + + change(50, w["id"], "before", "put") + if got := <-handled; got != `before="v"` { + t.Fatalf("handled %q", got) + } + rt.call(2, "stop", nil) + rt.next() + change(51, w["id"], "after", "put") + change(52, w["id"], "after", "delete") + select { + case got := <-handled: + t.Fatalf("the handler was called after its context ended: %q", got) + case <-time.After(100 * time.Millisecond): + } + }) +} -- 2.54.0 From 326d729c0b88aba67973fca6f907234b253768b0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:09:45 +0100 Subject: [PATCH 49/94] =?UTF-8?q?Placement=20and=20Identity-and-access=20c?= =?UTF-8?q?ontracts:=20mesh-module-assigner=20and=20mesh-wants=20whole=20w?= =?UTF-8?q?ith=20states,=20mesh-vault=20moves,=20mesh-node-enroller=20whol?= =?UTF-8?q?e,=20node-secret-service=20moves;=20the=20two=20seats=20renamed?= =?UTF-8?q?=20(ADR=200634=20=C2=A77,=20=C2=A79;=20C-5,=20C-10;=20issue=201?= =?UTF-8?q?966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/seats/meshvault/go.mod | 3 - .../go/seats/meshnodeenroller/contract.go | 260 +++++ .../seats/meshnodeenroller/contract_test.go | 60 ++ .../go/seats/meshnodeenroller/go.mod | 3 + .../go}/seats/meshvault/contract.go | 145 ++- .../go}/seats/meshvault/contract_test.go | 6 +- identity-and-access/go/seats/meshvault/go.mod | 3 + identity-and-access/module.json | 67 ++ identity-and-access/seats/index.json | 8 + .../mesh-node-enroller/fixtures/events.json | 12 + .../mesh-node-enroller/fixtures/states.json | 20 + .../seats/mesh-node-enroller}/seat.json | 156 ++- .../seats}/mesh-vault/fixtures/events.json | 2 +- .../seats/mesh-vault/fixtures/states.json | 22 + .../seats}/mesh-vault/fixtures/verbs.json | 0 .../seats}/mesh-vault/seat.json | 137 ++- .../seats}/node-secret-service/seat.json | 0 .../seats/meshnodeenroller/package-lock.json | 12 + .../ts/seats/meshnodeenroller/package.json | 13 + .../ts/seats/meshnodeenroller/src/index.ts | 191 ++++ .../ts/seats/meshnodeenroller}/tsconfig.json | 4 +- .../ts/seats/meshvault/package-lock.json | 12 + .../ts/seats/meshvault/package.json | 13 + .../ts}/seats/meshvault/src/index.ts | 90 +- .../ts/seats/meshvault/tsconfig.json | 12 + module.json | 20 - .../go/seats/meshmoduleassigner/contract.go | 665 +++++++++++++ .../seats/meshmoduleassigner/contract_test.go | 86 ++ placement/go/seats/meshmoduleassigner/go.mod | 3 + placement/go/seats/meshwants/contract.go | 331 ++++++ placement/go/seats/meshwants/contract_test.go | 76 ++ placement/go/seats/meshwants/go.mod | 3 + placement/module.json | 58 ++ placement/seats/index.json | 7 + .../mesh-module-assigner/fixtures/events.json | 29 + .../mesh-module-assigner/fixtures/states.json | 109 ++ .../seats/mesh-module-assigner/seat.json | 941 ++++++++++++++++++ .../seats/mesh-wants/fixtures/states.json | 47 + placement/seats/mesh-wants/seat.json | 279 ++++++ .../meshmoduleassigner/package-lock.json | 12 + .../ts/seats/meshmoduleassigner/package.json | 13 + .../ts/seats/meshmoduleassigner/src/index.ts | 488 +++++++++ .../ts/seats/meshmoduleassigner/tsconfig.json | 12 + .../ts/seats/meshwants/package-lock.json | 12 + placement/ts/seats/meshwants/package.json | 13 + placement/ts/seats/meshwants/src/index.ts | 211 ++++ placement/ts/seats/meshwants/tsconfig.json | 12 + seats/index.json | 7 +- seats/mesh-placement/seat.json | 137 --- seats/mesh-wants/seat.json | 51 - ts/seats/meshvault/package.json | 19 - 51 files changed, 4626 insertions(+), 266 deletions(-) delete mode 100644 go/seats/meshvault/go.mod create mode 100644 identity-and-access/go/seats/meshnodeenroller/contract.go create mode 100644 identity-and-access/go/seats/meshnodeenroller/contract_test.go create mode 100644 identity-and-access/go/seats/meshnodeenroller/go.mod rename {go => identity-and-access/go}/seats/meshvault/contract.go (75%) rename {go => identity-and-access/go}/seats/meshvault/contract_test.go (85%) create mode 100644 identity-and-access/go/seats/meshvault/go.mod create mode 100644 identity-and-access/module.json create mode 100644 identity-and-access/seats/index.json create mode 100644 identity-and-access/seats/mesh-node-enroller/fixtures/events.json create mode 100644 identity-and-access/seats/mesh-node-enroller/fixtures/states.json rename {seats/mesh-enrolment => identity-and-access/seats/mesh-node-enroller}/seat.json (56%) rename {seats => identity-and-access/seats}/mesh-vault/fixtures/events.json (98%) create mode 100644 identity-and-access/seats/mesh-vault/fixtures/states.json rename {seats => identity-and-access/seats}/mesh-vault/fixtures/verbs.json (100%) rename {seats => identity-and-access/seats}/mesh-vault/seat.json (83%) rename {seats => identity-and-access/seats}/node-secret-service/seat.json (100%) create mode 100644 identity-and-access/ts/seats/meshnodeenroller/package-lock.json create mode 100644 identity-and-access/ts/seats/meshnodeenroller/package.json create mode 100644 identity-and-access/ts/seats/meshnodeenroller/src/index.ts rename {ts/seats/meshvault => identity-and-access/ts/seats/meshnodeenroller}/tsconfig.json (77%) create mode 100644 identity-and-access/ts/seats/meshvault/package-lock.json create mode 100644 identity-and-access/ts/seats/meshvault/package.json rename {ts => identity-and-access/ts}/seats/meshvault/src/index.ts (79%) create mode 100644 identity-and-access/ts/seats/meshvault/tsconfig.json create mode 100644 placement/go/seats/meshmoduleassigner/contract.go create mode 100644 placement/go/seats/meshmoduleassigner/contract_test.go create mode 100644 placement/go/seats/meshmoduleassigner/go.mod create mode 100644 placement/go/seats/meshwants/contract.go create mode 100644 placement/go/seats/meshwants/contract_test.go create mode 100644 placement/go/seats/meshwants/go.mod create mode 100644 placement/module.json create mode 100644 placement/seats/index.json create mode 100644 placement/seats/mesh-module-assigner/fixtures/events.json create mode 100644 placement/seats/mesh-module-assigner/fixtures/states.json create mode 100644 placement/seats/mesh-module-assigner/seat.json create mode 100644 placement/seats/mesh-wants/fixtures/states.json create mode 100644 placement/seats/mesh-wants/seat.json create mode 100644 placement/ts/seats/meshmoduleassigner/package-lock.json create mode 100644 placement/ts/seats/meshmoduleassigner/package.json create mode 100644 placement/ts/seats/meshmoduleassigner/src/index.ts create mode 100644 placement/ts/seats/meshmoduleassigner/tsconfig.json create mode 100644 placement/ts/seats/meshwants/package-lock.json create mode 100644 placement/ts/seats/meshwants/package.json create mode 100644 placement/ts/seats/meshwants/src/index.ts create mode 100644 placement/ts/seats/meshwants/tsconfig.json delete mode 100644 seats/mesh-placement/seat.json delete mode 100644 seats/mesh-wants/seat.json delete mode 100644 ts/seats/meshvault/package.json diff --git a/go/seats/meshvault/go.mod b/go/seats/meshvault/go.mod deleted file mode 100644 index 10f7a40..0000000 --- a/go/seats/meshvault/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/meshvault - -go 1.26.0 diff --git a/identity-and-access/go/seats/meshnodeenroller/contract.go b/identity-and-access/go/seats/meshnodeenroller/contract.go new file mode 100644 index 0000000..9f1cc08 --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/contract.go @@ -0,0 +1,260 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshnodeenroller is the contract of the seat "mesh-node-enroller", defined whole (seats/mesh-node-enroller/seat.json): The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9). +package meshnodeenroller + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-node-enroller" + +const ( + VerbIssue = "issue" + VerbRedeem = "redeem" + VerbConfirm = "confirm" + VerbRevoke = "revoke" + VerbTokens = "tokens" + VerbAgain = "again" +) + +const ( + EventEnrolmentIssued = "enrolment-issued" + EventNodeEnrolled = "node-enrolled" + EventEnrolmentRefused = "enrolment-refused" + EventEnrolmentRevoked = "enrolment-revoked" + EventEnrolmentExpired = "enrolment-expired" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbIssue, VerbRedeem, VerbConfirm, VerbRevoke, VerbTokens, VerbAgain} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventEnrolmentIssued, EventNodeEnrolled, EventEnrolmentRefused, EventEnrolmentRevoked, EventEnrolmentExpired} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-node-enroller.events" + FamilyMaxAgeDays = 7 +) + +// SubjectEnrolmentIssued is where enrolment-issued is published. +const SubjectEnrolmentIssued = "mesh.seat.mesh-node-enroller.event.enrolment-issued" + +// SubjectNodeEnrolled is where node-enrolled is published. +const SubjectNodeEnrolled = "mesh.seat.mesh-node-enroller.event.node-enrolled" + +// SubjectEnrolmentRefused is where enrolment-refused is published. +const SubjectEnrolmentRefused = "mesh.seat.mesh-node-enroller.event.enrolment-refused" + +// SubjectEnrolmentRevoked is where enrolment-revoked is published. +const SubjectEnrolmentRevoked = "mesh.seat.mesh-node-enroller.event.enrolment-revoked" + +// SubjectEnrolmentExpired is where enrolment-expired is published. +const SubjectEnrolmentExpired = "mesh.seat.mesh-node-enroller.event.enrolment-expired" + +const ( + StateEnrolments = "enrolments" +) + +// EnrolmentsByNodeNodeKey is the key of one value of the state enrolments under the form "node.". +func EnrolmentsByNodeNodeKey(node string) string { return "node." + node } + +// ParseEnrolmentsByNodeNodeKey reads the parts of a key of the form "node.", in the order of the form; the last result is false for a key of another form. +func ParseEnrolmentsByNodeNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "node.") { + return "", false + } + rest = rest[5:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeEnrolmentsByNodeNodeValue reads one value of the state enrolments under the form "node.", refusing a field EnrolmentsByNodeNodeValue does not know and any text after the value. +func DecodeEnrolmentsByNodeNodeValue(raw []byte) (EnrolmentsByNodeNodeValue, error) { + var v EnrolmentsByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// EnrolmentsByAddressAddressKey is the key of one value of the state enrolments under the form "address.
". +func EnrolmentsByAddressAddressKey(address string) string { return "address." + address } + +// ParseEnrolmentsByAddressAddressKey reads the parts of a key of the form "address.
", in the order of the form; the last result is false for a key of another form. +func ParseEnrolmentsByAddressAddressKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "address.") { + return "", false + } + rest = rest[8:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeEnrolmentsByAddressAddressValue reads one value of the state enrolments under the form "address.
", refusing a field EnrolmentsByAddressAddressValue does not know and any text after the value. +func DecodeEnrolmentsByAddressAddressValue(raw []byte) (EnrolmentsByAddressAddressValue, error) { + var v EnrolmentsByAddressAddressValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Enrolment is one open enrolment. +type Enrolment struct { + Address string `json:"address"` + Ask string `json:"ask,omitempty"` + Caller string `json:"caller,omitempty"` + Expires string `json:"expires,omitempty"` + Issued string `json:"issued,omitempty"` + KeysDigest string `json:"keys-digest,omitempty"` + Node string `json:"node"` + OverlayKey string `json:"overlay-key,omitempty"` + // asked, live or redeeming + Stage string `json:"stage"` + V int64 `json:"v,omitempty"` + Why string `json:"why,omitempty"` +} + +// IssueInput is what issue takes. +type IssueInput struct { + // how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours + Expires string `json:"expires,omitempty"` + // the new machine's name in the mesh + Node string `json:"node"` + // the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it + OverlayKey string `json:"overlay_key"` + // why it joins, shown to the operator with the ask + Why string `json:"why"` +} + +// IssueAnswer is what issue answers. +type IssueAnswer = map[string]any + +// RedeemInput is what redeem takes. +type RedeemInput struct { + // the node the token was issued for + Node string `json:"node"` + // the private network key's public half + OverlayKey string `json:"overlay_key,omitempty"` + // what the machine is, as its record says it + Profile map[string]any `json:"profile,omitempty"` + // the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64 + Proof string `json:"proof"` + // the identity key's public half, base64 + PublicKey string `json:"public_key"` + // the sealing key's public half, which the account is sealed to + SealingKey string `json:"sealing_key"` + // the serving key's public half, which the certificate binds + ServingKey string `json:"serving_key,omitempty"` + // a tunnel found on the machine: everything but its private key + Tunnel map[string]any `json:"tunnel,omitempty"` +} + +// RedeemAnswer is what redeem answers. +type RedeemAnswer = map[string]any + +// ConfirmInput is what confirm takes. +type ConfirmInput struct { +} + +// ConfirmAnswer is what confirm answers. +type ConfirmAnswer = map[string]any + +// RevokeInput is what revoke takes. +type RevokeInput struct { + // the node + Node string `json:"node"` + // why, recorded with the hand act + Why string `json:"why"` +} + +// RevokeAnswer is what revoke answers. +type RevokeAnswer = map[string]any + +// TokensInput is what tokens takes. +type TokensInput struct { + // "true": the ended enrolments too + History string `json:"history,omitempty"` + // one node + Node string `json:"node,omitempty"` +} + +// The values of TokensInput's history. +const ( + TokensInputHistoryTrue = "true" + TokensInputHistoryFalse = "false" +) + +// TokensAnswer is what tokens answers. +type TokensAnswer = map[string]any + +// AgainInput is what again takes. +type AgainInput struct { + // the node whose ask is raised again + Node string `json:"node"` +} + +// AgainAnswer is what again answers. +type AgainAnswer = map[string]any + +// EnrolmentIssuedEvent is the body of the event enrolment-issued. +type EnrolmentIssuedEvent = Enrolment + +// NodeEnrolledEvent is the body of the event node-enrolled. +type NodeEnrolledEvent = Enrolment + +// EnrolmentRefusedEvent is the body of the event enrolment-refused. +type EnrolmentRefusedEvent = Enrolment + +// EnrolmentRevokedEvent is the body of the event enrolment-revoked. +type EnrolmentRevokedEvent = Enrolment + +// EnrolmentExpiredEvent is the body of the event enrolment-expired. +type EnrolmentExpiredEvent = Enrolment + +// EnrolmentsByNodeNodeValue is one value of the state enrolments under the key node.. +type EnrolmentsByNodeNodeValue = Enrolment + +// EnrolmentsByAddressAddressValue is one value of the state enrolments under the key address.
. +type EnrolmentsByAddressAddressValue struct { + Ask string `json:"ask,omitempty"` + Issued string `json:"issued,omitempty"` + Node string `json:"node"` + V int64 `json:"v,omitempty"` +} diff --git a/identity-and-access/go/seats/meshnodeenroller/contract_test.go b/identity-and-access/go/seats/meshnodeenroller/contract_test.go new file mode 100644 index 0000000..6e1d2bf --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/contract_test.go @@ -0,0 +1,60 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshnodeenroller + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-node-enroller/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "node-enrolled carries the enrolment", subject: "mesh.seat.mesh-node-enroller.event.node-enrolled", given: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\"}", wire: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\"}", + decode: func(b []byte) (any, error) { var v NodeEnrolledEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-node-enroller\\.event\\.node-enrolled$")}, + {name: "an open enrolment", subject: "", given: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\",\"v\":1}", wire: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v EnrolmentsByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a reserved address", subject: "", given: "{\"node\":\"newbox\",\"v\":1}", wire: "{\"node\":\"newbox\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v EnrolmentsByAddressAddressValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/identity-and-access/go/seats/meshnodeenroller/go.mod b/identity-and-access/go/seats/meshnodeenroller/go.mod new file mode 100644 index 0000000..61f3627 --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller + +go 1.26.0 diff --git a/go/seats/meshvault/contract.go b/identity-and-access/go/seats/meshvault/contract.go similarity index 75% rename from go/seats/meshvault/contract.go rename to identity-and-access/go/seats/meshvault/contract.go index df00f7c..2f3fe11 100644 --- a/go/seats/meshvault/contract.go +++ b/identity-and-access/go/seats/meshvault/contract.go @@ -3,6 +3,13 @@ // Package meshvault is the contract of the seat "mesh-vault", defined whole (seats/mesh-vault/seat.json): The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2). package meshvault +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + // Seat is the seat's name. const Seat = "mesh-vault" @@ -95,6 +102,102 @@ func HolderSubjectOwnRetired(holder string) string { return "mesh.mod." + holder + ".event.own-retired" } +const ( + StateSigners = "signers" + StateNodeLogins = "node-logins" +) + +// SignersByKeyKey is the key of one value of the state signers under the form "key". +func SignersByKeyKey() string { return "key" } + +// ParseSignersByKeyKey reads the parts of a key of the form "key", in the order of the form; the last result is false for a key of another form. +func ParseSignersByKeyKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "key") { + return false + } + rest = rest[3:] + if rest != "" { + return false + } + return true +} + +// DecodeSignersByKeyValue reads one value of the state signers under the form "key", refusing a field SignersByKeyValue does not know and any text after the value. +func DecodeSignersByKeyValue(raw []byte) (SignersByKeyValue, error) { + var v SignersByKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SignersByAuthorityKey is the key of one value of the state signers under the form "authority". +func SignersByAuthorityKey() string { return "authority" } + +// ParseSignersByAuthorityKey reads the parts of a key of the form "authority", in the order of the form; the last result is false for a key of another form. +func ParseSignersByAuthorityKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "authority") { + return false + } + rest = rest[9:] + if rest != "" { + return false + } + return true +} + +// DecodeSignersByAuthorityValue reads one value of the state signers under the form "authority", refusing a field SignersByAuthorityValue does not know and any text after the value. +func DecodeSignersByAuthorityValue(raw []byte) (SignersByAuthorityValue, error) { + var v SignersByAuthorityValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// NodeLoginsByNodeKey is the key of one value of the state node-logins under the form "". +func NodeLoginsByNodeKey(node string) string { return node } + +// ParseNodeLoginsByNodeKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseNodeLoginsByNodeKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeNodeLoginsByNodeValue reads one value of the state node-logins under the form "", refusing a field NodeLoginsByNodeValue does not know and any text after the value. +func DecodeNodeLoginsByNodeValue(raw []byte) (NodeLoginsByNodeValue, error) { + var v NodeLoginsByNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // GetInput is what get takes. type GetInput struct { // the secret's name @@ -241,11 +344,11 @@ type IssueAccountAnswer = map[string]any type SignInput struct { // node-certificate or enrolment Kind string `json:"kind"` - // the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols + // the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols Node string `json:"node,omitempty"` // the redeem answer's bytes in base64, for an enrolment Reply string `json:"reply,omitempty"` - // from mesh-enrolment: the serving key's public half the enrolling node presented + // from mesh-node-enroller: the serving key's public half the enrolling node presented ServingKey string `json:"serving_key,omitempty"` } @@ -267,7 +370,7 @@ type SignAnswer struct { Signer string `json:"signer"` } -// EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. +// EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. type EnrolmentTokenInputTunnel struct { // the joining node's tunnel address Address string `json:"address"` @@ -291,7 +394,7 @@ type EnrolmentTokenInput struct { Node string `json:"node"` // "true": answer the token sealed to the operator key Sealed string `json:"sealed,omitempty"` - // the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range + // the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range Tunnel *EnrolmentTokenInputTunnel `json:"tunnel,omitempty"` } @@ -431,3 +534,37 @@ type OwnRetiredHolderEvent struct { // the node Node string `json:"node"` } + +// SignersByKeyValue is one value of the state signers under the key key. +type SignersByKeyValue struct { + Fingerprint string `json:"fingerprint"` + // the signing key's public half, standard base64 + Key string `json:"key"` + Made string `json:"made"` + V int64 `json:"v,omitempty"` +} + +// SignersByAuthorityValue is one value of the state signers under the key authority. +type SignersByAuthorityValue struct { + // the authority certificate, PEM + Certificate string `json:"certificate"` + Fingerprint string `json:"fingerprint"` + Made string `json:"made"` + V int64 `json:"v,omitempty"` +} + +// NodeLoginsByNodeValue is one value of the state node-logins under the key . +type NodeLoginsByNodeValue struct { + Acts []string `json:"acts,omitempty"` + // the user list's revision that held both logins, from ready on + ListRevision int64 `json:"list-revision,omitempty"` + // the login in use + Login string `json:"login"` + Node string `json:"node"` + // the login rotating to, empty when none + Rotating string `json:"rotating,omitempty"` + Since string `json:"since"` + // begun, ready or confirmed; empty when no rotation + Stage string `json:"stage,omitempty"` + V int64 `json:"v,omitempty"` +} diff --git a/go/seats/meshvault/contract_test.go b/identity-and-access/go/seats/meshvault/contract_test.go similarity index 85% rename from go/seats/meshvault/contract_test.go rename to identity-and-access/go/seats/meshvault/contract_test.go index a358263..8b8c520 100644 --- a/go/seats/meshvault/contract_test.go +++ b/identity-and-access/go/seats/meshvault/contract_test.go @@ -29,7 +29,7 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-replaced$")}, {name: "account-issued under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.account-issued", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.account-issued$")}, - {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", + {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-node-enroller\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-node-enroller\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", decode: func(b []byte) (any, error) { var v SignedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.signed$")}, {name: "rotation-begun under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotation-begun", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-begun$")}, @@ -39,6 +39,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-done$")}, {name: "own-retired", subject: "mesh.mod.mesh-vault.event.own-retired", given: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v OwnRetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-retired$")}, + {name: "the signing key", subject: "", given: "{\"fingerprint\":\"sha256:aa\",\"key\":\"AAAA\",\"made\":\"2026-11-05T12:00:00Z\",\"v\":1}", wire: "{\"fingerprint\":\"sha256:aa\",\"key\":\"AAAA\",\"made\":\"2026-11-05T12:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v SignersByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's login", subject: "", given: "{\"login\":\"node.ace\",\"node\":\"ace\",\"since\":\"2026-11-05T12:00:00Z\",\"v\":1}", wire: "{\"login\":\"node.ace\",\"node\":\"ace\",\"since\":\"2026-11-05T12:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v NodeLoginsByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "retire on a warrant", subject: "", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", decode: func(b []byte) (any, error) { var v RetireInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a node certificate signed", subject: "", given: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", diff --git a/identity-and-access/go/seats/meshvault/go.mod b/identity-and-access/go/seats/meshvault/go.mod new file mode 100644 index 0000000..a6cf36c --- /dev/null +++ b/identity-and-access/go/seats/meshvault/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault + +go 1.26.0 diff --git a/identity-and-access/module.json b/identity-and-access/module.json new file mode 100644 index 0000000..20e2d2c --- /dev/null +++ b/identity-and-access/module.json @@ -0,0 +1,67 @@ +{ + "module": "mesh-identity-and-access-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshvault", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshnodeenroller", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshnodeenroller" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshvaultclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshvaultclient" + ], + "series": "0.1" + }, + { + "name": "go-meshnodeenrollerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshnodeenrollerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshvault", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshnodeenroller", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshnodeenroller" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/identity-and-access/seats/index.json b/identity-and-access/seats/index.json new file mode 100644 index 0000000..72e28e3 --- /dev/null +++ b/identity-and-access/seats/index.json @@ -0,0 +1,8 @@ +{ + "format": 1, + "seats": [ + "mesh-node-enroller", + "mesh-vault", + "node-secret-service" + ] +} diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/events.json b/identity-and-access/seats/mesh-node-enroller/fixtures/events.json new file mode 100644 index 0000000..22c07c1 --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/events.json @@ -0,0 +1,12 @@ +[ + { + "name": "node-enrolled carries the enrolment", + "of": "event:node-enrolled", + "subject": "mesh.seat.mesh-node-enroller.event.node-enrolled", + "given": { + "node": "newbox", + "address": "10.0.0.9", + "stage": "live" + } + } +] diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/states.json b/identity-and-access/seats/mesh-node-enroller/fixtures/states.json new file mode 100644 index 0000000..e0e547f --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/states.json @@ -0,0 +1,20 @@ +[ + { + "name": "an open enrolment", + "of": "state:enrolments/node.", + "given": { + "v": 1, + "node": "newbox", + "address": "10.0.0.9", + "stage": "live" + } + }, + { + "name": "a reserved address", + "of": "state:enrolments/address.
", + "given": { + "v": 1, + "node": "newbox" + } + } +] diff --git a/seats/mesh-enrolment/seat.json b/identity-and-access/seats/mesh-node-enroller/seat.json similarity index 56% rename from seats/mesh-enrolment/seat.json rename to identity-and-access/seats/mesh-node-enroller/seat.json index b611558..3bbe3be 100644 --- a/seats/mesh-enrolment/seat.json +++ b/identity-and-access/seats/mesh-node-enroller/seat.json @@ -1,9 +1,10 @@ { "format": 1, - "seat": "mesh-enrolment", + "seat": "mesh-node-enroller", "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0587", + "grain": "whole", + "decision": "novox/hq ADR 0587, ADR 0634, ADR 0635", + "description": "The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9).", "verbs": [ { "name": "issue", @@ -33,11 +34,15 @@ "overlay_key" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment's stage, address and ask, or every finding refused at once" } }, { "name": "redeem", - "description": "Spend a token: asked by a joining node-engine dialled as enrol.\u003cnode\u003e alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", + "description": "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", "input": { "additionalProperties": false, "properties": { @@ -81,6 +86,10 @@ "proof" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault" } }, { @@ -89,6 +98,10 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment ended" } }, { @@ -110,6 +123,10 @@ "why" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment ended" } }, { @@ -131,6 +148,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "every open enrolment, or one" } }, { @@ -147,24 +168,139 @@ "node" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the ask raised again" } } ], "events": [ { - "name": "enrolment-issued" + "name": "enrolment-issued", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "node-enrolled" + "name": "node-enrolled", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-refused" + "name": "enrolment-refused", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-revoked" + "name": "enrolment-revoked", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-expired" + "name": "enrolment-expired", + "body": { + "$ref": "#/defs/Enrolment" + } } - ] + ], + "family": { + "name": "mesh-node-enroller.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "enrolments", + "template": "state", + "key-forms": [ + { + "key": "node.", + "value": { + "$ref": "#/defs/Enrolment" + }, + "description": "a node's enrolment, living until the token expires while live" + }, + { + "key": "address.
", + "value": { + "type": "object", + "description": "the private-network address reserved at issue", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "issued": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "node" + ] + }, + "description": "an address reserved for a node" + } + ], + "history": 5, + "description": "open enrolments and the addresses reserved for them" + } + ], + "defs": { + "Enrolment": { + "type": "object", + "description": "one open enrolment", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "address": { + "type": "string" + }, + "why": { + "type": "string" + }, + "caller": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "stage": { + "type": "string", + "description": "asked, live or redeeming" + }, + "issued": { + "type": "string", + "format": "date-time" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "keys-digest": { + "type": "string" + }, + "overlay-key": { + "type": "string" + } + }, + "required": [ + "node", + "address", + "stage" + ] + } + } } diff --git a/seats/mesh-vault/fixtures/events.json b/identity-and-access/seats/mesh-vault/fixtures/events.json similarity index 98% rename from seats/mesh-vault/fixtures/events.json rename to identity-and-access/seats/mesh-vault/fixtures/events.json index ef46519..4a64125 100644 --- a/seats/mesh-vault/fixtures/events.json +++ b/identity-and-access/seats/mesh-vault/fixtures/events.json @@ -40,7 +40,7 @@ "of": "holder-event:signed", "subject": "mesh.mod.mesh-vault.event.signed", "given": { - "caller": "mesh-enrolment", + "caller": "mesh-node-enroller", "kind": "enrolment", "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "signer": "sha256:1111111111111111111111111111111111111111111111111111111111111111", diff --git a/identity-and-access/seats/mesh-vault/fixtures/states.json b/identity-and-access/seats/mesh-vault/fixtures/states.json new file mode 100644 index 0000000..f0b72c0 --- /dev/null +++ b/identity-and-access/seats/mesh-vault/fixtures/states.json @@ -0,0 +1,22 @@ +[ + { + "name": "the signing key", + "of": "state:signers/key", + "given": { + "v": 1, + "key": "AAAA", + "fingerprint": "sha256:aa", + "made": "2026-11-05T12:00:00Z" + } + }, + { + "name": "a node's login", + "of": "state:node-logins/", + "given": { + "v": 1, + "node": "ace", + "login": "node.ace", + "since": "2026-11-05T12:00:00Z" + } + } +] diff --git a/seats/mesh-vault/fixtures/verbs.json b/identity-and-access/seats/mesh-vault/fixtures/verbs.json similarity index 100% rename from seats/mesh-vault/fixtures/verbs.json rename to identity-and-access/seats/mesh-vault/fixtures/verbs.json diff --git a/seats/mesh-vault/seat.json b/identity-and-access/seats/mesh-vault/seat.json similarity index 83% rename from seats/mesh-vault/seat.json rename to identity-and-access/seats/mesh-vault/seat.json index 5a115a6..177adcc 100644 --- a/seats/mesh-vault/seat.json +++ b/identity-and-access/seats/mesh-vault/seat.json @@ -3,7 +3,7 @@ "seat": "mesh-vault", "scope": "mesh", "grain": "whole", - "decision": "novox/hq ADR 0161", + "decision": "novox/hq ADR 0161, ADR 0634, ADR 0635", "delivers": "secret", "verbs": [ { @@ -299,7 +299,7 @@ }, { "name": "sign", - "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", + "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-node-enroller for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-node-enroller alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", "optional": true, "input": { "properties": { @@ -308,7 +308,7 @@ "type": "string" }, "node": { - "description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", + "description": "the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols", "type": "string" }, "reply": { @@ -316,7 +316,7 @@ "type": "string" }, "serving_key": { - "description": "from mesh-enrolment: the serving key's public half the enrolling node presented", + "description": "from mesh-node-enroller: the serving key's public half the enrolling node presented", "type": "string" } }, @@ -367,7 +367,7 @@ }, { "name": "enrolment-token", - "description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", + "description": "Make a one-time token for a node mesh-node-enroller issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-node-enroller alone, only from the lease write. The token carries the first tunnel mesh-node-enroller built at issue (ADR 0587 §3, insight of 2026-11-02).", "optional": true, "input": { "properties": { @@ -397,7 +397,7 @@ }, "tunnel": { "additionalProperties": false, - "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", + "description": "the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", "properties": { "address": { "description": "the joining node's tunnel address", @@ -471,7 +471,7 @@ }, { "name": "node-account", - "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", + "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-node-enroller alone. Never the value in the clear.", "optional": true, "input": { "properties": { @@ -757,5 +757,128 @@ ] } } + ], + "states": [ + { + "name": "signers", + "template": "state", + "key-forms": [ + { + "key": "key", + "value": { + "type": "object", + "description": "the vault's signing key's public half", + "properties": { + "v": { + "type": "integer" + }, + "key": { + "type": "string", + "description": "the signing key's public half, standard base64" + }, + "fingerprint": { + "type": "string" + }, + "made": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "key", + "fingerprint", + "made" + ] + }, + "description": "the signing key's public half" + }, + { + "key": "authority", + "value": { + "type": "object", + "description": "the vault's authority certificate", + "properties": { + "v": { + "type": "integer" + }, + "certificate": { + "type": "string", + "description": "the authority certificate, PEM" + }, + "fingerprint": { + "type": "string" + }, + "made": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "certificate", + "fingerprint", + "made" + ] + }, + "description": "the authority certificate" + } + ], + "history": 5, + "description": "the vault's public signing halves; written by the vault alone" + }, + { + "name": "node-logins", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "a node's two logins and the rotation's stage", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "login": { + "type": "string", + "description": "the login in use" + }, + "rotating": { + "type": "string", + "description": "the login rotating to, empty when none" + }, + "stage": { + "type": "string", + "description": "begun, ready or confirmed; empty when no rotation" + }, + "since": { + "type": "string", + "format": "date-time" + }, + "list-revision": { + "type": "integer", + "description": "the user list's revision that held both logins, from ready on" + }, + "acts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "node", + "login", + "since" + ] + }, + "description": "a node" + } + ], + "history": 5, + "description": "which login a node uses and the stage of its rotation; the key lives while the node does" + } ] } diff --git a/seats/node-secret-service/seat.json b/identity-and-access/seats/node-secret-service/seat.json similarity index 100% rename from seats/node-secret-service/seat.json rename to identity-and-access/seats/node-secret-service/seat.json diff --git a/identity-and-access/ts/seats/meshnodeenroller/package-lock.json b/identity-and-access/ts/seats/meshnodeenroller/package-lock.json new file mode 100644 index 0000000..8e7f57d --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "packages": { + "": { + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshnodeenroller/package.json b/identity-and-access/ts/seats/meshnodeenroller/package.json new file mode 100644 index 0000000..88192fc --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-node-enroller, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts new file mode 100644 index 0000000..4068a1d --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts @@ -0,0 +1,191 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-node-enroller", defined whole (seats/mesh-node-enroller/seat.json): The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9). + +export const seat = "mesh-node-enroller"; + +export const verbs = { + issue: "issue", + redeem: "redeem", + confirm: "confirm", + revoke: "revoke", + tokens: "tokens", + again: "again", +} as const; + +export const events = { + enrolmentIssued: "enrolment-issued", + nodeEnrolled: "node-enrolled", + enrolmentRefused: "enrolment-refused", + enrolmentRevoked: "enrolment-revoked", + enrolmentExpired: "enrolment-expired", +} as const; + +export const family = { name: "mesh-node-enroller.events", maxAgeDays: 7 } as const; + +export const enrolmentIssuedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-issued"; + +export const nodeEnrolledSubject = "mesh.seat.mesh-node-enroller.event.node-enrolled"; + +export const enrolmentRefusedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-refused"; + +export const enrolmentRevokedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-revoked"; + +export const enrolmentExpiredSubject = "mesh.seat.mesh-node-enroller.event.enrolment-expired"; + +export const states = { + enrolments: "enrolments", +} as const; + +/** The key of one value of the state enrolments under the form "node.". */ +export function enrolmentsByNodeNodeKey(node: string): string { + return "node." + node; +} + +/** The parts of a key of the form "node.", or undefined for a key of another form. */ +export function parseEnrolmentsByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("node.")) return undefined; + rest = rest.slice(5); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state enrolments under the form "address.
". */ +export function enrolmentsByAddressAddressKey(address: string): string { + return "address." + address; +} + +/** The parts of a key of the form "address.
", or undefined for a key of another form. */ +export function parseEnrolmentsByAddressAddressKey(key: string): { address: string } | undefined { + let rest = key; + if (!rest.startsWith("address.")) return undefined; + rest = rest.slice(8); + if (rest === "") return undefined; + const address = rest; + rest = ""; + if (rest !== "") return undefined; + return { address }; +} + +/** one open enrolment */ +export interface Enrolment { + "address": string; + "ask"?: string; + "caller"?: string; + "expires"?: string; + "issued"?: string; + "keys-digest"?: string; + "node": string; + "overlay-key"?: string; + /** asked, live or redeeming */ + "stage": string; + "v"?: number; + "why"?: string; +} + +/** is what issue takes. */ +export interface IssueInput { + /** how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours */ + "expires"?: string; + /** the new machine's name in the mesh */ + "node": string; + /** the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it */ + "overlay_key": string; + /** why it joins, shown to the operator with the ask */ + "why": string; +} + +/** is what issue answers. */ +export type IssueAnswer = Record; + +/** is what redeem takes. */ +export interface RedeemInput { + /** the node the token was issued for */ + "node": string; + /** the private network key's public half */ + "overlay_key"?: string; + /** what the machine is, as its record says it */ + "profile"?: Record; + /** the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64 */ + "proof": string; + /** the identity key's public half, base64 */ + "public_key": string; + /** the sealing key's public half, which the account is sealed to */ + "sealing_key": string; + /** the serving key's public half, which the certificate binds */ + "serving_key"?: string; + /** a tunnel found on the machine: everything but its private key */ + "tunnel"?: Record; +} + +/** is what redeem answers. */ +export type RedeemAnswer = Record; + +/** is what confirm takes. */ +export interface ConfirmInput { +} + +/** is what confirm answers. */ +export type ConfirmAnswer = Record; + +/** is what revoke takes. */ +export interface RevokeInput { + /** the node */ + "node": string; + /** why, recorded with the hand act */ + "why": string; +} + +/** is what revoke answers. */ +export type RevokeAnswer = Record; + +/** is what tokens takes. */ +export interface TokensInput { + /** "true": the ended enrolments too */ + "history"?: "true" | "false"; + /** one node */ + "node"?: string; +} + +/** is what tokens answers. */ +export type TokensAnswer = Record; + +/** is what again takes. */ +export interface AgainInput { + /** the node whose ask is raised again */ + "node": string; +} + +/** is what again answers. */ +export type AgainAnswer = Record; + +/** is the body of the event enrolment-issued. */ +export type EnrolmentIssuedEvent = Enrolment; + +/** is the body of the event node-enrolled. */ +export type NodeEnrolledEvent = Enrolment; + +/** is the body of the event enrolment-refused. */ +export type EnrolmentRefusedEvent = Enrolment; + +/** is the body of the event enrolment-revoked. */ +export type EnrolmentRevokedEvent = Enrolment; + +/** is the body of the event enrolment-expired. */ +export type EnrolmentExpiredEvent = Enrolment; + +/** is one value of the state enrolments under the key node.. */ +export type EnrolmentsByNodeNodeValue = Enrolment; + +/** is one value of the state enrolments under the key address.
. */ +export interface EnrolmentsByAddressAddressValue { + "ask"?: string; + "issued"?: string; + "node": string; + "v"?: number; +} + diff --git a/ts/seats/meshvault/tsconfig.json b/identity-and-access/ts/seats/meshnodeenroller/tsconfig.json similarity index 77% rename from ts/seats/meshvault/tsconfig.json rename to identity-and-access/ts/seats/meshnodeenroller/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshvault/tsconfig.json +++ b/identity-and-access/ts/seats/meshnodeenroller/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/identity-and-access/ts/seats/meshvault/package-lock.json b/identity-and-access/ts/seats/meshvault/package-lock.json new file mode 100644 index 0000000..c7aa797 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "packages": { + "": { + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshvault/package.json b/identity-and-access/ts/seats/meshvault/package.json new file mode 100644 index 0000000..39944e5 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-vault, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/ts/seats/meshvault/src/index.ts b/identity-and-access/ts/seats/meshvault/src/index.ts similarity index 79% rename from ts/seats/meshvault/src/index.ts rename to identity-and-access/ts/seats/meshvault/src/index.ts index c78145a..112701a 100644 --- a/ts/seats/meshvault/src/index.ts +++ b/identity-and-access/ts/seats/meshvault/src/index.ts @@ -80,6 +80,54 @@ export function ownRetiredHolderSubject(holder: string): string { return "mesh.mod." + holder + ".event.own-retired"; } +export const states = { + signers: "signers", + nodeLogins: "node-logins", +} as const; + +/** The key of one value of the state signers under the form "key". */ +export function signersByKeyKey(): string { + return "key"; +} + +/** The parts of a key of the form "key", or undefined for a key of another form. */ +export function parseSignersByKeyKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("key")) return undefined; + rest = rest.slice(3); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state signers under the form "authority". */ +export function signersByAuthorityKey(): string { + return "authority"; +} + +/** The parts of a key of the form "authority", or undefined for a key of another form. */ +export function parseSignersByAuthorityKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("authority")) return undefined; + rest = rest.slice(9); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state node-logins under the form "". */ +export function nodeLoginsByNodeKey(node: string): string { + return node; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseNodeLoginsByNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + /** is what get takes. */ export interface GetInput { /** the secret's name */ @@ -225,11 +273,11 @@ export type IssueAccountAnswer = Record; export interface SignInput { /** node-certificate or enrolment */ "kind": string; - /** the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols */ + /** the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols */ "node"?: string; /** the redeem answer's bytes in base64, for an enrolment */ "reply"?: string; - /** from mesh-enrolment: the serving key's public half the enrolling node presented */ + /** from mesh-node-enroller: the serving key's public half the enrolling node presented */ "serving_key"?: string; } @@ -251,7 +299,7 @@ export interface SignAnswer { "signer": string; } -/** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ +/** the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ export interface EnrolmentTokenInputTunnel { /** the joining node's tunnel address */ "address": string; @@ -275,7 +323,7 @@ export interface EnrolmentTokenInput { "node": string; /** "true": answer the token sealed to the operator key */ "sealed"?: "true" | "false"; - /** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ + /** the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ "tunnel"?: EnrolmentTokenInputTunnel; } @@ -398,3 +446,37 @@ export interface OwnRetiredHolderEvent { "node": string; } +/** is one value of the state signers under the key key. */ +export interface SignersByKeyValue { + "fingerprint": string; + /** the signing key's public half, standard base64 */ + "key": string; + "made": string; + "v"?: number; +} + +/** is one value of the state signers under the key authority. */ +export interface SignersByAuthorityValue { + /** the authority certificate, PEM */ + "certificate": string; + "fingerprint": string; + "made": string; + "v"?: number; +} + +/** is one value of the state node-logins under the key . */ +export interface NodeLoginsByNodeValue { + "acts"?: string[]; + /** the user list's revision that held both logins, from ready on */ + "list-revision"?: number; + /** the login in use */ + "login": string; + "node": string; + /** the login rotating to, empty when none */ + "rotating"?: string; + "since": string; + /** begun, ready or confirmed; empty when no rotation */ + "stage"?: string; + "v"?: number; +} + diff --git a/identity-and-access/ts/seats/meshvault/tsconfig.json b/identity-and-access/ts/seats/meshvault/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/module.json b/module.json index c850c06..ca42cfb 100644 --- a/module.json +++ b/module.json @@ -99,16 +99,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "go-meshvault", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/meshvault" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "go-nodeengine", "kind": "package", @@ -189,16 +179,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "ts-meshvault", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/meshvault" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "ts-nodeengine", "kind": "package", diff --git a/placement/go/seats/meshmoduleassigner/contract.go b/placement/go/seats/meshmoduleassigner/contract.go new file mode 100644 index 0000000..843340f --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/contract.go @@ -0,0 +1,665 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). +package meshmoduleassigner + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-module-assigner" + +const ( + VerbPlacement = "placement" + VerbProposals = "proposals" + VerbPropose = "propose" + VerbWithdraw = "withdraw" + VerbAgain = "again" +) + +const ( + EventPlacementProposed = "placement-proposed" + EventPlacementWithdrawn = "placement-withdrawn" + EventPlacementJudged = "placement-judged" + EventPlacementChanged = "placement-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPlacement, VerbProposals, VerbPropose, VerbWithdraw, VerbAgain} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPlacementProposed, EventPlacementWithdrawn, EventPlacementJudged, EventPlacementChanged} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-module-assigner.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPlacementProposed is where placement-proposed is published. +const SubjectPlacementProposed = "mesh.seat.mesh-module-assigner.event.placement-proposed" + +// SubjectPlacementWithdrawn is where placement-withdrawn is published. +const SubjectPlacementWithdrawn = "mesh.seat.mesh-module-assigner.event.placement-withdrawn" + +// SubjectPlacementJudged is where placement-judged is published. +const SubjectPlacementJudged = "mesh.seat.mesh-module-assigner.event.placement-judged" + +// SubjectPlacementChanged is where placement-changed is published. +const SubjectPlacementChanged = "mesh.seat.mesh-module-assigner.event.placement-changed" + +const ( + StatePlacement = "placement" + StateProposals = "proposals" + StateCandidates = "candidates" + StatePulses = "pulses" + StateSeatHolders = "seat-holders" +) + +// PlacementByNodeNodeKey is the key of one value of the state placement under the form ".node". +func PlacementByNodeNodeKey(node string) string { return node + ".node" } + +// ParsePlacementByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. +func ParsePlacementByNodeNodeKey(key string) (string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".node") + if !found0 || v0 == "" { + return "", false + } + rest = after0 + if rest != "" { + return "", false + } + return v0, true +} + +// DecodePlacementByNodeNodeValue reads one value of the state placement under the form ".node", refusing a field PlacementByNodeNodeValue does not know and any text after the value. +func DecodePlacementByNodeNodeValue(raw []byte) (PlacementByNodeNodeValue, error) { + var v PlacementByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PlacementByNodeModuleKey is the key of one value of the state placement under the form ".". +func PlacementByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParsePlacementByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParsePlacementByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodePlacementByNodeModuleValue reads one value of the state placement under the form ".", refusing a field PlacementByNodeModuleValue does not know and any text after the value. +func DecodePlacementByNodeModuleValue(raw []byte) (PlacementByNodeModuleValue, error) { + var v PlacementByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ProposalsByProposalKey is the key of one value of the state proposals under the form "". +func ProposalsByProposalKey(proposal string) string { return proposal } + +// ParseProposalsByProposalKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseProposalsByProposalKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeProposalsByProposalValue reads one value of the state proposals under the form "", refusing a field ProposalsByProposalValue does not know and any text after the value. +func DecodeProposalsByProposalValue(raw []byte) (ProposalsByProposalValue, error) { + var v ProposalsByProposalValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// CandidatesByProposalNodeKey is the key of one value of the state candidates under the form ".". +func CandidatesByProposalNodeKey(proposal, node string) string { return proposal + "." + node } + +// ParseCandidatesByProposalNodeKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseCandidatesByProposalNodeKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeCandidatesByProposalNodeValue reads one value of the state candidates under the form ".", refusing a field CandidatesByProposalNodeValue does not know and any text after the value. +func DecodeCandidatesByProposalNodeValue(raw []byte) (CandidatesByProposalNodeValue, error) { + var v CandidatesByProposalNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PulsesByAnswerProposalNodeKey is the key of one value of the state pulses under the form "answer..". +func PulsesByAnswerProposalNodeKey(proposal, node string) string { + return "answer." + proposal + "." + node +} + +// ParsePulsesByAnswerProposalNodeKey reads the parts of a key of the form "answer..", in the order of the form; the last result is false for a key of another form. +func ParsePulsesByAnswerProposalNodeKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "answer.") { + return "", "", false + } + rest = rest[7:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodePulsesByAnswerProposalNodeValue reads one value of the state pulses under the form "answer..", refusing a field PulsesByAnswerProposalNodeValue does not know and any text after the value. +func DecodePulsesByAnswerProposalNodeValue(raw []byte) (PulsesByAnswerProposalNodeValue, error) { + var v PulsesByAnswerProposalNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PulsesByAskProposalKey is the key of one value of the state pulses under the form "ask.". +func PulsesByAskProposalKey(proposal string) string { return "ask." + proposal } + +// ParsePulsesByAskProposalKey reads the parts of a key of the form "ask.", in the order of the form; the last result is false for a key of another form. +func ParsePulsesByAskProposalKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "ask.") { + return "", false + } + rest = rest[4:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePulsesByAskProposalValue reads one value of the state pulses under the form "ask.", refusing a field PulsesByAskProposalValue does not know and any text after the value. +func DecodePulsesByAskProposalValue(raw []byte) (PulsesByAskProposalValue, error) { + var v PulsesByAskProposalValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SeatHoldersBySeatKey is the key of one value of the state seat-holders under the form "". +func SeatHoldersBySeatKey(seat string) string { return seat } + +// ParseSeatHoldersBySeatKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseSeatHoldersBySeatKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeSeatHoldersBySeatValue reads one value of the state seat-holders under the form "", refusing a field SeatHoldersBySeatValue does not know and any text after the value. +func DecodeSeatHoldersBySeatValue(raw []byte) (SeatHoldersBySeatValue, error) { + var v SeatHoldersBySeatValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SeatHoldersBySeatNodeKey is the key of one value of the state seat-holders under the form ".". +func SeatHoldersBySeatNodeKey(seat, node string) string { return seat + "." + node } + +// ParseSeatHoldersBySeatNodeKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseSeatHoldersBySeatNodeKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeSeatHoldersBySeatNodeValue reads one value of the state seat-holders under the form ".", refusing a field SeatHoldersBySeatNodeValue does not know and any text after the value. +func DecodeSeatHoldersBySeatNodeValue(raw []byte) (SeatHoldersBySeatNodeValue, error) { + var v SeatHoldersBySeatNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Holding is one seat a module placed on the node holds. +type Holding struct { + Claim string `json:"claim"` + Kind string `json:"kind,omitempty"` + Module string `json:"module"` + Scope string `json:"scope,omitempty"` + Site string `json:"site,omitempty"` +} + +// NodeDecided is what a person decides of a node; no secret and no key material. +type NodeDecided struct { + Account string `json:"account,omitempty"` + AccountHome string `json:"account-home,omitempty"` + Address string `json:"address,omitempty"` + Adopted bool `json:"adopted,omitempty"` + AgentAccount string `json:"agent-account,omitempty"` + AgentAccountHome string `json:"agent-account-home,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Hub bool `json:"hub,omitempty"` + // on the hub, the name a person gave a peer of that tunnel, by its public key + PeerNames map[string]string `json:"peer-names,omitempty"` + PublicDomain string `json:"public-domain,omitempty"` + Site string `json:"site,omitempty"` + Taken []string `json:"taken,omitempty"` + // the tunnel found on the node when it was adopted + Tunnel map[string]any `json:"tunnel,omitempty"` +} + +// PlacedModule is one module placed on a node. +type PlacedModule struct { + // by provision, the provider each requirement that keeps data is bound to + Bindings map[string]Provider `json:"bindings,omitempty"` + HasLayer bool `json:"has-layer,omitempty"` + HasMeshWide bool `json:"has-mesh-wide,omitempty"` + // this node's settings layer of the module's placement + Layer map[string]any `json:"layer,omitempty"` + // the whole mesh's settings layer of the module's placement + MeshWide map[string]any `json:"mesh-wide,omitempty"` + // config values a person set in the whole mesh's layer, for a module in the settings form + MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"` + Module string `json:"module"` + // the host port each declared port is published on, by the port the software listens on + Ports map[string]int64 `json:"ports,omitempty"` + // config values a person set in this node's layer, for a module in the settings form + Values map[string]any `json:"values,omitempty"` +} + +// Placement is one node's placement assembled. +type Placement struct { + Decided NodeDecided `json:"decided"` + Holds []Holding `json:"holds,omitempty"` + Modules []PlacedModule `json:"modules"` + Node string `json:"node"` + // by provision, the provider chosen for the node + Pins map[string]Provider `json:"pins,omitempty"` + Revision string `json:"revision,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PlacementEventWarrant is who approved it, through which channel, when. +type PlacementEventWarrant struct { + At string `json:"at"` + Warrant string `json:"warrant"` + Words string `json:"words"` +} + +// PlacementEvent is what each placement event carries. +type PlacementEvent struct { + // for placement-withdrawn, why it ended + Ended string `json:"ended,omitempty"` + // for placement-changed, by node the revisions before and after and the modules added, removed or changed + Nodes []map[string]any `json:"nodes,omitempty"` + Proposal Proposal `json:"proposal"` + // who approved it, through which channel, when + Warrant *PlacementEventWarrant `json:"warrant,omitempty"` +} + +// Proposal is an open proposal. +type Proposal struct { + Again string `json:"again,omitempty"` + Ask string `json:"ask,omitempty"` + At string `json:"at"` + // by node whose key it changes, the revision of the key it was laid over (0: none) + Base map[string]int64 `json:"base"` + By string `json:"by"` + Changes []ProposalChange `json:"changes"` + ExportDigest string `json:"export-digest,omitempty"` + HandBack bool `json:"hand-back,omitempty"` + ID string `json:"id"` + Must []string `json:"must,omitempty"` + Seed bool `json:"seed,omitempty"` + // answering, failed or asked + State string `json:"state"` + V int64 `json:"v,omitempty"` + Verdict *Verdict `json:"verdict,omitempty"` + Warrant *Warrant `json:"warrant,omitempty"` + Why string `json:"why"` + Withdrawn string `json:"withdrawn,omitempty"` + Words string `json:"words,omitempty"` + Written string `json:"written,omitempty"` +} + +// ProposalChangeKeys is the public halves of a joining node's keys. +type ProposalChangeKeys struct { + Identity string `json:"identity"` + Overlay string `json:"overlay,omitempty"` + Sealing string `json:"sealing"` + Serving string `json:"serving,omitempty"` +} + +// ProposalChange is one change of a proposal. +type ProposalChange struct { + Bindings map[string]Provider `json:"bindings,omitempty"` + Claim string `json:"claim,omitempty"` + Decided *NodeDecided `json:"decided,omitempty"` + Field string `json:"field,omitempty"` + // the public halves of a joining node's keys + Keys *ProposalChangeKeys `json:"keys,omitempty"` + Kind string `json:"kind,omitempty"` + Layer map[string]any `json:"layer,omitempty"` + MeshWide map[string]any `json:"mesh-wide,omitempty"` + MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"` + Module string `json:"module,omitempty"` + Node string `json:"node"` + // place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node + Op string `json:"op"` + Ports map[string]int64 `json:"ports,omitempty"` + Provider *Provider `json:"provider,omitempty"` + Provision string `json:"provision,omitempty"` + Scope string `json:"scope,omitempty"` + Site string `json:"site,omitempty"` + // a decided field's value + Value any `json:"value,omitempty"` + Values map[string]any `json:"values,omitempty"` + // the node's whole placement, for a seed + Whole *Placement `json:"whole,omitempty"` +} + +// Provider is the module on a node that answers a requirement or holds a seat. +type Provider struct { + Module string `json:"module"` + Node string `json:"node"` +} + +// SeatHoldersHoldersItem is a type of the contract. +type SeatHoldersHoldersItem struct { + Module string `json:"module"` + Node string `json:"node"` +} + +// SeatHolders is one key of seat-holders. +type SeatHolders struct { + Holders []SeatHoldersHoldersItem `json:"holders"` + Node string `json:"node,omitempty"` + // the claim names no seat of the seat list + Outside bool `json:"outside,omitempty"` + Scope string `json:"scope"` + Seat string `json:"seat"` + V int64 `json:"v,omitempty"` +} + +// Verdict is the holder's judgement of a proposal's answers. +type Verdict struct { + // every node's answer to its candidate, by node + Answers map[string]map[string]any `json:"answers,omitempty"` + At string `json:"at"` + Carried map[string][]string `json:"carried,omitempty"` + Fails []string `json:"fails,omitempty"` + Intended map[string][]string `json:"intended,omitempty"` + Notes []string `json:"notes,omitempty"` + Silent []string `json:"silent,omitempty"` + // waits, pass or fail + State string `json:"state"` + Unintended map[string][]string `json:"unintended,omitempty"` + Until string `json:"until,omitempty"` +} + +// Warrant is the enrolment's warrant an add-node carries. +type Warrant struct { + Address string `json:"address"` + Ask string `json:"ask"` + Node string `json:"node"` + Warrant string `json:"warrant"` +} + +// PlacementInput is what placement takes. +type PlacementInput struct { + // "true": the changes in order, from the stream and the state's revisions + History string `json:"history,omitempty"` + // one module: the nodes it is placed on + Module string `json:"module,omitempty"` + // one node + Node string `json:"node,omitempty"` + // "true": who holds each seat, and on which node (the state seat-holders); with node, that node's + Seats string `json:"seats,omitempty"` +} + +// The values of PlacementInput's history. +const ( + PlacementInputHistoryTrue = "true" + PlacementInputHistoryFalse = "false" +) + +// The values of PlacementInput's seats. +const ( + PlacementInputSeatsTrue = "true" + PlacementInputSeatsFalse = "false" +) + +// PlacementAnswer is what placement answers. +type PlacementAnswer = map[string]any + +// ProposalsInput is what proposals takes. +type ProposalsInput struct { + // one proposal's id + ID string `json:"id,omitempty"` +} + +// ProposalsAnswer is what proposals answers. +type ProposalsAnswer = map[string]any + +// ProposeInput is what propose takes. +type ProposeInput struct { + // the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant) + Changes []map[string]any `json:"changes"` + // for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address + Warrant map[string]any `json:"warrant,omitempty"` + // why, shown to the operator with the proposal + Why string `json:"why"` +} + +// ProposeAnswer is what propose answers. +type ProposeAnswer = map[string]any + +// WithdrawInput is what withdraw takes. +type WithdrawInput struct { + // the proposal's id + ID string `json:"id"` + // why, kept in the event + Why string `json:"why"` +} + +// WithdrawAnswer is what withdraw answers. +type WithdrawAnswer = map[string]any + +// AgainInput is what again takes. +type AgainInput struct { + // the proposal's id + ID string `json:"id"` +} + +// AgainAnswer is what again answers. +type AgainAnswer = map[string]any + +// PlacementProposedEvent is the body of the event placement-proposed. +type PlacementProposedEvent = PlacementEvent + +// PlacementWithdrawnEvent is the body of the event placement-withdrawn. +type PlacementWithdrawnEvent = PlacementEvent + +// PlacementJudgedEvent is the body of the event placement-judged. +type PlacementJudgedEvent = PlacementEvent + +// PlacementChangedEvent is the body of the event placement-changed. +type PlacementChangedEvent = PlacementEvent + +// PlacementByNodeNodeValueModulesItem is a type of the contract. +type PlacementByNodeNodeValueModulesItem struct { + // the digest of the module key's value it stands for + Digest string `json:"digest"` + Module string `json:"module"` +} + +// PlacementByNodeNodeValue is one value of the state placement under the key .node. +type PlacementByNodeNodeValue struct { + Decided NodeDecided `json:"decided"` + Holds []Holding `json:"holds,omitempty"` + // the modules placed, in the order the composition reads them + Modules []PlacementByNodeNodeValueModulesItem `json:"modules"` + Node string `json:"node"` + Pins map[string]Provider `json:"pins,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PlacementByNodeModuleValue is one value of the state placement under the key .. +type PlacementByNodeModuleValue struct { + Node string `json:"node"` + Placed PlacedModule `json:"placed"` + // the value it replaces while a write is under way + Previous *PlacedModule `json:"previous,omitempty"` + V int64 `json:"v,omitempty"` +} + +// ProposalsByProposalValue is one value of the state proposals under the key . +type ProposalsByProposalValue = Proposal + +// CandidatesByProposalNodeValue is one value of the state candidates under the key .. +type CandidatesByProposalNodeValue struct { + // the node's placement with the proposal laid over it; absent when the proposal takes its key away + Placement *Placement `json:"placement,omitempty"` + Proposal string `json:"proposal"` + // the proposal changes this node's composition through another's key + Reached bool `json:"reached,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PulsesByAnswerProposalNodeValue is one value of the state pulses under the key answer... +type PulsesByAnswerProposalNodeValue struct { + V int64 `json:"v,omitempty"` +} + +// PulsesByAskProposalValue is one value of the state pulses under the key ask.. +type PulsesByAskProposalValue struct { + V int64 `json:"v,omitempty"` +} + +// SeatHoldersBySeatValue is one value of the state seat-holders under the key . +type SeatHoldersBySeatValue = SeatHolders + +// SeatHoldersBySeatNodeValue is one value of the state seat-holders under the key .. +type SeatHoldersBySeatNodeValue = SeatHolders diff --git a/placement/go/seats/meshmoduleassigner/contract_test.go b/placement/go/seats/meshmoduleassigner/contract_test.go new file mode 100644 index 0000000..178ad88 --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/contract_test.go @@ -0,0 +1,86 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshmoduleassigner + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-module-assigner/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a proposal says placement-proposed", subject: "mesh.seat.mesh-module-assigner.event.placement-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", + decode: func(b []byte) (any, error) { var v PlacementProposedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.placement-proposed$")}, + {name: "a node's key", subject: "", given: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v PlacementByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's key", subject: "", given: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", wire: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", + decode: func(b []byte) (any, error) { + var v PlacementByNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "an open proposal", subject: "", given: "{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}", wire: "{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}", + decode: func(b []byte) (any, error) { + var v ProposalsByProposalValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a candidate", subject: "", given: "{\"proposal\":\"p-20261105t120000-aabbccdd\",\"v\":1}", wire: "{\"proposal\":\"p-20261105t120000-aabbccdd\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v CandidatesByProposalNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "the bound on an answer", subject: "", given: "{\"v\":1}", wire: "{\"v\":1}", + decode: func(b []byte) (any, error) { + var v PulsesByAnswerProposalNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a seat held once", subject: "", given: "{\"holders\":[{\"module\":\"mesh-delivery-assigner\",\"node\":\"novox\"}],\"scope\":\"mesh\",\"seat\":\"mesh-module-assigner\",\"v\":1}", wire: "{\"holders\":[{\"module\":\"mesh-delivery-assigner\",\"node\":\"novox\"}],\"scope\":\"mesh\",\"seat\":\"mesh-module-assigner\",\"v\":1}", + decode: func(b []byte) (any, error) { var v SeatHoldersBySeatValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node seat on a node", subject: "", given: "{\"holders\":[{\"module\":\"node-files\",\"node\":\"ace\"}],\"node\":\"ace\",\"scope\":\"node\",\"seat\":\"node-files\",\"v\":1}", wire: "{\"holders\":[{\"module\":\"node-files\",\"node\":\"ace\"}],\"node\":\"ace\",\"scope\":\"node\",\"seat\":\"node-files\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v SeatHoldersBySeatNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/placement/go/seats/meshmoduleassigner/go.mod b/placement/go/seats/meshmoduleassigner/go.mod new file mode 100644 index 0000000..cda234d --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner + +go 1.26.0 diff --git a/placement/go/seats/meshwants/contract.go b/placement/go/seats/meshwants/contract.go new file mode 100644 index 0000000..fc93351 --- /dev/null +++ b/placement/go/seats/meshwants/contract.go @@ -0,0 +1,331 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshwants is the contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). +package meshwants + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-wants" + +const ( + VerbUpgrade = "upgrade" + VerbBus = "bus" +) + +const ( + EventPolicyChanged = "policy-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbUpgrade, VerbBus} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPolicyChanged} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-wants.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPolicyChanged is where policy-changed is published. +const SubjectPolicyChanged = "mesh.seat.mesh-wants.event.policy-changed" + +const ( + StateWants = "wants" + StateDifferences = "differences" + StateCompositions = "compositions" +) + +// WantsByWantNodeModuleKey is the key of one value of the state wants under the form "want..". +func WantsByWantNodeModuleKey(node, module string) string { return "want." + node + "." + module } + +// ParseWantsByWantNodeModuleKey reads the parts of a key of the form "want..", in the order of the form; the last result is false for a key of another form. +func ParseWantsByWantNodeModuleKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "want.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeWantsByWantNodeModuleValue reads one value of the state wants under the form "want..", refusing a field WantsByWantNodeModuleValue does not know and any text after the value. +func DecodeWantsByWantNodeModuleValue(raw []byte) (WantsByWantNodeModuleValue, error) { + var v WantsByWantNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WantsByWantNodeNodeKey is the key of one value of the state wants under the form "want..node". +func WantsByWantNodeNodeKey(node string) string { return "want." + node + ".node" } + +// ParseWantsByWantNodeNodeKey reads the parts of a key of the form "want..node", in the order of the form; the last result is false for a key of another form. +func ParseWantsByWantNodeNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "want.") { + return "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".node") + if !found1 || v1 == "" { + return "", false + } + rest = after1 + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeWantsByWantNodeNodeValue reads one value of the state wants under the form "want..node", refusing a field WantsByWantNodeNodeValue does not know and any text after the value. +func DecodeWantsByWantNodeNodeValue(raw []byte) (WantsByWantNodeNodeValue, error) { + var v WantsByWantNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WantsByHeldModuleFingerprintKey is the key of one value of the state wants under the form "held..". +func WantsByHeldModuleFingerprintKey(module, fingerprint string) string { + return "held." + module + "." + fingerprint +} + +// ParseWantsByHeldModuleFingerprintKey reads the parts of a key of the form "held..", in the order of the form; the last result is false for a key of another form. +func ParseWantsByHeldModuleFingerprintKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "held.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeWantsByHeldModuleFingerprintValue reads one value of the state wants under the form "held..", refusing a field WantsByHeldModuleFingerprintValue does not know and any text after the value. +func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFingerprintValue, error) { + var v WantsByHeldModuleFingerprintValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// DifferencesByNodeModuleKey is the key of one value of the state differences under the form ".". +func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParseDifferencesByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseDifferencesByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeDifferencesByNodeModuleValue reads one value of the state differences under the form ".", refusing a field DifferencesByNodeModuleValue does not know and any text after the value. +func DecodeDifferencesByNodeModuleValue(raw []byte) (DifferencesByNodeModuleValue, error) { + var v DifferencesByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// CompositionsByGroupHeadsKey is the key of one value of the state compositions under the form ".". +func CompositionsByGroupHeadsKey(group, heads string) string { return group + "." + heads } + +// ParseCompositionsByGroupHeadsKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseCompositionsByGroupHeadsKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeCompositionsByGroupHeadsValue reads one value of the state compositions under the form ".", refusing a field CompositionsByGroupHeadsValue does not know and any text after the value. +func DecodeCompositionsByGroupHeadsValue(raw []byte) (CompositionsByGroupHeadsValue, error) { + var v CompositionsByGroupHeadsValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// UpgradeInput is what upgrade takes. +type UpgradeInput struct { + // "true": each node's own composition, by its digest, and the assigner's findings + Compare string `json:"compare,omitempty"` + // one module + Module string `json:"module,omitempty"` + // with module: roll, together, record, or default to take a person's choice back + Policy string `json:"policy,omitempty"` + // with policy: why, required and kept + Why string `json:"why,omitempty"` +} + +// The values of UpgradeInput's compare. +const ( + UpgradeInputCompareTrue = "true" + UpgradeInputCompareFalse = "false" +) + +// UpgradeAnswer is what upgrade answers. +type UpgradeAnswer = map[string]any + +// BusInput is what bus takes. +type BusInput struct { +} + +// BusAnswer is what bus answers. +type BusAnswer = map[string]any + +// PolicyChangedEvent is the body of the event policy-changed. +type PolicyChangedEvent = map[string]any + +// WantsByWantNodeModuleValue is one value of the state wants under the key want... +type WantsByWantNodeModuleValue struct { + // again or skip + Backup string `json:"backup,omitempty"` + Composed string `json:"composed,omitempty"` + Fp string `json:"fp,omitempty"` + Hold bool `json:"hold,omitempty"` + Newest string `json:"newest,omitempty"` + Put bool `json:"put,omitempty"` + Release string `json:"release,omitempty"` + Removed bool `json:"removed,omitempty"` + // the rule that holds the node back, 1 to 10 + Rule int64 `json:"rule,omitempty"` + Self bool `json:"self,omitempty"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` + Yes string `json:"yes,omitempty"` +} + +// WantsByWantNodeNodeValue is one value of the state wants under the key want..node. +type WantsByWantNodeNodeValue struct { + Composed string `json:"composed"` + Self bool `json:"self,omitempty"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` +} + +// WantsByHeldModuleFingerprintValue is one value of the state wants under the key held... +type WantsByHeldModuleFingerprintValue struct { + Node string `json:"node"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` +} + +// DifferencesByNodeModuleValue is one value of the state differences under the key .. +type DifferencesByNodeModuleValue struct { + Rule int64 `json:"rule,omitempty"` + Sent string `json:"sent,omitempty"` + SentBuild string `json:"sent-build,omitempty"` + Unexplained bool `json:"unexplained,omitempty"` + V int64 `json:"v,omitempty"` + Want string `json:"want,omitempty"` + WantRemoved bool `json:"want-removed,omitempty"` + Words string `json:"words,omitempty"` +} + +// CompositionsByGroupHeadsValue is one value of the state compositions under the key .. +type CompositionsByGroupHeadsValue struct { + Group string `json:"group"` + // 12 lowercase hex digits + Heads string `json:"heads"` + Machine string `json:"machine,omitempty"` + Machines []string `json:"machines,omitempty"` + Sdk string `json:"sdk,omitempty"` + // pass, fail or waits + State string `json:"state"` + // milliseconds + Took int64 `json:"took,omitempty"` + V int64 `json:"v,omitempty"` + Warnings []string `json:"warnings,omitempty"` + Words string `json:"words,omitempty"` +} diff --git a/placement/go/seats/meshwants/contract_test.go b/placement/go/seats/meshwants/contract_test.go new file mode 100644 index 0000000..1355a9b --- /dev/null +++ b/placement/go/seats/meshwants/contract_test.go @@ -0,0 +1,76 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshwants + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-wants/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByWantNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByWantNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a held fingerprint", subject: "", given: "{\"node\":\"ace\",\"v\":1}", wire: "{\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByHeldModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a difference", subject: "", given: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", wire: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", + decode: func(b []byte) (any, error) { + var v DifferencesByNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a composition", subject: "", given: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", wire: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v CompositionsByGroupHeadsValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/placement/go/seats/meshwants/go.mod b/placement/go/seats/meshwants/go.mod new file mode 100644 index 0000000..a075868 --- /dev/null +++ b/placement/go/seats/meshwants/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants + +go 1.26.0 diff --git a/placement/module.json b/placement/module.json new file mode 100644 index 0000000..9be9cf6 --- /dev/null +++ b/placement/module.json @@ -0,0 +1,58 @@ +{ + "module": "mesh-placement-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshmoduleassigner", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshwants", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshwants" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshmoduleassignerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshmoduleassignerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshmoduleassigner", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshwants", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshwants" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/placement/seats/index.json b/placement/seats/index.json new file mode 100644 index 0000000..e7d0756 --- /dev/null +++ b/placement/seats/index.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seats": [ + "mesh-module-assigner", + "mesh-wants" + ] +} diff --git a/placement/seats/mesh-module-assigner/fixtures/events.json b/placement/seats/mesh-module-assigner/fixtures/events.json new file mode 100644 index 0000000..1886c81 --- /dev/null +++ b/placement/seats/mesh-module-assigner/fixtures/events.json @@ -0,0 +1,29 @@ +[ + { + "name": "a proposal says placement-proposed", + "of": "event:placement-proposed", + "subject": "mesh.seat.mesh-module-assigner.event.placement-proposed", + "given": { + "proposal": { + "id": "p-20261105t120000-aabbccdd", + "by": "operator", + "why": "move plex", + "at": "2026-11-05T12:00:00Z", + "changes": [ + { + "op": "place", + "node": "ace", + "module": "plex", + "ports": { + "32400": 32400 + } + } + ], + "base": { + "ace": 3 + }, + "state": "answering" + } + } + } +] diff --git a/placement/seats/mesh-module-assigner/fixtures/states.json b/placement/seats/mesh-module-assigner/fixtures/states.json new file mode 100644 index 0000000..2ccef79 --- /dev/null +++ b/placement/seats/mesh-module-assigner/fixtures/states.json @@ -0,0 +1,109 @@ +[ + { + "name": "a node's key", + "of": "state:placement/.node", + "given": { + "v": 1, + "node": "ace", + "decided": { + "address": "10.0.0.2" + }, + "modules": [ + { + "module": "plex", + "digest": "0123456789abcdef" + } + ] + } + }, + { + "name": "a module's key", + "of": "state:placement/.", + "given": { + "v": 1, + "node": "ace", + "placed": { + "module": "plex", + "ports": { + "32400": 32400 + }, + "bindings": { + "database": { + "node": "ace", + "module": "postgres" + } + } + } + } + }, + { + "name": "an open proposal", + "of": "state:proposals/", + "given": { + "id": "p-20261105t120000-aabbccdd", + "by": "operator", + "why": "move plex", + "at": "2026-11-05T12:00:00Z", + "changes": [ + { + "op": "place", + "node": "ace", + "module": "plex", + "ports": { + "32400": 32400 + } + } + ], + "base": { + "ace": 3 + }, + "state": "answering" + } + }, + { + "name": "a candidate", + "of": "state:candidates/.", + "given": { + "v": 1, + "proposal": "p-20261105t120000-aabbccdd" + } + }, + { + "name": "the bound on an answer", + "of": "state:pulses/answer..", + "given": { + "v": 1 + } + }, + { + "name": "a seat held once", + "of": "state:seat-holders/", + "given": { + "v": 1, + "seat": "mesh-module-assigner", + "scope": "mesh", + "holders": [ + { + "module": "mesh-delivery-assigner", + "node": "novox" + } + ] + } + }, + { + "name": "a node seat on a node", + "of": "state:seat-holders/.", + "given": { + "v": 1, + "seat": "node-files", + "scope": "node", + "node": "ace", + "holders": [ + { + "module": "node-files", + "node": "ace" + } + ] + } + } +] diff --git a/placement/seats/mesh-module-assigner/seat.json b/placement/seats/mesh-module-assigner/seat.json new file mode 100644 index 0000000..8d987c9 --- /dev/null +++ b/placement/seats/mesh-module-assigner/seat.json @@ -0,0 +1,941 @@ +{ + "format": 1, + "seat": "mesh-module-assigner", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0567, ADR 0634, ADR 0635", + "description": "The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).", + "verbs": [ + { + "name": "placement", + "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", + "optional": true, + "input": { + "properties": { + "history": { + "description": "\"true\": the changes in order, from the stream and the state's revisions", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module: the nodes it is placed on", + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + }, + "seats": { + "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "placement as read: nodes, keys with their revisions, history, seat holders, the state's fill" + } + }, + { + "name": "proposals", + "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.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "one proposal's id", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the open proposals" + } + }, + { + "name": "propose", + "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", + "optional": true, + "input": { + "additionalProperties": false, + "properties": { + "changes": { + "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", + "items": { + "required": [ + "op", + "node" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "warrant": { + "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", + "required": [ + "node", + "address", + "ask", + "warrant" + ], + "type": "object" + }, + "why": { + "description": "why, shown to the operator with the proposal", + "type": "string" + } + }, + "required": [ + "why", + "changes" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the proposal's id and its verdict, or every finding refused at once" + } + }, + { + "name": "withdraw", + "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + }, + "why": { + "description": "why, kept in the event", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the proposal withdrawn" + } + }, + { + "name": "again", + "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what was judged or asked again" + } + } + ], + "events": [ + { + "name": "placement-proposed", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-withdrawn", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-judged", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-changed", + "body": { + "$ref": "#/defs/PlacementEvent" + } + } + ], + "family": { + "name": "mesh-module-assigner.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "placement", + "template": "state", + "key-forms": [ + { + "key": ".node", + "value": { + "type": "object", + "description": "a node's key: what a person decided of it, its pins and holdings, and the modules placed on it; the commit of a write", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "digest": { + "type": "string", + "description": "the digest of the module key's value it stands for" + } + }, + "required": [ + "module", + "digest" + ] + }, + "description": "the modules placed, in the order the composition reads them" + }, + "pins": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + } + }, + "holds": { + "type": "array", + "items": { + "$ref": "#/defs/Holding" + } + } + }, + "required": [ + "node", + "decided", + "modules" + ] + }, + "description": "a node's key" + }, + { + "key": ".", + "value": { + "type": "object", + "description": "a module's key: the module as placed", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "placed": { + "$ref": "#/defs/PlacedModule" + }, + "previous": { + "$ref": "#/defs/PlacedModule", + "description": "the value it replaces while a write is under way" + } + }, + "required": [ + "node", + "placed" + ] + }, + "description": "one module on a node; no module is named node" + } + ], + "history": 64, + "description": "placements, one key per node and one per module placed on it, written only on the operator's Approve of a proposal by compare-and-set at the revision it was laid over" + }, + { + "name": "proposals", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "$ref": "#/defs/Proposal" + }, + "description": "an open proposal, by its id" + } + ], + "history": 5, + "description": "open proposals; a key leaves when the proposal is applied, withdrawn, declined or expired" + }, + { + "name": "candidates", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "one node's candidate in one proposal", + "properties": { + "v": { + "type": "integer" + }, + "proposal": { + "type": "string" + }, + "reached": { + "type": "boolean", + "description": "the proposal changes this node's composition through another's key" + }, + "placement": { + "$ref": "#/defs/Placement", + "description": "the node's placement with the proposal laid over it; absent when the proposal takes its key away" + } + }, + "required": [ + "proposal" + ] + }, + "description": "a node's candidate" + } + ], + "history": 5, + "description": "the candidates a proposal needs answers on; a node watches its own" + }, + { + "name": "pulses", + "template": "state", + "key-forms": [ + { + "key": "answer..", + "value": { + "type": "object", + "description": "the bound on one node's answer", + "properties": { + "v": { + "type": "integer" + } + } + }, + "description": "the bound on a node's answer" + }, + { + "key": "ask.", + "value": { + "type": "object", + "description": "the bound on a raised ask", + "properties": { + "v": { + "type": "integer" + } + } + }, + "description": "the bound on a proposal's raised ask" + } + ], + "description": "bounds, each a key that lives for the bound; the server's delete marker when one ends is the event that fails a silent node or an unanswered ask" + }, + { + "name": "seat-holders", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "$ref": "#/defs/SeatHolders" + }, + "description": "a seat held once for the mesh" + }, + { + "key": ".", + "value": { + "$ref": "#/defs/SeatHolders" + }, + "description": "a node seat on one node" + } + ], + "description": "who holds which seat on which node, composed from every node's placement" + } + ], + "defs": { + "Provider": { + "type": "object", + "description": "the module on a node that answers a requirement or holds a seat", + "properties": { + "node": { + "type": "string" + }, + "module": { + "type": "string" + } + }, + "required": [ + "node", + "module" + ] + }, + "PlacedModule": { + "type": "object", + "description": "one module placed on a node", + "properties": { + "module": { + "type": "string" + }, + "ports": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "the host port each declared port is published on, by the port the software listens on" + }, + "bindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + }, + "description": "by provision, the provider each requirement that keeps data is bound to" + }, + "mesh-wide": { + "type": "object", + "description": "the whole mesh's settings layer of the module's placement" + }, + "has-mesh-wide": { + "type": "boolean" + }, + "layer": { + "type": "object", + "description": "this node's settings layer of the module's placement" + }, + "has-layer": { + "type": "boolean" + }, + "mesh-wide-values": { + "type": "object", + "description": "config values a person set in the whole mesh's layer, for a module in the settings form" + }, + "values": { + "type": "object", + "description": "config values a person set in this node's layer, for a module in the settings form" + } + }, + "required": [ + "module" + ] + }, + "Holding": { + "type": "object", + "description": "one seat a module placed on the node holds", + "properties": { + "claim": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "module": { + "type": "string" + }, + "site": { + "type": "string" + }, + "kind": { + "type": "string" + } + }, + "required": [ + "claim", + "module" + ] + }, + "NodeDecided": { + "type": "object", + "description": "what a person decides of a node; no secret and no key material", + "properties": { + "endpoint": { + "type": "string" + }, + "site": { + "type": "string" + }, + "hub": { + "type": "boolean" + }, + "address": { + "type": "string" + }, + "public-domain": { + "type": "string" + }, + "account": { + "type": "string" + }, + "account-home": { + "type": "string" + }, + "agent-account": { + "type": "string" + }, + "agent-account-home": { + "type": "string" + }, + "adopted": { + "type": "boolean" + }, + "taken": { + "type": "array", + "items": { + "type": "string" + } + }, + "tunnel": { + "type": "object", + "description": "the tunnel found on the node when it was adopted" + }, + "peer-names": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "on the hub, the name a person gave a peer of that tunnel, by its public key" + } + } + }, + "Placement": { + "type": "object", + "description": "one node's placement assembled", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/defs/PlacedModule" + } + }, + "pins": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + }, + "description": "by provision, the provider chosen for the node" + }, + "holds": { + "type": "array", + "items": { + "$ref": "#/defs/Holding" + } + } + }, + "required": [ + "node", + "decided", + "modules" + ] + }, + "ProposalChange": { + "type": "object", + "description": "one change of a proposal", + "properties": { + "op": { + "type": "string", + "description": "place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node" + }, + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "ports": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "bindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + } + }, + "provision": { + "type": "string" + }, + "provider": { + "$ref": "#/defs/Provider" + }, + "claim": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "site": { + "type": "string" + }, + "field": { + "type": "string" + }, + "value": { + "description": "a decided field's value" + }, + "mesh-wide": { + "type": "object" + }, + "layer": { + "type": "object" + }, + "mesh-wide-values": { + "type": "object" + }, + "values": { + "type": "object" + }, + "whole": { + "$ref": "#/defs/Placement", + "description": "the node's whole placement, for a seed" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "keys": { + "type": "object", + "properties": { + "identity": { + "type": "string" + }, + "overlay": { + "type": "string" + }, + "sealing": { + "type": "string" + }, + "serving": { + "type": "string" + } + }, + "required": [ + "identity", + "sealing" + ], + "description": "the public halves of a joining node's keys" + } + }, + "required": [ + "op", + "node" + ] + }, + "Warrant": { + "type": "object", + "description": "the enrolment's warrant an add-node carries", + "properties": { + "node": { + "type": "string" + }, + "address": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "warrant": { + "type": "string" + } + }, + "required": [ + "node", + "address", + "ask", + "warrant" + ] + }, + "Verdict": { + "type": "object", + "description": "the holder's judgement of a proposal's answers", + "properties": { + "state": { + "type": "string", + "description": "waits, pass or fail" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "fails": { + "type": "array", + "items": { + "type": "string" + } + }, + "silent": { + "type": "array", + "items": { + "type": "string" + } + }, + "until": { + "type": "string", + "format": "date-time" + }, + "intended": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "unintended": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "carried": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "answers": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "description": "every node's answer to its candidate, by node" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "state", + "at" + ] + }, + "Proposal": { + "type": "object", + "description": "an open proposal", + "properties": { + "v": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "written": { + "type": "string", + "format": "date-time" + }, + "changes": { + "type": "array", + "items": { + "$ref": "#/defs/ProposalChange" + } + }, + "seed": { + "type": "boolean" + }, + "export-digest": { + "type": "string" + }, + "base": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "by node whose key it changes, the revision of the key it was laid over (0: none)" + }, + "must": { + "type": "array", + "items": { + "type": "string" + } + }, + "hand-back": { + "type": "boolean" + }, + "state": { + "type": "string", + "description": "answering, failed or asked" + }, + "verdict": { + "$ref": "#/defs/Verdict" + }, + "ask": { + "type": "string" + }, + "words": { + "type": "string" + }, + "again": { + "type": "string", + "format": "date-time" + }, + "warrant": { + "$ref": "#/defs/Warrant" + }, + "withdrawn": { + "type": "string" + } + }, + "required": [ + "id", + "by", + "why", + "at", + "changes", + "base", + "state" + ] + }, + "PlacementEvent": { + "type": "object", + "description": "what each placement event carries", + "properties": { + "proposal": { + "$ref": "#/defs/Proposal" + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + }, + "description": "for placement-changed, by node the revisions before and after and the modules added, removed or changed" + }, + "warrant": { + "type": "object", + "description": "who approved it, through which channel, when", + "properties": { + "warrant": { + "type": "string" + }, + "words": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "warrant", + "words", + "at" + ] + }, + "ended": { + "type": "string", + "description": "for placement-withdrawn, why it ended" + } + }, + "required": [ + "proposal" + ] + }, + "SeatHolders": { + "type": "object", + "description": "one key of seat-holders", + "properties": { + "v": { + "type": "integer" + }, + "seat": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "node": { + "type": "string" + }, + "outside": { + "type": "boolean", + "description": "the claim names no seat of the seat list" + }, + "holders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + }, + "required": [ + "module", + "node" + ] + } + } + }, + "required": [ + "seat", + "scope", + "holders" + ] + } + } +} diff --git a/placement/seats/mesh-wants/fixtures/states.json b/placement/seats/mesh-wants/fixtures/states.json new file mode 100644 index 0000000..cb38a39 --- /dev/null +++ b/placement/seats/mesh-wants/fixtures/states.json @@ -0,0 +1,47 @@ +[ + { + "name": "a want", + "of": "state:wants/want..", + "given": { + "v": 1, + "fp": "ab12", + "rule": 3 + } + }, + { + "name": "a node's own want", + "of": "state:wants/want..node", + "given": { + "v": 1, + "composed": "sha256:aa", + "self": true + } + }, + { + "name": "a held fingerprint", + "of": "state:wants/held..", + "given": { + "v": 1, + "node": "ace" + } + }, + { + "name": "a difference", + "of": "state:differences/.", + "given": { + "v": 1, + "want": "ab12", + "unexplained": true + } + }, + { + "name": "a composition", + "of": "state:compositions/.", + "given": { + "v": 1, + "group": "g1", + "heads": "0123456789ab", + "state": "pass" + } + } +] diff --git a/placement/seats/mesh-wants/seat.json b/placement/seats/mesh-wants/seat.json new file mode 100644 index 0000000..141b7d0 --- /dev/null +++ b/placement/seats/mesh-wants/seat.json @@ -0,0 +1,279 @@ +{ + "format": 1, + "seat": "mesh-wants", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", + "description": "The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).", + "verbs": [ + { + "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, each node's own composition and the assigner's findings.", + "input": { + "properties": { + "compare": { + "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module", + "type": "string" + }, + "policy": { + "description": "with module: roll, together, record, or default to take a person's choice back", + "type": "string" + }, + "why": { + "description": "with policy: why, required and kept", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "each module's policy, or the one chosen, or the comparison" + } + }, + { + "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": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what a bus upgrade would do, read only" + } + } + ], + "events": [ + { + "name": "policy-changed", + "body": { + "type": "object" + } + } + ], + "family": { + "name": "mesh-wants.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "wants", + "template": "state", + "key-forms": [ + { + "key": "want..", + "value": { + "type": "object", + "description": "a node's want for a module", + "properties": { + "v": { + "type": "integer" + }, + "fp": { + "type": "string" + }, + "composed": { + "type": "string" + }, + "removed": { + "type": "boolean" + }, + "newest": { + "type": "string" + }, + "rule": { + "type": "integer", + "description": "the rule that holds the node back, 1 to 10" + }, + "backup": { + "type": "string", + "description": "again or skip" + }, + "words": { + "type": "string" + }, + "put": { + "type": "boolean" + }, + "hold": { + "type": "boolean" + }, + "yes": { + "type": "string" + }, + "release": { + "type": "string" + }, + "self": { + "type": "boolean" + } + } + }, + "description": "a node's want for a module" + }, + { + "key": "want..node", + "value": { + "type": "object", + "description": "a node's own want", + "properties": { + "v": { + "type": "integer" + }, + "composed": { + "type": "string" + }, + "words": { + "type": "string" + }, + "self": { + "type": "boolean" + } + }, + "required": [ + "composed" + ] + }, + "description": "a node's own want, naming the digest of its own composition" + }, + { + "key": "held..", + "value": { + "type": "object", + "description": "a fingerprint the assigner holds", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "words": { + "type": "string" + } + }, + "required": [ + "node" + ] + }, + "description": "a module's fingerprint the assigner holds" + } + ], + "history": 20, + "description": "what each node should run, and the fingerprints held back" + }, + { + "name": "differences", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "the difference between a want and what is sent", + "properties": { + "v": { + "type": "integer" + }, + "want": { + "type": "string" + }, + "want-removed": { + "type": "boolean" + }, + "sent": { + "type": "string" + }, + "sent-build": { + "type": "string" + }, + "rule": { + "type": "integer" + }, + "unexplained": { + "type": "boolean" + }, + "words": { + "type": "string" + } + } + }, + "description": "a module on a node" + } + ], + "history": 64, + "description": "differences between a want and what the node-engine is sent" + }, + { + "name": "compositions", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "the composition verdict of a group's check at its heads", + "properties": { + "v": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "heads": { + "type": "string", + "description": "12 lowercase hex digits" + }, + "state": { + "type": "string", + "description": "pass, fail or waits" + }, + "words": { + "type": "string" + }, + "sdk": { + "type": "string" + }, + "machines": { + "type": "array", + "items": { + "type": "string" + } + }, + "machine": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "took": { + "type": "integer", + "description": "milliseconds" + } + }, + "required": [ + "group", + "heads", + "state" + ] + }, + "description": "a group's check at its heads" + } + ], + "history": 5, + "description": "composition verdicts of a group's check; the checker reads them" + } + ] +} diff --git a/placement/ts/seats/meshmoduleassigner/package-lock.json b/placement/ts/seats/meshmoduleassigner/package-lock.json new file mode 100644 index 0000000..b346586 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "packages": { + "": { + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshmoduleassigner/package.json b/placement/ts/seats/meshmoduleassigner/package.json new file mode 100644 index 0000000..5c25c3f --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-module-assigner, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshmoduleassigner/src/index.ts b/placement/ts/seats/meshmoduleassigner/src/index.ts new file mode 100644 index 0000000..8f958a4 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/src/index.ts @@ -0,0 +1,488 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). + +export const seat = "mesh-module-assigner"; + +export const verbs = { + placement: "placement", + proposals: "proposals", + propose: "propose", + withdraw: "withdraw", + again: "again", +} as const; + +export const events = { + placementProposed: "placement-proposed", + placementWithdrawn: "placement-withdrawn", + placementJudged: "placement-judged", + placementChanged: "placement-changed", +} as const; + +export const family = { name: "mesh-module-assigner.events", maxAgeDays: 7 } as const; + +export const placementProposedSubject = "mesh.seat.mesh-module-assigner.event.placement-proposed"; + +export const placementWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.placement-withdrawn"; + +export const placementJudgedSubject = "mesh.seat.mesh-module-assigner.event.placement-judged"; + +export const placementChangedSubject = "mesh.seat.mesh-module-assigner.event.placement-changed"; + +export const states = { + placement: "placement", + proposals: "proposals", + candidates: "candidates", + pulses: "pulses", + seatHolders: "seat-holders", +} as const; + +/** The key of one value of the state placement under the form ".node". */ +export function placementByNodeNodeKey(node: string): string { + return node + ".node"; +} + +/** The parts of a key of the form ".node", or undefined for a key of another form. */ +export function parsePlacementByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + const at0 = rest.indexOf(".node"); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 5); + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state placement under the form ".". */ +export function placementByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parsePlacementByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state proposals under the form "". */ +export function proposalsByProposalKey(proposal: string): string { + return proposal; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseProposalsByProposalKey(key: string): { proposal: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const proposal = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal }; +} + +/** The key of one value of the state candidates under the form ".". */ +export function candidatesByProposalNodeKey(proposal: string, node: string): string { + return proposal + "." + node; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseCandidatesByProposalNodeKey(key: string): { proposal: string; node: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const proposal = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal, node }; +} + +/** The key of one value of the state pulses under the form "answer..". */ +export function pulsesByAnswerProposalNodeKey(proposal: string, node: string): string { + return "answer." + proposal + "." + node; +} + +/** The parts of a key of the form "answer..", or undefined for a key of another form. */ +export function parsePulsesByAnswerProposalNodeKey(key: string): { proposal: string; node: string } | undefined { + let rest = key; + if (!rest.startsWith("answer.")) return undefined; + rest = rest.slice(7); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const proposal = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal, node }; +} + +/** The key of one value of the state pulses under the form "ask.". */ +export function pulsesByAskProposalKey(proposal: string): string { + return "ask." + proposal; +} + +/** The parts of a key of the form "ask.", or undefined for a key of another form. */ +export function parsePulsesByAskProposalKey(key: string): { proposal: string } | undefined { + let rest = key; + if (!rest.startsWith("ask.")) return undefined; + rest = rest.slice(4); + if (rest === "") return undefined; + const proposal = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal }; +} + +/** The key of one value of the state seat-holders under the form "". */ +export function seatHoldersBySeatKey(seat: string): string { + return seat; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseSeatHoldersBySeatKey(key: string): { seat: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const seat = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat }; +} + +/** The key of one value of the state seat-holders under the form ".". */ +export function seatHoldersBySeatNodeKey(seat: string, node: string): string { + return seat + "." + node; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseSeatHoldersBySeatNodeKey(key: string): { seat: string; node: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const seat = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat, node }; +} + +/** one seat a module placed on the node holds */ +export interface Holding { + "claim": string; + "kind"?: string; + "module": string; + "scope"?: string; + "site"?: string; +} + +/** what a person decides of a node; no secret and no key material */ +export interface NodeDecided { + "account"?: string; + "account-home"?: string; + "address"?: string; + "adopted"?: boolean; + "agent-account"?: string; + "agent-account-home"?: string; + "endpoint"?: string; + "hub"?: boolean; + /** on the hub, the name a person gave a peer of that tunnel, by its public key */ + "peer-names"?: Record; + "public-domain"?: string; + "site"?: string; + "taken"?: string[]; + /** the tunnel found on the node when it was adopted */ + "tunnel"?: Record; +} + +/** one module placed on a node */ +export interface PlacedModule { + /** by provision, the provider each requirement that keeps data is bound to */ + "bindings"?: Record; + "has-layer"?: boolean; + "has-mesh-wide"?: boolean; + /** this node's settings layer of the module's placement */ + "layer"?: Record; + /** the whole mesh's settings layer of the module's placement */ + "mesh-wide"?: Record; + /** config values a person set in the whole mesh's layer, for a module in the settings form */ + "mesh-wide-values"?: Record; + "module": string; + /** the host port each declared port is published on, by the port the software listens on */ + "ports"?: Record; + /** config values a person set in this node's layer, for a module in the settings form */ + "values"?: Record; +} + +/** one node's placement assembled */ +export interface Placement { + "decided": NodeDecided; + "holds"?: Holding[]; + "modules": PlacedModule[]; + "node": string; + /** by provision, the provider chosen for the node */ + "pins"?: Record; + "revision"?: string; + "v"?: number; +} + +/** who approved it, through which channel, when */ +export interface PlacementEventWarrant { + "at": string; + "warrant": string; + "words": string; +} + +/** what each placement event carries */ +export interface PlacementEvent { + /** for placement-withdrawn, why it ended */ + "ended"?: string; + /** for placement-changed, by node the revisions before and after and the modules added, removed or changed */ + "nodes"?: Record[]; + "proposal": Proposal; + /** who approved it, through which channel, when */ + "warrant"?: PlacementEventWarrant; +} + +/** an open proposal */ +export interface Proposal { + "again"?: string; + "ask"?: string; + "at": string; + /** by node whose key it changes, the revision of the key it was laid over (0: none) */ + "base": Record; + "by": string; + "changes": ProposalChange[]; + "export-digest"?: string; + "hand-back"?: boolean; + "id": string; + "must"?: string[]; + "seed"?: boolean; + /** answering, failed or asked */ + "state": string; + "v"?: number; + "verdict"?: Verdict; + "warrant"?: Warrant; + "why": string; + "withdrawn"?: string; + "words"?: string; + "written"?: string; +} + +/** the public halves of a joining node's keys */ +export interface ProposalChangeKeys { + "identity": string; + "overlay"?: string; + "sealing": string; + "serving"?: string; +} + +/** one change of a proposal */ +export interface ProposalChange { + "bindings"?: Record; + "claim"?: string; + "decided"?: NodeDecided; + "field"?: string; + /** the public halves of a joining node's keys */ + "keys"?: ProposalChangeKeys; + "kind"?: string; + "layer"?: Record; + "mesh-wide"?: Record; + "mesh-wide-values"?: Record; + "module"?: string; + "node": string; + /** place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node */ + "op": string; + "ports"?: Record; + "provider"?: Provider; + "provision"?: string; + "scope"?: string; + "site"?: string; + /** a decided field's value */ + "value"?: unknown; + "values"?: Record; + /** the node's whole placement, for a seed */ + "whole"?: Placement; +} + +/** the module on a node that answers a requirement or holds a seat */ +export interface Provider { + "module": string; + "node": string; +} + +export interface SeatHoldersHoldersItem { + "module": string; + "node": string; +} + +/** one key of seat-holders */ +export interface SeatHolders { + "holders": SeatHoldersHoldersItem[]; + "node"?: string; + /** the claim names no seat of the seat list */ + "outside"?: boolean; + "scope": string; + "seat": string; + "v"?: number; +} + +/** the holder's judgement of a proposal's answers */ +export interface Verdict { + /** every node's answer to its candidate, by node */ + "answers"?: Record>; + "at": string; + "carried"?: Record; + "fails"?: string[]; + "intended"?: Record; + "notes"?: string[]; + "silent"?: string[]; + /** waits, pass or fail */ + "state": string; + "unintended"?: Record; + "until"?: string; +} + +/** the enrolment's warrant an add-node carries */ +export interface Warrant { + "address": string; + "ask": string; + "node": string; + "warrant": string; +} + +/** is what placement takes. */ +export interface PlacementInput { + /** "true": the changes in order, from the stream and the state's revisions */ + "history"?: "true" | "false"; + /** one module: the nodes it is placed on */ + "module"?: string; + /** one node */ + "node"?: string; + /** "true": who holds each seat, and on which node (the state seat-holders); with node, that node's */ + "seats"?: "true" | "false"; +} + +/** is what placement answers. */ +export type PlacementAnswer = Record; + +/** is what proposals takes. */ +export interface ProposalsInput { + /** one proposal's id */ + "id"?: string; +} + +/** is what proposals answers. */ +export type ProposalsAnswer = Record; + +/** is what propose takes. */ +export interface ProposeInput { + /** the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant) */ + "changes": Record[]; + /** for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address */ + "warrant"?: Record; + /** why, shown to the operator with the proposal */ + "why": string; +} + +/** is what propose answers. */ +export type ProposeAnswer = Record; + +/** is what withdraw takes. */ +export interface WithdrawInput { + /** the proposal's id */ + "id": string; + /** why, kept in the event */ + "why": string; +} + +/** is what withdraw answers. */ +export type WithdrawAnswer = Record; + +/** is what again takes. */ +export interface AgainInput { + /** the proposal's id */ + "id": string; +} + +/** is what again answers. */ +export type AgainAnswer = Record; + +/** is the body of the event placement-proposed. */ +export type PlacementProposedEvent = PlacementEvent; + +/** is the body of the event placement-withdrawn. */ +export type PlacementWithdrawnEvent = PlacementEvent; + +/** is the body of the event placement-judged. */ +export type PlacementJudgedEvent = PlacementEvent; + +/** is the body of the event placement-changed. */ +export type PlacementChangedEvent = PlacementEvent; + +export interface PlacementByNodeNodeValueModulesItem { + /** the digest of the module key's value it stands for */ + "digest": string; + "module": string; +} + +/** is one value of the state placement under the key .node. */ +export interface PlacementByNodeNodeValue { + "decided": NodeDecided; + "holds"?: Holding[]; + /** the modules placed, in the order the composition reads them */ + "modules": PlacementByNodeNodeValueModulesItem[]; + "node": string; + "pins"?: Record; + "v"?: number; +} + +/** is one value of the state placement under the key .. */ +export interface PlacementByNodeModuleValue { + "node": string; + "placed": PlacedModule; + /** the value it replaces while a write is under way */ + "previous"?: PlacedModule; + "v"?: number; +} + +/** is one value of the state proposals under the key . */ +export type ProposalsByProposalValue = Proposal; + +/** is one value of the state candidates under the key .. */ +export interface CandidatesByProposalNodeValue { + /** the node's placement with the proposal laid over it; absent when the proposal takes its key away */ + "placement"?: Placement; + "proposal": string; + /** the proposal changes this node's composition through another's key */ + "reached"?: boolean; + "v"?: number; +} + +/** is one value of the state pulses under the key answer... */ +export interface PulsesByAnswerProposalNodeValue { + "v"?: number; +} + +/** is one value of the state pulses under the key ask.. */ +export interface PulsesByAskProposalValue { + "v"?: number; +} + +/** is one value of the state seat-holders under the key . */ +export type SeatHoldersBySeatValue = SeatHolders; + +/** is one value of the state seat-holders under the key .. */ +export type SeatHoldersBySeatNodeValue = SeatHolders; + diff --git a/placement/ts/seats/meshmoduleassigner/tsconfig.json b/placement/ts/seats/meshmoduleassigner/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/placement/ts/seats/meshwants/package-lock.json b/placement/ts/seats/meshwants/package-lock.json new file mode 100644 index 0000000..e274634 --- /dev/null +++ b/placement/ts/seats/meshwants/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-placement-contracts-mesh-wants", + "packages": { + "": { + "name": "@novox/mesh-placement-contracts-mesh-wants", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshwants/package.json b/placement/ts/seats/meshwants/package.json new file mode 100644 index 0000000..d4a29ee --- /dev/null +++ b/placement/ts/seats/meshwants/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-wants, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-placement-contracts-mesh-wants", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshwants/src/index.ts b/placement/ts/seats/meshwants/src/index.ts new file mode 100644 index 0000000..ad3dab8 --- /dev/null +++ b/placement/ts/seats/meshwants/src/index.ts @@ -0,0 +1,211 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). + +export const seat = "mesh-wants"; + +export const verbs = { + upgrade: "upgrade", + bus: "bus", +} as const; + +export const events = { + policyChanged: "policy-changed", +} as const; + +export const family = { name: "mesh-wants.events", maxAgeDays: 7 } as const; + +export const policyChangedSubject = "mesh.seat.mesh-wants.event.policy-changed"; + +export const states = { + wants: "wants", + differences: "differences", + compositions: "compositions", +} as const; + +/** The key of one value of the state wants under the form "want..". */ +export function wantsByWantNodeModuleKey(node: string, module: string): string { + return "want." + node + "." + module; +} + +/** The parts of a key of the form "want..", or undefined for a key of another form. */ +export function parseWantsByWantNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + if (!rest.startsWith("want.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const node = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state wants under the form "want..node". */ +export function wantsByWantNodeNodeKey(node: string): string { + return "want." + node + ".node"; +} + +/** The parts of a key of the form "want..node", or undefined for a key of another form. */ +export function parseWantsByWantNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("want.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf(".node"); + if (at1 <= 0) return undefined; + const node = rest.slice(0, at1); + rest = rest.slice(at1 + 5); + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state wants under the form "held..". */ +export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: string): string { + return "held." + module + "." + fingerprint; +} + +/** The parts of a key of the form "held..", or undefined for a key of another form. */ +export function parseWantsByHeldModuleFingerprintKey(key: string): { module: string; fingerprint: string } | undefined { + let rest = key; + if (!rest.startsWith("held.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const module = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const fingerprint = rest; + rest = ""; + if (rest !== "") return undefined; + return { module, fingerprint }; +} + +/** The key of one value of the state differences under the form ".". */ +export function differencesByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseDifferencesByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state compositions under the form ".". */ +export function compositionsByGroupHeadsKey(group: string, heads: string): string { + return group + "." + heads; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseCompositionsByGroupHeadsKey(key: string): { group: string; heads: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const group = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const heads = rest; + rest = ""; + if (rest !== "") return undefined; + return { group, heads }; +} + +/** is what upgrade takes. */ +export interface UpgradeInput { + /** "true": each node's own composition, by its digest, and the assigner's findings */ + "compare"?: "true" | "false"; + /** one module */ + "module"?: string; + /** with module: roll, together, record, or default to take a person's choice back */ + "policy"?: string; + /** with policy: why, required and kept */ + "why"?: string; +} + +/** is what upgrade answers. */ +export type UpgradeAnswer = Record; + +/** is what bus takes. */ +export interface BusInput { +} + +/** is what bus answers. */ +export type BusAnswer = Record; + +/** is the body of the event policy-changed. */ +export type PolicyChangedEvent = Record; + +/** is one value of the state wants under the key want... */ +export interface WantsByWantNodeModuleValue { + /** again or skip */ + "backup"?: string; + "composed"?: string; + "fp"?: string; + "hold"?: boolean; + "newest"?: string; + "put"?: boolean; + "release"?: string; + "removed"?: boolean; + /** the rule that holds the node back, 1 to 10 */ + "rule"?: number; + "self"?: boolean; + "v"?: number; + "words"?: string; + "yes"?: string; +} + +/** is one value of the state wants under the key want..node. */ +export interface WantsByWantNodeNodeValue { + "composed": string; + "self"?: boolean; + "v"?: number; + "words"?: string; +} + +/** is one value of the state wants under the key held... */ +export interface WantsByHeldModuleFingerprintValue { + "node": string; + "v"?: number; + "words"?: string; +} + +/** is one value of the state differences under the key .. */ +export interface DifferencesByNodeModuleValue { + "rule"?: number; + "sent"?: string; + "sent-build"?: string; + "unexplained"?: boolean; + "v"?: number; + "want"?: string; + "want-removed"?: boolean; + "words"?: string; +} + +/** is one value of the state compositions under the key .. */ +export interface CompositionsByGroupHeadsValue { + "group": string; + /** 12 lowercase hex digits */ + "heads": string; + "machine"?: string; + "machines"?: string[]; + "sdk"?: string; + /** pass, fail or waits */ + "state": string; + /** milliseconds */ + "took"?: number; + "v"?: number; + "warnings"?: string[]; + "words"?: string; +} + diff --git a/placement/ts/seats/meshwants/tsconfig.json b/placement/ts/seats/meshwants/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/placement/ts/seats/meshwants/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/seats/index.json b/seats/index.json index eed4efb..00b0a20 100644 --- a/seats/index.json +++ b/seats/index.json @@ -3,15 +3,11 @@ "seats": [ "mesh-store", "mesh-broker", - "mesh-vault", "mesh-config", "mesh-artifact-store", "mesh-catalog", "mesh-delivery", - "mesh-placement", "mesh-delivery-checker", - "mesh-wants", - "mesh-enrolment", "mesh-artifact-sweep", "npm-package-registry", "go-package-registry", @@ -42,7 +38,6 @@ "node-lock-screen", "node-clipboard", "node-bar", - "node-compositor", - "node-secret-service" + "node-compositor" ] } diff --git a/seats/mesh-placement/seat.json b/seats/mesh-placement/seat.json deleted file mode 100644 index cc3d44c..0000000 --- a/seats/mesh-placement/seat.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "format": 1, - "seat": "mesh-placement", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0567", - "verbs": [ - { - "name": "placement", - "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", - "optional": true, - "input": { - "properties": { - "history": { - "description": "\"true\": the changes in order, from the stream and the state's revisions", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module: the nodes it is placed on", - "type": "string" - }, - "node": { - "description": "one node", - "type": "string" - }, - "seats": { - "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "proposals", - "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.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "one proposal's id", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "propose", - "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", - "optional": true, - "input": { - "additionalProperties": false, - "properties": { - "changes": { - "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", - "items": { - "required": [ - "op", - "node" - ], - "type": "object" - }, - "minItems": 1, - "type": "array" - }, - "warrant": { - "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", - "required": [ - "node", - "address", - "ask", - "warrant" - ], - "type": "object" - }, - "why": { - "description": "why, shown to the operator with the proposal", - "type": "string" - } - }, - "required": [ - "why", - "changes" - ], - "type": "object" - } - }, - { - "name": "withdraw", - "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - }, - "why": { - "description": "why, kept in the event", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - } - }, - { - "name": "again", - "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - } - } - ] -} diff --git a/seats/mesh-wants/seat.json b/seats/mesh-wants/seat.json deleted file mode 100644 index 8b50c1a..0000000 --- a/seats/mesh-wants/seat.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "format": 1, - "seat": "mesh-wants", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0565", - "verbs": [ - { - "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, each node's own composition and the assigner's findings.", - "input": { - "properties": { - "compare": { - "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module", - "type": "string" - }, - "policy": { - "description": "with module: roll, together, record, or default to take a person's choice back", - "type": "string" - }, - "why": { - "description": "with policy: why, required and kept", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": {}, - "type": "object" - } - } - ], - "events": [ - { - "name": "policy-changed" - } - ] -} diff --git a/ts/seats/meshvault/package.json b/ts/seats/meshvault/package.json deleted file mode 100644 index b811690..0000000 --- a/ts/seats/meshvault/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "description": "The contract of the seat mesh-vault, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, - "exports": { - ".": "./dist/index.js" - }, - "files": [ - "dist" - ], - "name": "@novox/mesh-contracts-mesh-vault", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, - "type": "module", - "version": "0.1.0" -} -- 2.54.0 From 929bf5539ef3100e25aac42fa2007527d727c39c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:11:19 +0100 Subject: [PATCH 50/94] =?UTF-8?q?Every=20seat=20definition=20names=20its?= =?UTF-8?q?=20glossary=20domain;=20the=20schema=20requires=20it=20(ADR=200?= =?UTF-8?q?636=20=C2=A75;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- connectivity/seats/mesh-dns-resolver/seat.json | 1 + connectivity/seats/node-hostname/seat.json | 1 + .../seats/node-intrusion-prevention/seat.json | 1 + connectivity/seats/node-packet-filter/seat.json | 1 + connectivity/seats/node-resolver/seat.json | 1 + connectivity/seats/node-uplink/seat.json | 1 + .../seats/the-private-network/seat.json | 1 + format/seat-definition.schema.json | 17 +++++++++++++++++ .../seats/mesh-conditions/seat.json | 1 + .../seats/mesh-hand-acts/seat.json | 1 + health-and-repair/seats/mesh-healers/seat.json | 1 + .../seats/mesh-self-check/seat.json | 1 + .../seats/mesh-watchdogs/seat.json | 1 + .../seats/operator-channel/seat.json | 1 + provisioning/seats/mesh-provisioner/seat.json | 1 + provisioning/seats/node-mounts/seat.json | 1 + provisioning/seats/node-nfs-server/seat.json | 1 + provisioning/seats/node-provisioner/seat.json | 1 + seats/container-image-registry/seat.json | 1 + seats/git/seat.json | 1 + seats/go-package-registry/seat.json | 1 + seats/mesh-artifact-store/seat.json | 1 + seats/mesh-artifact-sweep/seat.json | 1 + seats/mesh-broker/seat.json | 1 + seats/mesh-catalog/seat.json | 1 + seats/mesh-config/seat.json | 1 + seats/mesh-delivery-checker/seat.json | 1 + seats/mesh-delivery/seat.json | 1 + seats/mesh-enrolment/seat.json | 1 + seats/mesh-placement/seat.json | 1 + seats/mesh-store/seat.json | 1 + seats/mesh-vault/seat.json | 1 + seats/mesh-wants/seat.json | 1 + seats/node-backup/seat.json | 1 + seats/node-bar/seat.json | 1 + seats/node-build-go/seat.json | 1 + seats/node-build-image/seat.json | 1 + seats/node-build-node/seat.json | 1 + seats/node-clipboard/seat.json | 1 + seats/node-compositor/seat.json | 1 + seats/node-container-runtime/seat.json | 1 + seats/node-display-server/seat.json | 1 + seats/node-display-session/seat.json | 1 + seats/node-engine/seat.json | 1 + seats/node-environment/seat.json | 1 + seats/node-files/seat.json | 1 + seats/node-hotkeys/seat.json | 1 + seats/node-launcher/seat.json | 1 + seats/node-lock-screen/seat.json | 1 + seats/node-login-manager/seat.json | 1 + seats/node-login-shell/seat.json | 1 + seats/node-message-bus/seat.json | 1 + seats/node-notifier/seat.json | 1 + seats/node-package-manager/seat.json | 1 + seats/node-power/seat.json | 1 + seats/node-reads/seat.json | 1 + seats/node-secret-service/seat.json | 1 + seats/node-service-manager/seat.json | 1 + seats/node-terminal-emulator/seat.json | 1 + seats/npm-package-registry/seat.json | 1 + seats/oci-artifact-registry/seat.json | 1 + 61 files changed, 77 insertions(+) diff --git a/connectivity/seats/mesh-dns-resolver/seat.json b/connectivity/seats/mesh-dns-resolver/seat.json index 14480ac..0868271 100644 --- a/connectivity/seats/mesh-dns-resolver/seat.json +++ b/connectivity/seats/mesh-dns-resolver/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-dns-resolver", "scope": "mesh", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0194, ADR 0223", "delivers": "wildcard-resolution", "replicated": true diff --git a/connectivity/seats/node-hostname/seat.json b/connectivity/seats/node-hostname/seat.json index 0f646af..327c4d5 100644 --- a/connectivity/seats/node-hostname/seat.json +++ b/connectivity/seats/node-hostname/seat.json @@ -3,6 +3,7 @@ "seat": "node-hostname", "scope": "node", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0199, ADR 0223", "verbs": [ { diff --git a/connectivity/seats/node-intrusion-prevention/seat.json b/connectivity/seats/node-intrusion-prevention/seat.json index a987a70..b2bc355 100644 --- a/connectivity/seats/node-intrusion-prevention/seat.json +++ b/connectivity/seats/node-intrusion-prevention/seat.json @@ -3,6 +3,7 @@ "seat": "node-intrusion-prevention", "scope": "node", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0121", "verbs": [ { diff --git a/connectivity/seats/node-packet-filter/seat.json b/connectivity/seats/node-packet-filter/seat.json index fb69e1c..005b50c 100644 --- a/connectivity/seats/node-packet-filter/seat.json +++ b/connectivity/seats/node-packet-filter/seat.json @@ -3,6 +3,7 @@ "seat": "node-packet-filter", "scope": "node", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0121", "verbs": [ { diff --git a/connectivity/seats/node-resolver/seat.json b/connectivity/seats/node-resolver/seat.json index 8689bcd..be8a91f 100644 --- a/connectivity/seats/node-resolver/seat.json +++ b/connectivity/seats/node-resolver/seat.json @@ -3,6 +3,7 @@ "seat": "node-resolver", "scope": "node", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0247", "verbs": [ { diff --git a/connectivity/seats/node-uplink/seat.json b/connectivity/seats/node-uplink/seat.json index 0ecfbf1..cd59ee5 100644 --- a/connectivity/seats/node-uplink/seat.json +++ b/connectivity/seats/node-uplink/seat.json @@ -3,6 +3,7 @@ "seat": "node-uplink", "scope": "node", "grain": "seat-list", + "domain": "Connectivity", "decision": "novox/hq ADR 0117", "verbs": [ { diff --git a/connectivity/seats/the-private-network/seat.json b/connectivity/seats/the-private-network/seat.json index 2646620..bb27936 100644 --- a/connectivity/seats/the-private-network/seat.json +++ b/connectivity/seats/the-private-network/seat.json @@ -3,6 +3,7 @@ "seat": "the-private-network", "scope": "node", "grain": "whole", + "domain": "Connectivity", "decision": "novox/hq ADR 0110, ADR 0634, ADR 0635", "description": "The seat of the private network (ADR 0110): the one network the mesh runs over, which a machine can have only one of; the mesh provides its holder itself and the provision carries no fields.", "verbs": [], diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index 3202f33..9dbc5ce 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -10,6 +10,7 @@ "seat", "scope", "grain", + "domain", "decision" ], "properties": { @@ -32,6 +33,22 @@ "seat-list" ] }, + "domain": { + "description": "the glossary domain the seat belongs to, by its heading (ADR 0636 \u00a75)", + "enum": [ + "Module", + "Config", + "Core", + "Placement", + "Provisioning", + "Identity and access", + "Delivery", + "Health and repair", + "Data", + "Connectivity", + "Operator and conversation" + ] + }, "decision": { "type": "string", "minLength": 1 diff --git a/health-and-repair/seats/mesh-conditions/seat.json b/health-and-repair/seats/mesh-conditions/seat.json index ee5b697..76f464c 100644 --- a/health-and-repair/seats/mesh-conditions/seat.json +++ b/health-and-repair/seats/mesh-conditions/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-conditions", "scope": "mesh", "grain": "whole", + "domain": "Health and repair", "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", "description": "What is wrong with the mesh now (ADR 0564): the open conditions raised from the observers' findings and cleared when a finding goes.", "verbs": [ diff --git a/health-and-repair/seats/mesh-hand-acts/seat.json b/health-and-repair/seats/mesh-hand-acts/seat.json index e635d7a..a075cfc 100644 --- a/health-and-repair/seats/mesh-hand-acts/seat.json +++ b/health-and-repair/seats/mesh-hand-acts/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-hand-acts", "scope": "mesh", "grain": "whole", + "domain": "Health and repair", "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", "description": "Acts done by hand, recorded with who, why and cause (ADR 0565): the log every module records through.", "verbs": [ diff --git a/health-and-repair/seats/mesh-healers/seat.json b/health-and-repair/seats/mesh-healers/seat.json index e402fe6..23772b0 100644 --- a/health-and-repair/seats/mesh-healers/seat.json +++ b/health-and-repair/seats/mesh-healers/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-healers", "scope": "mesh", "grain": "whole", + "domain": "Health and repair", "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", "description": "The healers (ADR 0564): each a registered response to one kind of condition, within a budget and the mesh-wide brake.", "verbs": [ diff --git a/health-and-repair/seats/mesh-self-check/seat.json b/health-and-repair/seats/mesh-self-check/seat.json index d73fc2d..40025a8 100644 --- a/health-and-repair/seats/mesh-self-check/seat.json +++ b/health-and-repair/seats/mesh-self-check/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-self-check", "scope": "mesh", "grain": "whole", + "domain": "Health and repair", "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", "description": "The mesh checking itself (ADR 0564): probes run every five minutes, each failure a finding the seat's conditions raise.", "verbs": [ diff --git a/health-and-repair/seats/mesh-watchdogs/seat.json b/health-and-repair/seats/mesh-watchdogs/seat.json index ecf09a3..35a4341 100644 --- a/health-and-repair/seats/mesh-watchdogs/seat.json +++ b/health-and-repair/seats/mesh-watchdogs/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-watchdogs", "scope": "mesh", "grain": "whole", + "domain": "Health and repair", "decision": "novox/hq ADR 0564, ADR 0634, ADR 0635", "description": "The watchman's seat (ADR 0564, ADR 0433): signals that expire unless renewed, what stopped moving, who is present, the dead letters.", "verbs": [ diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index 9770349..0bd8c21 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -3,6 +3,7 @@ "seat": "operator-channel", "scope": "mesh", "grain": "whole", + "domain": "Operator and conversation", "decision": "novox/hq ADR 0259", "holder-declares": true, "description": "Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26).", diff --git a/provisioning/seats/mesh-provisioner/seat.json b/provisioning/seats/mesh-provisioner/seat.json index 8641fa8..0806593 100644 --- a/provisioning/seats/mesh-provisioner/seat.json +++ b/provisioning/seats/mesh-provisioner/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-provisioner", "scope": "mesh", "grain": "whole", + "domain": "Provisioning", "decision": "novox/hq ADR 0349, ADR 0634, ADR 0635", "description": "The provisioner (ADR 0349): which domain answers which kind of placeholder, what each module needs, what is applied where.", "verbs": [ diff --git a/provisioning/seats/node-mounts/seat.json b/provisioning/seats/node-mounts/seat.json index 43f1655..eadb518 100644 --- a/provisioning/seats/node-mounts/seat.json +++ b/provisioning/seats/node-mounts/seat.json @@ -3,6 +3,7 @@ "seat": "node-mounts", "scope": "node", "grain": "seat-list", + "domain": "Provisioning", "decision": "novox/hq ADR 0263", "verbs": [ { diff --git a/provisioning/seats/node-nfs-server/seat.json b/provisioning/seats/node-nfs-server/seat.json index 4cedbde..664dd3d 100644 --- a/provisioning/seats/node-nfs-server/seat.json +++ b/provisioning/seats/node-nfs-server/seat.json @@ -3,6 +3,7 @@ "seat": "node-nfs-server", "scope": "node", "grain": "seat-list", + "domain": "Provisioning", "decision": "novox/hq ADR 0263", "verbs": [ { diff --git a/provisioning/seats/node-provisioner/seat.json b/provisioning/seats/node-provisioner/seat.json index 9752f8b..3510f6f 100644 --- a/provisioning/seats/node-provisioner/seat.json +++ b/provisioning/seats/node-provisioner/seat.json @@ -3,5 +3,6 @@ "seat": "node-provisioner", "scope": "node", "grain": "seat-list", + "domain": "Provisioning", "decision": "novox/hq ADR 0349" } diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json index b1a595f..3de19d8 100644 --- a/seats/container-image-registry/seat.json +++ b/seats/container-image-registry/seat.json @@ -3,6 +3,7 @@ "seat": "container-image-registry", "scope": "mesh", "grain": "whole", + "domain": "Delivery", "decision": "novox/hq ADR 0605", "description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.", "includes": [ diff --git a/seats/git/seat.json b/seats/git/seat.json index 836285a..b7c55a1 100644 --- a/seats/git/seat.json +++ b/seats/git/seat.json @@ -3,6 +3,7 @@ "seat": "git", "scope": "mesh", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0111", "delivers": "git" } diff --git a/seats/go-package-registry/seat.json b/seats/go-package-registry/seat.json index edf3998..9d42636 100644 --- a/seats/go-package-registry/seat.json +++ b/seats/go-package-registry/seat.json @@ -3,6 +3,7 @@ "seat": "go-package-registry", "scope": "mesh", "grain": "whole", + "domain": "Delivery", "decision": "novox/hq ADR 0605", "description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.", "delivers": "go-package-registry", diff --git a/seats/mesh-artifact-store/seat.json b/seats/mesh-artifact-store/seat.json index 84c8a57..d82ab33 100644 --- a/seats/mesh-artifact-store/seat.json +++ b/seats/mesh-artifact-store/seat.json @@ -3,5 +3,6 @@ "seat": "mesh-artifact-store", "scope": "mesh", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0075" } diff --git a/seats/mesh-artifact-sweep/seat.json b/seats/mesh-artifact-sweep/seat.json index 602a5f8..2e5b673 100644 --- a/seats/mesh-artifact-sweep/seat.json +++ b/seats/mesh-artifact-sweep/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-artifact-sweep", "scope": "mesh", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0571", "verbs": [ { diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index f0ebf4a..6fc5d4e 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-broker", "scope": "mesh", "grain": "whole", + "domain": "Core", "decision": "novox/hq ADR 0079", "delivers": "mesh-bus", "description": "The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone.", diff --git a/seats/mesh-catalog/seat.json b/seats/mesh-catalog/seat.json index 40b6618..5f71e23 100644 --- a/seats/mesh-catalog/seat.json +++ b/seats/mesh-catalog/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-catalog", "scope": "mesh", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0604", "verbs": [ { diff --git a/seats/mesh-config/seat.json b/seats/mesh-config/seat.json index 640ccbb..83c63f3 100644 --- a/seats/mesh-config/seat.json +++ b/seats/mesh-config/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-config", "scope": "mesh", "grain": "whole", + "domain": "Config", "decision": "novox/hq ADR 0348, ADR 0565", "verbs": [ { diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json index 7b56ab5..808b3e9 100644 --- a/seats/mesh-delivery-checker/seat.json +++ b/seats/mesh-delivery-checker/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-delivery-checker", "scope": "mesh", "grain": "whole", + "domain": "Delivery", "decision": "novox/hq ADR 0547", "verbs": [ { diff --git a/seats/mesh-delivery/seat.json b/seats/mesh-delivery/seat.json index bafd85c..43ba193 100644 --- a/seats/mesh-delivery/seat.json +++ b/seats/mesh-delivery/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-delivery", "scope": "mesh", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0239", "verbs": [ { diff --git a/seats/mesh-enrolment/seat.json b/seats/mesh-enrolment/seat.json index b611558..474653a 100644 --- a/seats/mesh-enrolment/seat.json +++ b/seats/mesh-enrolment/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-enrolment", "scope": "mesh", "grain": "seat-list", + "domain": "Identity and access", "decision": "novox/hq ADR 0587", "verbs": [ { diff --git a/seats/mesh-placement/seat.json b/seats/mesh-placement/seat.json index cc3d44c..c38d9f4 100644 --- a/seats/mesh-placement/seat.json +++ b/seats/mesh-placement/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-placement", "scope": "mesh", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0567", "verbs": [ { diff --git a/seats/mesh-store/seat.json b/seats/mesh-store/seat.json index f02517a..fe1c4dd 100644 --- a/seats/mesh-store/seat.json +++ b/seats/mesh-store/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-store", "scope": "mesh", "grain": "seat-list", + "domain": "Module", "decision": "novox/hq ADR 0079", "delivers": "postgres-database", "verbs": [ diff --git a/seats/mesh-vault/seat.json b/seats/mesh-vault/seat.json index 5a115a6..e83f02a 100644 --- a/seats/mesh-vault/seat.json +++ b/seats/mesh-vault/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-vault", "scope": "mesh", "grain": "whole", + "domain": "Identity and access", "decision": "novox/hq ADR 0161", "delivers": "secret", "verbs": [ diff --git a/seats/mesh-wants/seat.json b/seats/mesh-wants/seat.json index 8b50c1a..3de06f7 100644 --- a/seats/mesh-wants/seat.json +++ b/seats/mesh-wants/seat.json @@ -3,6 +3,7 @@ "seat": "mesh-wants", "scope": "mesh", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0565", "verbs": [ { diff --git a/seats/node-backup/seat.json b/seats/node-backup/seat.json index 465f68f..f5c59d2 100644 --- a/seats/node-backup/seat.json +++ b/seats/node-backup/seat.json @@ -3,6 +3,7 @@ "seat": "node-backup", "scope": "node", "grain": "seat-list", + "domain": "Data", "decision": "novox/hq ADR 0214", "verbs": [ { diff --git a/seats/node-bar/seat.json b/seats/node-bar/seat.json index dc73191..92249d1 100644 --- a/seats/node-bar/seat.json +++ b/seats/node-bar/seat.json @@ -3,6 +3,7 @@ "seat": "node-bar", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "receives": [ { diff --git a/seats/node-build-go/seat.json b/seats/node-build-go/seat.json index 5beead8..ccb5664 100644 --- a/seats/node-build-go/seat.json +++ b/seats/node-build-go/seat.json @@ -3,6 +3,7 @@ "seat": "node-build-go", "scope": "node", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", "pulled-by-kind": true, "yields": true, diff --git a/seats/node-build-image/seat.json b/seats/node-build-image/seat.json index 96065eb..b275f85 100644 --- a/seats/node-build-image/seat.json +++ b/seats/node-build-image/seat.json @@ -3,6 +3,7 @@ "seat": "node-build-image", "scope": "node", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", "pulled-by-kind": true, "yields": true, diff --git a/seats/node-build-node/seat.json b/seats/node-build-node/seat.json index d2b8fb7..aa7f182 100644 --- a/seats/node-build-node/seat.json +++ b/seats/node-build-node/seat.json @@ -3,6 +3,7 @@ "seat": "node-build-node", "scope": "node", "grain": "seat-list", + "domain": "Delivery", "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", "pulled-by-kind": true, "yields": true, diff --git a/seats/node-clipboard/seat.json b/seats/node-clipboard/seat.json index 0f21e51..88dde07 100644 --- a/seats/node-clipboard/seat.json +++ b/seats/node-clipboard/seat.json @@ -3,6 +3,7 @@ "seat": "node-clipboard", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-compositor/seat.json b/seats/node-compositor/seat.json index 4ce95ee..f126432 100644 --- a/seats/node-compositor/seat.json +++ b/seats/node-compositor/seat.json @@ -3,5 +3,6 @@ "seat": "node-compositor", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208" } diff --git a/seats/node-container-runtime/seat.json b/seats/node-container-runtime/seat.json index 4a68219..1b1bf02 100644 --- a/seats/node-container-runtime/seat.json +++ b/seats/node-container-runtime/seat.json @@ -3,5 +3,6 @@ "seat": "node-container-runtime", "scope": "node", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0166, ADR 0207" } diff --git a/seats/node-display-server/seat.json b/seats/node-display-server/seat.json index 8523e84..20c587d 100644 --- a/seats/node-display-server/seat.json +++ b/seats/node-display-server/seat.json @@ -3,6 +3,7 @@ "seat": "node-display-server", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-display-session/seat.json b/seats/node-display-session/seat.json index 6edac04..a815db1 100644 --- a/seats/node-display-session/seat.json +++ b/seats/node-display-session/seat.json @@ -3,6 +3,7 @@ "seat": "node-display-session", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 0987936..20ed8a1 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -3,6 +3,7 @@ "seat": "node-engine", "scope": "node", "grain": "whole", + "domain": "Core", "decision": "novox/hq ADR 0563", "description": "The node seat every node's engine holds (novox/hq ADR 0563, ADR 0618 §3, §4): its hand acts on the operator's warrant, its read-only judgements, its compositions and what it reads of a manifest. Each verb is answered on this node's subject, mesh.seat.node-engine.tool.., and each event is published on mesh.seat.node-engine.event.., the node's name its last token.", "held-by-the-engine": true, diff --git a/seats/node-environment/seat.json b/seats/node-environment/seat.json index 064beb0..75bb5d3 100644 --- a/seats/node-environment/seat.json +++ b/seats/node-environment/seat.json @@ -3,5 +3,6 @@ "seat": "node-environment", "scope": "node", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0203" } diff --git a/seats/node-files/seat.json b/seats/node-files/seat.json index 7ad768f..6a43cf4 100644 --- a/seats/node-files/seat.json +++ b/seats/node-files/seat.json @@ -3,6 +3,7 @@ "seat": "node-files", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0394", "verbs": [ { diff --git a/seats/node-hotkeys/seat.json b/seats/node-hotkeys/seat.json index cedd627..aec1269 100644 --- a/seats/node-hotkeys/seat.json +++ b/seats/node-hotkeys/seat.json @@ -3,6 +3,7 @@ "seat": "node-hotkeys", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0212", "receives": [ { diff --git a/seats/node-launcher/seat.json b/seats/node-launcher/seat.json index 3eab2eb..977e537 100644 --- a/seats/node-launcher/seat.json +++ b/seats/node-launcher/seat.json @@ -3,6 +3,7 @@ "seat": "node-launcher", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-lock-screen/seat.json b/seats/node-lock-screen/seat.json index 4cd19ed..38e5340 100644 --- a/seats/node-lock-screen/seat.json +++ b/seats/node-lock-screen/seat.json @@ -3,6 +3,7 @@ "seat": "node-lock-screen", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-login-manager/seat.json b/seats/node-login-manager/seat.json index e0a70ba..2b3d4fa 100644 --- a/seats/node-login-manager/seat.json +++ b/seats/node-login-manager/seat.json @@ -3,6 +3,7 @@ "seat": "node-login-manager", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-login-shell/seat.json b/seats/node-login-shell/seat.json index c868e5b..9d6295a 100644 --- a/seats/node-login-shell/seat.json +++ b/seats/node-login-shell/seat.json @@ -3,6 +3,7 @@ "seat": "node-login-shell", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0204", "verbs": [ { diff --git a/seats/node-message-bus/seat.json b/seats/node-message-bus/seat.json index 83c6744..4c19846 100644 --- a/seats/node-message-bus/seat.json +++ b/seats/node-message-bus/seat.json @@ -3,5 +3,6 @@ "seat": "node-message-bus", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0215" } diff --git a/seats/node-notifier/seat.json b/seats/node-notifier/seat.json index f391310..565d898 100644 --- a/seats/node-notifier/seat.json +++ b/seats/node-notifier/seat.json @@ -3,6 +3,7 @@ "seat": "node-notifier", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/node-package-manager/seat.json b/seats/node-package-manager/seat.json index c2b32b0..034e878 100644 --- a/seats/node-package-manager/seat.json +++ b/seats/node-package-manager/seat.json @@ -3,5 +3,6 @@ "seat": "node-package-manager", "scope": "node", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0207" } diff --git a/seats/node-power/seat.json b/seats/node-power/seat.json index 762a79f..13679b4 100644 --- a/seats/node-power/seat.json +++ b/seats/node-power/seat.json @@ -3,5 +3,6 @@ "seat": "node-power", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0211" } diff --git a/seats/node-reads/seat.json b/seats/node-reads/seat.json index 938a71a..5dae465 100644 --- a/seats/node-reads/seat.json +++ b/seats/node-reads/seat.json @@ -3,6 +3,7 @@ "seat": "node-reads", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0310", "verbs": [ { diff --git a/seats/node-secret-service/seat.json b/seats/node-secret-service/seat.json index fd5b84b..9dc3e27 100644 --- a/seats/node-secret-service/seat.json +++ b/seats/node-secret-service/seat.json @@ -3,5 +3,6 @@ "seat": "node-secret-service", "scope": "node", "grain": "seat-list", + "domain": "Identity and access", "decision": "novox/hq ADR 0208" } diff --git a/seats/node-service-manager/seat.json b/seats/node-service-manager/seat.json index 9270cc2..695bc00 100644 --- a/seats/node-service-manager/seat.json +++ b/seats/node-service-manager/seat.json @@ -3,6 +3,7 @@ "seat": "node-service-manager", "scope": "node", "grain": "seat-list", + "domain": "Placement", "decision": "novox/hq ADR 0177", "verbs": [ { diff --git a/seats/node-terminal-emulator/seat.json b/seats/node-terminal-emulator/seat.json index 732a5ba..dd856ee 100644 --- a/seats/node-terminal-emulator/seat.json +++ b/seats/node-terminal-emulator/seat.json @@ -3,6 +3,7 @@ "seat": "node-terminal-emulator", "scope": "node", "grain": "seat-list", + "domain": "Core", "decision": "novox/hq ADR 0208", "verbs": [ { diff --git a/seats/npm-package-registry/seat.json b/seats/npm-package-registry/seat.json index 61f77c3..ae2ab3d 100644 --- a/seats/npm-package-registry/seat.json +++ b/seats/npm-package-registry/seat.json @@ -3,6 +3,7 @@ "seat": "npm-package-registry", "scope": "mesh", "grain": "whole", + "domain": "Delivery", "decision": "novox/hq ADR 0109", "description": "The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.", "delivers": "npm-package-registry", diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json index 9d37fce..a9d969c 100644 --- a/seats/oci-artifact-registry/seat.json +++ b/seats/oci-artifact-registry/seat.json @@ -3,6 +3,7 @@ "seat": "oci-artifact-registry", "scope": "mesh", "grain": "whole", + "domain": "Delivery", "decision": "novox/hq ADR 0605", "description": "The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.", "includes": [ -- 2.54.0 From c5fc93644f7b9176758d2c204a395eef5485e514 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:14:01 +0100 Subject: [PATCH 51/94] =?UTF-8?q?mesh-module-assigner=20client:=20the=20pl?= =?UTF-8?q?acement=20keys=20assembled=20into=20a=20node's=20placement,=20w?= =?UTF-8?q?ho=20stands=20where=20since=20when,=20who=20holds=20a=20seat,?= =?UTF-8?q?=20what=20a=20placement=20says=20to=20the=20composition;=20the?= =?UTF-8?q?=20node=20key's=20digest=20kept=20as=20the=20first=20writer=20m?= =?UTF-8?q?ade=20it=20(ADR=200634=20=C2=A74;=20C-6;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meshmoduleassignerclient/client_test.go | 257 +++++++++ .../clients/meshmoduleassignerclient/go.mod | 8 + .../meshmoduleassignerclient/holders.go | 53 ++ .../meshmoduleassignerclient/holders_test.go | 65 +++ .../clients/meshmoduleassignerclient/keys.go | 545 ++++++++++++++++++ .../meshmoduleassignerclient/placed.go | 159 +++++ .../meshmoduleassignerclient/placement.go | 295 ++++++++++ 7 files changed, 1382 insertions(+) create mode 100644 placement/go/clients/meshmoduleassignerclient/client_test.go create mode 100644 placement/go/clients/meshmoduleassignerclient/go.mod create mode 100644 placement/go/clients/meshmoduleassignerclient/holders.go create mode 100644 placement/go/clients/meshmoduleassignerclient/holders_test.go create mode 100644 placement/go/clients/meshmoduleassignerclient/keys.go create mode 100644 placement/go/clients/meshmoduleassignerclient/placed.go create mode 100644 placement/go/clients/meshmoduleassignerclient/placement.go diff --git a/placement/go/clients/meshmoduleassignerclient/client_test.go b/placement/go/clients/meshmoduleassignerclient/client_test.go new file mode 100644 index 0000000..09ec425 --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/client_test.go @@ -0,0 +1,257 @@ +package meshmoduleassignerclient + +import ( + "errors" + "fmt" + "reflect" + "strings" + "testing" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +func TestPlacementClient(t *testing.T) { + t.Run("key and file", func(t *testing.T) { + // Research 072 row 37: one node key and one key per module, a legacy key read beside them, any other refused. + for key, want := range map[string]struct { + node, module string + kind KeyKind + }{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind}, + "placement.g14": {"g14", "", LegacyKind}} { + if n, m, k, err := ParseKey(key); err != nil || n != want.node || m != want.module || k != want.kind { + t.Errorf("ParseKey(%q) = %q, %q, %d, %v", key, n, m, k, err) + } + } + for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b", "g14", "g14.a.b", ".node"} { + if _, _, _, err := ParseKey(bad); err == nil { + t.Errorf("ParseKey(%q) took it", bad) + } + } + if k, err := NodeKey("g14"); err != nil || k != "g14.node" { + t.Errorf("NodeKey = %q, %v", k, err) + } + if k, err := ModuleKey("g14", "app"); err != nil || k != "g14.app" { + t.Errorf("ModuleKey = %q, %v", k, err) + } + if k, err := NodeKeys("g14"); err != nil || k != "g14.>" { + t.Errorf("NodeKeys = %q, %v", k, err) + } + if k, err := LegacyKey("g14"); err != nil || k != "placement.g14" { + t.Errorf("LegacyKey = %q, %v", k, err) + } + for name, err := range map[string]error{"a dotted node": func() error { _, err := NodeKey("a.b"); return err }(), + "a node named placement": func() error { _, err := NodeKey("placement"); return err }(), + "a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} { + if err == nil { + t.Errorf("%s was taken", name) + } + } + }) + + g14 := seat.Placement{Node: "g14", Revision: "abc", + Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}}, + Modules: []seat.PlacedModule{ + {Module: "zsh"}, + {Module: "app", Ports: map[string]int64{"8080": 18080}, Bindings: map[string]seat.Provider{"postgres": {Node: "novox", Module: "postgres"}}}, + }, + Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}}, + Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}}, + } + novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}}, + Holds: []seat.Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}} + + t.Run("value", func(t *testing.T) { + raw, err := EncodePlacement(g14) + if err != nil { + t.Fatal(err) + } + back, err := DecodePlacement(raw) + if err != nil || back.V != Version || !SamePlacement(back, g14) { + t.Fatalf("round trip: %+v, %v", back, err) + } + moved := g14 + moved.Revision = "def" + if !SamePlacement(g14, moved) { + t.Error("a new revision alone counts as a change") + } + if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) { + t.Errorf("version 9: %v", err) + } + for name, bad := range map[string]string{ + "module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`, + "port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`, + "host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`, + "binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`, + "pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`, + "hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`, + "node not a name": `{"v":1,"node":"g.14","modules":[]}`, + "value not shaped": `{"v":1,"node":"g14","modules":{}}`, + } { + if _, err := DecodePlacement([]byte(bad)); err == nil { + t.Errorf("%s: taken", name) + } + } + }) + + t.Run("as the composition reads it", func(t *testing.T) { + if got := Assigned(g14); !reflect.DeepEqual(got, []string{"zsh", "app"}) { + t.Errorf("Assigned = %v, want the file's order", got) + } + if got := ChosenPins(g14); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) { + t.Errorf("ChosenPins = %v", got) + } + if got := Bound(g14); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) { + t.Errorf("Bound = %v", got) + } + if got := HostPorts(g14); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) { + t.Errorf("HostPorts = %v", got) + } + if got := Held(g14); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) { + t.Errorf("Held = %v", got) + } + layered := seat.PlacedModule{Module: "app", HasMeshWide: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}} + want := []catalogue.Layer{ + {From: catalogue.MeshWideLayer, Values: map[string]any{}}, + {From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}}, + } + if got := Layers(layered, "g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) { + t.Errorf("Layers = %#v, want %#v", got, want) + } + if got := Layers(seat.PlacedModule{Module: "a"}, "g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" { + t.Errorf("values alone make the node's layer: %#v", got) + } + if got := Layers(seat.PlacedModule{Module: "a", HasLayer: true}, "g14", nil); len(got) != 1 || got[0].Values == nil { + t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got) + } + if got := Layers(seat.PlacedModule{Module: "a"}, "g14", nil); got != nil { + t.Errorf("no layer and no value is no layer: %#v", got) + } + empty := seat.Placement{Node: "x"} + if ChosenPins(empty) != nil || Bound(empty) != nil || HostPorts(empty) != nil || len(Held(empty)) != 0 { + t.Error("an empty placement reads as something") + } + }) + + // Research 072 row 37: a node's placement as its node key and module keys, written so a reader never takes a part of + // a write, read beside the form before, and converted from it once. + t.Run("one key per module and node", func(t *testing.T) { + values, err := PlacementValues(g14) + if err != nil || len(values) != 4 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil || + values["placement.g14"] == nil { + t.Fatalf("PlacementValues = %v, %v", values, err) + } + got, err := PlacementsOf(values) + if err != nil || !SamePlacement(got["g14"], g14) || len(got) != 1 { + t.Fatalf("PlacementsOf = %+v, %v", got, err) + } + if got := InsideAddresses(map[string]seat.Placement{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" { + t.Errorf("InsideAddresses = %v", got) + } + legacy, _ := EncodePlacement(novox) + stale, _ := EncodePlacement(seat.Placement{Node: "g14"}) + both := map[string][]byte{"placement.novox": legacy, "placement.g14": stale} + for k, v := range values { + both[k] = v + } + if got, err := PlacementsOf(both); err != nil || !SamePlacement(got["g14"], g14) || !SamePlacement(got["novox"], novox) { + t.Errorf("a node key wins over a legacy key, and a legacy key alone places its node: %+v, %v", got, err) + } + + keys := NewKeys() + rev := uint64(0) + apply := func(writes []KeyWrite, upTo int) { + t.Helper() + for i, w := range writes { + if i == upTo { + return + } + rev++ + if _, err := keys.Put(w.Key, w.Value, rev, w.Value == nil); err != nil { + t.Fatalf("write %d (%s): %v", i, w.Key, err) + } + } + } + // The form before converts once: its node and module keys, the legacy key left for the readers not yet delivered. + rev++ + legacyG14, _ := EncodePlacement(g14) + if _, err := keys.Put("placement.g14", legacyG14, rev, false); err != nil || !keys.Legacy("g14") { + t.Fatalf("the legacy key: %v", err) + } + migrations, err := keys.Migrations() + if err != nil || len(migrations["g14"]) != 3 || !migrations["g14"][2].Commit { + // The legacy key holds the placement already: no mirror write. + t.Fatalf("Migrations = %+v, %v", migrations, err) + } + apply(migrations["g14"], -1) + if m, _ := keys.Migrations(); len(m) != 0 || keys.Legacy("g14") { + t.Fatalf("a converted node converts again: %+v", m) + } + if p, r, ok := keys.Of("g14"); !ok || !SamePlacement(p, g14) || r != rev { + t.Fatalf("Of after the conversion = %+v, %d, %v", p, r, ok) + } + + // A write under way is never read half: app's new port is not read before the node key names it. + after := clonePlacement(g14) + after.Modules = []seat.PlacedModule{{Module: "app", Ports: map[string]int64{"8080": 18081}}, {Module: "new"}} + after.Holds, after.Pins = nil, nil + writes, err := keys.Writes("g14", &after) + if err != nil { + t.Fatal(err) + } + var kinds []string + for _, w := range writes { + kinds = append(kinds, fmt.Sprintf("%s:%v:%d", w.Key, w.Value != nil, w.Base)) + } + if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 placement.g14:true:1 g14.zsh:false:2" || + !writes[3].Mirror { + t.Fatalf("Writes = %v", kinds) + } + apply(writes, 2) + if p, _, _ := keys.Of("g14"); !SamePlacement(p, g14) { + t.Errorf("the module keys written before the node key are read: %+v", p) + } + apply(writes[2:], -1) + if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) { + t.Errorf("the write done is not read whole: %+v", p) + } + if legacy, _ := PlacementsOf(map[string][]byte{"placement.g14": writes[3].Value}); !SamePlacement(legacy["g14"], after) { + t.Errorf("the mirror is not the placement written: %+v", legacy) + } + // A module key that stays is not written again, nor a mirror that holds it. + if again, _ := keys.Writes("g14", &after); len(again) != 1 || !again[0].Commit { + t.Errorf("an unchanged placement writes %+v", again) + } + // Keys that do not agree keep what the node had, and say so. + stray, _ := EncodeModule(seat.PlacementByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}}) + if _, err := keys.Put("g14.app", stray, 99, false); err == nil { + t.Error("a module key the node key does not name the value of was taken silently") + } + if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) { + t.Errorf("a stray module key moved the placement: %+v", p) + } + // The holder's next write puts the module key right again. + repair, _ := keys.Writes("g14", &after) + if len(repair) != 2 || repair[0].Key != "g14.app" || repair[0].Base != 99 || !repair[1].Commit { + t.Fatalf("the repair writes %+v", repair) + } + apply(repair, -1) + // No placement: the legacy key first, then the node key, then the module keys; none is read after. + gone, _ := keys.Writes("g14", nil) + kinds = nil + for _, w := range gone { + kinds = append(kinds, w.Key) + } + if strings.Join(kinds, " ") != "placement.g14 g14.node g14.app g14.new" { + t.Errorf("Writes(nil) = %v", kinds) + } + apply(gone, 2) + if _, _, ok := keys.Of("g14"); ok { + t.Error("a node key deleted still places its node") + } + if _, err := DecodeNode([]byte(`{"v":1,"node":"g14","modules":[{"module":"a","digest":"x"},{"module":"a","digest":"x"}]}`)); err == nil { + t.Error("a node key naming a module twice reads") + } + }) + +} diff --git a/placement/go/clients/meshmoduleassignerclient/go.mod b/placement/go/clients/meshmoduleassignerclient/go.mod new file mode 100644 index 0000000..2ebb4aa --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 +) diff --git a/placement/go/clients/meshmoduleassignerclient/holders.go b/placement/go/clients/meshmoduleassignerclient/holders.go new file mode 100644 index 0000000..763d2be --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/holders.go @@ -0,0 +1,53 @@ +package meshmoduleassignerclient + +import ( + "encoding/json" + "fmt" + "strings" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" +) + +// SeatHoldersKey is a seat's key in the state `seat-holders`: `` for a seat held once for the mesh (node empty), +// `.` for a node seat on one node. +func SeatHoldersKey(name, node string) (string, error) { + if err := checkName("seat", name); err != nil { + return "", err + } + if node == "" { + return seat.SeatHoldersBySeatKey(name), nil + } + if err := checkName("node", node); err != nil { + return "", err + } + return seat.SeatHoldersBySeatNodeKey(name, node), nil +} + +// HoldersOf reads who holds the seat named (on node, for a node seat) from the state's keys as a reader watched them: the +// node's key first, then the seat's. None when nobody holds it. +func HoldersOf(keys map[string][]byte, name, node string) ([]seat.SeatHoldersHoldersItem, error) { + for _, n := range []string{node, ""} { + key, err := SeatHoldersKey(name, n) + if err != nil { + if n == "" { + return nil, err + } + continue + } + raw, ok := keys[key] + if !ok { + continue + } + var sh seat.SeatHolders + if err := json.Unmarshal(raw, &sh); err != nil { + return nil, fmt.Errorf("meshmoduleassignerclient: the holders of %s are not readable: %w", key, err) + } + if sh.V != Version { + return nil, fmt.Errorf("meshmoduleassignerclient: the holders of %s are of version %d, and this reads %d", key, sh.V, Version) + } + return sh.Holders, nil + } + return nil, nil +} + +var _ = strings.Cut diff --git a/placement/go/clients/meshmoduleassignerclient/holders_test.go b/placement/go/clients/meshmoduleassignerclient/holders_test.go new file mode 100644 index 0000000..88d520c --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/holders_test.go @@ -0,0 +1,65 @@ +package meshmoduleassignerclient + +import ( + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" +) + +// The digest a node key names is kept as the first writer made it, so a node key written before this client reads. +func TestDigestIsTheFirstWritersAndHoldersAndStandingRead(t *testing.T) { + m := seat.PlacedModule{Module: "app", Ports: map[string]int64{"8080": 18080}, Bindings: map[string]seat.Provider{"postgres": {Node: "novox", Module: "postgres"}}, + HasMeshWide: true, MeshWide: map[string]any{"a": 1.0}, Layer: map[string]any{"expose": "x"}, HasLayer: true, + MeshWideValues: map[string]any{"k": "v"}, Values: map[string]any{"p": 2.0}} + if got := Digest(m); got != "62b3c2796d264c42" { + t.Errorf("Digest = %s, the first writer's is 62b3c2796d264c42", got) + } + + keys := map[string][]byte{ + "mesh-module-assigner": []byte(`{"v":1,"seat":"mesh-module-assigner","scope":"mesh","holders":[{"module":"mesh-delivery-assigner","node":"novox"}]}`), + "node-files.ace": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"ace","holders":[{"module":"node-files","node":"ace"}]}`), + "mesh-vault": []byte(`{"v":2,"seat":"mesh-vault","scope":"mesh","holders":[]}`), + } + for _, c := range []struct { + seat, node string + want int + bad bool + }{{"mesh-module-assigner", "", 1, false}, {"node-files", "ace", 1, false}, {"node-files", "g14", 0, false}, {"mesh-vault", "", 0, true}} { + got, err := HoldersOf(keys, c.seat, c.node) + if (err != nil) != c.bad || len(got) != c.want { + t.Errorf("HoldersOf(%s, %q) = %v, %v", c.seat, c.node, got, err) + } + } + + // A module stands since the revision of its node's key that first named it; the first revision heard stands since + // the zero time. + p := NewPlaced() + t0 := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + node := func(modules ...string) []byte { + n := seat.PlacementByNodeNodeValue{Node: "ace"} + for _, name := range modules { + n.Modules = append(n.Modules, seat.PlacementByNodeNodeValueModulesItem{Module: name, Digest: "d"}) + } + raw, err := EncodeNode(n) + if err != nil { + t.Fatal(err) + } + return raw + } + if _, changed, err := p.Put("ace.node", node("a"), false, t0); err != nil || len(changed) != 1 { + t.Fatalf("first: %v, %v", changed, err) + } + if _, changed, _ := p.Put("ace.node", node("a", "b"), false, t0.Add(time.Hour)); len(changed) != 1 || changed[0] != "b" { + t.Fatalf("second: %v", changed) + } + if since, ok := p.Since("ace", "a"); !ok || !since.IsZero() { + t.Errorf("a stands since the zero time: %v %v", since, ok) + } + if since, ok := p.Since("ace", "b"); !ok || !since.Equal(t0.Add(time.Hour)) { + t.Errorf("b stands since the second revision: %v %v", since, ok) + } + if got := p.PlacedNodes("b"); len(got) != 1 || got[0] != "ace" { + t.Errorf("PlacedNodes = %v", got) + } +} diff --git a/placement/go/clients/meshmoduleassignerclient/keys.go b/placement/go/clients/meshmoduleassignerclient/keys.go new file mode 100644 index 0000000..27822a8 --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/keys.go @@ -0,0 +1,545 @@ +package meshmoduleassignerclient + +import ( + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + "sync" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" +) + +// **One key per module and node** (research 072 row 37; issue 774). The seat's state `placement` keeps a node's placement +// under the node's own keys, so a node watches `.>` alone and a change of one module writes that module's key: +// `.node`, the node key (what a person decided of the node, its pins and holdings, and the modules placed on it, +// each with the digest of its module key's value), and `.`, a module key. The node key is the commit of a +// write; a reader takes for each module the node key names the value of its module key whose digest the node key names, +// the current one once the node key is written, the previous one while the write is under way, so a reader never +// composes a part of a write. The form before, one value `placement.` per node, is still read and mirrored until +// a later change deletes it. + +// KeyKind is the kind of a key of the state `placement`. +type KeyKind int + +// The kinds of key: a node key, a module key, and the form before (one value per node). +const ( + NodeKind KeyKind = iota + 1 + ModuleKind + LegacyKind +) + +// NodeKey is a node's node key, `.node`. A node named `placement` is refused: its keys would read as the +// legacy form's. +func NodeKey(node string) (string, error) { + if err := checkPlacementNode(node); err != nil { + return "", err + } + return node + "." + NodeWord, nil +} + +// ModuleKey is a module's key on a node, `.`; a module named `node` is refused. +func ModuleKey(node, module string) (string, error) { + if err := checkPlacementNode(node); err != nil { + return "", err + } + if err := checkPlacementModule(module); err != nil { + return "", err + } + return node + "." + module, nil +} + +// NodeKeys is the pattern of every key of one node, `.>`: what a node watches of its own placement. +func NodeKeys(node string) (string, error) { + if err := checkPlacementNode(node); err != nil { + return "", err + } + return node + ".>", nil +} + +// LegacyKey is a node's key in the form before, `placement.`, read while this change is delivered. +func LegacyKey(node string) (string, error) { + if err := checkName("node", node); err != nil { + return "", err + } + return legacyWord + "." + node, nil +} + +// ParseKey reads a key of the state `placement` in either form: its node, its module (empty for a node key and +// a legacy key) and its kind. Any other key is refused, naming it. +func ParseKey(key string) (node, module string, kind KeyKind, err error) { + first, rest, ok := strings.Cut(key, ".") + if !ok || strings.Contains(rest, ".") || checkName("node", first) != nil || checkName("module", rest) != nil { + return "", "", 0, fmt.Errorf("meshmoduleassignerclient: %q is not a placement key, .node or .", key) + } + switch { + case first == legacyWord: + return rest, "", LegacyKind, nil + case rest == NodeWord: + return first, "", NodeKind, nil + default: + return first, rest, ModuleKind, nil + } +} + +func checkPlacementNode(node string) error { + if err := checkName("node", node); err != nil { + return err + } + if node == legacyWord { + return fmt.Errorf("meshmoduleassignerclient: no node is named %q: its placement keys would read as the form before", legacyWord) + } + return nil +} + +func checkPlacementModule(module string) error { + if err := checkName("module", module); err != nil { + return err + } + if module == NodeWord { + return fmt.Errorf("meshmoduleassignerclient: no module is named %q: it is the word of a node's own key", NodeWord) + } + return nil +} + +// EncodeNode is n as JSON at Version. +func EncodeNode(n seat.PlacementByNodeNodeValue) ([]byte, error) { + if err := checkPlacementNode(n.Node); err != nil { + return nil, err + } + n.V = Version + return json.Marshal(n) +} + +// DecodeNode reads a node key's value; it refuses an unknown version, a node or module that is not a name, and a module +// named twice or without a digest. +func DecodeNode(data []byte) (seat.PlacementByNodeNodeValue, error) { + if err := checkVersion(data); err != nil { + return seat.PlacementByNodeNodeValue{}, err + } + n, err := seat.DecodePlacementByNodeNodeValue(data) + if err != nil { + return n, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) + } + if err := checkPlacementNode(n.Node); err != nil { + return seat.PlacementByNodeNodeValue{}, err + } + seen := map[string]bool{} + for _, r := range n.Modules { + if err := checkPlacementModule(r.Module); err != nil { + return seat.PlacementByNodeNodeValue{}, err + } + if seen[r.Module] || r.Digest == "" { + return seat.PlacementByNodeNodeValue{}, fmt.Errorf("meshmoduleassignerclient: the node key of %s names %s twice or without a digest", n.Node, r.Module) + } + seen[r.Module] = true + } + return n, nil +} + +// EncodeModule is m as JSON at Version. +func EncodeModule(m seat.PlacementByNodeModuleValue) ([]byte, error) { + if err := checkPlacementNode(m.Node); err != nil { + return nil, err + } + if err := checkPlacementModule(m.Placed.Module); err != nil { + return nil, err + } + if m.Previous != nil && m.Previous.Module != m.Placed.Module { + return nil, fmt.Errorf("meshmoduleassignerclient: the key of %s on %s carries %s as its previous value", m.Placed.Module, m.Node, m.Previous.Module) + } + m.V = Version + return json.Marshal(m) +} + +// DecodeModule reads a module key's value. +func DecodeModule(data []byte) (seat.PlacementByNodeModuleValue, error) { + if err := checkVersion(data); err != nil { + return seat.PlacementByNodeModuleValue{}, err + } + m, err := seat.DecodePlacementByNodeModuleValue(data) + if err != nil { + return m, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) + } + if err := checkPlacementNode(m.Node); err != nil { + return seat.PlacementByNodeModuleValue{}, err + } + if err := checkPlacementModule(m.Placed.Module); err != nil { + return seat.PlacementByNodeModuleValue{}, err + } + return m, nil +} + +// Keys is the state `placement` as a reader holds it, key by key in either form, and every node's placement +// assembled from it: the one reader of the state's keys for every part (issue 774), so no part parses them itself. A +// write of the holder's under way is never assembled half: a module key's value is taken only as far as the node key +// names its digest (see the package's comment above NodeWord), and a node whose keys do not agree keeps the +// placement last assembled for it, Put saying why. +// +// Safe for use from a watch and from a reader at once. +type Keys struct { + mu sync.Mutex + nodes map[string]*keysOf +} + +// keysOf is what is held of one node's keys. +type keysOf struct { + node *seat.PlacementByNodeNodeValue + nodeRev uint64 + modules map[string]seat.PlacementByNodeModuleValue + moduleRevs map[string]uint64 + legacy *seat.Placement + legacyRev uint64 + last *seat.Placement // the placement last assembled whole, nil when none +} + +// NewKeys holds no key yet. +func NewKeys() *Keys { return &Keys{nodes: map[string]*keysOf{}} } + +// PlacementsOf is every node's placement assembled from the state's keys as a reader holds them (key to value; a +// deleted key absent), and an error naming every key that did not read and every node whose keys do not agree (that +// node is then assembled from its module keys' values as they are). +func PlacementsOf(keys map[string][]byte) (map[string]seat.Placement, error) { + k := NewKeys() + var errs []error + names := make([]string, 0, len(keys)) + for key := range keys { + names = append(names, key) + } + sort.Strings(names) + for _, key := range names { + // A node key read before its module keys does not agree yet; whether the keys agree is asked once all are in. + if _, readErr, _ := k.put(key, keys[key], 0, false); readErr != nil { + errs = append(errs, readErr) + } + } + k.mu.Lock() + for _, node := range sortedKeys(k.nodes) { + if _, _, err := k.nodes[node].assemble(node); err != nil { + errs = append(errs, err) + } + } + k.mu.Unlock() + return k.All(), errors.Join(errs...) +} + +// Put takes one key's value in (revision: the bus's revision of it, 0 when the reader has none; deleted: a delete or a +// purge). It answers the node the key is of and an error for a key or a value that does not read, or for a node whose +// keys do not agree; the node then keeps what it had. +func (k *Keys) Put(key string, value []byte, revision uint64, deleted bool) (string, error) { + node, readErr, err := k.put(key, value, revision, deleted) + if readErr != nil { + return node, readErr + } + return node, err +} + +// put is Put telling an error of the key or value it took (readErr) from the node's keys not agreeing after it (err). +func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (node string, readErr, err error) { + node, module, kind, err := ParseKey(key) + if err != nil { + return "", err, nil + } + k.mu.Lock() + defer k.mu.Unlock() + of := k.nodes[node] + if of == nil { + of = &keysOf{modules: map[string]seat.PlacementByNodeModuleValue{}, moduleRevs: map[string]uint64{}} + k.nodes[node] = of + } + switch kind { + case NodeKind: + of.node, of.nodeRev = nil, 0 + if !deleted { + n, derr := DecodeNode(value) + switch { + case derr != nil: + readErr = fmt.Errorf("the node key %s does not read, so it places nothing there: %w", key, derr) + case n.Node != node: + readErr = fmt.Errorf("the node key %s names the node %q, so it places nothing there", key, n.Node) + default: + of.node, of.nodeRev = &n, revision + } + } + case ModuleKind: + delete(of.modules, module) + delete(of.moduleRevs, module) + if !deleted { + m, derr := DecodeModule(value) + switch { + case derr != nil: + readErr = fmt.Errorf("the module key %s does not read: %w", key, derr) + case m.Node != node || m.Placed.Module != module: + readErr = fmt.Errorf("the module key %s holds %s on %s", key, m.Placed.Module, m.Node) + default: + of.modules[module] = m + } + // Its revision is kept even when its value does not read, so the holder can write over it. + of.moduleRevs[module] = revision + } + case LegacyKind: + of.legacy, of.legacyRev = nil, 0 + if !deleted { + p, derr := DecodePlacement(value) + switch { + case derr != nil: + readErr = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr) + case p.Node != node: + readErr = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, p.Node) + default: + of.legacy, of.legacyRev = &p, revision + } + } + } + p, whole, err := of.assemble(node) + switch { + case p == nil: + of.last = nil + case whole || of.last == nil: + of.last = p + } + return node, readErr, err +} + +// assemble is the node's placement from its keys: nil when none places it, whole false (and an error) when the node key +// names a module digest no module key's value has. +func (of *keysOf) assemble(node string) (*seat.Placement, bool, error) { + if of.node == nil { + if of.legacy == nil { + return nil, true, nil + } + p := clonePlacement(*of.legacy) + return &p, true, nil + } + n := of.node + p := seat.Placement{V: Version, Node: node, Decided: n.Decided, Pins: n.Pins, Holds: n.Holds, Modules: make([]seat.PlacedModule, 0, len(n.Modules))} + var astray []string + for _, r := range n.Modules { + m, ok := of.modules[r.Module] + switch { + case ok && Digest(m.Placed) == r.Digest: + p.Modules = append(p.Modules, m.Placed) + case ok && m.Previous != nil && Digest(*m.Previous) == r.Digest: + p.Modules = append(p.Modules, *m.Previous) + case ok: + astray = append(astray, r.Module) + p.Modules = append(p.Modules, m.Placed) + default: + astray = append(astray, r.Module) + } + } + if len(astray) > 0 { + return &p, false, fmt.Errorf("the node key of %s names %s at a value no module key holds: %s keeps the placement it had", + node, strings.Join(astray, ", "), node) + } + return &p, true, nil +} + +// Of is node's placement and the revision of the key it was read from (its node key, or its legacy key), and whether +// any key places it. +func (k *Keys) Of(node string) (seat.Placement, uint64, bool) { + k.mu.Lock() + defer k.mu.Unlock() + of := k.nodes[node] + if of == nil || of.last == nil { + return seat.Placement{}, 0, false + } + rev := of.nodeRev + if of.node == nil { + rev = of.legacyRev + } + return clonePlacement(*of.last), rev, true +} + +// All is every node's placement, by node, and the revision of each one's key (Of). +func (k *Keys) All() map[string]seat.Placement { + out, _ := k.AllRevisions() + return out +} + +// AllRevisions is All with the revision of each node's key. +func (k *Keys) AllRevisions() (map[string]seat.Placement, map[string]uint64) { + k.mu.Lock() + nodes := make([]string, 0, len(k.nodes)) + for n := range k.nodes { + nodes = append(nodes, n) + } + k.mu.Unlock() + out, revs := map[string]seat.Placement{}, map[string]uint64{} + for _, n := range nodes { + if p, rev, ok := k.Of(n); ok { + out[n], revs[n] = p, rev + } + } + return out, revs +} + +// KeyWrite is one write of a key of the state `placement`, by compare-and-set: Value nil deletes the key at +// Base, Base 0 creates it, any other Base updates it at that revision. +type KeyWrite struct { + Node string + Key string + Value []byte + Base uint64 + // Commit says this write is the node key's: once it is written, the node's placement is the new one. + Commit bool + // Mirror says this write is the legacy key's, `placement.`, kept in step after the commit for the readers not + // yet delivered: a reader of both forms never reads it once the node key is there, so a mirror write that fails + // harms no such reader, and is made again at the node's next write. + Mirror bool +} + +// Writes are the writes that take node from the placement its keys hold to after (nil: no placement), in the order +// they are made: the module keys that change, each carrying the value it replaces; then the node key, the commit; then +// the legacy key `placement.` with the whole placement, a mirror for the readers not yet delivered, unless it +// holds it already; then the deletes of the module keys after no longer names. For no placement the legacy key goes +// first, then the node key, then the module keys, so a crash between them never leaves the legacy value to be read +// again. A module key whose value stays is not written. +func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { + nodeKey, err := NodeKey(node) + if err != nil { + return nil, err + } + k.mu.Lock() + defer k.mu.Unlock() + of := k.nodes[node] + if of == nil { + of = &keysOf{modules: map[string]seat.PlacementByNodeModuleValue{}, moduleRevs: map[string]uint64{}} + } + var out []KeyWrite + deleteModules := func(keep map[string]bool) { + for _, module := range sortedKeys(of.moduleRevs) { + if keep[module] { + continue + } + key, _ := ModuleKey(node, module) + out = append(out, KeyWrite{Node: node, Key: key, Base: of.moduleRevs[module]}) + } + } + if after == nil { + if of.legacy != nil { + key, _ := LegacyKey(node) + out = append(out, KeyWrite{Node: node, Key: key, Base: of.legacyRev}) + } + if of.node != nil { + out = append(out, KeyWrite{Node: node, Key: nodeKey, Base: of.nodeRev, Commit: true}) + } + deleteModules(nil) + return out, nil + } + if after.Node != node { + return nil, fmt.Errorf("meshmoduleassignerclient: the placement written under %s names the node %q", node, after.Node) + } + if err := CheckPlacement(*after); err != nil { + return nil, err + } + committed := map[string]seat.PlacedModule{} + if of.last != nil { + for _, m := range of.last.Modules { + committed[m.Module] = m + } + } + n := seat.PlacementByNodeNodeValue{Node: node, Decided: after.Decided, Pins: after.Pins, Holds: after.Holds, Modules: make([]seat.PlacementByNodeNodeValueModulesItem, 0, len(after.Modules))} + keep := map[string]bool{} + for _, m := range after.Modules { + if err := checkPlacementModule(m.Module); err != nil { + return nil, err + } + digest := Digest(m) + n.Modules = append(n.Modules, seat.PlacementByNodeNodeValueModulesItem{Module: m.Module, Digest: digest}) + keep[m.Module] = true + was, wasPlaced := committed[m.Module] + if held, ok := of.modules[m.Module]; ok && Digest(held.Placed) == digest && + (held.Previous == nil || wasPlaced && Digest(was) == digest) { + continue // the key holds it, and no previous value a reader could take in its place + } + v := seat.PlacementByNodeModuleValue{Node: node, Placed: m} + if wasPlaced && Digest(was) != digest { + v.Previous = &was + } + raw, err := EncodeModule(v) + if err != nil { + return nil, err + } + key, _ := ModuleKey(node, m.Module) + out = append(out, KeyWrite{Node: node, Key: key, Value: raw, Base: of.moduleRevs[m.Module]}) + } + raw, err := EncodeNode(n) + if err != nil { + return nil, err + } + out = append(out, KeyWrite{Node: node, Key: nodeKey, Value: raw, Base: of.nodeRev, Commit: true}) + // The mirror of the form before, while readers of it alone may run (the delivery of this change); a later change + // stops writing it and deletes it. + if of.legacy == nil || !SamePlacement(*of.legacy, *after) { + legacy, err := EncodePlacement(*after) + if err != nil { + return nil, err + } + key, _ := LegacyKey(node) + out = append(out, KeyWrite{Node: node, Key: key, Value: legacy, Base: of.legacyRev, Mirror: true}) + } + deleteModules(keep) + return out, nil +} + +// Migrations are, by node, the writes that convert every node held only in the form before (its legacy key, no node +// key) into its node and module keys, its legacy key kept as the mirror for the readers not yet delivered (Writes); empty +// once every node is converted, so the holder's conversion at each start is idempotent. +func (k *Keys) Migrations() (map[string][]KeyWrite, error) { + k.mu.Lock() + var legacy []string + for node, of := range k.nodes { + if of.node == nil && of.legacy != nil && checkPlacementNode(node) == nil { + legacy = append(legacy, node) + } + } + k.mu.Unlock() + sort.Strings(legacy) + out := map[string][]KeyWrite{} + var errs []error + for _, node := range legacy { + p, _, _ := k.Of(node) + w, err := k.Writes(node, &p) + if err != nil { + errs = append(errs, fmt.Errorf("%s: %w", node, err)) + continue + } + out[node] = w + } + return out, errors.Join(errs...) +} + +// Legacy says node is held only in the form before. +func (k *Keys) Legacy(node string) bool { + k.mu.Lock() + defer k.mu.Unlock() + of := k.nodes[node] + return of != nil && of.node == nil && of.legacy != nil +} + +// PlacementValues is every key and value of p written whole onto no keys before: its module keys, its node key and the +// legacy mirror, as a writer that holds no key of the node (a test's bus, a fresh mesh) writes them. +func PlacementValues(p seat.Placement) (map[string][]byte, error) { + writes, err := NewKeys().Writes(p.Node, &p) + if err != nil { + return nil, err + } + out := make(map[string][]byte, len(writes)) + for _, w := range writes { + out[w.Key] = w.Value + } + return out, nil +} + +// clonePlacement is p with its own maps and slices, so a reader's copy is not a held one. +func clonePlacement(p seat.Placement) seat.Placement { + raw, _ := json.Marshal(p) + var out seat.Placement + _ = json.Unmarshal(raw, &out) + if out.Modules == nil { + out.Modules = []seat.PlacedModule{} + } + return out +} diff --git a/placement/go/clients/meshmoduleassignerclient/placed.go b/placement/go/clients/meshmoduleassignerclient/placed.go new file mode 100644 index 0000000..bf9fe53 --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/placed.go @@ -0,0 +1,159 @@ +package meshmoduleassignerclient + +import ( + "fmt" + "maps" + "slices" + "sync" + "time" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" +) + +// Placed is where the placement (the seat mesh-placement's state `placement`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when: +// the one reader of that state for every part that asks "does module stand on node" (issue 774, design 47's third +// milestone), in place of each part reading the controller's assignments (`mesh-controller_assignments`, which goes +// with the controller's lease, ADR 0549 §6). +// +// Its input is the state's revisions in the order the bus wrote them, as a watch with the history (jetstream's +// IncludeHistory) hands them: Put takes each, in either form of the state's keys (keys.go): a node's node key +// names its modules (a module key says nothing of which stand), and its legacy key `placement.` does until a node +// key of it is heard. A module stands since the revision of its node's key that first named it +// in an unbroken run. A module in the first revision heard of a key stands since the zero time: that revision is the +// key's first (the seed of ADR 0549 §4 placed what already ran there) or the oldest the bus still keeps +// (the state's history), and either way the module may have run there before it. A key's value that does not read places +// nothing on its node, and Put says why. +// +// Safe for use from a watch and from a reader at once. +type Placed struct { + mu sync.Mutex + since map[string]map[string]time.Time // node -> module -> since when it stands + heard map[string]bool // the nodes whose key was heard at all + // newForm are the nodes whose node key `.node` was heard: their legacy key `placement.` is no longer + // read (keys.go). + newForm map[string]bool +} + +// NewPlaced is a Placed that holds no placement yet. +func NewPlaced() *Placed { + return &Placed{since: map[string]map[string]time.Time{}, heard: map[string]bool{}, newForm: map[string]bool{}} +} + +// Put takes one revision of a key of the state `placement` (its value, whether it is a delete, and when the bus wrote +// it). It answers the node the key is of and the modules whose standing on it changed (placed or removed), in name +// order, and an error for a key or a value that does not read (the node then places nothing). +func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node string, changed []string, err error) { + node, _, kind, err := ParseKey(key) + if err != nil { + return "", nil, err + } + if kind == ModuleKind { + return node, nil, nil // which modules stand is the node key's to say + } + p.mu.Lock() + if kind == LegacyKind && p.newForm[node] { + p.mu.Unlock() + return node, nil, nil // the node is read from its node key once one was heard + } + if kind == NodeKind { + p.newForm[node] = true + } + p.mu.Unlock() + now := map[string]bool{} + if !deleted { + var modules []string + var derr error + var named string + if kind == NodeKind { + var n seat.PlacementByNodeNodeValue + n, derr = DecodeNode(value) + named = n.Node + for _, r := range n.Modules { + modules = append(modules, r.Module) + } + } else { + var pl seat.Placement + pl, derr = DecodePlacement(value) + named, modules = pl.Node, Assigned(pl) + } + switch { + case derr != nil: + err = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr) + case named != node: + err = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, named) + default: + for _, m := range modules { + now[m] = true + } + } + } + p.mu.Lock() + defer p.mu.Unlock() + if !p.heard[node] { + p.heard[node] = true + at = time.Time{} + } + was := p.since[node] + next := map[string]time.Time{} + for m := range now { + if s, ok := was[m]; ok { + next[m] = s + } else { + next[m] = at + changed = append(changed, m) + } + } + for m := range was { + if !now[m] { + changed = append(changed, m) + } + } + if len(next) == 0 { + delete(p.since, node) + } else { + p.since[node] = next + } + slices.Sort(changed) + return node, changed, err +} + +// Seeded says some node's placement is held: before the assigner's adopt writes the first placement nothing is placed, +// and a reader keeps what it did before (ADR 0549 §5). +func (p *Placed) Seeded() bool { + p.mu.Lock() + defer p.mu.Unlock() + return len(p.since) > 0 +} + +// Since says whether module stands on node, and since when (the zero time: since before the revisions kept). +func (p *Placed) Since(node, module string) (time.Time, bool) { + p.mu.Lock() + defer p.mu.Unlock() + s, ok := p.since[node][module] + return s, ok +} + +// PlacedNodes is every node module is placed on, in name order. +func (p *Placed) PlacedNodes(module string) []string { + p.mu.Lock() + defer p.mu.Unlock() + var out []string + for node, mods := range p.since { + if _, ok := mods[module]; ok { + out = append(out, node) + } + } + slices.Sort(out) + return out +} + +// ByNode is, by node, the modules placed on it in name order. +func (p *Placed) ByNode() map[string][]string { + p.mu.Lock() + defer p.mu.Unlock() + out := make(map[string][]string, len(p.since)) + for node, mods := range p.since { + out[node] = slices.Sorted(maps.Keys(mods)) + } + return out +} diff --git a/placement/go/clients/meshmoduleassignerclient/placement.go b/placement/go/clients/meshmoduleassignerclient/placement.go new file mode 100644 index 0000000..823287e --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/placement.go @@ -0,0 +1,295 @@ +// Package meshmoduleassignerclient is the logic every module that reads where modules are placed shares over the seat +// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `placement` assembled into a +// node's placement, who stands on which node since when, who holds a seat, and what a placement says to the composition. +// The layouts are the seat's contract's (package meshmoduleassigner); the check of a proposal is the assigner's own. +package meshmoduleassignerclient + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// Version is the version of every value of the seat's states. +const Version = keys.Version + +// NodeWord is the last word of a node key, `.node`; no module is named it. +const NodeWord = "node" + +const ( + legacyWord = "placement" + digestLen = 16 +) + +// ErrUnknownVersion is what a decode returns for a value whose version is missing or not Version. +var ErrUnknownVersion = errors.New("meshmoduleassignerclient: unknown value version") + +var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) + +func checkName(kind, name string) error { + if name == "" { + return fmt.Errorf("meshmoduleassignerclient: the %s name is empty", kind) + } + if !nameOK.MatchString(name) { + return fmt.Errorf("meshmoduleassignerclient: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name) + } + return nil +} + +func checkVersion(data []byte) error { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return fmt.Errorf("meshmoduleassignerclient: value is not JSON: %w", err) + } + if probe.V == nil { + return fmt.Errorf("%w: no version in the value, this reader reads %d", ErrUnknownVersion, Version) + } + if *probe.V != Version { + return fmt.Errorf("%w: the value is version %d, this reader reads %d", ErrUnknownVersion, *probe.V, Version) + } + return nil +} + +// CheckPlacement refuses a placement that names a node or module that is not a name, a module twice, a port that is not +// a number, a binding or pin naming no provider, and a holding by a module not placed on the node. +func CheckPlacement(p seat.Placement) error { + if err := checkName("node", p.Node); err != nil { + return err + } + seen := map[string]bool{} + for _, m := range p.Modules { + if err := checkName("module", m.Module); err != nil { + return err + } + if seen[m.Module] { + return fmt.Errorf("meshmoduleassignerclient: the placement of %s names %s twice", p.Node, m.Module) + } + seen[m.Module] = true + for port, at := range m.Ports { + if n, err := strconv.Atoi(port); err != nil || n <= 0 || n > 65535 { + return fmt.Errorf("meshmoduleassignerclient: %s's port %q on %s is not a port number", m.Module, port, p.Node) + } + if at <= 0 || at > 65535 { + return fmt.Errorf("meshmoduleassignerclient: %s's port %s on %s is published on %d, which is not a port number", m.Module, port, p.Node, at) + } + } + for provision, to := range m.Bindings { + if provision == "" || to.Node == "" || to.Module == "" { + return fmt.Errorf("meshmoduleassignerclient: %s on %s has a binding with no provision, node or module", m.Module, p.Node) + } + } + } + for provision, to := range p.Pins { + if provision == "" || to.Node == "" || to.Module == "" { + return fmt.Errorf("meshmoduleassignerclient: the placement of %s has a pin with no provision, node or module", p.Node) + } + } + for _, h := range p.Holds { + if h.Claim == "" || !seen[h.Module] { + return fmt.Errorf("meshmoduleassignerclient: the placement of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim) + } + } + return nil +} + +// EncodePlacement is p as JSON at Version; it refuses what DecodePlacement refuses. +func EncodePlacement(p seat.Placement) ([]byte, error) { + if err := CheckPlacement(p); err != nil { + return nil, err + } + p.V = Version + return json.Marshal(p) +} + +// DecodePlacement reads a placement file or key value (the form before the node and module keys). +func DecodePlacement(data []byte) (seat.Placement, error) { + if err := checkVersion(data); err != nil { + return seat.Placement{}, err + } + var p seat.Placement + if err := json.Unmarshal(data, &p); err != nil { + return seat.Placement{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) + } + if err := CheckPlacement(p); err != nil { + return seat.Placement{}, err + } + return p, nil +} + +// SamePlacement says whether two placements are the same value, so the holder writes only when they differ. +func SamePlacement(a, b seat.Placement) bool { + a.Revision, b.Revision = "", "" + ja, errA := EncodePlacement(a) + jb, errB := EncodePlacement(b) + return errA == nil && errB == nil && string(ja) == string(jb) +} + +// Assigned is the modules placed on the node, in the file's order. +func Assigned(p seat.Placement) []string { + out := make([]string, 0, len(p.Modules)) + for _, m := range p.Modules { + out = append(out, m.Module) + } + return out +} + +// Chosen is p as the composition names a chosen provider. +func Chosen(p seat.Provider) catalogue.Chosen { + return catalogue.Chosen{Node: p.Node, Module: p.Module} +} + +// ChosenPins is the node's pins as the composition reads them; nil when it has none. +func ChosenPins(p seat.Placement) map[string]catalogue.Chosen { + if len(p.Pins) == 0 { + return nil + } + out := make(map[string]catalogue.Chosen, len(p.Pins)) + for provision, to := range p.Pins { + out[provision] = Chosen(to) + } + return out +} + +// Bound is, by consumer module and provision, where each binding sends it; nil when none. +func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen { + var out map[string]map[string]catalogue.Chosen + for _, m := range p.Modules { + if len(m.Bindings) == 0 { + continue + } + if out == nil { + out = map[string]map[string]catalogue.Chosen{} + } + out[m.Module] = make(map[string]catalogue.Chosen, len(m.Bindings)) + for provision, to := range m.Bindings { + out[m.Module][provision] = Chosen(to) + } + } + return out +} + +// HostPorts is, by module and the port it declares, the host port it is published on; nil when none. The check has +// refused a port that is not a number, so none is dropped here. +func HostPorts(p seat.Placement) map[string]map[int]int { + var out map[string]map[int]int + for _, m := range p.Modules { + if len(m.Ports) == 0 { + continue + } + if out == nil { + out = map[string]map[int]int{} + } + out[m.Module] = make(map[int]int, len(m.Ports)) + for port, at := range m.Ports { + n, _ := strconv.Atoi(port) + out[m.Module][n] = int(at) + } + } + return out +} + +// Layers is one module's settings layers on the node, as the composition reads them: the whole mesh's layer when it has +// one, then the node's, which carries the module's config values as mesh-config resolved them for this node (values, +// nil when none) beside its placement; a module with no layer and no value has none. A module in the settings form +// carries its values in the file (MeshWideValues, Values), which then stand for mesh-config's. +func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue.Layer { + var out []catalogue.Layer + if m.HasMeshWide || len(m.MeshWide) > 0 || len(m.MeshWideValues) > 0 { + mesh := map[string]any{} + for k, v := range m.MeshWideValues { + mesh[k] = v + } + out = append(out, catalogue.Layer{From: catalogue.MeshWideLayer, Values: mesh, Placement: m.MeshWide}) + } + if len(m.Values) > 0 { + values = make(map[string]any, len(m.Values)) + for k, v := range m.Values { + values[k] = v + } + } + if m.HasLayer || len(m.Layer) > 0 || len(values) > 0 { + if values == nil { + values = map[string]any{} + } + out = append(out, catalogue.Layer{From: node, Values: values, Placement: m.Layer}) + } + return out +} + +// Held is the node's holdings as the composition reads them. +func Held(p seat.Placement) []catalogue.Held { + out := make([]catalogue.Held, 0, len(p.Holds)) + for _, h := range p.Holds { + out = append(out, catalogue.Held{Claim: h.Claim, Scope: h.Scope, Node: p.Node, Module: h.Module, Site: h.Site, Kind: h.Kind}) + } + return out +} + +// InsideAddresses is every node's address on the private network, sorted and without repeats. +func InsideAddresses(placements map[string]seat.Placement) []string { + seen := map[string]bool{} + out := []string{} + for _, p := range placements { + if a := strings.TrimSpace(p.Decided.Address); a != "" && !seen[a] { + seen[a] = true + out = append(out, a) + } + } + sort.Strings(out) + return out +} + +// digestForm is a placed module as its digest has always been made: the fields in the order the first writer wrote them, +// so a node key written before this client names a digest this client computes the same. +type digestForm struct { + Module string `json:"module"` + Ports map[string]int64 `json:"ports,omitempty"` + Bindings map[string]digestProvider `json:"bindings,omitempty"` + MeshWide map[string]any `json:"mesh-wide,omitempty"` + HasMesh bool `json:"has-mesh-wide,omitempty"` + Layer map[string]any `json:"layer,omitempty"` + HasLayer bool `json:"has-layer,omitempty"` + MeshValues map[string]any `json:"mesh-wide-values,omitempty"` + Values map[string]any `json:"values,omitempty"` +} + +type digestProvider struct { + Node string `json:"node"` + Module string `json:"module"` +} + +// Digest is the digest of a placed module's value, as a node key names it. +func Digest(m seat.PlacedModule) string { + var bindings map[string]digestProvider + if len(m.Bindings) > 0 { + bindings = make(map[string]digestProvider, len(m.Bindings)) + for k, v := range m.Bindings { + bindings[k] = digestProvider{Node: v.Node, Module: v.Module} + } + } + raw, _ := json.Marshal(digestForm{Module: m.Module, Ports: m.Ports, Bindings: bindings, MeshWide: m.MeshWide, HasMesh: m.HasMeshWide, + Layer: m.Layer, HasLayer: m.HasLayer, MeshValues: m.MeshWideValues, Values: m.Values}) + sum := sha256.Sum256(raw) + return hex.EncodeToString(sum[:])[:digestLen] +} + +func sortedKeys[V any](m map[string]V) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} -- 2.54.0 From 935a0c77539fb300cc6cf6ec27e42ef73a7c0574 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:14:32 +0100 Subject: [PATCH 52/94] =?UTF-8?q?Each=20generated=20TypeScript=20package?= =?UTF-8?q?=20gets=20its=20lock,=20its=20package.json=20without=20dependen?= =?UTF-8?q?cies=20and=20its=20type-check=20config=20(ADR=200634=20=C2=A78,?= =?UTF-8?q?=20F7;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ts/seats/meshconditions/package-lock.json | 12 ++++++++++++ .../ts/seats/meshconditions/package.json | 12 +++--------- .../ts/seats/meshconditions/tsconfig.json | 4 +--- .../ts/seats/meshhandacts/package-lock.json | 12 ++++++++++++ health-and-repair/ts/seats/meshhandacts/package.json | 12 +++--------- .../ts/seats/meshhandacts/tsconfig.json | 4 +--- .../ts/seats/meshhealers/package-lock.json | 12 ++++++++++++ health-and-repair/ts/seats/meshhealers/package.json | 12 +++--------- health-and-repair/ts/seats/meshhealers/tsconfig.json | 4 +--- .../ts/seats/meshselfcheck/package-lock.json | 12 ++++++++++++ .../ts/seats/meshselfcheck/package.json | 12 +++--------- .../ts/seats/meshselfcheck/tsconfig.json | 4 +--- .../ts/seats/meshwatchdogs/package-lock.json | 12 ++++++++++++ .../ts/seats/meshwatchdogs/package.json | 12 +++--------- .../ts/seats/meshwatchdogs/tsconfig.json | 4 +--- .../ts/seats/operatorchannel/package-lock.json | 12 ++++++++++++ .../ts/seats/operatorchannel/package.json | 12 +++--------- .../ts/seats/operatorchannel/tsconfig.json | 4 +--- .../ts/seats/meshprovisioner/package-lock.json | 12 ++++++++++++ provisioning/ts/seats/meshprovisioner/package.json | 12 +++--------- provisioning/ts/seats/meshprovisioner/tsconfig.json | 4 +--- ts/seats/containerimageregistry/package-lock.json | 12 ++++++++++++ ts/seats/containerimageregistry/package.json | 12 +++--------- ts/seats/containerimageregistry/tsconfig.json | 4 +--- ts/seats/gopackageregistry/package-lock.json | 12 ++++++++++++ ts/seats/gopackageregistry/package.json | 12 +++--------- ts/seats/gopackageregistry/tsconfig.json | 4 +--- ts/seats/meshbroker/package-lock.json | 12 ++++++++++++ ts/seats/meshbroker/package.json | 12 +++--------- ts/seats/meshbroker/tsconfig.json | 4 +--- ts/seats/meshconfig/package-lock.json | 12 ++++++++++++ ts/seats/meshconfig/package.json | 12 +++--------- ts/seats/meshconfig/tsconfig.json | 4 +--- ts/seats/meshdeliverychecker/package-lock.json | 12 ++++++++++++ ts/seats/meshdeliverychecker/package.json | 12 +++--------- ts/seats/meshdeliverychecker/tsconfig.json | 4 +--- ts/seats/meshvault/package-lock.json | 12 ++++++++++++ ts/seats/meshvault/package.json | 12 +++--------- ts/seats/meshvault/tsconfig.json | 4 +--- ts/seats/nodeengine/package-lock.json | 12 ++++++++++++ ts/seats/nodeengine/package.json | 12 +++--------- ts/seats/nodeengine/tsconfig.json | 4 +--- ts/seats/npmpackageregistry/package-lock.json | 12 ++++++++++++ ts/seats/npmpackageregistry/package.json | 12 +++--------- ts/seats/npmpackageregistry/tsconfig.json | 4 +--- ts/seats/ociartifactregistry/package-lock.json | 12 ++++++++++++ ts/seats/ociartifactregistry/package.json | 12 +++--------- ts/seats/ociartifactregistry/tsconfig.json | 4 +--- 48 files changed, 256 insertions(+), 192 deletions(-) create mode 100644 health-and-repair/ts/seats/meshconditions/package-lock.json create mode 100644 health-and-repair/ts/seats/meshhandacts/package-lock.json create mode 100644 health-and-repair/ts/seats/meshhealers/package-lock.json create mode 100644 health-and-repair/ts/seats/meshselfcheck/package-lock.json create mode 100644 health-and-repair/ts/seats/meshwatchdogs/package-lock.json create mode 100644 operator-and-conversation/ts/seats/operatorchannel/package-lock.json create mode 100644 provisioning/ts/seats/meshprovisioner/package-lock.json create mode 100644 ts/seats/containerimageregistry/package-lock.json create mode 100644 ts/seats/gopackageregistry/package-lock.json create mode 100644 ts/seats/meshbroker/package-lock.json create mode 100644 ts/seats/meshconfig/package-lock.json create mode 100644 ts/seats/meshdeliverychecker/package-lock.json create mode 100644 ts/seats/meshvault/package-lock.json create mode 100644 ts/seats/nodeengine/package-lock.json create mode 100644 ts/seats/npmpackageregistry/package-lock.json create mode 100644 ts/seats/ociartifactregistry/package-lock.json diff --git a/health-and-repair/ts/seats/meshconditions/package-lock.json b/health-and-repair/ts/seats/meshconditions/package-lock.json new file mode 100644 index 0000000..6aa81e6 --- /dev/null +++ b/health-and-repair/ts/seats/meshconditions/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-health-and-repair-contracts-mesh-conditions", + "packages": { + "": { + "name": "@novox/mesh-health-and-repair-contracts-mesh-conditions", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshconditions/package.json b/health-and-repair/ts/seats/meshconditions/package.json index f02f3b7..0be4987 100644 --- a/health-and-repair/ts/seats/meshconditions/package.json +++ b/health-and-repair/ts/seats/meshconditions/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-conditions, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-health-and-repair-contracts-mesh-conditions", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/health-and-repair/ts/seats/meshconditions/tsconfig.json b/health-and-repair/ts/seats/meshconditions/tsconfig.json index c610421..60274c6 100644 --- a/health-and-repair/ts/seats/meshconditions/tsconfig.json +++ b/health-and-repair/ts/seats/meshconditions/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/health-and-repair/ts/seats/meshhandacts/package-lock.json b/health-and-repair/ts/seats/meshhandacts/package-lock.json new file mode 100644 index 0000000..c94f1e5 --- /dev/null +++ b/health-and-repair/ts/seats/meshhandacts/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-health-and-repair-contracts-mesh-hand-acts", + "packages": { + "": { + "name": "@novox/mesh-health-and-repair-contracts-mesh-hand-acts", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshhandacts/package.json b/health-and-repair/ts/seats/meshhandacts/package.json index c5da858..20ebafd 100644 --- a/health-and-repair/ts/seats/meshhandacts/package.json +++ b/health-and-repair/ts/seats/meshhandacts/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-hand-acts, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-health-and-repair-contracts-mesh-hand-acts", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/health-and-repair/ts/seats/meshhandacts/tsconfig.json b/health-and-repair/ts/seats/meshhandacts/tsconfig.json index c610421..60274c6 100644 --- a/health-and-repair/ts/seats/meshhandacts/tsconfig.json +++ b/health-and-repair/ts/seats/meshhandacts/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/health-and-repair/ts/seats/meshhealers/package-lock.json b/health-and-repair/ts/seats/meshhealers/package-lock.json new file mode 100644 index 0000000..9a26d5f --- /dev/null +++ b/health-and-repair/ts/seats/meshhealers/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-health-and-repair-contracts-mesh-healers", + "packages": { + "": { + "name": "@novox/mesh-health-and-repair-contracts-mesh-healers", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshhealers/package.json b/health-and-repair/ts/seats/meshhealers/package.json index 6ffcf90..3d48314 100644 --- a/health-and-repair/ts/seats/meshhealers/package.json +++ b/health-and-repair/ts/seats/meshhealers/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-healers, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-health-and-repair-contracts-mesh-healers", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/health-and-repair/ts/seats/meshhealers/tsconfig.json b/health-and-repair/ts/seats/meshhealers/tsconfig.json index c610421..60274c6 100644 --- a/health-and-repair/ts/seats/meshhealers/tsconfig.json +++ b/health-and-repair/ts/seats/meshhealers/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/health-and-repair/ts/seats/meshselfcheck/package-lock.json b/health-and-repair/ts/seats/meshselfcheck/package-lock.json new file mode 100644 index 0000000..1590123 --- /dev/null +++ b/health-and-repair/ts/seats/meshselfcheck/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-health-and-repair-contracts-mesh-self-check", + "packages": { + "": { + "name": "@novox/mesh-health-and-repair-contracts-mesh-self-check", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshselfcheck/package.json b/health-and-repair/ts/seats/meshselfcheck/package.json index 9cd3661..f0027c9 100644 --- a/health-and-repair/ts/seats/meshselfcheck/package.json +++ b/health-and-repair/ts/seats/meshselfcheck/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-self-check, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-health-and-repair-contracts-mesh-self-check", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/health-and-repair/ts/seats/meshselfcheck/tsconfig.json b/health-and-repair/ts/seats/meshselfcheck/tsconfig.json index c610421..60274c6 100644 --- a/health-and-repair/ts/seats/meshselfcheck/tsconfig.json +++ b/health-and-repair/ts/seats/meshselfcheck/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/health-and-repair/ts/seats/meshwatchdogs/package-lock.json b/health-and-repair/ts/seats/meshwatchdogs/package-lock.json new file mode 100644 index 0000000..b236c0c --- /dev/null +++ b/health-and-repair/ts/seats/meshwatchdogs/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-health-and-repair-contracts-mesh-watchdogs", + "packages": { + "": { + "name": "@novox/mesh-health-and-repair-contracts-mesh-watchdogs", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/health-and-repair/ts/seats/meshwatchdogs/package.json b/health-and-repair/ts/seats/meshwatchdogs/package.json index f6d5493..9dc0933 100644 --- a/health-and-repair/ts/seats/meshwatchdogs/package.json +++ b/health-and-repair/ts/seats/meshwatchdogs/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-watchdogs, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-health-and-repair-contracts-mesh-watchdogs", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json b/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json index c610421..60274c6 100644 --- a/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json +++ b/health-and-repair/ts/seats/meshwatchdogs/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/operator-and-conversation/ts/seats/operatorchannel/package-lock.json b/operator-and-conversation/ts/seats/operatorchannel/package-lock.json new file mode 100644 index 0000000..3061cb7 --- /dev/null +++ b/operator-and-conversation/ts/seats/operatorchannel/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-operator-and-conversation-contracts-operator-channel", + "packages": { + "": { + "name": "@novox/mesh-operator-and-conversation-contracts-operator-channel", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/operator-and-conversation/ts/seats/operatorchannel/package.json b/operator-and-conversation/ts/seats/operatorchannel/package.json index e2143b1..945f309 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/package.json +++ b/operator-and-conversation/ts/seats/operatorchannel/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat operator-channel, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-operator-and-conversation-contracts-operator-channel", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json b/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json index c610421..60274c6 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json +++ b/operator-and-conversation/ts/seats/operatorchannel/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/provisioning/ts/seats/meshprovisioner/package-lock.json b/provisioning/ts/seats/meshprovisioner/package-lock.json new file mode 100644 index 0000000..fe0cee0 --- /dev/null +++ b/provisioning/ts/seats/meshprovisioner/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-provisioning-contracts-mesh-provisioner", + "packages": { + "": { + "name": "@novox/mesh-provisioning-contracts-mesh-provisioner", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/provisioning/ts/seats/meshprovisioner/package.json b/provisioning/ts/seats/meshprovisioner/package.json index ff54abf..022c75d 100644 --- a/provisioning/ts/seats/meshprovisioner/package.json +++ b/provisioning/ts/seats/meshprovisioner/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-provisioner, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-provisioning-contracts-mesh-provisioner", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/provisioning/ts/seats/meshprovisioner/tsconfig.json b/provisioning/ts/seats/meshprovisioner/tsconfig.json index c610421..60274c6 100644 --- a/provisioning/ts/seats/meshprovisioner/tsconfig.json +++ b/provisioning/ts/seats/meshprovisioner/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/containerimageregistry/package-lock.json b/ts/seats/containerimageregistry/package-lock.json new file mode 100644 index 0000000..acf5337 --- /dev/null +++ b/ts/seats/containerimageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-container-image-registry", + "packages": { + "": { + "name": "@novox/mesh-contracts-container-image-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/containerimageregistry/package.json b/ts/seats/containerimageregistry/package.json index 5a3ac9d..2becd87 100644 --- a/ts/seats/containerimageregistry/package.json +++ b/ts/seats/containerimageregistry/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat container-image-registry, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-container-image-registry", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/containerimageregistry/tsconfig.json b/ts/seats/containerimageregistry/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/containerimageregistry/tsconfig.json +++ b/ts/seats/containerimageregistry/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/gopackageregistry/package-lock.json b/ts/seats/gopackageregistry/package-lock.json new file mode 100644 index 0000000..20966bb --- /dev/null +++ b/ts/seats/gopackageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-go-package-registry", + "packages": { + "": { + "name": "@novox/mesh-contracts-go-package-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/gopackageregistry/package.json b/ts/seats/gopackageregistry/package.json index f8dd34e..03317fe 100644 --- a/ts/seats/gopackageregistry/package.json +++ b/ts/seats/gopackageregistry/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat go-package-registry, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-go-package-registry", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/gopackageregistry/tsconfig.json b/ts/seats/gopackageregistry/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/gopackageregistry/tsconfig.json +++ b/ts/seats/gopackageregistry/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/meshbroker/package-lock.json b/ts/seats/meshbroker/package-lock.json new file mode 100644 index 0000000..2f792b1 --- /dev/null +++ b/ts/seats/meshbroker/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-mesh-broker", + "packages": { + "": { + "name": "@novox/mesh-contracts-mesh-broker", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/meshbroker/package.json b/ts/seats/meshbroker/package.json index d93aec0..0d845e8 100644 --- a/ts/seats/meshbroker/package.json +++ b/ts/seats/meshbroker/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-broker, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-mesh-broker", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/meshbroker/tsconfig.json b/ts/seats/meshbroker/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshbroker/tsconfig.json +++ b/ts/seats/meshbroker/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/meshconfig/package-lock.json b/ts/seats/meshconfig/package-lock.json new file mode 100644 index 0000000..a4037cc --- /dev/null +++ b/ts/seats/meshconfig/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-mesh-config", + "packages": { + "": { + "name": "@novox/mesh-contracts-mesh-config", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/meshconfig/package.json b/ts/seats/meshconfig/package.json index af62267..3c4108c 100644 --- a/ts/seats/meshconfig/package.json +++ b/ts/seats/meshconfig/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-config, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-mesh-config", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/meshconfig/tsconfig.json b/ts/seats/meshconfig/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshconfig/tsconfig.json +++ b/ts/seats/meshconfig/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/meshdeliverychecker/package-lock.json b/ts/seats/meshdeliverychecker/package-lock.json new file mode 100644 index 0000000..9caec88 --- /dev/null +++ b/ts/seats/meshdeliverychecker/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-mesh-delivery-checker", + "packages": { + "": { + "name": "@novox/mesh-contracts-mesh-delivery-checker", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/meshdeliverychecker/package.json b/ts/seats/meshdeliverychecker/package.json index b842d55..d732a5b 100644 --- a/ts/seats/meshdeliverychecker/package.json +++ b/ts/seats/meshdeliverychecker/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-delivery-checker, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-mesh-delivery-checker", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/meshdeliverychecker/tsconfig.json b/ts/seats/meshdeliverychecker/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshdeliverychecker/tsconfig.json +++ b/ts/seats/meshdeliverychecker/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/meshvault/package-lock.json b/ts/seats/meshvault/package-lock.json new file mode 100644 index 0000000..0ef1f38 --- /dev/null +++ b/ts/seats/meshvault/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-mesh-vault", + "packages": { + "": { + "name": "@novox/mesh-contracts-mesh-vault", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/meshvault/package.json b/ts/seats/meshvault/package.json index b811690..b07d017 100644 --- a/ts/seats/meshvault/package.json +++ b/ts/seats/meshvault/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat mesh-vault, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-mesh-vault", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/meshvault/tsconfig.json b/ts/seats/meshvault/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshvault/tsconfig.json +++ b/ts/seats/meshvault/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/nodeengine/package-lock.json b/ts/seats/nodeengine/package-lock.json new file mode 100644 index 0000000..1fd0476 --- /dev/null +++ b/ts/seats/nodeengine/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-node-engine", + "packages": { + "": { + "name": "@novox/mesh-contracts-node-engine", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/nodeengine/package.json b/ts/seats/nodeengine/package.json index 0c2e7fd..b7ac56a 100644 --- a/ts/seats/nodeengine/package.json +++ b/ts/seats/nodeengine/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat node-engine, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-node-engine", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/nodeengine/tsconfig.json b/ts/seats/nodeengine/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/nodeengine/tsconfig.json +++ b/ts/seats/nodeengine/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/npmpackageregistry/package-lock.json b/ts/seats/npmpackageregistry/package-lock.json new file mode 100644 index 0000000..642a03f --- /dev/null +++ b/ts/seats/npmpackageregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-npm-package-registry", + "packages": { + "": { + "name": "@novox/mesh-contracts-npm-package-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/npmpackageregistry/package.json b/ts/seats/npmpackageregistry/package.json index 506e958..d864fb3 100644 --- a/ts/seats/npmpackageregistry/package.json +++ b/ts/seats/npmpackageregistry/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat npm-package-registry, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-npm-package-registry", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/npmpackageregistry/tsconfig.json b/ts/seats/npmpackageregistry/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/npmpackageregistry/tsconfig.json +++ b/ts/seats/npmpackageregistry/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/ts/seats/ociartifactregistry/package-lock.json b/ts/seats/ociartifactregistry/package-lock.json new file mode 100644 index 0000000..a1f63b3 --- /dev/null +++ b/ts/seats/ociartifactregistry/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-contracts-oci-artifact-registry", + "packages": { + "": { + "name": "@novox/mesh-contracts-oci-artifact-registry", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/ts/seats/ociartifactregistry/package.json b/ts/seats/ociartifactregistry/package.json index 822b791..e8c251f 100644 --- a/ts/seats/ociartifactregistry/package.json +++ b/ts/seats/ociartifactregistry/package.json @@ -1,19 +1,13 @@ { "description": "The contract of the seat oci-artifact-registry, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, "exports": { - ".": "./dist/index.js" + ".": "./src/index.ts" }, "files": [ - "dist" + "src" ], "name": "@novox/mesh-contracts-oci-artifact-registry", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, "type": "module", + "types": "./src/index.ts", "version": "0.1.0" } diff --git a/ts/seats/ociartifactregistry/tsconfig.json b/ts/seats/ociartifactregistry/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/ociartifactregistry/tsconfig.json +++ b/ts/seats/ociartifactregistry/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true -- 2.54.0 From 97b7da6128b09346760781356bd634cfdf84189c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:15:36 +0100 Subject: [PATCH 53/94] =?UTF-8?q?mesh-provisioner:=20the=20provision's=20p?= =?UTF-8?q?lan=20entry=20typed=20in=20the=20definition,=20and=20fixtures?= =?UTF-8?q?=20for=20its=20two=20states=20(ADR=200634=20=C2=A76;=20issue=20?= =?UTF-8?q?1966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../go/seats/meshprovisioner/contract.go | 46 ++++++- .../go/seats/meshprovisioner/contract_test.go | 66 ++++++++++ .../mesh-provisioner/fixtures/states.json | 56 ++++++++ provisioning/seats/mesh-provisioner/seat.json | 124 +++++++++++++++++- .../ts/seats/meshprovisioner/src/index.ts | 46 ++++++- 5 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 provisioning/go/seats/meshprovisioner/contract_test.go create mode 100644 provisioning/seats/mesh-provisioner/fixtures/states.json diff --git a/provisioning/go/seats/meshprovisioner/contract.go b/provisioning/go/seats/meshprovisioner/contract.go index 1bacedd..3d54da9 100644 --- a/provisioning/go/seats/meshprovisioner/contract.go +++ b/provisioning/go/seats/meshprovisioner/contract.go @@ -184,6 +184,25 @@ type ProvisionsByNodeConsumerProvisionValueConsumer struct { Node string `json:"node,omitempty"` } +// ProvisionsByNodeConsumerProvisionValuePlan is the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126). +type ProvisionsByNodeConsumerProvisionValuePlan struct { + As string `json:"as,omitempty"` + At string `json:"at,omitempty"` + // the provider's own contract's bytes, carried unread + Contributed map[string]any `json:"contributed,omitempty"` + Contributes map[string]any `json:"contributes,omitempty"` + CredentialForm any `json:"credential-form,omitempty"` + CredentialTool string `json:"credential-tool,omitempty"` + From string `json:"from,omitempty"` + Local string `json:"local,omitempty"` + Name string `json:"name,omitempty"` + Names map[string]string `json:"names,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider"` + ProviderModule string `json:"provider-module"` + Provision string `json:"provision"` +} + // ProvisionsByNodeConsumerProvisionValueProvider is a type of the contract. type ProvisionsByNodeConsumerProvisionValueProvider struct { Module string `json:"module,omitempty"` @@ -211,8 +230,8 @@ type ProvisionsByNodeConsumerProvisionValue struct { Consumer ProvisionsByNodeConsumerProvisionValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` - // the plan's provision - Plan map[string]any `json:"plan,omitempty"` + // the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) + Plan *ProvisionsByNodeConsumerProvisionValuePlan `json:"plan,omitempty"` Provider ProvisionsByNodeConsumerProvisionValueProvider `json:"provider"` Provision string `json:"provision"` Reference *ProvisionsByNodeConsumerProvisionValueReference `json:"reference,omitempty"` @@ -237,6 +256,25 @@ type ProvisionsByNodeConsumerProvisionLocalValueConsumer struct { Node string `json:"node,omitempty"` } +// ProvisionsByNodeConsumerProvisionLocalValuePlan is the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126). +type ProvisionsByNodeConsumerProvisionLocalValuePlan struct { + As string `json:"as,omitempty"` + At string `json:"at,omitempty"` + // the provider's own contract's bytes, carried unread + Contributed map[string]any `json:"contributed,omitempty"` + Contributes map[string]any `json:"contributes,omitempty"` + CredentialForm any `json:"credential-form,omitempty"` + CredentialTool string `json:"credential-tool,omitempty"` + From string `json:"from,omitempty"` + Local string `json:"local,omitempty"` + Name string `json:"name,omitempty"` + Names map[string]string `json:"names,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider"` + ProviderModule string `json:"provider-module"` + Provision string `json:"provision"` +} + // ProvisionsByNodeConsumerProvisionLocalValueProvider is a type of the contract. type ProvisionsByNodeConsumerProvisionLocalValueProvider struct { Module string `json:"module,omitempty"` @@ -264,8 +302,8 @@ type ProvisionsByNodeConsumerProvisionLocalValue struct { Consumer ProvisionsByNodeConsumerProvisionLocalValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` - // the plan's provision - Plan map[string]any `json:"plan,omitempty"` + // the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) + Plan *ProvisionsByNodeConsumerProvisionLocalValuePlan `json:"plan,omitempty"` Provider ProvisionsByNodeConsumerProvisionLocalValueProvider `json:"provider"` Provision string `json:"provision"` Reference *ProvisionsByNodeConsumerProvisionLocalValueReference `json:"reference,omitempty"` diff --git a/provisioning/go/seats/meshprovisioner/contract_test.go b/provisioning/go/seats/meshprovisioner/contract_test.go new file mode 100644 index 0000000..78917dc --- /dev/null +++ b/provisioning/go/seats/meshprovisioner/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshprovisioner + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-provisioner/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a database provision provided to a consumer under a local name", subject: "", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"keycloak\",\"node\":\"ace\"},\"depends-on\":[\"novox.postgres.secret-postgres-database\"],\"local\":\"main\",\"plan\":{\"credential-form\":\"pair\",\"local\":\"main\",\"names\":{\"name-main\":\"keycloak.example.internal\"},\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"reference\":{\"generation\":2,\"id\":\"pair/novox/postgres-database/ace/keycloak/main\"},\"state\":\"provided\",\"v\":1}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"keycloak\",\"node\":\"ace\"},\"depends-on\":[\"novox.postgres.secret-postgres-database\"],\"local\":\"main\",\"plan\":{\"credential-form\":\"pair\",\"local\":\"main\",\"names\":{\"name-main\":\"keycloak.example.internal\"},\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"reference\":{\"generation\":2,\"id\":\"pair/novox/postgres-database/ace/keycloak/main\"},\"state\":\"provided\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionLocalValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a provision that waits for the operator", subject: "", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"bazarr\",\"node\":\"ace\"},\"provider\":{\"module\":\"mesh-vault\",\"node\":\"novox\"},\"provision\":\"api-token\",\"state\":\"waits\",\"v\":1,\"why\":\"the token is the operator's to give\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"bazarr\",\"node\":\"ace\"},\"provider\":{\"module\":\"mesh-vault\",\"node\":\"novox\"},\"provision\":\"api-token\",\"state\":\"waits\",\"v\":1,\"why\":\"the token is the operator's to give\"}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a retiring provision with the operator's warrant", subject: "", given: "{\"ask\":\"a7\",\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"old-app\",\"node\":\"ace\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"state\":\"retiring\",\"v\":1,\"warrant\":{\"ask\":\"a7\",\"at\":\"2026-11-04T13:00:00Z\",\"by\":\"operator\",\"option\":\"retire\"},\"why\":\"no want needs it\"}", wire: "{\"ask\":\"a7\",\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"old-app\",\"node\":\"ace\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"state\":\"retiring\",\"v\":1,\"warrant\":{\"ask\":\"a7\",\"at\":\"2026-11-04T13:00:00Z\",\"by\":\"operator\",\"option\":\"retire\"},\"why\":\"no want needs it\"}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's plan on a node", subject: "", given: "{\"module\":\"keycloak\",\"node\":\"ace\",\"plan\":{\"provisions\":[{\"credential-form\":\"pair\",\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"}]},\"v\":1}", wire: "{\"module\":\"keycloak\",\"node\":\"ace\",\"plan\":{\"provisions\":[{\"credential-form\":\"pair\",\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"}]},\"v\":1}", + decode: func(b []byte) (any, error) { var v PlansByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/provisioning/seats/mesh-provisioner/fixtures/states.json b/provisioning/seats/mesh-provisioner/fixtures/states.json new file mode 100644 index 0000000..1f499cd --- /dev/null +++ b/provisioning/seats/mesh-provisioner/fixtures/states.json @@ -0,0 +1,56 @@ +[ + { + "name": "a database provision provided to a consumer under a local name", + "of": "state:provisions/...", + "given": { + "v": 1, + "provision": "postgres-database", + "local": "main", + "consumer": {"module": "keycloak", "node": "ace"}, + "provider": {"module": "postgres", "node": "novox"}, + "depends-on": ["novox.postgres.secret-postgres-database"], + "state": "provided", + "reference": {"id": "pair/novox/postgres-database/ace/keycloak/main", "generation": 2}, + "plan": {"provision": "postgres-database", "local": "main", "provider": "novox", "provider-module": "postgres", "credential-form": "pair", "names": {"name-main": "keycloak.example.internal"}}, + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a provision that waits for the operator", + "of": "state:provisions/..", + "given": { + "v": 1, + "provision": "api-token", + "consumer": {"module": "bazarr", "node": "ace"}, + "provider": {"module": "mesh-vault", "node": "novox"}, + "state": "waits", + "why": "the token is the operator's to give", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a retiring provision with the operator's warrant", + "of": "state:provisions/..", + "given": { + "v": 1, + "provision": "postgres-database", + "consumer": {"module": "old-app", "node": "ace"}, + "provider": {"module": "postgres", "node": "novox"}, + "state": "retiring", + "why": "no want needs it", + "ask": "a7", + "warrant": {"ask": "a7", "option": "retire", "by": "operator", "at": "2026-11-04T13:00:00Z"}, + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a module's plan on a node", + "of": "state:plans/.", + "given": { + "v": 1, + "node": "ace", + "module": "keycloak", + "plan": {"provisions": [{"provision": "postgres-database", "provider": "novox", "provider-module": "postgres", "credential-form": "pair"}]} + } + } +] diff --git a/provisioning/seats/mesh-provisioner/seat.json b/provisioning/seats/mesh-provisioner/seat.json index 0806593..ad9b169 100644 --- a/provisioning/seats/mesh-provisioner/seat.json +++ b/provisioning/seats/mesh-provisioner/seat.json @@ -168,7 +168,67 @@ }, "plan": { "type": "object", - "description": "the plan's provision" + "description": "the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126)", + "properties": { + "name": { + "type": "string" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider-module": { + "type": "string" + }, + "credential-form": { + "enum": [ + "pair", + "shared", + "issued", + "given", + "none" + ] + }, + "credential-tool": { + "type": "string" + }, + "path": { + "type": "string" + }, + "contributes": { + "type": "object" + }, + "at": { + "type": "string" + }, + "from": { + "type": "string" + }, + "as": { + "type": "string" + }, + "names": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "contributed": { + "type": "object", + "description": "the provider's own contract's bytes, carried unread" + } + }, + "required": [ + "provision", + "provider", + "provider-module" + ], + "additionalProperties": false }, "at": { "type": "string", @@ -275,7 +335,67 @@ }, "plan": { "type": "object", - "description": "the plan's provision" + "description": "the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126)", + "properties": { + "name": { + "type": "string" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider-module": { + "type": "string" + }, + "credential-form": { + "enum": [ + "pair", + "shared", + "issued", + "given", + "none" + ] + }, + "credential-tool": { + "type": "string" + }, + "path": { + "type": "string" + }, + "contributes": { + "type": "object" + }, + "at": { + "type": "string" + }, + "from": { + "type": "string" + }, + "as": { + "type": "string" + }, + "names": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "contributed": { + "type": "object", + "description": "the provider's own contract's bytes, carried unread" + } + }, + "required": [ + "provision", + "provider", + "provider-module" + ], + "additionalProperties": false }, "at": { "type": "string", diff --git a/provisioning/ts/seats/meshprovisioner/src/index.ts b/provisioning/ts/seats/meshprovisioner/src/index.ts index ae048d9..a438b91 100644 --- a/provisioning/ts/seats/meshprovisioner/src/index.ts +++ b/provisioning/ts/seats/meshprovisioner/src/index.ts @@ -111,6 +111,25 @@ export interface ProvisionsByNodeConsumerProvisionValueConsumer { "node"?: string; } +/** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ +export interface ProvisionsByNodeConsumerProvisionValuePlan { + "as"?: string; + "at"?: string; + /** the provider's own contract's bytes, carried unread */ + "contributed"?: Record; + "contributes"?: Record; + "credential-form"?: unknown; + "credential-tool"?: string; + "from"?: string; + "local"?: string; + "name"?: string; + "names"?: Record; + "path"?: string; + "provider": string; + "provider-module": string; + "provision": string; +} + export interface ProvisionsByNodeConsumerProvisionValueProvider { "module"?: string; "node"?: string; @@ -135,8 +154,8 @@ export interface ProvisionsByNodeConsumerProvisionValue { "consumer": ProvisionsByNodeConsumerProvisionValueConsumer; "depends-on"?: string[]; "local"?: string; - /** the plan's provision */ - "plan"?: Record; + /** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ + "plan"?: ProvisionsByNodeConsumerProvisionValuePlan; "provider": ProvisionsByNodeConsumerProvisionValueProvider; "provision": string; "reference"?: ProvisionsByNodeConsumerProvisionValueReference; @@ -151,6 +170,25 @@ export interface ProvisionsByNodeConsumerProvisionLocalValueConsumer { "node"?: string; } +/** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ +export interface ProvisionsByNodeConsumerProvisionLocalValuePlan { + "as"?: string; + "at"?: string; + /** the provider's own contract's bytes, carried unread */ + "contributed"?: Record; + "contributes"?: Record; + "credential-form"?: unknown; + "credential-tool"?: string; + "from"?: string; + "local"?: string; + "name"?: string; + "names"?: Record; + "path"?: string; + "provider": string; + "provider-module": string; + "provision": string; +} + export interface ProvisionsByNodeConsumerProvisionLocalValueProvider { "module"?: string; "node"?: string; @@ -175,8 +213,8 @@ export interface ProvisionsByNodeConsumerProvisionLocalValue { "consumer": ProvisionsByNodeConsumerProvisionLocalValueConsumer; "depends-on"?: string[]; "local"?: string; - /** the plan's provision */ - "plan"?: Record; + /** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ + "plan"?: ProvisionsByNodeConsumerProvisionLocalValuePlan; "provider": ProvisionsByNodeConsumerProvisionLocalValueProvider; "provision": string; "reference"?: ProvisionsByNodeConsumerProvisionLocalValueReference; -- 2.54.0 From fb5451f6940e58696ad99bc84227f138ec7cdf1b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:16:00 +0100 Subject: [PATCH 54/94] =?UTF-8?q?mesh-vault=20and=20mesh-node-enroller=20c?= =?UTF-8?q?lients:=20the=20signing,=20the=20node's=20two=20logins=20and=20?= =?UTF-8?q?the=20own-secret=20drift;=20the=20token=20and=20the=20redeem=20?= =?UTF-8?q?proof=20(ADR=200634=20=C2=A74;=20C-10;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meshnodeenrollerclient/enrolment.go | 38 +++ .../meshnodeenrollerclient/enrolment_test.go | 30 ++ .../go/clients/meshnodeenrollerclient/go.mod | 3 + .../clients/meshnodeenrollerclient/redeem.go | 65 ++++ .../clients/meshnodeenrollerclient/token.go | 118 +++++++ .../meshnodeenrollerclient/token_test.go | 53 ++++ .../go/clients/meshvaultclient/client_test.go | 73 +++++ .../go/clients/meshvaultclient/go.mod | 8 + .../go/clients/meshvaultclient/nodelogins.go | 124 ++++++++ .../go/clients/meshvaultclient/owndrift.go | 95 ++++++ .../go/clients/meshvaultclient/signers.go | 296 ++++++++++++++++++ 11 files changed, 903 insertions(+) create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/enrolment.go create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/go.mod create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/redeem.go create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/token.go create mode 100644 identity-and-access/go/clients/meshnodeenrollerclient/token_test.go create mode 100644 identity-and-access/go/clients/meshvaultclient/client_test.go create mode 100644 identity-and-access/go/clients/meshvaultclient/go.mod create mode 100644 identity-and-access/go/clients/meshvaultclient/nodelogins.go create mode 100644 identity-and-access/go/clients/meshvaultclient/owndrift.go create mode 100644 identity-and-access/go/clients/meshvaultclient/signers.go diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment.go b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment.go new file mode 100644 index 0000000..65a3ed0 --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment.go @@ -0,0 +1,38 @@ +package meshnodeenrollerclient + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +const ( + // DefaultTokenLife is a token's life when issue names none; MostTokenLife the longest it may name (ADR 0587 §4). + DefaultTokenLife = time.Hour + MostTokenLife = 24 * time.Hour + + // UserPrefix is the bus user a joining machine dials as, `enrol.` (ADR 0587 §4). + UserPrefix = "enrol." +) + +// User is the bus user a joining machine dials as. +func User(node string) string { return UserPrefix + node } + +// IssuedID is the id of the enrolment-issued event of node's enrolment issued at issued: +// `enrolment-issued..`, what enrolment-expired names. +func IssuedID(node string, issued time.Time) string { + return "enrolment-issued." + node + "." + strconv.FormatInt(issued.UnixNano(), 10) +} + +// TokenLife is the life an issue's `expires` asks for: a duration, one hour when empty, at most MostTokenLife. +func TokenLife(expires string) (time.Duration, error) { + if strings.TrimSpace(expires) == "" { + return DefaultTokenLife, nil + } + d, err := time.ParseDuration(strings.TrimSpace(expires)) + if err != nil || d <= 0 || d > MostTokenLife { + return 0, fmt.Errorf("meshnodeenrollerclient: a token lasts a duration up to %s, such as 1h, not %q", MostTokenLife, expires) + } + return d, nil +} diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go new file mode 100644 index 0000000..41098fe --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go @@ -0,0 +1,30 @@ +package meshnodeenrollerclient + +import ( + "testing" + "time" +) + +func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) { + for life, ok := range map[string]bool{"": true, "1h": true, "24h": true, "25h": false, "0s": false, "-1h": false, "soon": false} { + if _, err := TokenLife(life); (err == nil) != ok { + t.Errorf("TokenLife(%q) = %v, want ok %v", life, err, ok) + } + } + if d, _ := TokenLife(""); d != DefaultTokenLife { + t.Errorf("an empty life is %s", d) + } + if got := IssuedID("g14", time.Unix(0, 42)); got != "enrolment-issued.g14.42" { + t.Errorf("IssuedID = %s", got) + } + if User("g14") != "enrol.g14" { + t.Error("the joining machine dials as enrol.") + } + r := Redeem{Node: "g14", PublicKey: "p", OverlayKey: "o", SealingKey: "s", ServingKey: "v"} + if string(RedeemProof("g14", "p", "o", "s", "v")) != "novox-mesh-redeem v1\x00g14\x00p\x00o\x00s\x00v" || r.KeysDigest() != KeysDigest("p", "o", "s", "v") { + t.Error("the proof bytes or the keys' digest changed") + } + if got := (RedeemAnswer{Node: "g14", Signature: "x"}).SignedBytes(); string(got) == "" || string(got) == "x" { + t.Errorf("SignedBytes = %s", got) + } +} diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/go.mod b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod new file mode 100644 index 0000000..4b99275 --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient + +go 1.26.0 diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/redeem.go b/identity-and-access/go/clients/meshnodeenrollerclient/redeem.go new file mode 100644 index 0000000..9ad4ac1 --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/redeem.go @@ -0,0 +1,65 @@ +package meshnodeenrollerclient + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "strings" +) + +// **Redeem** (§6): what a joining node-engine sends, and what the holder answers. Shared by the holder and the engine, +// which live in two repositories. + +// Redeem is the redeem verb's call: the node, the public halves of its keys and its identity key's signature over +// RedeemProof. No token: the bus's login check verified it. +type Redeem struct { + Node string `json:"node"` + PublicKey string `json:"public_key"` + OverlayKey string `json:"overlay_key,omitempty"` + SealingKey string `json:"sealing_key"` + ServingKey string `json:"serving_key,omitempty"` + Profile map[string]any `json:"profile,omitempty"` + Tunnel json.RawMessage `json:"tunnel,omitempty"` + Proof string `json:"proof"` +} + +// RedeemProof is the bytes a node signs with its identity key when it redeems: its name and every public key it +// presents, so a proof cannot be moved to another node or other keys. The holder builds the same bytes. +func RedeemProof(node, public, overlay, sealing, serving string) []byte { + return []byte("novox-mesh-redeem v1\x00" + node + "\x00" + public + "\x00" + overlay + "\x00" + sealing + "\x00" + serving) +} + +// KeysDigest is the digest of the public keys a redeem presents, `sha256:`: what the key at redeeming and the +// events carry, the one hash an event carries (§1). +func KeysDigest(public, overlay, sealing, serving string) string { + sum := sha256.Sum256([]byte(strings.Join([]string{public, overlay, sealing, serving}, "\x00"))) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// KeysDigest is r's keys' digest. +func (r Redeem) KeysDigest() string { + return KeysDigest(r.PublicKey, r.OverlayKey, r.SealingKey, r.ServingKey) +} + +// ProofBytes is r's proof decoded from base64. +func (r Redeem) ProofBytes() ([]byte, error) { return base64.StdEncoding.DecodeString(r.Proof) } + +// RedeemAnswer is what redeem answers (§6): the node, the login it dials as next, its account sealed to the sealing key it +// presented (base64), its certificate as PEM, the vault's public signing half (base64), and the vault's signature over +// SignedBytes under the context of KindEnrolment. The engine refuses an answer not signed by the key its token carries. +type RedeemAnswer struct { + Node string `json:"node"` + Login string `json:"login"` + Account string `json:"account"` + Certificate string `json:"certificate"` + Signer string `json:"signer"` + Signature string `json:"signature,omitempty"` +} + +// SignedBytes is what the vault signs of a: a without its signature, as JSON. +func (a RedeemAnswer) SignedBytes() []byte { + a.Signature = "" + raw, _ := json.Marshal(a) + return raw +} diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/token.go b/identity-and-access/go/clients/meshnodeenrollerclient/token.go new file mode 100644 index 0000000..d8d44d6 --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/token.go @@ -0,0 +1,118 @@ +package meshnodeenrollerclient + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "strings" +) + +// Token is what a person carries to a machine that is joining (novox/hq ADR 0004): where the bus is, what certificate +// to expect there, whose signature to believe afterwards, and a one-time right to join; and, when it was issued for the +// machine's tunnel key, the machine's first tunnel (ADR 0169). +// +// Shared so the vault, which makes the +// token from the lease write on (novox/hq ADR 0587 §3, issue 774), mesh-cli, which opens and prints it, and the +// node-engine, which parses it, read one shape. The field names are the wire format the controller's `token issue` +// writes, unchanged, so a token of either issuer reads the same; the tunnel stays in it (the lead's settlement of +// ADR 0587 §3, 2026-11-02). +type Token struct { + Version int `json:"v"` + + // Node is what the mesh calls this machine: the bus user it dials as is named after it, so it must be known before + // the mesh can say anything. + Node string `json:"node,omitempty"` + + Broker string `json:"broker,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Signer []byte `json:"signer,omitempty"` + Secret string `json:"secret"` + + // Adopted says this node joins adopted (novox/hq ADR 0100); absent for a converged node. + Adopted bool `json:"adopted,omitempty"` + + // Tunnel is this machine's first tunnel, when the token was issued for the key it made (novox/hq ADR 0169): its own + // address and the hub to reach, so it reaches the bus over the private network and the bus never faces the internet. + Tunnel *TokenTunnel `json:"tunnel,omitempty"` +} + +// TokenTunnel is the joining machine's side of its first tunnel, in the field names the controller writes. +type TokenTunnel struct { + Key string `json:"key"` + Address string `json:"address"` + Range string `json:"range"` + HubKey string `json:"hub_key"` + HubEndpoint string `json:"hub_endpoint"` +} + +// ParseToken reads a token a person pasted. Every refusal says *this is not a token* rather than *this is the wrong +// token*: telling "not from the mesh I joined" apart from "malformed" is a signature check later (ADR 0004). A token +// missing a part is refused whole, naming every part, never used partially: without the fingerprint a node connects +// to whatever answers, without the signing key it cannot tell a declaration from a forgery. +func ParseToken(encoded string) (Token, error) { + raw, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(encoded)) + if err != nil { + return Token{}, fmt.Errorf("this is not a token: %w", err) + } + var t Token + if err := json.Unmarshal(raw, &t); err != nil { + return Token{}, fmt.Errorf("this is not a token: %w", err) + } + if t.Version != 1 { + return Token{}, fmt.Errorf( + "this token says it is version %d, and this host understands version 1", t.Version) + } + if missing := t.Missing(); len(missing) > 0 { + return Token{}, fmt.Errorf( + "this token is missing %s, so it cannot be used to join anything", + strings.Join(missing, ", ")) + } + return t, nil +} + +// Missing names every part of t a joining machine needs and t lacks. +func (t Token) Missing() []string { + var missing []string + if strings.TrimSpace(t.Broker) == "" { + missing = append(missing, "the broker's address") + } + if strings.TrimSpace(t.Fingerprint) == "" { + missing = append(missing, "the broker certificate's fingerprint") + } + if len(t.Signer) != ed25519.PublicKeySize { + missing = append(missing, "the vault's signing key") + } + if strings.TrimSpace(t.Secret) == "" { + missing = append(missing, "the one-time secret") + } + if tt := t.Tunnel; tt != nil { + for _, part := range []struct{ value, says string }{ + {tt.Key, "the tunnel key it was issued for"}, {tt.Address, "this machine's address"}, + {tt.Range, "the private network's range"}, {tt.HubKey, "the hub's tunnel key"}, + {tt.HubEndpoint, "where the hub's tunnel is dialled"}, + } { + if strings.TrimSpace(part.value) == "" { + missing = append(missing, part.says) + } + } + } + return missing +} + +// Encode is t as one line a person carries: base64 (URL alphabet, no padding) of its JSON at version 1, as the +// controller's `token issue` writes it. Refused when a part is missing. +func (t Token) Encode() (string, error) { + t.Version = 1 + if missing := t.Missing(); len(missing) > 0 { + return "", fmt.Errorf("meshnodeenrollerclient: the token is missing %s", strings.Join(missing, ", ")) + } + raw, err := json.Marshal(t) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(raw), nil +} + +// SignerKey is the signing key the token carries, as a key: the controller's before the lease write, the vault's after. +func (t Token) SignerKey() ed25519.PublicKey { return ed25519.PublicKey(t.Signer) } diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/token_test.go b/identity-and-access/go/clients/meshnodeenrollerclient/token_test.go new file mode 100644 index 0000000..97e5832 --- /dev/null +++ b/identity-and-access/go/clients/meshnodeenrollerclient/token_test.go @@ -0,0 +1,53 @@ +package meshnodeenrollerclient + +import ( + "encoding/hex" + "reflect" + "strings" + "testing" +) + +// controllerToken is a token made by the controller's own `token issue` code: mesh-control origin/main 5c281ad9, +// internal/token Token.Encode, run on 2026-11-02 for node anchor, adopted, with a first tunnel, its signer the ed25519 +// key of the seed 00 01 .. 1f. Copied, never written by hand. +const controllerToken = "eyJ2IjoxLCJub2RlIjoiYW5jaG9yIiwiYnJva2VyIjoiMTkyLjAuMi4xMDo0MjIyIiwiZmluZ2VycHJpbnQiOiJzaGEyNTY6YWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYiIsInNpZ25lciI6IkE2RUh2L1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmc9Iiwic2VjcmV0IjoiYS1vbmUtdGltZS1zZWNyZXQiLCJhZG9wdGVkIjp0cnVlLCJ0dW5uZWwiOnsia2V5IjoiZEdobExXMWhZMmhwYm1WekxXdGxlUT09IiwiYWRkcmVzcyI6IjEwLjAuMC43LzI0IiwicmFuZ2UiOiIxMC4wLjAuMC8yNCIsImh1Yl9rZXkiOiJkR2hsTFdoMVluTXRhMlY1IiwiaHViX2VuZHBvaW50IjoiMTk4LjUxLjEwMC4xOjUxODIwIn19" + +// 774/S36/AC1: a token the controller made reads, with the moved code, field for field as it was written; encoding it +// again gives the same line; a part missing is refused naming it; garbage and another version are not tokens. +func TestATokenTheControllerMadeReads(t *testing.T) { + signer, _ := hex.DecodeString("03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8") + want := Token{Version: 1, Node: "anchor", Broker: "192.0.2.10:4222", + Fingerprint: "sha256:" + strings.Repeat("ab", 32), Signer: signer, Secret: "a-one-time-secret", Adopted: true, + Tunnel: &TokenTunnel{Key: "dGhlLW1hY2hpbmVzLWtleQ==", Address: "10.0.0.7/24", Range: "10.0.0.0/24", + HubKey: "dGhlLWh1YnMta2V5", HubEndpoint: "198.51.100.1:51820"}} + got, err := ParseToken(" " + controllerToken + "\n") + if err != nil || !reflect.DeepEqual(got, want) || len(got.SignerKey()) != 32 { + t.Fatalf("parsed %+v, %v", got, err) + } + if again, err := got.Encode(); err != nil || again != controllerToken { + t.Errorf("encoded again: %q, %v", again, err) + } + for _, c := range []struct { + cut func(*Token) + expect string + }{ + {func(t *Token) { t.Broker = "" }, "broker's address"}, + {func(t *Token) { t.Fingerprint = "" }, "fingerprint"}, + {func(t *Token) { t.Signer = t.Signer[:5] }, "signing key"}, + {func(t *Token) { t.Secret = "" }, "one-time secret"}, + {func(t *Token) { t.Tunnel = &TokenTunnel{Key: "k"} }, "where the hub's tunnel is dialled"}, + } { + cut := want + cut.Tunnel = &TokenTunnel{} + *cut.Tunnel = *want.Tunnel + c.cut(&cut) + if _, err := cut.Encode(); err == nil || !strings.Contains(err.Error(), c.expect) { + t.Errorf("a token without %s: %v", c.expect, err) + } + } + for _, bad := range []string{"", "!!!not base64!!!", "aGVsbG8", "eyJ2Ijo5OX0"} { + if _, err := ParseToken(bad); err == nil { + t.Errorf("%q parsed as a token", bad) + } + } +} diff --git a/identity-and-access/go/clients/meshvaultclient/client_test.go b/identity-and-access/go/clients/meshvaultclient/client_test.go new file mode 100644 index 0000000..2c00bf5 --- /dev/null +++ b/identity-and-access/go/clients/meshvaultclient/client_test.go @@ -0,0 +1,73 @@ +package meshvaultclient + +import ( + "crypto/ed25519" + "strings" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" +) + +func TestSigningNodeLoginsAndOwnDrift(t *testing.T) { + pub, priv, _ := ed25519.GenerateKey(nil) + k := NewSignerKey(pub, time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)) + if got, err := SignerPublicKey(k); err != nil || !got.Equal(pub) { + t.Fatalf("the key round trips: %v", err) + } + k.Fingerprint = "sha256:00" + if _, err := SignerPublicKey(k); err == nil { + t.Error("a key with another fingerprint was taken") + } + ctx, err := SignContext(KindEnrolment) + if err != nil || ctx != "novox enrolment v1\n" { + t.Fatalf("SignContext = %q, %v", ctx, err) + } + if _, err := SignContext("other"); err == nil { + t.Error("an unknown kind has a context") + } + sig := SignMessage(priv, ctx, []byte("m")) + if s, err := VerifyBy([]Signer{{Name: SignerVault, Key: pub}}, ctx, []byte("m"), sig); err != nil || s.Name != SignerVault { + t.Errorf("VerifyBy = %v, %v", s, err) + } + other, _ := SignContext(KindAuthority) + if _, err := VerifyBy([]Signer{{Name: SignerVault, Key: pub}}, other, []byte("m"), sig); err == nil { + t.Error("a signature made for one kind verified as another's") + } + + // A node's two logins: the first, the second, a rotation to the other only, a stage only with a rotation. + since := "2026-11-02T01:00:00Z" + for name, c := range map[string]struct { + l seat.NodeLoginsByNodeValue + ok bool + }{ + "first login": {seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.g14", Since: since}, true}, + "rotating to other": {seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.g14", Rotating: "node.g14--b", Stage: RotationBegun, Since: since}, true}, + "rotating to itself": {seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.g14", Rotating: "node.g14", Stage: RotationBegun, Since: since}, false}, + "stage without rotate": {seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.g14", Stage: RotationReady, Since: since}, false}, + "another's login": {seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.ace", Since: since}, false}, + } { + if err := CheckNodeLogin(c.l); (err == nil) != c.ok { + t.Errorf("%s: %v", name, err) + } + } + if raw, err := EncodeNodeLogin(seat.NodeLoginsByNodeValue{Node: "g14", Login: "node.g14", Since: since}); err != nil { + t.Fatal(err) + } else if back, err := DecodeNodeLogin(raw); err != nil || Rotates(back) { + t.Errorf("round trip: %+v %v", back, err) + } + if _, err := DecodeNodeLogin([]byte(`{"v":9,"node":"g14","login":"node.g14","since":"` + since + `"}`)); err == nil { + t.Error("version 9 was taken") + } + if n, ok := LoginNode("node.g14--b"); !ok || n != "g14" { + t.Errorf("LoginNode = %q %v", n, ok) + } + + masked := []ModuleBodies{{Module: "nats", Bodies: []Body{{ID: "nats.needs-jwt", Composed: "x " + MaskedAnotherValue}, {ID: "nats.conf", Running: MaskedAnotherValue}}}} + if own := OwnValuesDiffer(masked); len(own) != 1 || own[0] != (OwnValueDiffers{Module: "nats", Name: "jwt"}) || ValuesDiffer(masked) != 2 { + t.Errorf("OwnValuesDiffer = %v, ValuesDiffer = %d", own, ValuesDiffer(masked)) + } + if !strings.Contains(MaskedAnotherValue, "another value") { + t.Error("the mask word changed") + } +} diff --git a/identity-and-access/go/clients/meshvaultclient/go.mod b/identity-and-access/go/clients/meshvaultclient/go.mod new file mode 100644 index 0000000..39ad4cc --- /dev/null +++ b/identity-and-access/go/clients/meshvaultclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 +) diff --git a/identity-and-access/go/clients/meshvaultclient/nodelogins.go b/identity-and-access/go/clients/meshvaultclient/nodelogins.go new file mode 100644 index 0000000..314af7f --- /dev/null +++ b/identity-and-access/go/clients/meshvaultclient/nodelogins.go @@ -0,0 +1,124 @@ +package meshvaultclient + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + + seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" +) + +// **A node's two logins** (novox/hq ADR 0583): a node's bus account rotates between the login `node.` and +// `node.--b`. The layout of the state `node-logins` is the seat's contract's; this is the logic every reader of it +// shares. +const ( + // The stages of a rotation (ADR 0583 §2-§4). + RotationBegun = "begun" + RotationReady = "ready" + RotationConfirmed = "confirmed" + + nodeLoginPrefix = "node." + // SecondLoginSuffix marks a node's second login, as a module's account rotates over `--b` (ADR 0114). + SecondLoginSuffix = "--b" + + // Version is the version of every value of the seat's states. + Version = 1 +) + +// ErrUnknownVersion is what a decode returns for a value whose version is not Version. +var ErrUnknownVersion = errors.New("meshvaultclient: unknown value version") + +var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) + +func checkName(kind, name string) error { + if name == "" || !nameOK.MatchString(name) { + return fmt.Errorf("meshvaultclient: the %s name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", kind, name) + } + return nil +} + +// FirstLogin is a node's first login, `node.`; SecondLogin its other. +func FirstLogin(node string) string { return nodeLoginPrefix + node } +func SecondLogin(node string) string { return nodeLoginPrefix + node + SecondLoginSuffix } + +// OtherLogin is the login a node rotates to from login: the second from the first, the first from the second. +func OtherLogin(node, login string) string { + if login == SecondLogin(node) { + return FirstLogin(node) + } + return SecondLogin(node) +} + +// LoginNode is the node a node login names, either of its two, and whether login is one. +func LoginNode(login string) (string, bool) { + rest, ok := strings.CutPrefix(login, nodeLoginPrefix) + rest = strings.TrimSuffix(rest, SecondLoginSuffix) + return rest, ok && checkName("node", rest) == nil +} + +// Rotates says the node is rotating: a login rotating is named. +func Rotates(l seat.NodeLoginsByNodeValue) bool { return l.Rotating != "" } + +// CheckNodeLogin refuses a login that is not one of the node's two, a rotation to anything but the other, and a stage +// without a rotation or an unknown one. +func CheckNodeLogin(l seat.NodeLoginsByNodeValue) error { + if err := checkName("node", l.Node); err != nil { + return err + } + if l.Login != FirstLogin(l.Node) && l.Login != SecondLogin(l.Node) { + return fmt.Errorf("meshvaultclient: the node %s logs in as %q, neither of its two logins", l.Node, l.Login) + } + for _, act := range l.Acts { + if strings.TrimSpace(act) == "" { + return fmt.Errorf("meshvaultclient: the node %s names an empty hand act among its rotations' warrants", l.Node) + } + } + if !Rotates(l) { + if l.Stage != "" || l.ListRevision != 0 { + return fmt.Errorf("meshvaultclient: the node %s rotates to nothing and names stage %q", l.Node, l.Stage) + } + return nil + } + if l.Rotating != OtherLogin(l.Node, l.Login) { + return fmt.Errorf("meshvaultclient: the node %s rotates from %s to %q, not its other login", l.Node, l.Login, l.Rotating) + } + switch l.Stage { + case RotationBegun, RotationReady, RotationConfirmed: + default: + return fmt.Errorf("meshvaultclient: the node %s's rotation is at stage %q, not begun, ready or confirmed", l.Node, l.Stage) + } + return nil +} + +// EncodeNodeLogin is l as JSON at Version; it refuses what DecodeNodeLogin refuses. +func EncodeNodeLogin(l seat.NodeLoginsByNodeValue) ([]byte, error) { + if err := CheckNodeLogin(l); err != nil { + return nil, err + } + l.V = Version + return json.Marshal(l) +} + +// DecodeNodeLogin reads a key of `node-logins`; it refuses an unknown version (ErrUnknownVersion) and what +// CheckNodeLogin refuses. +func DecodeNodeLogin(data []byte) (seat.NodeLoginsByNodeValue, error) { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return seat.NodeLoginsByNodeValue{}, fmt.Errorf("meshvaultclient: value is not JSON: %w", err) + } + if probe.V == nil || *probe.V != Version { + return seat.NodeLoginsByNodeValue{}, fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version) + } + l, err := seat.DecodeNodeLoginsByNodeValue(data) + if err != nil { + return l, fmt.Errorf("meshvaultclient: value does not fit its shape: %w", err) + } + if err := CheckNodeLogin(l); err != nil { + return seat.NodeLoginsByNodeValue{}, err + } + return l, nil +} diff --git a/identity-and-access/go/clients/meshvaultclient/owndrift.go b/identity-and-access/go/clients/meshvaultclient/owndrift.go new file mode 100644 index 0000000..ebf5f9d --- /dev/null +++ b/identity-and-access/go/clients/meshvaultclient/owndrift.go @@ -0,0 +1,95 @@ +package meshvaultclient + +import ( + "sort" + "strings" + + "git.novox.be/novox/mesh-sdk/go/catalogue" +) + +// The own secrets a node's shadow answer finds the vault holds another value for than the node runs (issue 1126, cause +// C4): the vault's `move-in` never replaces a value it holds, so a value made anew later runs while the vault keeps the +// first one it was handed. The node opens both seals with its own key and masks the value as MaskedAnotherValue; this +// reads which own secrets those are, for the node-engine (which hands the vault what runs) and for the vault's own +// drift tool, so both read the answer one way. + +// MaskedAnotherValue is the word a shown body masks a value with when the node opened both sides with its key and they +// are two values. +const MaskedAnotherValue = "" + +// OwnValueDiffers is one own secret of a module whose value composed (the vault's) is another than the one that runs. +type OwnValueDiffers struct { + Module string `json:"module"` + Name string `json:"name"` +} + +// Body is one differing resource of a module as a node's answer shows it: the resource's id and both sides, masked. +type Body struct { + ID string + Composed string + Running string +} + +// ModuleBodies is the shown bodies of one module whose composition differs. +type ModuleBodies struct { + Module string + Bodies []Body +} + +var ownNeedPrefix = catalogue.NeedID("") + +// OwnNeedName is the name of module's own secret whose file a resource id names, and whether it names one. On a node a +// resource's id carries its module's name (`only-office.needs-jwt`); the bare form a declaration composes +// (`needs-jwt`, catalogue.NeedID) is read too. An id of another module's is not this module's. +func OwnNeedName(module, id string) (string, bool) { + if module != "" { + id = strings.TrimPrefix(id, module+".") + } + name, own := strings.CutPrefix(id, ownNeedPrefix) + return name, own && name != "" && !strings.Contains(name, ".") +} + +// OwnValuesDiffer is every own secret whose shown bodies say it holds another value composed than running, sorted by +// module and name, each once. A body the answer did not show is not read. +func OwnValuesDiffer(differs []ModuleBodies) []OwnValueDiffers { + seen := map[OwnValueDiffers]bool{} + var out []OwnValueDiffers + for _, d := range differs { + for _, b := range d.Bodies { + name, own := OwnNeedName(d.Module, b.ID) + if !own || !anotherValue(b) { + continue + } + o := OwnValueDiffers{Module: d.Module, Name: name} + if !seen[o] { + seen[o] = true + out = append(out, o) + } + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].Module != out[j].Module { + return out[i].Module < out[j].Module + } + return out[i].Name < out[j].Name + }) + return out +} + +// ValuesDiffer is how many shown bodies hold a value masked MaskedAnotherValue, own secrets and every other resource +// alike. +func ValuesDiffer(differs []ModuleBodies) int { + n := 0 + for _, d := range differs { + for _, b := range d.Bodies { + if anotherValue(b) { + n++ + } + } + } + return n +} + +func anotherValue(b Body) bool { + return strings.Contains(b.Composed, MaskedAnotherValue) || strings.Contains(b.Running, MaskedAnotherValue) +} diff --git a/identity-and-access/go/clients/meshvaultclient/signers.go b/identity-and-access/go/clients/meshvaultclient/signers.go new file mode 100644 index 0000000..1ef79ce --- /dev/null +++ b/identity-and-access/go/clients/meshvaultclient/signers.go @@ -0,0 +1,296 @@ +package meshvaultclient + +import ( + "crypto/ed25519" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" + "math/big" + "slices" + "strings" + "time" +) + +// **The vault signs** (novox/hq ADR 0550 §2; design 47's L3 item d). The vault makes an ed25519 signing key and an x509 +// authority once, keeps both as its own secrets with their operator copies, and publishes their public halves in its +// state `signers`, which only the vault writes and every node principal and composing part reads. Its verb `sign` signs a +// node's certificate for that node's own node-engine, from bus state alone (the name from the node's placement, the +// serving key from its record), and says `signed` naming the caller, the kind, the node and the digest, never the key. +// +// A node trusts the vault's key through the bus grants alone (the lead's design limit, 2026-10-28): only the vault may +// write `signers`, the same trust the wants and the placement rest on. A node trusts the vault's key alone since the +// controller ended (ADR 0590). What a node trusts now it states in its record, the node record's trusts. + +const ( + // The verb sign, its answer and the event signed are the vault's contract's (mesh-contracts meshvault: VerbSign, + // SignAnswer, HolderEventSigned, HolderSubjectSigned, SignedHolderEvent; novox/hq ADR 0627 §1). + + // KindNodeCertificate is a node's certificate for its internal name, asked by that node's node-engine alone. + KindNodeCertificate = "node-certificate" + // KindEnrolment is a redeem answer (RedeemAnswer), signed under its own context for the seat mesh-node-enroller alone (ADR 0587 + // §3, §6). + KindEnrolment = "enrolment" + // KindAuthority is the vault's own authority certificate, which the vault signs for itself. + KindAuthority = "authority" + + // SignerVault names the signer a node trusts. + SignerVault = "vault" + + // CertificateLife is how long a certificate the vault signs for a node lasts: ten years, renewed by asking again, + // never by a clock running out unseen. + CertificateLife = 10 * 365 * 24 * time.Hour +) + +// SignKinds are the kinds `sign` knows. +var SignKinds = []string{KindNodeCertificate, KindEnrolment, KindAuthority} + +// SignContext is the line a message of kind is signed after, so a signature made for one kind never verifies as +// another's (as the composition's and the asks' contexts are kept apart). +func SignContext(kind string) (string, error) { + if !slices.Contains(SignKinds, kind) { + return "", fmt.Errorf("meshvaultclient: %q is not a kind the vault signs (%s)", kind, strings.Join(SignKinds, ", ")) + } + return "novox " + kind + " v1\n", nil +} + +// SignerFingerprint is an ed25519 public key's fingerprint, `sha256:` of its 32 bytes. +func SignerFingerprint(key ed25519.PublicKey) string { + sum := sha256.Sum256(key) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// AuthorityFingerprint is an authority certificate's fingerprint, `sha256:` of its DER; refused when certificate is +// not one PEM certificate. +func AuthorityFingerprint(certificate string) (string, error) { + c, err := ParseCertificate(certificate) + if err != nil { + return "", err + } + sum := sha256.Sum256(c.Raw) + return "sha256:" + hex.EncodeToString(sum[:]), nil +} + +// ParseCertificate is the first certificate of a PEM text. +func ParseCertificate(text string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(strings.TrimSpace(text))) + if block == nil || block.Type != "CERTIFICATE" { + return nil, errors.New("meshvaultclient: not a PEM certificate") + } + return x509.ParseCertificate(block.Bytes) +} + +// NewSignerKey is the value of `key` for pub. +func NewSignerKey(pub ed25519.PublicKey, made time.Time) seat.SignersByKeyValue { + return seat.SignersByKeyValue{V: 1, Key: base64.StdEncoding.EncodeToString(pub), Fingerprint: SignerFingerprint(pub), Made: made.UTC().Format(time.RFC3339Nano)} +} + +// PublicKey is k's key, checked: an ed25519 public key whose fingerprint is the one k names. +func SignerPublicKey(k seat.SignersByKeyValue) (ed25519.PublicKey, error) { + raw, err := base64.StdEncoding.DecodeString(k.Key) + if err != nil || len(raw) != ed25519.PublicKeySize { + return nil, errors.New("meshvaultclient: the signers' key is not an ed25519 public key in base64") + } + if fp := SignerFingerprint(raw); fp != k.Fingerprint { + return nil, fmt.Errorf("meshvaultclient: the signers' key has the fingerprint %s, and names %s", fp, k.Fingerprint) + } + return ed25519.PublicKey(raw), nil +} + +// DecodeSignerKey reads the key `key` of `signers`, refused when it does not check. +func DecodeSignerKey(raw []byte) (seat.SignersByKeyValue, error) { + k, err := seat.DecodeSignersByKeyValue(raw) + if err != nil { + return k, fmt.Errorf("meshvaultclient: signers %s: %w", "key", err) + } + if _, err := SignerPublicKey(k); err != nil { + return seat.SignersByKeyValue{}, err + } + return k, nil +} + +// DecodeSignerAuthority reads the key `authority` of `signers`: one authority certificate (a CA that signs leaves +// alone) whose fingerprint is the one named. +func DecodeSignerAuthority(raw []byte) (seat.SignersByAuthorityValue, error) { + a, err := seat.DecodeSignersByAuthorityValue(raw) + if err != nil { + return a, fmt.Errorf("meshvaultclient: signers %s: %w", "authority", err) + } + c, err := ParseCertificate(a.Certificate) + if err != nil { + return seat.SignersByAuthorityValue{}, err + } + if !c.IsCA { + return seat.SignersByAuthorityValue{}, errors.New("meshvaultclient: the signers' authority is not an authority certificate") + } + if fp, _ := AuthorityFingerprint(a.Certificate); fp != a.Fingerprint { + return seat.SignersByAuthorityValue{}, fmt.Errorf("meshvaultclient: the signers' authority has the fingerprint %s, and names %s", fp, a.Fingerprint) + } + return a, nil +} + +// Signer is one key a node trusts, by who holds it (SignerVault, or the operator's). +type Signer struct { + Name string + Key ed25519.PublicKey +} + +// Fingerprint is the signer's key's fingerprint. +func (s Signer) Fingerprint() string { return SignerFingerprint(s.Key) } + +// VerifyBy is the trusted signer whose key verifies sig over context and msg: refused when no trusted key does, saying +// so, and when a key is not an ed25519 key. A signature made under another context never verifies. +func VerifyBy(trusted []Signer, context string, msg, sig []byte) (Signer, error) { + if len(sig) != ed25519.SignatureSize { + return Signer{}, errors.New("meshvaultclient: not an ed25519 signature") + } + message := append([]byte(context), msg...) + for _, s := range trusted { + if len(s.Key) != ed25519.PublicKeySize { + continue + } + if ed25519.Verify(s.Key, message, sig) { + return s, nil + } + } + names := make([]string, 0, len(trusted)) + for _, s := range trusted { + names = append(names, s.Name) + } + return Signer{}, fmt.Errorf("meshvaultclient: the signature verifies against no trusted key (%s)", strings.Join(names, ", ")) +} + +// SignMessage is sig over context and msg by key: what the vault's `sign` does for a kind whose bytes are given. +func SignMessage(key ed25519.PrivateKey, context string, msg []byte) []byte { + return ed25519.Sign(key, append([]byte(context), msg...)) +} + +// DigestOf is the digest a `signed` names: `sha256:` of the bytes signed. +func DigestOf(b []byte) string { + sum := sha256.Sum256(b) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// NodeCertificateTemplate is the certificate the vault signs for node's internal name, as the controller's authority +// shaped it (identity.Certify): the name in the subject and the subject alternative names, server and client use, the +// serial derived from the name and the key so the same ask gives the same certificate, and its life from notBefore. +func NodeCertificateTemplate(name string, servingKey ed25519.PublicKey, notBefore time.Time) *x509.Certificate { + sum := sha256.Sum256(append([]byte(name+"\n"), servingKey...)) + return &x509.Certificate{ + SerialNumber: new(big.Int).SetBytes(sum[:16]), + Subject: pkix.Name{CommonName: name}, + DNSNames: []string{name}, + NotBefore: notBefore.UTC().Truncate(time.Second), + NotAfter: notBefore.UTC().Truncate(time.Second).Add(CertificateLife), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + } +} + +// AuthorityTemplate is the vault's authority certificate for its key, made once: an authority that signs leaves and no +// other authority, as the controller's. +func AuthorityTemplate(key ed25519.PublicKey, made time.Time) *x509.Certificate { + sum := sha256.Sum256(append([]byte("authority\n"), key...)) + return &x509.Certificate{ + SerialNumber: new(big.Int).SetBytes(sum[:16]), + Subject: pkix.Name{CommonName: "the mesh's vault"}, + NotBefore: made.UTC().Truncate(time.Second), + NotAfter: made.UTC().Truncate(time.Second).Add(2 * CertificateLife), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLen: 0, + MaxPathLenZero: true, + } +} + +// CheckNodeCertificate says why certificate is not a certificate of name for servingKey by one of the authorities, or +// nil: a node uses it to tell whether its record holds a certificate from an authority it trusts, and another node to +// check one the vault signed (ADR 0550 §2's live check). +func CheckNodeCertificate(certificate string, authorities []string, name string, servingKey ed25519.PublicKey, at time.Time) error { + c, err := ParseCertificate(certificate) + if err != nil { + return err + } + pool := x509.NewCertPool() + for _, a := range authorities { + pool.AppendCertsFromPEM([]byte(a)) // a bundle is every certificate it holds + } + if _, err := c.Verify(x509.VerifyOptions{Roots: pool, DNSName: name, CurrentTime: at, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}); err != nil { + return fmt.Errorf("meshvaultclient: the certificate of %s is not one a trusted authority signed for it: %w", name, err) + } + held, ok := c.PublicKey.(ed25519.PublicKey) + if !ok || !held.Equal(servingKey) { + return fmt.Errorf("meshvaultclient: the certificate of %s binds another key than the node serves with", name) + } + return nil +} + +// AuthorityBundle is the authorities given as one PEM text, each certificate once, in the order given, blanks left out: +// what a module asking for the mesh's authority is given once the vault signs beside the controller (ADR 0550 §2). An +// authority given is split into its certificates, so a bundle given again (the one a node kept from its last +// composition) adds nothing twice; a text that holds no PEM block is taken whole. +func AuthorityBundle(authorities ...string) string { + var out []string + for _, a := range authorities { + for _, piece := range pemPieces(a) { + if !slices.Contains(out, piece) { + out = append(out, piece) + } + } + } + if len(out) == 0 { + return "" + } + return strings.Join(out, "\n") + "\n" +} + +// pemPieces is each PEM block of text re-encoded, or the trimmed text whole when it holds none. +func pemPieces(text string) []string { + text = strings.TrimSpace(text) + if text == "" { + return nil + } + var out []string + rest := []byte(text) + for { + block, more := pem.Decode(rest) + if block == nil { + break + } + out = append(out, strings.TrimSpace(string(pem.EncodeToMemory(block)))) + rest = more + } + if len(out) == 0 { + return []string{text} + } + return out +} + +// Trusts is what a node states it trusts now (the node record's trusts): ` key ` for each signer and +// ` authority ` for each authority certificate, by who holds it, each once, sorted. The gate before the +// lease write reads `vault key ` in every node's record. +func Trusts(signers []Signer, authorities map[string]string) []string { + var out []string + for _, s := range signers { + if len(s.Key) == ed25519.PublicKeySize { + out = append(out, s.Name+" key "+s.Fingerprint()) + } + } + for who, a := range authorities { + if fp, err := AuthorityFingerprint(a); err == nil { + out = append(out, who+" authority "+fp) + } + } + slices.Sort(out) + return slices.Compact(out) +} + +// TrustsKey says whether trusts names the key with fingerprint fp held by who. +func TrustsKey(trusts []string, who, fp string) bool { return slices.Contains(trusts, who+" key "+fp) } -- 2.54.0 From af804f5abf858f3ac0ea6c51facf4bd100008bec Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:16:53 +0100 Subject: [PATCH 55/94] =?UTF-8?q?Hand-acts=20recorder=20is=20mesh-sdk's=20?= =?UTF-8?q?views.HandActs;=20the=20conditions=20client=20writes=20a=20part?= =?UTF-8?q?'s=20findings=20as=20views.FindingWriter=20(ADR=200634=20=C2=A7?= =?UTF-8?q?4,=20ADR=200635=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../go/clients/meshconditionsclient/client.go | 42 +++++++++++++++++++ .../meshconditionsclient/client_test.go | 39 +++++++++++++++++ .../go/clients/meshconditionsclient/go.mod | 2 + .../go/clients/meshhandactsclient/client.go | 10 +++++ .../clients/meshhandactsclient/client_test.go | 20 +++++++++ .../go/clients/meshhandactsclient/go.mod | 2 + 6 files changed, 115 insertions(+) diff --git a/health-and-repair/go/clients/meshconditionsclient/client.go b/health-and-repair/go/clients/meshconditionsclient/client.go index 5a28c7b..8e3a014 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client.go +++ b/health-and-repair/go/clients/meshconditionsclient/client.go @@ -11,6 +11,7 @@ import ( "time" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/views" ) // The two severities, how soon the operator is needed. @@ -170,3 +171,44 @@ func SilencedAt(c seat.ConditionsByKeyValue, now time.Time) bool { func Escalated(c seat.ConditionsByKeyValue) bool { return c.Resolver == ResolverOperator && len(c.Tried) > 0 } + +// FindingVersion is the layout version of a finding's value. +const FindingVersion = 1 + +// FindingState is a part's own state `findings`, where the condition keeper reads what a part saw: the runtime's kept +// state (mesh-sdk's KeptState) satisfies it. +type FindingState interface { + Put(key string, value any) (uint64, error) + Delete(key string) error +} + +// FindingWriter writes and clears a part's findings of a module in its own state `findings`, in this seat's layout; it +// is mesh-sdk's views.FindingWriter. A finding is a warning about the module its ID names, and is checked +// (CheckFinding) before it is written. +type FindingWriter struct{ State FindingState } + +var _ views.FindingWriter = FindingWriter{} + +// FindingFor is the value and key of the seat's finding for f. +func FindingFor(f views.Finding) seat.FindingsByKeyValue { + at := f.At.UTC().Format(time.RFC3339Nano) + return seat.FindingsByKeyValue{V: FindingVersion, Key: Key(ScopeModule, f.ID, f.Kind), Kind: f.Kind, + Subject: seat.Subject{Scope: ScopeModule, ID: f.ID}, Severity: Warning, Summary: f.Summary, Headline: f.Headline, + Explanation: f.Explanation, Evidence: []seat.Evidence{{At: at, Said: f.Summary}}, Source: f.Observer + "." + f.Kind, + Observer: f.Observer, At: at} +} + +// Put writes the finding. +func (w FindingWriter) Put(f views.Finding) error { + v := FindingFor(f) + if err := CheckFinding(v); err != nil { + return err + } + _, err := w.State.Put(v.Key, v) + return err +} + +// Delete clears the finding. +func (w FindingWriter) Delete(f views.Finding) error { + return w.State.Delete(Key(ScopeModule, f.ID, f.Kind)) +} diff --git a/health-and-repair/go/clients/meshconditionsclient/client_test.go b/health-and-repair/go/clients/meshconditionsclient/client_test.go index 7baef69..746368a 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -6,6 +6,7 @@ import ( "time" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/views" ) // ADR 0564 clause 3: the key rule, the checks of a finding, the order and the silence a person reads conditions by. @@ -60,3 +61,41 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { t.Errorf("the seat's addresses: %+v", a) } } + +type fakeFindings struct { + put map[string]any + deleted []string +} + +func (f *fakeFindings) Put(key string, value any) (uint64, error) { + if f.put == nil { + f.put = map[string]any{} + } + f.put[key] = value + return 1, nil +} +func (f *fakeFindings) Delete(key string) error { f.deleted = append(f.deleted, key); return nil } + +func TestTheFindingWriterWritesAndClearsAModulesFindingInTheSeatsLayout(t *testing.T) { + state := &fakeFindings{} + var w views.FindingWriter = FindingWriter{State: state} + f := views.Finding{Observer: "mesh-config", Kind: "unanswered", ID: "mesh-config.mesh-vault.secrets", Summary: "no first view", + Headline: "waits", Explanation: "look", At: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)} + if err := w.Put(f); err != nil { + t.Fatal(err) + } + key := "module.mesh-config.mesh-vault.secrets.unanswered" + v, ok := state.put[key].(seat.FindingsByKeyValue) + if !ok || v.Key != key || v.Severity != Warning || v.Source != "mesh-config.unanswered" || v.Subject.Scope != ScopeModule || len(v.Evidence) != 1 { + t.Fatalf("written %#v", state.put) + } + if err := CheckFinding(v); err != nil { + t.Error(err) + } + if err := w.Delete(f); err != nil || len(state.deleted) != 1 || state.deleted[0] != key { + t.Errorf("deleted %v: %v", state.deleted, err) + } + if err := w.Put(views.Finding{Observer: "x", Kind: "k", ID: "i"}); err == nil { + t.Error("a finding that says nothing was written") + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index c29f214..f2409b8 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondit go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef diff --git a/health-and-repair/go/clients/meshhandactsclient/client.go b/health-and-repair/go/clients/meshhandactsclient/client.go index e9fbaa7..fd7fc97 100644 --- a/health-and-repair/go/clients/meshhandactsclient/client.go +++ b/health-and-repair/go/clients/meshhandactsclient/client.go @@ -10,6 +10,7 @@ import ( "strings" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" + "git.novox.be/novox/mesh-sdk/go/views" ) // The verbs an act is recorded under in the log, and the causes and kinds that are no repair. @@ -71,6 +72,15 @@ func (r Recorder) Record(q Request) (Taken, error) { return r.record(seat.VerbHandAct, body) } +// RecordHandAct records a part's act before it does it, as mesh-sdk's views.HandActs asks: the recorder is the +// interface's implementation, and an act the seat does not take is the error that says nothing was recorded. +func (r Recorder) RecordHandAct(a views.HandAct) error { + _, err := r.Record(Request{What: a.What, Why: a.Why, Cause: a.Cause, Verb: a.Verb}) + return err +} + +var _ views.HandActs = Recorder{} + // RecordWarrant records what the asking module did on the operator's warrant for its ask. func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) { return r.record(seat.VerbWarranted, map[string]any{"asker": asker, "ask": ask}) diff --git a/health-and-repair/go/clients/meshhandactsclient/client_test.go b/health-and-repair/go/clients/meshhandactsclient/client_test.go index a85870e..a4ac41d 100644 --- a/health-and-repair/go/clients/meshhandactsclient/client_test.go +++ b/health-and-repair/go/clients/meshhandactsclient/client_test.go @@ -3,11 +3,13 @@ package meshhandactsclient import ( "encoding/json" "errors" + "fmt" "reflect" "strings" "testing" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts" + "git.novox.be/novox/mesh-sdk/go/views" ) // ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a @@ -71,3 +73,21 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) { t.Error("an act with no why was recorded") } } + +func TestTheRecorderIsMeshSDKsHandActRecorder(t *testing.T) { + var asked []string + r := Recorder{Ask: func(key string, body any) (json.RawMessage, error) { + asked = append(asked, key+" "+fmt.Sprint(body)) + return nil, nil + }} + var acts views.HandActs = r + if err := acts.RecordHandAct(views.HandAct{What: "asked a view again", Why: "it is stale", Cause: "stale"}); err != nil { + t.Fatal(err) + } + if len(asked) != 1 || !strings.HasPrefix(asked[0], "seat:mesh-hand-acts.hand-act ") || !strings.Contains(asked[0], "asked a view again") { + t.Errorf("asked %v", asked) + } + if err := acts.RecordHandAct(views.HandAct{What: "x"}); err == nil { + t.Error("an act with no why was taken") + } +} diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod index 75a57a4..67224be 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.mod +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -3,3 +3,5 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandac go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-20261105000000-000000000000 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef -- 2.54.0 From f4a69e85f2b8cb92e64e55dae0c6ca641fa23f3e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:34:07 +0100 Subject: [PATCH 56/94] =?UTF-8?q?node-engine:=20the=20verbs=20seat-list=20?= =?UTF-8?q?and=20seat-list-again=20(the=20operator=20and=20the=20healers)?= =?UTF-8?q?=20and=20the=20state=20findings,=20with=20fixtures=20(ADR=20063?= =?UTF-8?q?6=20=C2=A71,=20=C2=A77;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/seats/nodeengine/contract.go | 157 +++++++++++++- go/seats/nodeengine/contract_test.go | 6 + seats/node-engine/fixtures/states.json | 17 ++ seats/node-engine/fixtures/verbs.json | 45 ++++ seats/node-engine/seat.json | 286 +++++++++++++++++++++++++ ts/seats/nodeengine/src/index.ts | 119 ++++++++++ 6 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 seats/node-engine/fixtures/states.json diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index b06da58..dd4e2d4 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -25,6 +25,8 @@ const ( VerbCompose = "compose" VerbComposeCandidate = "compose-candidate" VerbManifestReads = "manifest-reads" + VerbSeatList = "seat-list" + VerbSeatListAgain = "seat-list-again" ) const ( @@ -36,7 +38,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbManifestReads} + return []string{VerbHandOver, VerbAccessAct, VerbJudgeDirectories, VerbSetuidSearch, VerbRootFree, VerbActs, VerbPackages, VerbComposition, VerbCompose, VerbComposeCandidate, VerbManifestReads, VerbSeatList, VerbSeatListAgain} } // Events is every event published on the seat's subject: a new copy each call. @@ -70,6 +72,7 @@ const ( StateData = "data" StatePackages = "packages" StateHealth = "health" + StateFindings = "findings" ) // HaveByNodeModuleKey is the key of one value of the state have under the form ".". @@ -280,6 +283,37 @@ func DecodeHealthByNodeValue(raw []byte) (HealthByNodeValue, error) { return v, nil } +// FindingsByKeyKey is the key of one value of the state findings under the form "". +func FindingsByKeyKey(key string) string { return key } + +// ParseFindingsByKeyKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseFindingsByKeyKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeFindingsByKeyValue reads one value of the state findings under the form "", refusing a field FindingsByKeyValue does not know and any text after the value. +func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) { + var v FindingsByKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // Act is one act asked through the seat, as acts reads it (ADR 0563 §9). type Act struct { // the act: hand-over, access-act, judge-directories or setuid-search @@ -371,6 +405,40 @@ type Asked struct { Says string `json:"says"` } +// FindingAction is one answer the operator may give from a notification. +type FindingAction struct { + Arguments map[string]string `json:"arguments,omitempty"` + Label string `json:"label"` + Level string `json:"level,omitempty"` + Machine string `json:"machine,omitempty"` + // the seat verb the operator channel calls + Verb string `json:"verb"` +} + +// The values of FindingAction's level. +const ( + FindingActionLevelAcknowledge = "acknowledge" + FindingActionLevelApprove = "approve" +) + +// FindingEvidence is one observation, as it was said. +type FindingEvidence struct { + At string `json:"at"` + Said string `json:"said"` +} + +// FindingSubject is what the condition is about. +type FindingSubject struct { + // the other machines it concerns + Also []string `json:"also,omitempty"` + // its id within the scope + ID string `json:"id"` + // the machine it concerns, when there is one + Machine string `json:"machine,omitempty"` + // machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module + Scope string `json:"scope"` +} + // ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). type ManifestReadsChanged struct { // when it says it, RFC 3339 @@ -450,6 +518,42 @@ type Said struct { Said string `json:"said"` } +// SeatListStatus is a type of the contract. +type SeatListStatus struct { + // the seat mesh-catalog has answered + Heard bool `json:"heard"` + Held []SeatListHeld `json:"held"` + LastError string `json:"last-error,omitempty"` + // a trunk definition that does not parse, by seat: the one held stands + Refused map[string]string `json:"refused,omitempty"` + Revision int64 `json:"revision,omitempty"` + // the seats held from the bootstrap alone + Waiting []string `json:"waiting,omitempty"` +} + +// SeatList is what an engine holds of the seat list (novox/hq ADR 0636 §7). +type SeatList struct { + // why the read failed, for seat-list-again + Error string `json:"error,omitempty"` + // the one line the engine says of it; empty when nothing waits + Health string `json:"health"` + Status SeatListStatus `json:"status"` +} + +// SeatListHeld is a type of the contract. +type SeatListHeld struct { + // the digest of the definition held; empty for a bootstrapped one + Digest string `json:"digest,omitempty"` + Seat string `json:"seat"` + Source string `json:"source"` +} + +// The values of SeatListHeld's source. +const ( + SeatListHeldSourceTrunk = "trunk" + SeatListHeldSourceBootstrap = "bootstrap" +) + // HandOverInput is what hand-over takes. type HandOverInput struct { // the directory's absolute path, as the engine states it @@ -773,6 +877,20 @@ type ManifestReadsAnswer struct { V int64 `json:"v"` } +// SeatListInput is what seat-list takes. +type SeatListInput struct { +} + +// SeatListAnswer is what seat-list answers. +type SeatListAnswer = SeatList + +// SeatListAgainInput is what seat-list-again takes. +type SeatListAgainInput struct { +} + +// SeatListAgainAnswer is what seat-list-again answers. +type SeatListAgainAnswer = SeatList + // AskedEvent is the body of the event asked. type AskedEvent = ActSaid @@ -885,3 +1003,40 @@ type HealthByNodeValue struct { Units map[string]any `json:"units,omitempty"` V int64 `json:"v"` } + +// FindingsByKeyValue is one value of the state findings under the key . +type FindingsByKeyValue struct { + Actions []FindingAction `json:"actions,omitempty"` + At string `json:"at"` + // newest first, at most 10 + Evidence []FindingEvidence `json:"evidence,omitempty"` + Explanation string `json:"explanation,omitempty"` + Headline string `json:"headline,omitempty"` + // scope.id.token + Key string `json:"key"` + Kind string `json:"kind"` + Needs string `json:"needs,omitempty"` + // the module that wrote it + Observer string `json:"observer"` + // the key's last part when it is not the kind + Ref string `json:"ref,omitempty"` + Resolved string `json:"resolved,omitempty"` + // self, operator, agent or healer: + Resolver string `json:"resolver,omitempty"` + // what of its own saw it last + Run string `json:"run,omitempty"` + Severity string `json:"severity"` + // the row, probe or event that saw it + Source string `json:"source"` + Subject FindingSubject `json:"subject"` + // one line + Summary string `json:"summary"` + // layout version, 1 + V int64 `json:"v"` +} + +// The values of FindingsByKeyValue's severity. +const ( + FindingsByKeyValueSeverityUrgent = "urgent" + FindingsByKeyValueSeverityWarning = "warning" +) diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index b1d7720..f54e529 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -35,6 +35,8 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, + {name: "the finding of a seat list unread, as the engine writes it", subject: "", given: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", wire: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", @@ -45,6 +47,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v ComposeCandidateAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "acts answers each act kept", subject: "", given: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", wire: "{\"acts\":[{\"act\":\"hand-over\",\"ask\":\"hand-over-1\",\"at\":\"2026-11-04T09:00:00Z\",\"caller\":\"mesh-tools\",\"expires\":\"2026-11-05T09:00:00Z\",\"id\":\"hand-over-1\",\"state\":\"asked\",\"subject\":\"/srv/media\"}],\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v ActsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "seat-list answers what the engine holds and what it waits for", subject: "", given: "{\"health\":\"the seat list is held from the bootstrap and nothing new is acted on\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"},{\"digest\":\"3f2a9c\",\"seat\":\"node-files\",\"source\":\"trunk\"}],\"last-error\":\"no answer\",\"waiting\":[\"node-engine\"]}}", wire: "{\"health\":\"the seat list is held from the bootstrap and nothing new is acted on\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"},{\"digest\":\"3f2a9c\",\"seat\":\"node-files\",\"source\":\"trunk\"}],\"last-error\":\"no answer\",\"waiting\":[\"node-engine\"]}}", + decode: func(b []byte) (any, error) { var v SeatListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "seat-list-again answers the error when the read failed", subject: "", given: "{\"error\":\"seatlist: no answer\",\"health\":\"the seat list is held from the bootstrap\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"}],\"waiting\":[\"node-engine\"]}}", wire: "{\"error\":\"seatlist: no answer\",\"health\":\"the seat list is held from the bootstrap\",\"status\":{\"heard\":false,\"held\":[{\"seat\":\"node-engine\",\"source\":\"bootstrap\"}],\"waiting\":[\"node-engine\"]}}", + decode: func(b []byte) (any, error) { var v SeatListAgainAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json new file mode 100644 index 0000000..2712dfd --- /dev/null +++ b/seats/node-engine/fixtures/states.json @@ -0,0 +1,17 @@ +[ + { + "name": "the finding of a seat list unread, as the engine writes it", + "of": "state:findings/", + "given": { + "v": 1, + "key": "machine.ace.seat-list-unread", + "kind": "seat-list-unread", + "subject": {"scope": "machine", "id": "ace", "machine": "ace"}, + "severity": "warning", + "summary": "the seat list has not been read for five minutes", + "source": "node-engine.seat-list", + "observer": "mesh-host", + "at": "2026-11-05T14:00:00Z" + } + } +] diff --git a/seats/node-engine/fixtures/verbs.json b/seats/node-engine/fixtures/verbs.json index 40fff31..3375279 100644 --- a/seats/node-engine/fixtures/verbs.json +++ b/seats/node-engine/fixtures/verbs.json @@ -72,5 +72,50 @@ } ] } + }, + { + "name": "seat-list answers what the engine holds and what it waits for", + "of": "verb:seat-list.answer", + "given": { + "status": { + "heard": false, + "held": [ + { + "seat": "node-engine", + "source": "bootstrap" + }, + { + "seat": "node-files", + "source": "trunk", + "digest": "3f2a9c" + } + ], + "waiting": [ + "node-engine" + ], + "last-error": "no answer" + }, + "health": "the seat list is held from the bootstrap and nothing new is acted on" + } + }, + { + "name": "seat-list-again answers the error when the read failed", + "of": "verb:seat-list-again.answer", + "given": { + "status": { + "heard": false, + "held": [ + { + "seat": "node-engine", + "source": "bootstrap" + } + ], + "waiting": [ + "node-engine" + ] + }, + "health": "the seat list is held from the bootstrap", + "error": "seatlist: no answer" + } } ] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 20ed8a1..541d242 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -674,6 +674,30 @@ ], "description": "what this machine's engine reads of a manifest (ADR 0618 §3)" } + }, + { + "name": "seat-list", + "description": "The seat list this engine holds and what it waits for (novox/hq ADR 0636 §7): each seat's source (trunk or bootstrap) and digest, the seats held from the bootstrap alone, the definitions refused, the last error and whether the seat mesh-catalog has answered. Read-only.", + "callers": "the operator and the healers", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "$ref": "#/defs/SeatList" + } + }, + { + "name": "seat-list-again", + "description": "Make this engine read the seat list from the seat mesh-catalog now (novox/hq ADR 0636 §7), the recovery of a list unread; it answers what it holds afterwards, and the error when the read failed. A read that changed nothing still ends the engine's wait.", + "callers": "the operator and the healers", + "input": { + "type": "object", + "properties": {} + }, + "answer": { + "$ref": "#/defs/SeatList" + } } ], "events": [ @@ -1069,6 +1093,109 @@ } ], "description": "each node's health as its engine reports it" + }, + { + "name": "findings", + "template": "state", + "writer": "holder", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "one thing an observer sees wrong now", + "properties": { + "v": { + "type": "integer", + "description": "layout version, 1" + }, + "key": { + "type": "string", + "description": "scope.id.token" + }, + "kind": { + "type": "string" + }, + "subject": { + "$ref": "#/defs/FindingSubject" + }, + "severity": { + "type": "string", + "enum": [ + "urgent", + "warning" + ] + }, + "summary": { + "type": "string", + "description": "one line" + }, + "headline": { + "type": "string" + }, + "explanation": { + "type": "string" + }, + "resolved": { + "type": "string" + }, + "needs": { + "type": "string" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/defs/FindingAction" + } + }, + "evidence": { + "type": "array", + "description": "newest first, at most 10", + "items": { + "$ref": "#/defs/FindingEvidence" + } + }, + "source": { + "type": "string", + "description": "the row, probe or event that saw it" + }, + "resolver": { + "type": "string", + "description": "self, operator, agent or healer:" + }, + "ref": { + "type": "string", + "description": "the key's last part when it is not the kind" + }, + "observer": { + "type": "string", + "description": "the module that wrote it" + }, + "run": { + "type": "string", + "description": "what of its own saw it last" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "v", + "key", + "kind", + "subject", + "severity", + "summary", + "source", + "observer", + "at" + ] + }, + "description": "one finding of the observer that wrote it" + } + ], + "description": "the findings this node's engine raises about itself, one key per finding, in the layout of the seat mesh-conditions (its findings state): a key written raises the condition of the same key, a key deleted clears it (novox/hq ADR 0564 §2, ADR 0636 §1); today `machine..seat-list-unread`" } ], "defs": { @@ -1412,6 +1539,165 @@ "said" ], "description": "the answer of a read-only judgement" + }, + "SeatListHeld": { + "type": "object", + "properties": { + "seat": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "trunk", + "bootstrap" + ] + }, + "digest": { + "type": "string", + "description": "the digest of the definition held; empty for a bootstrapped one" + } + }, + "required": [ + "seat", + "source" + ] + }, + "SeatList": { + "type": "object", + "description": "what an engine holds of the seat list (novox/hq ADR 0636 §7)", + "properties": { + "status": { + "type": "object", + "properties": { + "heard": { + "type": "boolean", + "description": "the seat mesh-catalog has answered" + }, + "held": { + "type": "array", + "items": { + "$ref": "#/defs/SeatListHeld" + } + }, + "waiting": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the seats held from the bootstrap alone" + }, + "refused": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "a trunk definition that does not parse, by seat: the one held stands" + }, + "last-error": { + "type": "string" + }, + "revision": { + "type": "integer" + } + }, + "required": [ + "heard", + "held" + ] + }, + "health": { + "type": "string", + "description": "the one line the engine says of it; empty when nothing waits" + }, + "error": { + "type": "string", + "description": "why the read failed, for seat-list-again" + } + }, + "required": [ + "status", + "health" + ] + }, + "FindingSubject": { + "type": "object", + "description": "what the condition is about", + "properties": { + "scope": { + "type": "string", + "description": "machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module" + }, + "id": { + "type": "string", + "description": "its id within the scope" + }, + "machine": { + "type": "string", + "description": "the machine it concerns, when there is one" + }, + "also": { + "type": "array", + "description": "the other machines it concerns", + "items": { + "type": "string" + } + } + }, + "required": [ + "scope", + "id" + ] + }, + "FindingAction": { + "type": "object", + "description": "one answer the operator may give from a notification", + "properties": { + "label": { + "type": "string" + }, + "verb": { + "type": "string", + "description": "the seat verb the operator channel calls" + }, + "machine": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "level": { + "type": "string", + "enum": [ + "acknowledge", + "approve" + ] + } + }, + "required": [ + "label", + "verb" + ] + }, + "FindingEvidence": { + "type": "object", + "description": "one observation, as it was said", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "said": { + "type": "string" + } + }, + "required": [ + "at", + "said" + ] } } } diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 39feb40..60a34dc 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -16,6 +16,8 @@ export const verbs = { compose: "compose", composeCandidate: "compose-candidate", manifestReads: "manifest-reads", + seatList: "seat-list", + seatListAgain: "seat-list-again", } as const; export const events = { @@ -49,6 +51,7 @@ export const states = { data: "data", packages: "packages", health: "health", + findings: "findings", } as const; /** The key of one value of the state have under the form ".". */ @@ -158,6 +161,21 @@ export function parseHealthByNodeKey(key: string): { node: string } | undefined return { node }; } +/** The key of one value of the state findings under the form "". */ +export function findingsByKeyKey(key: string): string { + return key; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseFindingsByKeyKey(key: string): { key: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { key }; +} + /** one act asked through the seat, as acts reads it (ADR 0563 §9) */ export interface Act { /** the act: hand-over, access-act, judge-directories or setuid-search */ @@ -234,6 +252,34 @@ export interface Asked { "says": string; } +/** one answer the operator may give from a notification */ +export interface FindingAction { + "arguments"?: Record; + "label": string; + "level"?: "acknowledge" | "approve"; + "machine"?: string; + /** the seat verb the operator channel calls */ + "verb": string; +} + +/** one observation, as it was said */ +export interface FindingEvidence { + "at": string; + "said": string; +} + +/** what the condition is about */ +export interface FindingSubject { + /** the other machines it concerns */ + "also"?: string[]; + /** its id within the scope */ + "id": string; + /** the machine it concerns, when there is one */ + "machine"?: string; + /** machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module */ + "scope": string; +} + /** what a node-engine says when what it reads changed or its node returned (ADR 0618 §4) */ export interface ManifestReadsChanged { /** when it says it, RFC 3339 */ @@ -300,6 +346,34 @@ export interface Said { "said": string; } +export interface SeatListStatus { + /** the seat mesh-catalog has answered */ + "heard": boolean; + "held": SeatListHeld[]; + "last-error"?: string; + /** a trunk definition that does not parse, by seat: the one held stands */ + "refused"?: Record; + "revision"?: number; + /** the seats held from the bootstrap alone */ + "waiting"?: string[]; +} + +/** what an engine holds of the seat list (novox/hq ADR 0636 §7) */ +export interface SeatList { + /** why the read failed, for seat-list-again */ + "error"?: string; + /** the one line the engine says of it; empty when nothing waits */ + "health": string; + "status": SeatListStatus; +} + +export interface SeatListHeld { + /** the digest of the definition held; empty for a bootstrapped one */ + "digest"?: string; + "seat": string; + "source": "trunk" | "bootstrap"; +} + /** is what hand-over takes. */ export interface HandOverInput { /** the directory's absolute path, as the engine states it */ @@ -608,6 +682,20 @@ export interface ManifestReadsAnswer { "v": number; } +/** is what seat-list takes. */ +export interface SeatListInput { +} + +/** is what seat-list answers. */ +export type SeatListAnswer = SeatList; + +/** is what seat-list-again takes. */ +export interface SeatListAgainInput { +} + +/** is what seat-list-again answers. */ +export type SeatListAgainAnswer = SeatList; + /** is the body of the event asked. */ export type AskedEvent = ActSaid; @@ -720,3 +808,34 @@ export interface HealthByNodeValue { "v": number; } +/** is one value of the state findings under the key . */ +export interface FindingsByKeyValue { + "actions"?: FindingAction[]; + "at": string; + /** newest first, at most 10 */ + "evidence"?: FindingEvidence[]; + "explanation"?: string; + "headline"?: string; + /** scope.id.token */ + "key": string; + "kind": string; + "needs"?: string; + /** the module that wrote it */ + "observer": string; + /** the key's last part when it is not the kind */ + "ref"?: string; + "resolved"?: string; + /** self, operator, agent or healer: */ + "resolver"?: string; + /** what of its own saw it last */ + "run"?: string; + "severity": "urgent" | "warning"; + /** the row, probe or event that saw it */ + "source": string; + "subject": FindingSubject; + /** one line */ + "summary": string; + /** layout version, 1 */ + "v": number; +} + -- 2.54.0 From 551870d8d5af14903efb39e0d17de6c02496c36d Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:40:27 +0100 Subject: [PATCH 57/94] =?UTF-8?q?Fixtures=20captured=20from=20the=20holder?= =?UTF-8?q?s'=20own=20read=20tools=20for=20the=20states=20that=20had=20non?= =?UTF-8?q?e=20(conditions,=20findings,=20presence,=20stuck,=20would,=20re?= =?UTF-8?q?solved,=20asks,=20packages);=20the=20packages=20item=20requires?= =?UTF-8?q?=20absent=20(ADR=200634=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/seats/meshconfig/contract_test.go | 12 ++ go/seats/nodeengine/contract.go | 6 +- go/seats/nodeengine/contract_test.go | 2 + .../go/seats/meshconditions/contract_test.go | 54 ++++++++ .../go/seats/meshhealers/contract_test.go | 48 +++++++ .../go/seats/meshwatchdogs/contract_test.go | 56 +++++++++ .../mesh-conditions/fixtures/states.json | 117 ++++++++++++++++++ .../seats/mesh-healers/fixtures/states.json | 14 +++ .../seats/mesh-watchdogs/fixtures/states.json | 29 +++++ .../go/seats/operatorchannel/contract_test.go | 4 + .../operator-channel/fixtures/states.json | 36 ++++++ seats/mesh-config/fixtures/states.json | 25 ++++ seats/node-engine/fixtures/states.json | 37 +++++- seats/node-engine/seat.json | 7 +- ts/seats/nodeengine/src/index.ts | 6 +- 15 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 health-and-repair/go/seats/meshconditions/contract_test.go create mode 100644 health-and-repair/go/seats/meshhealers/contract_test.go create mode 100644 health-and-repair/go/seats/meshwatchdogs/contract_test.go create mode 100644 health-and-repair/seats/mesh-conditions/fixtures/states.json create mode 100644 health-and-repair/seats/mesh-healers/fixtures/states.json create mode 100644 health-and-repair/seats/mesh-watchdogs/fixtures/states.json create mode 100644 operator-and-conversation/seats/operator-channel/fixtures/states.json create mode 100644 seats/mesh-config/fixtures/states.json diff --git a/go/seats/meshconfig/contract_test.go b/go/seats/meshconfig/contract_test.go index 6d82f0f..7a994e6 100644 --- a/go/seats/meshconfig/contract_test.go +++ b/go/seats/meshconfig/contract_test.go @@ -25,6 +25,18 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RetiringHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retiring$")}, {name: "retired", subject: "mesh.stream.mesh-config.event.retired", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v RetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retired$")}, + {name: "a module's value that falls back to a predefined key (captured)", subject: "", given: "{\"key\":\"font-size\",\"module\":\"dunst\",\"node\":\"g14\",\"predefined\":\"font-size\",\"source\":\"module\",\"value\":13}", wire: "{\"key\":\"font-size\",\"module\":\"dunst\",\"node\":\"g14\",\"predefined\":\"font-size\",\"source\":\"module\",\"value\":13}", + decode: func(b []byte) (any, error) { + var v ResolvedByNodeModuleKeyValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's own value (captured)", subject: "", given: "{\"key\":\"width\",\"module\":\"dunst\",\"node\":\"g14\",\"source\":\"module\",\"value\":500}", wire: "{\"key\":\"width\",\"module\":\"dunst\",\"node\":\"g14\",\"source\":\"module\",\"value\":500}", + decode: func(b []byte) (any, error) { + var v ResolvedByNodeModuleKeyValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "renames answers the open windows", subject: "", given: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", wire: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", decode: func(b []byte) (any, error) { var v RenamesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index dd4e2d4..2fa471a 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -978,9 +978,9 @@ type DataByNodeModuleValue struct { // PackagesByNodeValuePackagesItem is a type of the contract. type PackagesByNodeValuePackagesItem struct { - Absent bool `json:"absent,omitempty"` - Modules []string `json:"modules,omitempty"` - Package string `json:"package,omitempty"` + Absent bool `json:"absent"` + Modules []string `json:"modules"` + Package string `json:"package"` } // PackagesByNodeValue is one value of the state packages under the key . diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index f54e529..aa467b2 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -37,6 +37,8 @@ func TestFixtures(t *testing.T) { }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, {name: "the finding of a seat list unread, as the engine writes it", subject: "", given: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", wire: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's packages, present and absent (captured from ace, shortened)", subject: "", given: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", wire: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", + decode: func(b []byte) (any, error) { var v PackagesByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", diff --git a/health-and-repair/go/seats/meshconditions/contract_test.go b/health-and-repair/go/seats/meshconditions/contract_test.go new file mode 100644 index 0000000..fc0717f --- /dev/null +++ b/health-and-repair/go/seats/meshconditions/contract_test.go @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshconditions + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-conditions/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a condition raised from a finding, as the keeper holds it (captured; empty fields left out)", subject: "", given: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"last-observed\":\"2026-10-26T20:19:41.474823121Z\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.076843727Z\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}", wire: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"last-observed\":\"2026-10-26T20:19:41.474823121Z\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.076843727Z\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}", + decode: func(b []byte) (any, error) { var v ConditionsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a condition with an empty evidence trail of headline (captured; empty fields left out)", subject: "", given: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-30T13:03:46.432309782Z\",\"said\":\"The backup of postgres failed on ace\"}],\"explanation\":\"Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.\",\"headline\":\"The backup of postgres failed on ace\",\"key\":\"module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup\",\"kind\":\"backup\",\"last-observed\":\"2026-10-30T13:03:46.432309782Z\",\"needs\":\"answer whether to back up again or go on without a backup\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.069818178Z\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"assigner.backup\",\"subject\":{\"id\":\"ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"The backup of postgres failed on ace\"}", wire: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-30T13:03:46.432309782Z\",\"said\":\"The backup of postgres failed on ace\"}],\"explanation\":\"Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.\",\"headline\":\"The backup of postgres failed on ace\",\"key\":\"module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup\",\"kind\":\"backup\",\"last-observed\":\"2026-10-30T13:03:46.432309782Z\",\"needs\":\"answer whether to back up again or go on without a backup\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.069818178Z\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"assigner.backup\",\"subject\":{\"id\":\"ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"The backup of postgres failed on ace\"}", + decode: func(b []byte) (any, error) { var v ConditionsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finding of the bus scope with its zero time (captured from the watchman; empty fields left out)", subject: "", given: "{\"at\":\"0001-01-01T00:00:00Z\",\"evidence\":[{\"at\":\"0001-01-01T00:00:00Z\",\"said\":\"1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)\"}],\"key\":\"bus.EVENTS.controller.max-deliveries\",\"kind\":\"max-deliveries\",\"needs\":\"deliver them again or drop them, from the mesh MCP server.\",\"observer\":\"mesh-delivery-watchman\",\"severity\":\"warning\",\"source\":\"S9\",\"subject\":{\"id\":\"EVENTS.controller\",\"scope\":\"bus\"},\"summary\":\"the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why\",\"v\":1}", wire: "{\"at\":\"0001-01-01T00:00:00Z\",\"evidence\":[{\"at\":\"0001-01-01T00:00:00Z\",\"said\":\"1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)\"}],\"key\":\"bus.EVENTS.controller.max-deliveries\",\"kind\":\"max-deliveries\",\"needs\":\"deliver them again or drop them, from the mesh MCP server.\",\"observer\":\"mesh-delivery-watchman\",\"severity\":\"warning\",\"source\":\"S9\",\"subject\":{\"id\":\"EVENTS.controller\",\"scope\":\"bus\"},\"summary\":\"the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finding of the module scope with its machine (captured from the watchman; empty fields left out)", subject: "", given: "{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observer\":\"mesh-delivery-watchman\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found\",\"v\":1}", wire: "{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observer\":\"mesh-delivery-watchman\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/go/seats/meshhealers/contract_test.go b/health-and-repair/go/seats/meshhealers/contract_test.go new file mode 100644 index 0000000..957da9d --- /dev/null +++ b/health-and-repair/go/seats/meshhealers/contract_test.go @@ -0,0 +1,48 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshhealers + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-healers/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "an act a healer would have made, kept from the controller's time (captured)", subject: "", given: "{\"act\":\"assert the module's buckets, logs and streams again through the grants part (grants_assert)\",\"at\":\"2026-11-02T08:15:56.49507336Z\",\"condition\":\"bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost\",\"healer\":\"H3\",\"kind\":\"consumer-lost\",\"why\":\"the controller's lease names a controller (or none is written, so one may take it): every act is written as would\"}", wire: "{\"act\":\"assert the module's buckets, logs and streams again through the grants part (grants_assert)\",\"at\":\"2026-11-02T08:15:56.49507336Z\",\"condition\":\"bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost\",\"healer\":\"H3\",\"kind\":\"consumer-lost\",\"why\":\"the controller's lease names a controller (or none is written, so one may take it): every act is written as would\"}", + decode: func(b []byte) (any, error) { var v WouldByWouldValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/go/seats/meshwatchdogs/contract_test.go b/health-and-repair/go/seats/meshwatchdogs/contract_test.go new file mode 100644 index 0000000..564f824 --- /dev/null +++ b/health-and-repair/go/seats/meshwatchdogs/contract_test.go @@ -0,0 +1,56 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshwatchdogs + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-watchdogs/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a node's agent online (captured)", subject: "", given: "{\"online\":true,\"v\":1}", wire: "{\"online\":true,\"v\":1}", + decode: func(b []byte) (any, error) { var v PresenceByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a delivery part's instance online (captured)", subject: "", given: "{\"online\":true,\"v\":1}", wire: "{\"online\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v PresenceByPartPartInstanceValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a thing that has not moved since a time, with what it waited for (captured)", subject: "", given: "{\"bucket\":\"mesh-delivery-watchman_presence\",\"key\":\"part.mesh-delivery-builder.novox\",\"since\":\"2026-10-25T11:52:58.051397201+01:00\",\"v\":1,\"waited\":\"mesh-delivery-builder on novox to come back online\"}", wire: "{\"bucket\":\"mesh-delivery-watchman_presence\",\"key\":\"part.mesh-delivery-builder.novox\",\"since\":\"2026-10-25T11:52:58.051397201+01:00\",\"v\":1,\"waited\":\"mesh-delivery-builder on novox to come back online\"}", + decode: func(b []byte) (any, error) { var v StuckByBucketKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/seats/mesh-conditions/fixtures/states.json b/health-and-repair/seats/mesh-conditions/fixtures/states.json new file mode 100644 index 0000000..0cac1dd --- /dev/null +++ b/health-and-repair/seats/mesh-conditions/fixtures/states.json @@ -0,0 +1,117 @@ +[ + { + "name": "a condition raised from a finding, as the keeper holds it (captured; empty fields left out)", + "of": "state:conditions/", + "given": { + "count": 1, + "evidence": [ + { + "at": "2026-10-26T20:19:41.474823121Z", + "said": "mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it \u2014 `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh" + } + ], + "explanation": "Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.", + "headline": "mesh-build-node waits for a directory on ace", + "key": "module.mesh-build-node.ace.directory-used-as-found", + "kind": "directory-used-as-found", + "last-observed": "2026-10-26T20:19:41.474823121Z", + "needs": "hand the directory over from the control-node, as the operator; the details name it and the line to type.", + "observations": 1, + "raised": "2026-10-31T08:19:57.076843727Z", + "resolved": "mesh-build-node's directory on ace is the mesh's", + "resolver": "self", + "severity": "warning", + "source": "health-report", + "subject": { + "id": "mesh-build-node.ace", + "machine": "ace", + "scope": "module" + }, + "summary": "mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it \u2014 `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh" + } + }, + { + "name": "a condition with an empty evidence trail of headline (captured; empty fields left out)", + "of": "state:conditions/", + "given": { + "count": 1, + "evidence": [ + { + "at": "2026-10-30T13:03:46.432309782Z", + "said": "The backup of postgres failed on ace" + } + ], + "explanation": "Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.", + "headline": "The backup of postgres failed on ace", + "key": "module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup", + "kind": "backup", + "last-observed": "2026-10-30T13:03:46.432309782Z", + "needs": "answer whether to back up again or go on without a backup", + "observations": 1, + "raised": "2026-10-31T08:19:57.069818178Z", + "resolver": "self", + "severity": "warning", + "source": "assigner.backup", + "subject": { + "id": "ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f", + "machine": "ace", + "scope": "module" + }, + "summary": "The backup of postgres failed on ace" + } + }, + { + "name": "a finding of the bus scope with its zero time (captured from the watchman; empty fields left out)", + "of": "state:findings/", + "given": { + "v": 1, + "key": "bus.EVENTS.controller.max-deliveries", + "kind": "max-deliveries", + "subject": { + "id": "EVENTS.controller", + "scope": "bus" + }, + "severity": "warning", + "summary": "the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why", + "needs": "deliver them again or drop them, from the mesh MCP server.", + "observer": "mesh-delivery-watchman", + "source": "S9", + "evidence": [ + { + "at": "0001-01-01T00:00:00Z", + "said": "1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)" + } + ], + "at": "0001-01-01T00:00:00Z" + } + }, + { + "name": "a finding of the module scope with its machine (captured from the watchman; empty fields left out)", + "of": "state:findings/", + "given": { + "v": 1, + "key": "module.mesh-build-node.ace.directory-used-as-found", + "kind": "directory-used-as-found", + "subject": { + "id": "mesh-build-node.ace", + "machine": "ace", + "scope": "module" + }, + "severity": "warning", + "summary": "mesh-build-node on ace uses mesh-build-node.cache as found", + "explanation": "Needs you: hand the directory over from the control-node, as the operator.", + "headline": "mesh-build-node waits for a directory on ace", + "needs": "hand the directory over from the control-node, as the operator; the details name it and the line to type.", + "resolved": "mesh-build-node's directory on ace is the mesh's", + "observer": "mesh-delivery-watchman", + "source": "health-report", + "evidence": [ + { + "at": "2026-10-26T20:19:41.474823121Z", + "said": "mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found" + } + ], + "at": "2026-10-26T20:19:41.474823121Z" + } + } +] diff --git a/health-and-repair/seats/mesh-healers/fixtures/states.json b/health-and-repair/seats/mesh-healers/fixtures/states.json new file mode 100644 index 0000000..01ede17 --- /dev/null +++ b/health-and-repair/seats/mesh-healers/fixtures/states.json @@ -0,0 +1,14 @@ +[ + { + "name": "an act a healer would have made, kept from the controller's time (captured)", + "of": "state:would/", + "given": { + "act": "assert the module's buckets, logs and streams again through the grants part (grants_assert)", + "at": "2026-11-02T08:15:56.49507336Z", + "condition": "bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost", + "healer": "H3", + "kind": "consumer-lost", + "why": "the controller's lease names a controller (or none is written, so one may take it): every act is written as would" + } + } +] diff --git a/health-and-repair/seats/mesh-watchdogs/fixtures/states.json b/health-and-repair/seats/mesh-watchdogs/fixtures/states.json new file mode 100644 index 0000000..6d8a3f8 --- /dev/null +++ b/health-and-repair/seats/mesh-watchdogs/fixtures/states.json @@ -0,0 +1,29 @@ +[ + { + "name": "a node's agent online (captured)", + "of": "state:presence/", + "given": { + "online": true, + "v": 1 + } + }, + { + "name": "a delivery part's instance online (captured)", + "of": "state:presence/part..", + "given": { + "online": true, + "v": 1 + } + }, + { + "name": "a thing that has not moved since a time, with what it waited for (captured)", + "of": "state:stuck/.", + "given": { + "bucket": "mesh-delivery-watchman_presence", + "key": "part.mesh-delivery-builder.novox", + "since": "2026-10-25T11:52:58.051397201+01:00", + "v": 1, + "waited": "mesh-delivery-builder on novox to come back online" + } + } +] diff --git a/operator-and-conversation/go/seats/operatorchannel/contract_test.go b/operator-and-conversation/go/seats/operatorchannel/contract_test.go index c5e8a8e..440ac8a 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract_test.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract_test.go @@ -31,6 +31,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v CancelAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.cancel\\.[A-Za-z0-9_-]+$")}, {name: "a condition by its publisher", subject: "mesh.seat.operator-channel.accept.condition.mesh-watcher", given: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", wire: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", decode: func(b []byte) (any, error) { var v ConditionAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.condition\\.[A-Za-z0-9_-]+$")}, + {name: "an ask chosen on a channel, as the router's tool states it (captured)", subject: "", given: "{\"answer\":\"the operator, via telegram (user id verified), chose Approve\",\"asker\":\"mesh-knowledge\",\"asker_told\":true,\"expires\":\"2026-11-06T12:17:43Z\",\"headline\":\"Parts wait for the seat list before acting\",\"key\":\"mesh-knowledge.kb-decision-636-1bcf1a2c\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T12:27:44Z\",\"state\":\"chosen\"}", wire: "{\"answer\":\"the operator, via telegram (user id verified), chose Approve\",\"asker\":\"mesh-knowledge\",\"asker_told\":true,\"expires\":\"2026-11-06T12:17:43Z\",\"headline\":\"Parts wait for the seat list before acting\",\"key\":\"mesh-knowledge.kb-decision-636-1bcf1a2c\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T12:27:44Z\",\"state\":\"chosen\"}", + decode: func(b []byte) (any, error) { var v AsksByAskerIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask its asker took back (captured)", subject: "", given: "{\"answer\":\"cancelled: its asker took it back\",\"asker\":\"mesh-delivery-teller\",\"asker_told\":true,\"expires\":\"2026-11-06T10:04:01Z\",\"headline\":\"Check failed - merge into main\",\"key\":\"mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T10:14:04Z\",\"state\":\"cancelled\"}", wire: "{\"answer\":\"cancelled: its asker took it back\",\"asker\":\"mesh-delivery-teller\",\"asker_told\":true,\"expires\":\"2026-11-06T10:04:01Z\",\"headline\":\"Check failed - merge into main\",\"key\":\"mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T10:14:04Z\",\"state\":\"cancelled\"}", + decode: func(b []byte) (any, error) { var v AsksByAskerIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/operator-and-conversation/seats/operator-channel/fixtures/states.json b/operator-and-conversation/seats/operator-channel/fixtures/states.json new file mode 100644 index 0000000..66bc5de --- /dev/null +++ b/operator-and-conversation/seats/operator-channel/fixtures/states.json @@ -0,0 +1,36 @@ +[ + { + "name": "an ask chosen on a channel, as the router's tool states it (captured)", + "of": "state:asks/.", + "given": { + "answer": "the operator, via telegram (user id verified), chose Approve", + "asker": "mesh-knowledge", + "asker_told": true, + "expires": "2026-11-06T12:17:43Z", + "headline": "Parts wait for the seat list before acting", + "key": "mesh-knowledge.kb-decision-636-1bcf1a2c", + "offered_on": [ + "telegram" + ], + "opened": "2026-11-05T12:27:44Z", + "state": "chosen" + } + }, + { + "name": "an ask its asker took back (captured)", + "of": "state:asks/.", + "given": { + "answer": "cancelled: its asker took it back", + "asker": "mesh-delivery-teller", + "asker_told": true, + "expires": "2026-11-06T10:04:01Z", + "headline": "Check failed - merge into main", + "key": "mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5", + "offered_on": [ + "telegram" + ], + "opened": "2026-11-05T10:14:04Z", + "state": "cancelled" + } + } +] diff --git a/seats/mesh-config/fixtures/states.json b/seats/mesh-config/fixtures/states.json new file mode 100644 index 0000000..ad3d582 --- /dev/null +++ b/seats/mesh-config/fixtures/states.json @@ -0,0 +1,25 @@ +[ + { + "name": "a module's value that falls back to a predefined key (captured)", + "of": "state:resolved/..", + "given": { + "node": "g14", + "module": "dunst", + "key": "font-size", + "predefined": "font-size", + "source": "module", + "value": 13 + } + }, + { + "name": "a module's own value (captured)", + "of": "state:resolved/..", + "given": { + "node": "g14", + "module": "dunst", + "key": "width", + "source": "module", + "value": 500 + } + } +] diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json index 2712dfd..aff5a04 100644 --- a/seats/node-engine/fixtures/states.json +++ b/seats/node-engine/fixtures/states.json @@ -6,12 +6,47 @@ "v": 1, "key": "machine.ace.seat-list-unread", "kind": "seat-list-unread", - "subject": {"scope": "machine", "id": "ace", "machine": "ace"}, + "subject": { + "scope": "machine", + "id": "ace", + "machine": "ace" + }, "severity": "warning", "summary": "the seat list has not been read for five minutes", "source": "node-engine.seat-list", "observer": "mesh-host", "at": "2026-11-05T14:00:00Z" } + }, + { + "name": "a node's packages, present and absent (captured from ace, shortened)", + "of": "state:packages/", + "given": { + "node": "ace", + "packages": [ + { + "absent": false, + "modules": [ + "avahi" + ], + "package": "avahi" + }, + { + "absent": false, + "modules": [ + "mesh-build-go", + "mesh-build-node" + ], + "package": "bubblewrap" + }, + { + "absent": true, + "modules": [ + "time-sync" + ], + "package": "ntp" + } + ] + } } ] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 541d242..fec0fbb 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -1012,7 +1012,12 @@ "absent": { "type": "boolean" } - } + }, + "required": [ + "package", + "modules", + "absent" + ] } } }, diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 60a34dc..a03d9ab 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -782,9 +782,9 @@ export interface DataByNodeModuleValue { } export interface PackagesByNodeValuePackagesItem { - "absent"?: boolean; - "modules"?: string[]; - "package"?: string; + "absent": boolean; + "modules": string[]; + "package": string; } /** is one value of the state packages under the key . */ -- 2.54.0 From 6d619921837e16856fbbab9eae5761838fb422b6 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:45:30 +0100 Subject: [PATCH 58/94] =?UTF-8?q?operator-channel:=20the=20bench=20shapes?= =?UTF-8?q?=20of=20channel=20and=20intake=20(Message,=20Button,=20Sender,?= =?UTF-8?q?=20Choice,=20LinkAsked,=20Failed,=20Standing,=20Code,=20ProofAn?= =?UTF-8?q?swer)=20in=20the=20definition,=20with=20a=20test=20against=20me?= =?UTF-8?q?sh-sdk's=20channel=20wire=20(C-12;=20ADR=200634=20=C2=A71;=20is?= =?UTF-8?q?sue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../operatorchannelclient/bench_test.go | 58 +++++ .../go/seats/operatorchannel/contract.go | 105 ++++++++ .../seats/operator-channel/seat.json | 240 ++++++++++++++++++ .../ts/seats/operatorchannel/src/index.ts | 105 ++++++++ 4 files changed, 508 insertions(+) create mode 100644 operator-and-conversation/go/clients/operatorchannelclient/bench_test.go diff --git a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go new file mode 100644 index 0000000..f8a7512 --- /dev/null +++ b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go @@ -0,0 +1,58 @@ +package operatorchannelclient + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" + "git.novox.be/novox/mesh-sdk/go/channel" +) + +// TestTheBenchShapesAreTheChannelsWire: each shape the benches channel and intake carry, written by mesh-sdk's channel +// package, reads into the contract's type with no field dropped and reads back as the same wire. +func TestTheBenchShapesAreTheChannelsWire(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + who := channel.Sender{Identity: "42", Display: "Op", Authenticated: true} + cases := []struct { + name string + sdk any + into any + }{ + {"message", channel.Message{Handle: "h1", Title: "t", Body: "b", Buttons: []channel.Button{{Label: "Approve", Ticket: "tk"}}, + Note: "n", Urgent: true, Silent: true, Reply: "r", To: "42", Secret: true}, &seat.Message{}}, + {"a send", channel.Message{Handle: "h2", Title: "t", Body: "b"}, &seat.Message{}}, + {"choice", channel.Choice{ID: "c1", Ticket: "tk", Handle: "h1", Sender: who, At: at}, &seat.Choice{}}, + {"link", channel.LinkAsked{ID: "l1", Sender: who, At: at}, &seat.LinkAsked{}}, + {"failed", channel.Failed{Handle: "h1", Why: "blocked", Permanent: true, At: at}, &seat.Failed{}}, + {"standing", channel.Standing{Ready: true, Why: "w", EditsSilently: true, At: at}, &seat.Standing{}}, + {"code", channel.Code{Sender: who, Code: "123456", Purpose: "approve"}, &seat.Code{}}, + {"proof answer", channel.ProofAnswer{Accepted: true, Words: "ok"}, &seat.ProofAnswer{}}, + } + for _, c := range cases { + wire, err := json.Marshal(c.sdk) + if err != nil { + t.Fatal(err) + } + dec := json.NewDecoder(bytes.NewReader(wire)) + dec.DisallowUnknownFields() + if err := dec.Decode(c.into); err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + back, _ := json.Marshal(c.into) + var got, want any + _ = json.Unmarshal(back, &got) + _ = json.Unmarshal(wire, &want) + if !jsonEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, wire) + } + } +} + +func jsonEqual(a, b any) bool { + x, _ := json.Marshal(a) + y, _ := json.Marshal(b) + return bytes.Equal(x, y) +} diff --git a/operator-and-conversation/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go index 540d371..f17556f 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -149,6 +149,36 @@ type Ask struct { WholeDetails string `json:"whole-details,omitempty"` } +// Button is one answer a channel offers: its label and the router's one-time ticket for it. +type Button struct { + // the button's label + Label string `json:"label"` + // the router's one-time ticket for it + Ticket string `json:"ticket"` +} + +// Choice is a button chosen on a channel, said on the bench intake as the event `choice`. +type Choice struct { + // when, RFC 3339 + At string `json:"at"` + // the message's handle + Handle string `json:"handle,omitempty"` + // the channel's own for this arrival, unique, so the router acts on it once + ID string `json:"id"` + Sender Sender `json:"sender"` + // the ticket of the button + Ticket string `json:"ticket"` +} + +// Code is a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept. +type Code struct { + // the code + Code string `json:"code"` + // what it is for + Purpose string `json:"purpose"` + Sender Sender `json:"sender"` +} + // ConditionActionsItem is a type of the contract. type ConditionActionsItem struct { Arguments map[string]string `json:"arguments,omitempty"` @@ -197,6 +227,51 @@ type Condition struct { V int64 `json:"v"` } +// Failed is a message a channel could not deliver, said on the bench intake as the event `failed`. +type Failed struct { + // when, RFC 3339 + At string `json:"at"` + // the message's handle + Handle string `json:"handle"` + // trying again cannot help + Permanent bool `json:"permanent,omitempty"` + // why + Why string `json:"why"` +} + +// LinkAsked is somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`. +type LinkAsked struct { + // when, RFC 3339 + At string `json:"at"` + // the channel's own for this arrival + ID string `json:"id"` + Sender Sender `json:"sender"` +} + +// Message is the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given. +type Message struct { + // its body + Body string `json:"body"` + // the answers it offers (none for a send) + Buttons []Button `json:"buttons,omitempty"` + // the router's name for it, the same across a show and its edits + Handle string `json:"handle"` + // its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details + Note string `json:"note,omitempty"` + // the choice or link request this answers, by its id: the channel shows it where that was made + Reply string `json:"reply,omitempty"` + // the words carry something shown once (a link's code): the channel keeps no copy + Secret bool `json:"secret,omitempty"` + // shown without notifying + Silent bool `json:"silent,omitempty"` + // its title + Title string `json:"title"` + // the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked + To string `json:"to,omitempty"` + // urgent + Urgent bool `json:"urgent,omitempty"` +} + // Option is a type of the contract. type Option struct { // the digest of exactly what the asker performs when it is chosen @@ -225,6 +300,36 @@ type Person struct { Who string `json:"who"` } +// ProofAnswer is the router's answer to a proof. +type ProofAnswer struct { + // whether it was taken + Accepted bool `json:"accepted"` + // words to say to the person + Words string `json:"words"` +} + +// Sender is who a channel's service says sent something; the router alone judges whether that is the operator. +type Sender struct { + // whether the service authenticated it + Authenticated bool `json:"authenticated"` + // the name it shows + Display string `json:"display,omitempty"` + // the account + Identity string `json:"identity"` +} + +// Standing is what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`. +type Standing struct { + // when, RFC 3339 + At string `json:"at"` + // its edits notify nobody + EditsSilently bool `json:"edits-silently,omitempty"` + // it can send now + Ready bool `json:"ready"` + // why not + Why string `json:"why,omitempty"` +} + // Warrant is a type of the contract. type Warrant struct { // what it was about diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index 0bd8c21..b40e63e 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -563,6 +563,246 @@ "subject", "state" ] + }, + "Button": { + "type": "object", + "description": "one answer a channel offers: its label and the router's one-time ticket for it", + "properties": { + "label": { + "type": "string", + "description": "the button's label" + }, + "ticket": { + "type": "string", + "description": "the router's one-time ticket for it" + } + }, + "required": [ + "label", + "ticket" + ] + }, + "Message": { + "type": "object", + "description": "the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given", + "properties": { + "handle": { + "type": "string", + "description": "the router's name for it, the same across a show and its edits" + }, + "title": { + "type": "string", + "description": "its title" + }, + "body": { + "type": "string", + "description": "its body" + }, + "buttons": { + "type": "array", + "items": { + "$ref": "#/defs/Button" + }, + "description": "the answers it offers (none for a send)" + }, + "note": { + "type": "string", + "description": "its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details" + }, + "urgent": { + "type": "boolean", + "description": "urgent" + }, + "silent": { + "type": "boolean", + "description": "shown without notifying" + }, + "reply": { + "type": "string", + "description": "the choice or link request this answers, by its id: the channel shows it where that was made" + }, + "to": { + "type": "string", + "description": "the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked" + }, + "secret": { + "type": "boolean", + "description": "the words carry something shown once (a link's code): the channel keeps no copy" + } + }, + "required": [ + "handle", + "title", + "body" + ] + }, + "Sender": { + "type": "object", + "description": "who a channel's service says sent something; the router alone judges whether that is the operator", + "properties": { + "identity": { + "type": "string", + "description": "the account" + }, + "display": { + "type": "string", + "description": "the name it shows" + }, + "authenticated": { + "type": "boolean", + "description": "whether the service authenticated it" + } + }, + "required": [ + "identity", + "authenticated" + ] + }, + "Choice": { + "type": "object", + "description": "a button chosen on a channel, said on the bench intake as the event `choice`", + "properties": { + "id": { + "type": "string", + "description": "the channel's own for this arrival, unique, so the router acts on it once" + }, + "ticket": { + "type": "string", + "description": "the ticket of the button" + }, + "handle": { + "type": "string", + "description": "the message's handle" + }, + "sender": { + "$ref": "#/defs/Sender" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "id", + "ticket", + "sender", + "at" + ] + }, + "LinkAsked": { + "type": "object", + "description": "somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`", + "properties": { + "id": { + "type": "string", + "description": "the channel's own for this arrival" + }, + "sender": { + "$ref": "#/defs/Sender" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "id", + "sender", + "at" + ] + }, + "Failed": { + "type": "object", + "description": "a message a channel could not deliver, said on the bench intake as the event `failed`", + "properties": { + "handle": { + "type": "string", + "description": "the message's handle" + }, + "why": { + "type": "string", + "description": "why" + }, + "permanent": { + "type": "boolean", + "description": "trying again cannot help" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "handle", + "why", + "at" + ] + }, + "Standing": { + "type": "object", + "description": "what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`", + "properties": { + "ready": { + "type": "boolean", + "description": "it can send now" + }, + "why": { + "type": "string", + "description": "why not" + }, + "edits-silently": { + "type": "boolean", + "description": "its edits notify nobody" + }, + "at": { + "type": "string", + "description": "when, RFC 3339" + } + }, + "required": [ + "ready", + "at" + ] + }, + "Code": { + "type": "object", + "description": "a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept", + "properties": { + "sender": { + "$ref": "#/defs/Sender" + }, + "code": { + "type": "string", + "description": "the code" + }, + "purpose": { + "type": "string", + "description": "what it is for" + } + }, + "required": [ + "sender", + "code", + "purpose" + ] + }, + "ProofAnswer": { + "type": "object", + "description": "the router's answer to a proof", + "properties": { + "accepted": { + "type": "boolean", + "description": "whether it was taken" + }, + "words": { + "type": "string", + "description": "words to say to the person" + } + }, + "required": [ + "accepted", + "words" + ] } } } diff --git a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts index 4b9e593..fbffec0 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -108,6 +108,36 @@ export interface Ask { "whole-details"?: string; } +/** one answer a channel offers: its label and the router's one-time ticket for it */ +export interface Button { + /** the button's label */ + "label": string; + /** the router's one-time ticket for it */ + "ticket": string; +} + +/** a button chosen on a channel, said on the bench intake as the event `choice` */ +export interface Choice { + /** when, RFC 3339 */ + "at": string; + /** the message's handle */ + "handle"?: string; + /** the channel's own for this arrival, unique, so the router acts on it once */ + "id": string; + "sender": Sender; + /** the ticket of the button */ + "ticket": string; +} + +/** a code a person typed on a channel, asked as the proof `code` on the bench intake by request and reply: never in an event, never kept */ +export interface Code { + /** the code */ + "code": string; + /** what it is for */ + "purpose": string; + "sender": Sender; +} + export interface ConditionActionsItem { "arguments"?: Record; /** label */ @@ -153,6 +183,51 @@ export interface Condition { "v": number; } +/** a message a channel could not deliver, said on the bench intake as the event `failed` */ +export interface Failed { + /** when, RFC 3339 */ + "at": string; + /** the message's handle */ + "handle": string; + /** trying again cannot help */ + "permanent"?: boolean; + /** why */ + "why": string; +} + +/** somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link` */ +export interface LinkAsked { + /** when, RFC 3339 */ + "at": string; + /** the channel's own for this arrival */ + "id": string; + "sender": Sender; +} + +/** the work a channel takes on the bench channel: shown with buttons (show), shown again in place (edit) or said (send); the words are the router's, shown as given */ +export interface Message { + /** its body */ + "body": string; + /** the answers it offers (none for a send) */ + "buttons"?: Button[]; + /** the router's name for it, the same across a show and its edits */ + "handle": string; + /** its secondary words after the body and the buttons: who asked and when, what each answer does, when it expires, the ask's details */ + "note"?: string; + /** the choice or link request this answers, by its id: the channel shows it where that was made */ + "reply"?: string; + /** the words carry something shown once (a link's code): the channel keeps no copy */ + "secret"?: boolean; + /** shown without notifying */ + "silent"?: boolean; + /** its title */ + "title": string; + /** the account linked as the operator on this kind, where the channel sends what is not a reply; empty when none is linked */ + "to"?: string; + /** urgent */ + "urgent"?: boolean; +} + export interface Option { /** the digest of exactly what the asker performs when it is chosen */ "binds"?: string; @@ -179,6 +254,36 @@ export interface Person { "who": string; } +/** the router's answer to a proof */ +export interface ProofAnswer { + /** whether it was taken */ + "accepted": boolean; + /** words to say to the person */ + "words": string; +} + +/** who a channel's service says sent something; the router alone judges whether that is the operator */ +export interface Sender { + /** whether the service authenticated it */ + "authenticated": boolean; + /** the name it shows */ + "display"?: string; + /** the account */ + "identity": string; +} + +/** what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing` */ +export interface Standing { + /** when, RFC 3339 */ + "at": string; + /** its edits notify nobody */ + "edits-silently"?: boolean; + /** it can send now */ + "ready": boolean; + /** why not */ + "why"?: string; +} + export interface Warrant { /** what it was about */ "about"?: string; -- 2.54.0 From 99adc0eb98a733de1807a3802a6acd765728903a Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:49:14 +0100 Subject: [PATCH 59/94] =?UTF-8?q?node-engine=20client:=20the=20keys=20of?= =?UTF-8?q?=20its=20states=20have=20and=20composed=20(HaveKey,=20ParseHave?= =?UTF-8?q?Key,=20ComposedKey,=20ParseComposedKey)=20that=20the=20assigner?= =?UTF-8?q?'s=20ask=20keys=20and=20the=20readers=20share=20(ADR=200634=20?= =?UTF-8?q?=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/keys.go | 122 +++++++++++++++++++++++ go/clients/nodeengineclient/keys_test.go | 45 +++++++++ 2 files changed, 167 insertions(+) create mode 100644 go/clients/nodeengineclient/keys.go create mode 100644 go/clients/nodeengineclient/keys_test.go diff --git a/go/clients/nodeengineclient/keys.go b/go/clients/nodeengineclient/keys.go new file mode 100644 index 0000000..edb9b0a --- /dev/null +++ b/go/clients/nodeengineclient/keys.go @@ -0,0 +1,122 @@ +package nodeengineclient + +import ( + "fmt" + "regexp" + "strings" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// NodeWord is the last part of a node's own key in the states have and composed. +const NodeWord = "node" + +var moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`) + +// moduleKeyPart is a module's name as a key part (issue 1150): a name may hold a dot (`novox.be`), which a key part may not, +// so a dot is escaped as keys.EscapeName writes it (`novox=2ebe`). A name that is empty, or holds a wildcard, whitespace, +// `=`, or an empty part between dots, is refused. +func moduleKeyPart(module string) (string, error) { + if module == "" { + return "", fmt.Errorf("node-engine: the module name is empty") + } + if !moduleNameOK.MatchString(module) { + return "", fmt.Errorf("node-engine: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module) + } + return keys.EscapeName(module), nil +} + +// parseModuleKeyPart is the inverse of moduleKeyPart; it refuses a part that is not the one moduleKeyPart writes for its name. +func parseModuleKeyPart(part string) (string, error) { + if err := keys.CheckName("module", part); err != nil { + return "", err + } + module, err := keys.UnescapeName(part) + if err != nil { + return "", err + } + if back, err := moduleKeyPart(module); err != nil || back != part { + return "", fmt.Errorf("node-engine: the module part %q is not a module name as a key writes it", part) + } + return module, nil +} + +// HaveKey is the key, in the state have, of a module's have on a node: `.`. A module named `node` is refused, +// since `.node` is the node's own facts. +func HaveKey(node, module string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + part, err := moduleKeyPart(module) + if err != nil { + return "", err + } + if module == NodeWord { + return "", fmt.Errorf(`node-engine: the module name "node" is refused: %s.node is the node's own key`, node) + } + return node + "." + part, nil +} + +// ParseHaveKey is the inverse of HaveKey and of the node's own key: isNode is true for `.node`, with module empty. +func ParseHaveKey(key string) (node, module string, isNode bool, err error) { + node, module, ok := strings.Cut(key, ".") + if !ok { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key, .", key) + } + if err := keys.CheckName("node", node); err != nil { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key: %w", key, err) + } + if module == NodeWord { + return node, "", true, nil + } + if module, err = parseModuleKeyPart(module); err != nil { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key: %w", key, err) + } + return node, module, false, nil +} + +// ComposedKey is the key, in the state composed, of a module's composition for a node at a fingerprint: +// `..`, the hex being the fingerprint's key part (64 lowercase hex digits, the mesh-catalog client's +// KeyPart). A module named `node` is refused. +func ComposedKey(node, module, fingerprintPart string) (string, error) { + have, err := HaveKey(node, module) + if err != nil { + return "", err + } + if !keys.IsHex64(fingerprintPart) { + return "", fmt.Errorf("node-engine: %q is not the key part of a fingerprint, 64 lowercase hex digits", fingerprintPart) + } + return have + "." + fingerprintPart, nil +} + +// ParseComposedKey is the inverse of ComposedKey and of the node's own key `.node` (isNode, with module and +// fingerprintPart empty). It refuses any other shape, naming the key. +func ParseComposedKey(key string) (node, module, fingerprintPart string, isNode bool, err error) { + refused := func(why error) (string, string, string, bool, error) { + return "", "", "", false, fmt.Errorf("node-engine: %q is not a composition key, .. or .node: %w", key, why) + } + parts := strings.Split(key, ".") + switch len(parts) { + case 2: + if parts[1] != NodeWord { + return refused(fmt.Errorf("a module's composition names its fingerprint")) + } + if err := keys.CheckName("node", parts[0]); err != nil { + return refused(err) + } + return parts[0], "", "", true, nil + case 3: + node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil { + return refused(err) + } + if isNode { + return refused(fmt.Errorf("the node's own key names no fingerprint")) + } + if !keys.IsHex64(parts[2]) { + return refused(fmt.Errorf("%q is not 64 lowercase hex digits", parts[2])) + } + return node, module, parts[2], false, nil + } + return refused(fmt.Errorf("the key has %d parts", len(parts))) +} diff --git a/go/clients/nodeengineclient/keys_test.go b/go/clients/nodeengineclient/keys_test.go new file mode 100644 index 0000000..d8500b1 --- /dev/null +++ b/go/clients/nodeengineclient/keys_test.go @@ -0,0 +1,45 @@ +package nodeengineclient + +import ( + "strings" + "testing" +) + +func TestTheHaveAndComposedKeysKeepTheirForms(t *testing.T) { + fp := strings.Repeat("ab", 32) + for module, want := range map[string]string{"gitea": "n1.gitea", "novox.be": "n1.novox=2ebe", "a/b": "n1.a/b"} { + key, err := HaveKey("n1", module) + if err != nil || key != want { + t.Errorf("HaveKey(%q) = %q, %v", module, key, err) + } + if node, back, isNode, err := ParseHaveKey(key); err != nil || node != "n1" || back != module || isNode { + t.Errorf("ParseHaveKey(%q) = %q %q %v %v", key, node, back, isNode, err) + } + ck, err := ComposedKey("n1", module, fp) + if err != nil || ck != want+"."+fp { + t.Errorf("ComposedKey(%q) = %q, %v", module, ck, err) + } + if node, back, part, isNode, err := ParseComposedKey(ck); err != nil || node != "n1" || back != module || part != fp || isNode { + t.Errorf("ParseComposedKey(%q) = %q %q %q %v %v", ck, node, back, part, isNode, err) + } + } + if node, module, isNode, err := ParseHaveKey("n1.node"); err != nil || node != "n1" || module != "" || !isNode { + t.Errorf("the node's own have key: %q %q %v %v", node, module, isNode, err) + } + if node, _, _, isNode, err := ParseComposedKey("n1.node"); err != nil || node != "n1" || !isNode { + t.Errorf("the node's own composition key: %q %v %v", node, isNode, err) + } + for _, bad := range [][2]string{{"", "m"}, {"n.1", "m"}, {"n1", ""}, {"n1", "node"}, {"n1", "a b"}, {"n1", "a=b"}, {"n1", "a..b"}} { + if _, err := HaveKey(bad[0], bad[1]); err == nil { + t.Errorf("HaveKey(%q, %q) keys", bad[0], bad[1]) + } + } + if _, err := ComposedKey("n1", "m", "src2:"+fp); err == nil { + t.Error("a fingerprint with its prefix is a key part") + } + for _, bad := range []string{"n1", "n1.m", "n1.m." + fp[:10], "n1.node." + fp, "n1.m." + fp + ".x"} { + if _, _, _, _, err := ParseComposedKey(bad); err == nil { + t.Errorf("ParseComposedKey(%q) reads a wrong key", bad) + } + } +} -- 2.54.0 From 074032e9d43c93d274cce7cbb77e271e1d5cee15 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:49:56 +0100 Subject: [PATCH 60/94] =?UTF-8?q?mesh-module-assigner=20client:=20CheckPro?= =?UTF-8?q?posalID,=20the=20id=20a=20proposal=20and=20the=20ask=20about=20?= =?UTF-8?q?it=20carry=20(ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshmoduleassignerclient/client_test.go | 7 +++++++ .../meshmoduleassignerclient/proposal.go | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 placement/go/clients/meshmoduleassignerclient/proposal.go diff --git a/placement/go/clients/meshmoduleassignerclient/client_test.go b/placement/go/clients/meshmoduleassignerclient/client_test.go index 09ec425..06453e0 100644 --- a/placement/go/clients/meshmoduleassignerclient/client_test.go +++ b/placement/go/clients/meshmoduleassignerclient/client_test.go @@ -12,6 +12,13 @@ import ( ) func TestPlacementClient(t *testing.T) { + t.Run("proposal id", func(t *testing.T) { + for id, ok := range map[string]bool{"p-20261028t183000-1a2b3c": true, "abc": true, "ab": false, "A-bc": false, "-abc": false, "a.bc": false, "": false} { + if (CheckProposalID(id) == nil) != ok { + t.Errorf("CheckProposalID(%q) = %v", id, CheckProposalID(id)) + } + } + }) t.Run("key and file", func(t *testing.T) { // Research 072 row 37: one node key and one key per module, a legacy key read beside them, any other refused. for key, want := range map[string]struct { diff --git a/placement/go/clients/meshmoduleassignerclient/proposal.go b/placement/go/clients/meshmoduleassignerclient/proposal.go new file mode 100644 index 0000000..284a358 --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/proposal.go @@ -0,0 +1,17 @@ +package meshmoduleassignerclient + +import ( + "fmt" + "regexp" +) + +var proposalIDOK = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,62}$`) + +// CheckProposalID refuses an id that is no proposal's: lower-case letters, digits and dashes, 3 to 63 of them, so it is one +// key token. The holder writes proposals under it, and the keys of the asks about a proposal (`placement.`) carry it. +func CheckProposalID(id string) error { + if !proposalIDOK.MatchString(id) { + return fmt.Errorf("mesh-module-assigner: %q is no proposal id (lower-case letters, digits and dashes, one key token)", id) + } + return nil +} -- 2.54.0 From f00aac3847eb02557852b3fbb17d53483fdb5ba3 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:50:04 +0100 Subject: [PATCH 61/94] =?UTF-8?q?node-engine:=20typed=20layouts=20for=20th?= =?UTF-8?q?e=20states=20have=20(both=20forms),=20composed=20(both=20forms,?= =?UTF-8?q?=20wire=20names),=20data=20and=20health,=20with=20fixtures;=20C?= =?UTF-8?q?andidates=20typed=20as=20CandidateAnswer=20(ADR=200634=20=C2=A7?= =?UTF-8?q?1,=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/seats/nodeengine/contract.go | 490 +++++++++-- go/seats/nodeengine/contract_test.go | 38 + seats/node-engine/fixtures/states.json | 467 +++++++++++ seats/node-engine/seat.json | 1045 ++++++++++++++++++++++-- ts/seats/nodeengine/src/index.ts | 401 ++++++++- 5 files changed, 2290 insertions(+), 151 deletions(-) diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index 2fa471a..f8267e8 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -185,6 +185,37 @@ func DecodeComposedByNodeModuleFingerprintValue(raw []byte) (ComposedByNodeModul return v, nil } +// ComposedByNodeNodeKey is the key of one value of the state composed under the form ".node". +func ComposedByNodeNodeKey(node string) string { return node + ".node" } + +// ParseComposedByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. +func ParseComposedByNodeNodeKey(key string) (string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".node") + if !found0 || v0 == "" { + return "", false + } + rest = after0 + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeComposedByNodeNodeValue reads one value of the state composed under the form ".node", refusing a field ComposedByNodeNodeValue does not know and any text after the value. +func DecodeComposedByNodeNodeValue(raw []byte) (ComposedByNodeNodeValue, error) { + var v ComposedByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // DataByNodeModuleKey is the key of one value of the state data under the form ".". func DataByNodeModuleKey(node, module string) string { return node + "." + module } @@ -314,6 +345,17 @@ func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) { return v, nil } +// AccountsHanded is the hashes of the users of the list, sealed to the vault. +type AccountsHanded struct { + // when + At string `json:"at"` + Box SealedBox `json:"box"` + From string `json:"from"` + // the fingerprint of the vault's key + To string `json:"to"` + Users int64 `json:"users"` +} + // Act is one act asked through the seat, as acts reads it (ADR 0563 §9). type Act struct { // the act: hand-over, access-act, judge-directories or setuid-search @@ -395,6 +437,14 @@ type Acts struct { Node string `json:"node"` } +// Adoption is the node is adopted and which of its modules are taken. +type Adoption struct { + // the modules taken + Taken []string `json:"taken"` + // by module, the resources left untaken + Untaken map[string][]string `json:"untaken,omitempty"` +} + // Asked is the answer of an act the operator is asked. type Asked struct { // the ask's id: acts shows where it stands @@ -405,6 +455,112 @@ type Asked struct { Says string `json:"says"` } +// AttachedNetwork is a network attached to a link. +type AttachedNetwork struct { + Link string `json:"link"` + Network string `json:"network"` +} + +// CandidateAdded is a module the candidate adds. +type CandidateAdded struct { + Composed string `json:"composed,omitempty"` + Module string `json:"module"` + Unissued []string `json:"unissued,omitempty"` +} + +// CandidateAnswer is the node's answer to one placement candidate naming it. +type CandidateAnswer struct { + Added []CandidateAdded `json:"added,omitempty"` + Differs []CandidateDiffers `json:"differs,omitempty"` + Held []CandidateRefusal `json:"held,omitempty"` + Note string `json:"note,omitempty"` + Proposal string `json:"proposal"` + Refusals []CandidateRefusal `json:"refusals,omitempty"` + // why the node cannot compose with the candidate at all + Refused string `json:"refused,omitempty"` + // modules the candidate removes + Removed []string `json:"removed,omitempty"` + Returns string `json:"returns,omitempty"` + Revision int64 `json:"revision"` + // modules whose composition is the one running + Same []string `json:"same,omitempty"` + ValuesDiffer int64 `json:"values_differ,omitempty"` + // same, differs or refused + Verdict string `json:"verdict"` +} + +// The values of CandidateAnswer's verdict. +const ( + CandidateAnswerVerdictSame = "same" + CandidateAnswerVerdictDiffers = "differs" + CandidateAnswerVerdictRefused = "refused" +) + +// CandidateBodies is the bodies of one resource that differ. +type CandidateBodies struct { + Composed string `json:"composed,omitempty"` + ID string `json:"id"` + Paths []string `json:"paths,omitempty"` + Running string `json:"running,omitempty"` + Why string `json:"why"` +} + +// CandidateDiffers is a module whose composition with the candidate differs from the one running. +type CandidateDiffers struct { + Bodies []CandidateBodies `json:"bodies,omitempty"` + Composed string `json:"composed,omitempty"` + Module string `json:"module"` + Resource string `json:"resource,omitempty"` + Running string `json:"running,omitempty"` + Why string `json:"why"` +} + +// CandidateRefusal is a module refused or held with the candidate. +type CandidateRefusal struct { + Module string `json:"module"` + Today string `json:"today,omitempty"` + Why string `json:"why"` +} + +// DeclaredGroup is a group. +type DeclaredGroup struct { + Gid int64 `json:"gid"` + Name string `json:"name"` +} + +// DeclaredIdentity is the identity of a module assigned to the node. +type DeclaredIdentity struct { + Gid int64 `json:"gid"` + Groups []int64 `json:"groups,omitempty"` + Module string `json:"module"` + Name string `json:"name"` + Runner string `json:"runner,omitempty"` + Uid int64 `json:"uid"` +} + +// DeclaredLibrary is a library the node is reached by. +type DeclaredLibrary struct { + Gid int64 `json:"gid"` + Group string `json:"group"` + Home bool `json:"home"` + Library string `json:"library"` + Paths []string `json:"paths,omitempty"` + Root string `json:"root"` + Runner string `json:"runner,omitempty"` + Taken bool `json:"taken,omitempty"` +} + +// FailedUnit is a unit that failed. +type FailedUnit struct { + Load string `json:"load,omitempty"` + Resource string `json:"resource,omitempty"` + Result string `json:"result,omitempty"` + Scope string `json:"scope"` + // since when + Since string `json:"since"` + Unit string `json:"unit"` +} + // FindingAction is one answer the operator may give from a notification. type FindingAction struct { Arguments map[string]string `json:"arguments,omitempty"` @@ -439,6 +595,15 @@ type FindingSubject struct { Scope string `json:"scope"` } +// LoginFallback is a login that fell back to another node. +type LoginFallback struct { + // when + At string `json:"at"` + From string `json:"from"` + Node string `json:"node"` + Why string `json:"why"` +} + // ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). type ManifestReadsChanged struct { // when it says it, RFC 3339 @@ -482,6 +647,115 @@ const ( ModuleStateHeld = "held" ) +// NetworkHealth is the node's network. +type NetworkHealth struct { + Parts []NetworkPart `json:"parts"` + // since when + Since string `json:"since"` + // running, degraded or unknown + State string `json:"state"` +} + +// NetworkPart is one part of the node's network. +type NetworkPart struct { + Owner string `json:"owner,omitempty"` + Part string `json:"part"` + Reason string `json:"reason,omitempty"` + Said string `json:"said,omitempty"` + // since when + Since string `json:"since"` + State string `json:"state"` + Streak int64 `json:"streak,omitempty"` + Targets []string `json:"targets,omitempty"` + Toward []string `json:"toward,omitempty"` + Writer string `json:"writer,omitempty"` +} + +// NodeAccount is an account the node needs to log in again. +type NodeAccount struct { + Account string `json:"account"` + Module string `json:"module,omitempty"` + Reason string `json:"reason"` + Resource string `json:"resource,omitempty"` + // since when + Since string `json:"since"` +} + +// NodeDirectory is a directory found where the node expected none. +type NodeDirectory struct { + Module string `json:"module"` + Path string `json:"path"` + Reason string `json:"reason"` + Resource string `json:"resource"` + // since when + Since string `json:"since"` +} + +// NodeOwn is what a node's own composition carries beside its resources. +type NodeOwn struct { + Adoption *Adoption `json:"adoption,omitempty"` + Groups []DeclaredGroup `json:"groups,omitempty"` + Identities []DeclaredIdentity `json:"identities,omitempty"` + Libraries []DeclaredLibrary `json:"libraries,omitempty"` + // the account the provisioner's client runs as on the node + ProvisionerAccount string `json:"provisioner_account,omitempty"` +} + +// NodeRecord is what the machine observes of itself, for every node and composing part to read. +type NodeRecord struct { + Authority string `json:"authority,omitempty"` + Capabilities []string `json:"capabilities,omitempty"` + Certificate string `json:"certificate,omitempty"` + Contract int64 `json:"contract,omitempty"` + ManifestReads []string `json:"manifest-reads,omitempty"` + Networks []AttachedNetwork `json:"networks,omitempty"` + NetworksSaid bool `json:"networks-said,omitempty"` + Node string `json:"node"` + OutwardLinks []string `json:"outward-links,omitempty"` + SealingKey string `json:"sealing-key,omitempty"` + ServingKey string `json:"serving-key,omitempty"` + Trusts []string `json:"trusts,omitempty"` + TunnelKey string `json:"tunnel-key,omitempty"` + V int64 `json:"v"` +} + +// PeerSeen is a tunnel peer and whether it is seen. +type PeerSeen struct { + Addresses []string `json:"addresses,omitempty"` + Key string `json:"key"` + Seen bool `json:"seen"` +} + +// PlacedDataItem is one data item a module declares, as placed on a node. +type PlacedDataItem struct { + Class string `json:"class"` + // the path whose snapshot keeps it, empty when it is not backed up + CoveredBy string `json:"covered_by,omitempty"` + Item string `json:"item"` + // walk, dataset or shallow + Measure string `json:"measure,omitempty"` + Module string `json:"module"` + // the item's place on the node + Path string `json:"path"` + // backup, redundancy, both or none + Protection string `json:"protection,omitempty"` +} + +// PullWait is the pull from a store a failed apply waits on. +type PullWait struct { + // when the pull began + Began string `json:"began"` + // that seat's holder, whose settling the apply waits for + Holder string `json:"holder"` + Image string `json:"image"` + // what triggered the last try again, in words + Retried string `json:"retried,omitempty"` + // when the apply was last tried again + RetriedAt string `json:"retried-at,omitempty"` + // the seat the pull came through + Seat string `json:"seat,omitempty"` +} + // Revision is one revision of a composition key. type Revision struct { // its digest @@ -498,6 +772,17 @@ type Revision struct { Revision int64 `json:"revision,omitempty"` } +// RootAccountVerdict is the engine's verdict of whether an account's resource is root-free, as the node's health lists it. +type RootAccountVerdict struct { + Account string `json:"account"` + Module string `json:"module"` + Reason string `json:"reason,omitempty"` + Resource string `json:"resource"` + // since when + Since string `json:"since"` + State string `json:"state"` +} + // RootVerdict is the engine's root-free verdict (ADR 0563 §6). type RootVerdict struct { // the machine is root-free @@ -518,6 +803,15 @@ type Said struct { Said string `json:"said"` } +// SealedBox is a value sealed to a key. +type SealedBox struct { + Ct string `json:"ct"` + Eph string `json:"eph"` + Iv string `json:"iv"` + Tag string `json:"tag"` + V int64 `json:"v"` +} + // SeatListStatus is a type of the contract. type SeatListStatus struct { // the seat mesh-catalog has answered @@ -554,6 +848,37 @@ const ( SeatListHeldSourceBootstrap = "bootstrap" ) +// UnitsHealth is the node's units. +type UnitsHealth struct { + Away []string `json:"away,omitempty"` + Failed []FailedUnit `json:"failed"` + State string `json:"state"` + Unread []string `json:"unread,omitempty"` +} + +// UserListWritten is the bus's user list as the engine of the bus node wrote it to the bus module's file. +type UserListWritten struct { + // when + At string `json:"at"` + // the digest of the file on the machine after the write + Digest string `json:"digest,omitempty"` + // why a write failed + Error string `json:"error,omitempty"` + // which list the node follows now + Follows string `json:"follows,omitempty"` + Handed *AccountsHanded `json:"handed,omitempty"` + // the bus node's sealing key, its public half + Key string `json:"key,omitempty"` + Revision int64 `json:"revision"` + // the key the list was taken from + Source string `json:"source"` + // why the bus node does not watch the grants part's key now + Unwatched string `json:"unwatched,omitempty"` + Users int64 `json:"users"` + // the digest of the list the source holds, sha256: + Wanted string `json:"wanted"` +} + // HandOverInput is what hand-over takes. type HandOverInput struct { // the directory's absolute path, as the engine states it @@ -905,75 +1230,119 @@ type ManifestReadsChangedEvent = ManifestReadsChanged // HaveByNodeModuleValue is one value of the state have under the key .. type HaveByNodeModuleValue struct { - Build string `json:"build,omitempty"` - BuildAlone bool `json:"build-alone,omitempty"` - // the composition's digest + Build string `json:"build,omitempty"` + // the move changes the build alone + BuildAlone bool `json:"build-alone,omitempty"` + // the digest of the composition the node runs Composed string `json:"composed,omitempty"` Detail string `json:"detail,omitempty"` // the fingerprint held - Fp string `json:"fp,omitempty"` - // the accounts handed - Handed map[string]any `json:"handed,omitempty"` - // how it is - Health string `json:"health"` - Judgement string `json:"judgement,omitempty"` - Kept []string `json:"kept,omitempty"` + Fp string `json:"fp,omitempty"` + Handed *AccountsHanded `json:"handed,omitempty"` + // what the module's own check says + Health string `json:"health"` + // in phase waiting-yes, the judgement of the move + Judgement string `json:"judgement,omitempty"` + // what the node keeps of the module as it runs + Kept []string `json:"kept,omitempty"` // how far it has come - Phase string `json:"phase"` - // a pull the engine waits for - PullWait map[string]any `json:"pull-wait,omitempty"` - Sealed []string `json:"sealed,omitempty"` - SealedAt map[string]any `json:"sealed-at,omitempty"` - // the user list written - UserList map[string]any `json:"user-list,omitempty"` - V int64 `json:"v"` - // what it waits for + Phase string `json:"phase"` + PullWait *PullWait `json:"pull-wait,omitempty"` + // each value sealed to the node's key + Sealed []string `json:"sealed,omitempty"` + // the sealed values by slot + SealedAt map[string]string `json:"sealed-at,omitempty"` + UserList *UserListWritten `json:"user-list,omitempty"` + V int64 `json:"v"` + // in phase waiting-yes, the digest of the composition that waits Waits string `json:"waits,omitempty"` } +// The values of HaveByNodeModuleValue's health. +const ( + HaveByNodeModuleValueHealthOk = "ok" + HaveByNodeModuleValueHealthFailed = "failed" + HaveByNodeModuleValueHealthWaits = "waits" + HaveByNodeModuleValueHealthUnknown = "unknown" +) + +// The values of HaveByNodeModuleValue's judgement. +const ( + HaveByNodeModuleValueJudgementRecreates = "recreates" + HaveByNodeModuleValueJudgementMovesData = "moves-data" + HaveByNodeModuleValueJudgementCannotJudge = "cannot-judge" +) + +// The values of HaveByNodeModuleValue's phase. +const ( + HaveByNodeModuleValuePhaseBackingUp = "backing-up" + HaveByNodeModuleValuePhaseApplying = "applying" + HaveByNodeModuleValuePhaseChecking = "checking" + HaveByNodeModuleValuePhaseRunning = "running" + HaveByNodeModuleValuePhaseFailed = "failed" + HaveByNodeModuleValuePhaseRemoved = "removed" + HaveByNodeModuleValuePhaseHeld = "held" + HaveByNodeModuleValuePhaseWaitingYes = "waiting-yes" +) + // HaveByNodeNodeValue is one value of the state have under the key .node. type HaveByNodeNodeValue struct { - Busy map[string]any `json:"busy,omitempty"` - Candidates map[string]map[string]any `json:"candidates,omitempty"` - Composed string `json:"composed,omitempty"` - // own, or the controller's - Composes string `json:"composes,omitempty"` - ComposesWhy string `json:"composes-why,omitempty"` - Filters map[string]any `json:"filters,omitempty"` - Profile map[string]any `json:"profile,omitempty"` - // the node's record: its tunnel and certificate - Record map[string]any `json:"record,omitempty"` - Sealed []string `json:"sealed,omitempty"` - SealedAt map[string]any `json:"sealed-at,omitempty"` - Strays map[string]any `json:"strays,omitempty"` - V int64 `json:"v"` - Windows map[string]any `json:"windows,omitempty"` + Busy map[string]any `json:"busy,omitempty"` + // the node's answer to each placement candidate naming it, by proposal id + Candidates map[string]CandidateAnswer `json:"candidates,omitempty"` + // the digest of the node's own composition it runs + Composed string `json:"composed,omitempty"` + // own, or none (empty) + Composes string `json:"composes,omitempty"` + ComposesWhy string `json:"composes-why,omitempty"` + Filters map[string]any `json:"filters,omitempty"` + Profile map[string]any `json:"profile,omitempty"` + Record *NodeRecord `json:"record,omitempty"` + Sealed []string `json:"sealed,omitempty"` + SealedAt map[string]string `json:"sealed-at,omitempty"` + Strays map[string]any `json:"strays,omitempty"` + V int64 `json:"v"` + Windows map[string]any `json:"windows,omitempty"` } // ComposedByNodeModuleFingerprintValue is one value of the state composed under the key ... type ComposedByNodeModuleFingerprintValue struct { - Data bool `json:"Data,omitempty"` - FP string `json:"FP"` - Filled []string `json:"Filled,omitempty"` - Module string `json:"Module"` - Node string `json:"Node"` - // the node's own adoption, identities, groups, libraries and provisioner account - Own map[string]any `json:"Own,omitempty"` - // the module's plan - Plan map[string]any `json:"Plan,omitempty"` - // why the composition was refused - Refused string `json:"Refused,omitempty"` - Resources []map[string]any `json:"Resources,omitempty"` - V int64 `json:"V,omitempty"` + // the module declares data + Data bool `json:"data,omitempty"` + // the files the provisioner fills + FilledFiles []string `json:"filled_files,omitempty"` + Fp string `json:"fp"` + Module string `json:"module"` + Node string `json:"node"` + // the module's whole provisioning plan, catalogue.Plan, kept whole: it is the manifest layer's type + Plan map[string]any `json:"plan,omitempty"` + // why the module cannot be composed at this fingerprint + Refused string `json:"refused,omitempty"` + // the declaration resources, each saying its module + Resources []map[string]any `json:"resources"` + V int64 `json:"v"` +} + +// ComposedByNodeNodeValue is one value of the state composed under the key .node. +type ComposedByNodeNodeValue struct { + // the module declares data + Data bool `json:"data,omitempty"` + // the files the provisioner fills + FilledFiles []string `json:"filled_files,omitempty"` + Node string `json:"node"` + Own *NodeOwn `json:"own,omitempty"` + // the declaration resources, each saying its module + Resources []map[string]any `json:"resources"` + V int64 `json:"v"` } // DataByNodeModuleValue is one value of the state data under the key .. type DataByNodeModuleValue struct { Digest string `json:"digest"` - Items []map[string]any `json:"items"` + Items []PlacedDataItem `json:"items"` Module string `json:"module"` Node string `json:"node"` - V int64 `json:"v,omitempty"` + V int64 `json:"v"` } // PackagesByNodeValuePackagesItem is a type of the contract. @@ -991,17 +1360,18 @@ type PackagesByNodeValue struct { // HealthByNodeValue is one value of the state health under the key . type HealthByNodeValue struct { - At string `json:"at"` - Contract int64 `json:"contract,omitempty"` - Fallback map[string]any `json:"fallback,omitempty"` - Found []map[string]any `json:"found,omitempty"` - Network map[string]any `json:"network,omitempty"` - Node string `json:"node"` - Peers []map[string]any `json:"peers,omitempty"` - Relogin []map[string]any `json:"relogin,omitempty"` - Root []map[string]any `json:"root,omitempty"` - Units map[string]any `json:"units,omitempty"` - V int64 `json:"v"` + // when + At string `json:"at"` + Contract int64 `json:"contract,omitempty"` + Fallback *LoginFallback `json:"fallback,omitempty"` + Found []NodeDirectory `json:"found,omitempty"` + Network *NetworkHealth `json:"network,omitempty"` + Node string `json:"node"` + Peers []PeerSeen `json:"peers,omitempty"` + Relogin []NodeAccount `json:"relogin,omitempty"` + Root []RootAccountVerdict `json:"root,omitempty"` + Units *UnitsHealth `json:"units,omitempty"` + V int64 `json:"v"` } // FindingsByKeyValue is one value of the state findings under the key . diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index aa467b2..5ca811b 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -39,6 +39,44 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a node's packages, present and absent (captured from ace, shortened)", subject: "", given: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", wire: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", decode: func(b []byte) (any, error) { var v PackagesByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "buildah composed on ace (resources captured from the declaration of 2026-10-24; the fingerprint stands in for the indexer's)", subject: "", given: "{\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"module\":\"buildah\",\"node\":\"ace\",\"resources\":[{\"id\":\"buildah.buildah\",\"module\":\"buildah\",\"package\":\"buildah\",\"type\":\"package\"}],\"v\":1}", wire: "{\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"module\":\"buildah\",\"node\":\"ace\",\"resources\":[{\"id\":\"buildah.buildah\",\"module\":\"buildah\",\"package\":\"buildah\",\"type\":\"package\"}],\"v\":1}", + decode: func(b []byte) (any, error) { + var v ComposedByNodeModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "node-tools composed with a filled file (captured; the fingerprint stands in)", subject: "", given: "{\"fp\":\"src2:6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f\",\"module\":\"node-tools\",\"node\":\"ace\",\"resources\":[{\"id\":\"node-tools.mesh-state\",\"mode\":\"0755\",\"module\":\"node-tools\",\"path\":\"/var/lib/mesh/node-tools\",\"type\":\"directory\"},{\"id\":\"node-tools.interpreter\",\"module\":\"node-tools\",\"package\":\"nodejs\",\"type\":\"package\"},{\"digest\":\"sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a\",\"env\":{\"MESH_AGENT_ACCOUNT\":\"ace\",\"MESH_AGENT_HOME\":\"/home/ace\",\"MESH_BROKER_FILE\":\"/var/lib/mesh/node-tools/broker\",\"MESH_OPERATOR_ACCOUNT\":\"ace\",\"MESH_OPERATOR_HOME\":\"/home/ace\",\"MESH_TOOL_ENV\":\"{\\\"baserow\\\":{\\\"MESH_BASEROW_CONFIG_FILE\\\":\\\"/var/lib/mesh/baserow/config.json\\\",\\\"MESH_BASEROW_URL\\\":\\\"http://127.0.0.1:20011\\\"},\\\"bazarr\\\":{\\\"MESH_BAZARR_CONFIG_DIR\\\":\\\"/var/lib/bazarr/config\\\",\\\"MESH_BAZARR_CONFIG_FILE\\\":\\\"/var/lib/bazarr/config.json\\\",\\\"MESH_BAZARR_CONTAINER\\\":\\\"bazarr\\\",\\\"MESH_BAZARR_URL\\\":\\\"http://127.0.0.1:20020\\\"},\\\"bookshelf\\\":{\\\"MESH_BOOKSHELF_CONFIG_DIR\\\":\\\"/var/lib/bookshelf/config\\\",\\\"MESH_BOOKSHELF_URL\\\":\\\"http://127.0.0.1:20021\\\"},\\\"claude-code\\\":{\\\"MESH_CLAUDE_CODE_FACTS\\\":\\\"/var/lib/claude-code/facts.json\\\",\\\"MESH_CLAUDE_CODE_SETTINGS\\\":\\\"/var/lib/claude-code/settings.json\\\",\\\"MESH_CLAUDE_CODE_STATE\\\":\\\"/var/lib/claude-code\\\"},\\\"grafana\\\":{\\\"MESH_GRAFANA_CONFIG_FILE\\\":\\\"/var/lib/mesh/grafana/config.json\\\",\\\"MESH_GRAFANA_URL\\\":\\\"http://127.0.0.1:20012\\\"},\\\"home-assistant\\\":{\\\"MESH_HOMEASSISTANT_CONFIG_FILE\\\":\\\"/var/lib/mesh/home-assistant/config.json\\\",\\\"MESH_HOMEASSISTANT_TOKEN_FILE\\\":\\\"/var/lib/mesh/home-assistant/token\\\",\\\"MESH_HOMEASSISTANT_URL\\\":\\\"http://127.0.0.1:8123\\\"},\\\"icecast\\\":{\\\"MESH_ICECAST_ADMIN_PASSWORD_FILE\\\":\\\"/var/lib/icecast/admin.secret\\\",\\\"MESH_ICECAST_CONFIG_FILE\\\":\\\"/var/lib/mesh/icecast/config.json\\\",\\\"MESH_ICECAST_URL\\\":\\\"http://127.0.0.1:20005\\\"},\\\"influxdb\\\":{\\\"MESH_INFLUXDB_CONFIG_FILE\\\":\\\"/var/lib/mesh/influxdb/config.json\\\",\\\"MESH_INFLUXDB_TOKEN_FILE\\\":\\\"/var/lib/influxdb/admin-token.secret\\\",\\\"MESH_INFLUXDB_URL\\\":\\\"http://127.0.0.1:8086\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/influxdb/grants/mesh.json\\\"},\\\"jackett\\\":{\\\"MESH_JACKETT_API_KEY_FILE\\\":\\\"/var/lib/mesh/jackett/api-key\\\",\\\"MESH_JACKETT_CONFIG_DIR\\\":\\\"/var/lib/jackett/config\\\",\\\"MESH_JACKETT_CONFIG_FILE\\\":\\\"/var/lib/jackett/config.json\\\",\\\"MESH_JACKETT_URL\\\":\\\"http://127.0.0.1:20004\\\"},\\\"kometa\\\":{\\\"MESH_KOMETA_CONFIG_DIR\\\":\\\"/var/lib/kometa/config\\\",\\\"MESH_KOMETA_CONFIG_FILE\\\":\\\"/var/lib/kometa/config.yml\\\"},\\\"letta\\\":{\\\"MESH_LETTA_CONFIG_FILE\\\":\\\"/var/lib/mesh/letta/config.json\\\",\\\"MESH_LETTA_URL\\\":\\\"http://127.0.0.1:20002\\\"},\\\"lidarr\\\":{\\\"MESH_LIDARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/lidarr/api-key\\\",\\\"MESH_LIDARR_CONFIG_DIR\\\":\\\"/var/lib/lidarr/config\\\",\\\"MESH_LIDARR_RECYCLE_BIN\\\":\\\"/recycle/\\\",\\\"MESH_LIDARR_RECYCLE_DAYS\\\":\\\"7\\\",\\\"MESH_LIDARR_URL\\\":\\\"http://127.0.0.1:20022\\\",\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/lidarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/lidarr/plex-api.secret\\\"},\\\"media-importer\\\":{\\\"MESH_MEDIA_IMPORTER_SETTINGS\\\":\\\"/var/lib/media-importer/settings.json\\\",\\\"MESH_MEDIA_IMPORTER_STATE\\\":\\\"/var/lib/media-importer\\\",\\\"MESH_MEDIA_LIBRARY\\\":\\\"/storage/media/poc\\\"},\\\"mesh-files\\\":{\\\"MESH_FILES_STATE\\\":\\\"/var/lib/mesh-files\\\",\\\"MESH_OWNED_PATHS\\\":\\\"{\\\\\\\"/etc/NetworkManager/conf.d/50-mesh.conf\\\\\\\":\\\\\\\"networkmanager (its own tools)\\\\\\\",\\\\\\\"/etc/claude-code\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/etc/claude-code/agent-account\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/etc/dnsmasq.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/docker/daemon.json\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/action.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/action.d/iptables-allports-dualchain.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/filter.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d/recidive.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d/sshd.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.local\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/mesh-own-addresses.json\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/hostname\\\\\\\":\\\\\\\"hostname (node-hostname)\\\\\\\",\\\\\\\"/etc/hosts\\\\\\\":\\\\\\\"hostname (node-hostname)\\\\\\\",\\\\\\\"/etc/locale.conf\\\\\\\":\\\\\\\"localization (its own tools)\\\\\\\",\\\\\\\"/etc/logrotate.conf\\\\\\\":\\\\\\\"logrotate (its own tools)\\\\\\\",\\\\\\\"/etc/logrotate.d/fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/after-boot\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/after-wake\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/before-shutdown\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/before-sleep\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/on-battery\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/on-mains\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-resolver/nodes.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-resolver/zones.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server/machines\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server/shares.conf\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs.conf.d/50-mesh.conf\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/pacman.conf\\\\\\\":\\\\\\\"pacman (its own tools)\\\\\\\",\\\\\\\"/etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/resolv.conf\\\\\\\":\\\\\\\"networkmanager (node-uplink)\\\\\\\",\\\\\\\"/etc/ssh/sshd_config.d/10-mesh.conf\\\\\\\":\\\\\\\"sshd (its own tools)\\\\\\\",\\\\\\\"/etc/sudoers.d/10-mesh-operator\\\\\\\":\\\\\\\"sudo (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/logind.conf.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/logind.conf.d/power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/dnsmasq.service.d\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/dnsmasq.service.d/mesh.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/docker-prune.service\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/docker-prune.timer\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hibernate.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hibernate.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hybrid-sleep.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hybrid-sleep.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-ca-trust.service\\\\\\\":\\\\\\\"ca-trust (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-filter.service\\\\\\\":\\\\\\\"nftables (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-kernel-boot-entries.service\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-kernel-huge-pages.service\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-after-boot.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-after-wake.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-before-shutdown.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-before-sleep.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-supply.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/nftables.service.d/mesh.conf\\\\\\\":\\\\\\\"nftables (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/sleep.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/sleep.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend-then-hibernate.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/timesyncd.conf.d/50-mesh.conf\\\\\\\":\\\\\\\"time-sync (its own tools)\\\\\\\",\\\\\\\"/etc/udev/rules.d/90-mesh-power.rules\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/vconsole.conf\\\\\\\":\\\\\\\"localization (its own tools)\\\\\\\",\\\\\\\"/etc/xdg/reflector/reflector.conf\\\\\\\":\\\\\\\"pacman (its own tools)\\\\\\\",\\\\\\\"/run/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/run/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/usr/lib/nox-mesh-host/launch\\\\\\\":\\\\\\\"mesh-host (its own tools)\\\\\\\",\\\\\\\"/usr/lib/nox-mesh-host/versions/4e121b6e88f1\\\\\\\":\\\\\\\"mesh-host (its own tools)\\\\\\\",\\\\\\\"/usr/local/bin\\\\\\\":\\\\\\\"mesh-cli (its own tools)\\\\\\\",\\\\\\\"/usr/local/bin/claude-agent\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/usr/local/lib/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/var/lib/dnsmasq-watch\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-fail2ban/own-addresses\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-store\\\\\\\":\\\\\\\"postgres (its own tools)\\\\\\\",\\\\\\\"/var/lib/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/var/lib/nfs-server/from-bus\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/var/lib/postgres\\\\\\\":\\\\\\\"postgres (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/acme\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/ca\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/routes\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/systemd\\\\\\\":\\\\\\\"systemd (its own tools)\\\\\\\",\\\\\\\"/var/log/fail2ban.log\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/run/fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\"}\\\",\\\"MESH_SECRET_PLACES\\\":\\\"[\\\\\\\"/var/lib/baserow\\\\\\\",\\\\\\\"/var/lib/bazarr/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/bazarr/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/car-hunter\\\\\\\",\\\\\\\"/var/lib/claude-code/gitea-read.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/lidarr-api.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/mqtt-topic.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/icecast/admin.secret\\\\\\\",\\\\\\\"/var/lib/icecast/relay.secret\\\\\\\",\\\\\\\"/var/lib/icecast/source.secret\\\\\\\",\\\\\\\"/var/lib/influxdb\\\\\\\",\\\\\\\"/var/lib/kometa\\\\\\\",\\\\\\\"/var/lib/letta\\\\\\\",\\\\\\\"/var/lib/lidarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/mesh-delivery-assigner\\\\\\\",\\\\\\\"/var/lib/mesh-provisioner-client\\\\\\\",\\\\\\\"/var/lib/mesh-reads\\\\\\\",\\\\\\\"/var/lib/mesh/dnsmasq\\\\\\\",\\\\\\\"/var/lib/mesh/grafana\\\\\\\",\\\\\\\"/var/lib/mesh/home-assistant\\\\\\\",\\\\\\\"/var/lib/mesh/jackett\\\\\\\",\\\\\\\"/var/lib/mesh/lidarr\\\\\\\",\\\\\\\"/var/lib/mesh/mosquitto\\\\\\\",\\\\\\\"/var/lib/mesh/node-tools\\\\\\\",\\\\\\\"/var/lib/mesh/nodered\\\\\\\",\\\\\\\"/var/lib/mesh/nzbget\\\\\\\",\\\\\\\"/var/lib/mesh/ombi\\\\\\\",\\\\\\\"/var/lib/mesh/pacman\\\\\\\",\\\\\\\"/var/lib/mesh/postgres\\\\\\\",\\\\\\\"/var/lib/mesh/qbittorrent\\\\\\\",\\\\\\\"/var/lib/mesh/radarr\\\\\\\",\\\\\\\"/var/lib/mesh/route-proxy\\\\\\\",\\\\\\\"/var/lib/mesh/searxng\\\\\\\",\\\\\\\"/var/lib/mesh/sonarr\\\\\\\",\\\\\\\"/var/lib/mesh/unifi\\\\\\\",\\\\\\\"/var/lib/mosquitto/grants\\\\\\\",\\\\\\\"/var/lib/mssql\\\\\\\",\\\\\\\"/var/lib/n8n/database.secret\\\\\\\",\\\\\\\"/var/lib/nodered/mqtt-topic.secret\\\\\\\",\\\\\\\"/var/lib/nzbgeek\\\\\\\",\\\\\\\"/var/lib/ombi/lidarr-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/redis/default.secret\\\\\\\",\\\\\\\"/var/lib/redis/grants\\\\\\\",\\\\\\\"/var/lib/restic\\\\\\\",\\\\\\\"/var/lib/sonarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/supabase\\\\\\\",\\\\\\\"/var/lib/tautulli/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/txt-game/database.secret\\\\\\\"]\\\"},\\\"mosquitto\\\":{\\\"MESH_MQTT_CTRL_CONTAINER\\\":\\\"mosquitto\\\",\\\"MESH_PROVISION_ADMIN_USER\\\":\\\"mesh-admin\\\",\\\"MESH_PROVISION_MQTT\\\":\\\"127.0.0.1:1883\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mesh/mosquitto/admin\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mosquitto/grants/mesh.json\\\"},\\\"mssql\\\":{\\\"MESH_MSSQL_READER_PASSWORD_FILE\\\":\\\"/var/lib/mssql/reader.secret\\\",\\\"MESH_PROVISION_MSSQL\\\":\\\"mssql://sa@127.0.0.1:4848/master\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mssql/sa.secret\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mssql/grants/mesh.json\\\"},\\\"nodered\\\":{\\\"MESH_NODERED_CONFIG_FILE\\\":\\\"/var/lib/mesh/nodered/config.json\\\",\\\"MESH_NODERED_URL\\\":\\\"http://127.0.0.1:20006\\\"},\\\"nzbgeek\\\":{\\\"MESH_NZBGEEK_KEY_FILE\\\":\\\"/var/lib/nzbgeek/api-key.secret\\\",\\\"MESH_NZBGEEK_SETTINGS\\\":\\\"/var/lib/nzbgeek/settings.json\\\"},\\\"nzbget\\\":{\\\"MESH_NZBGET_CONFIG_DIR\\\":\\\"/var/lib/nzbget/config\\\",\\\"MESH_NZBGET_CONFIG_FILE\\\":\\\"/var/lib/nzbget/config.json\\\",\\\"MESH_NZBGET_PASSWORD_FILE\\\":\\\"/var/lib/mesh/nzbget/password\\\",\\\"MESH_NZBGET_SECRETS_DIR\\\":\\\"/var/lib/mesh/nzbget\\\",\\\"MESH_NZBGET_STATE\\\":\\\"/var/lib/nzbget\\\",\\\"MESH_NZBGET_URL\\\":\\\"http://127.0.0.1:20017\\\"},\\\"ombi\\\":{\\\"MESH_OMBI_API_KEY_FILE\\\":\\\"/var/lib/mesh/ombi/api-key\\\",\\\"MESH_OMBI_CONFIG_FILE\\\":\\\"/var/lib/mesh/ombi/config.json\\\",\\\"MESH_OMBI_URL\\\":\\\"http://127.0.0.1:20024\\\"},\\\"pacman\\\":{\\\"PACMAN_KEEPER_CONFIG\\\":\\\"/var/lib/mesh/pacman/keeper.json\\\"},\\\"plex\\\":{\\\"MESH_PLEX_CONFIG_DIR\\\":\\\"/mnt/plex/config\\\",\\\"MESH_PLEX_URL\\\":\\\"http://127.0.0.1:32400\\\"},\\\"postgres\\\":{\\\"MESH_CONSUMERS_FILE\\\":\\\"/var/lib/mesh/postgres/consumers.json\\\",\\\"MESH_POSTGRES_READER_PASSWORD_FILE\\\":\\\"/var/lib/mesh/postgres/reader.secret\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mesh/postgres/superuser.secret\\\",\\\"MESH_PROVISION_POSTGRES\\\":\\\"postgres://postgres@127.0.0.1:5434/postgres?sslmode=disable\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mesh/postgres/grants/mesh.json\\\"},\\\"qbittorrent\\\":{\\\"MESH_QBITTORRENT_CONFIG_DIR\\\":\\\"/var/lib/qbittorrent/config\\\",\\\"MESH_QBITTORRENT_CONFIG_FILE\\\":\\\"/var/lib/qbittorrent/config.json\\\",\\\"MESH_QBITTORRENT_PASSWORD_FILE\\\":\\\"/var/lib/mesh/qbittorrent/password\\\",\\\"MESH_QBITTORRENT_URL\\\":\\\"http://127.0.0.1:8112\\\"},\\\"radarr\\\":{\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/radarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/radarr/plex-api.secret\\\",\\\"MESH_RADARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/radarr/api-key\\\",\\\"MESH_RADARR_CONFIG_DIR\\\":\\\"/var/lib/radarr/config\\\",\\\"MESH_RADARR_URL\\\":\\\"http://127.0.0.1:20019\\\"},\\\"redis\\\":{\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/redis/default.secret\\\",\\\"MESH_PROVISION_REDIS\\\":\\\"127.0.0.1:20003\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/redis/grants/mesh.json\\\"},\\\"restic\\\":{\\\"MESH_BACKUP_DATA\\\":\\\"/var/lib/restic/data.conf\\\",\\\"MESH_BACKUP_DECLARED\\\":\\\"/var/lib/restic/backups.conf\\\",\\\"MESH_BACKUP_PASSWORD_FILE\\\":\\\"/var/lib/restic/repository.secret\\\",\\\"MESH_BACKUP_REPOSITORY\\\":\\\"/storage/restic\\\",\\\"MESH_BACKUP_STATE\\\":\\\"/var/lib/restic\\\"},\\\"searxng\\\":{\\\"MESH_SEARXNG_CONFIG_FILE\\\":\\\"/var/lib/mesh/searxng/config.json\\\",\\\"MESH_SEARXNG_URL\\\":\\\"http://127.0.0.1:20000\\\"},\\\"sonarr\\\":{\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/sonarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/sonarr/plex-api.secret\\\",\\\"MESH_SONARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/sonarr/api-key\\\",\\\"MESH_SONARR_CONFIG_DIR\\\":\\\"/var/lib/sonarr/config\\\",\\\"MESH_SONARR_RECYCLE_DIR\\\":\\\"/var/lib/sonarr/recycle-bin\\\",\\\"MESH_SONARR_URL\\\":\\\"http://127.0.0.1:20018\\\"},\\\"systemd\\\":{\\\"MESH_SYSTEMD_STATE\\\":\\\"/var/lib/mesh/systemd\\\"},\\\"tautulli\\\":{\\\"MESH_TAUTULLI_CONFIG_DIR\\\":\\\"/var/lib/tautulli/config\\\",\\\"MESH_TAUTULLI_CONFIG_FILE\\\":\\\"/var/lib/mesh/tautulli/config.json\\\",\\\"MESH_TAUTULLI_CONTAINER\\\":\\\"tautulli\\\",\\\"MESH_TAUTULLI_URL\\\":\\\"http://127.0.0.1:20023\\\"},\\\"unifi\\\":{\\\"MESH_UNIFI_CONFIG_FILE\\\":\\\"/var/lib/mesh/unifi/config.json\\\",\\\"MESH_UNIFI_URL\\\":\\\"https://127.0.0.1:8443\\\"},\\\"youtube\\\":{\\\"MESH_YOUTUBE_DOWNLOADS\\\":\\\"/storage/downloads/youtube\\\",\\\"MESH_YOUTUBE_SETTINGS\\\":\\\"/var/lib/youtube/settings.json\\\"},\\\"zsh\\\":{\\\"ZSH_TOOLS_CONF\\\":\\\"/var/lib/zsh/tools.conf\\\"}}\",\"MESH_TOOL_MODULES\":\"avahi=/var/lib/mesh/bundles/avahi/tools/avahi-tools,baserow=/var/lib/mesh/bundles/baserow/tools/tools/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/tools/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/tools/index.serve.mjs,claude-code=/var/lib/mesh/bundles/claude-code/tools/claude-code,dbus=/var/lib/mesh/bundles/dbus/tools-go/dbus-tools,disk-load=/var/lib/mesh/bundles/disk-load/tools-go/disk-load,dnsmasq=/var/lib/mesh/bundles/dnsmasq/tools/dnsmasq-tools,docker=/var/lib/mesh/bundles/docker/tools/docker-tools,fail2ban=/var/lib/mesh/bundles/fail2ban/tools/fail2ban-tools,grafana=/var/lib/mesh/bundles/grafana/code/index.serve.mjs,grafana=/var/lib/mesh/bundles/grafana/code/tools/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/tools/index.serve.mjs,hostname=/var/lib/mesh/bundles/hostname/tools/hostname-tools,icecast=/var/lib/mesh/bundles/icecast/code/index.serve.mjs,icecast=/var/lib/mesh/bundles/icecast/code/tools/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/provisioner/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/tools/index.serve.mjs,jackett=/var/lib/mesh/bundles/jackett/code/tools/index.serve.mjs,kernel=/var/lib/mesh/bundles/kernel/tools/kernel-tools,kometa=/var/lib/mesh/bundles/kometa/code/tools/index.serve.mjs,letta=/var/lib/mesh/bundles/letta/tools/tools/index.serve.mjs,lidarr=/var/lib/mesh/bundles/lidarr/tools-go/lidarr,localization=/var/lib/mesh/bundles/localization/tools/localization-tools,logrotate=/var/lib/mesh/bundles/logrotate/tools/logrotate-tools,media-importer=/var/lib/mesh/bundles/media-importer/tools/media-importer,mesh-files=/var/lib/mesh/bundles/mesh-files/tools/mesh-files,mosquitto=/var/lib/mesh/bundles/mosquitto/code/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/provisioner/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/tools/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/provisioner/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/tools/index.serve.mjs,netcheck=/var/lib/mesh/bundles/netcheck/tools-go/netcheck,netcheck=/var/lib/mesh/bundles/netcheck/tools-typescript/tools/index.serve.mjs,networkmanager=/var/lib/mesh/bundles/networkmanager/tools/uplink-tools,nfs-server=/var/lib/mesh/bundles/nfs-server/tools/nfs-server,nftables=/var/lib/mesh/bundles/nftables/tools/tools/index.serve.mjs,nodered=/var/lib/mesh/bundles/nodered/code/tools/index.serve.mjs,nzbgeek=/var/lib/mesh/bundles/nzbgeek/tools/nzbgeek,nzbget=/var/lib/mesh/bundles/nzbget/code/nzbget,ombi=/var/lib/mesh/bundles/ombi/code/index.serve.mjs,ombi=/var/lib/mesh/bundles/ombi/code/tools/index.serve.mjs,pacman=/var/lib/mesh/bundles/pacman/tools/pacman-tools,plex=/var/lib/mesh/bundles/plex/code/plex,postgres=/var/lib/mesh/bundles/postgres/code/postgres-provider,power=/var/lib/mesh/bundles/power/tools-go/power,qbittorrent=/var/lib/mesh/bundles/qbittorrent/code/qbittorrent,radarr=/var/lib/mesh/bundles/radarr/tools-go/radarr,redis=/var/lib/mesh/bundles/redis/code/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/provisioner/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/tools/index.serve.mjs,restic=/var/lib/mesh/bundles/restic/tools/restic-backups,route-proxy=/var/lib/mesh/bundles/route-proxy/provider/route-provider,searxng=/var/lib/mesh/bundles/searxng/tools/tools/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/tools/index.serve.mjs,ssh-client=/var/lib/mesh/bundles/ssh-client/tools/ssh-client-tools,sudo=/var/lib/mesh/bundles/sudo/tools/sudo-tools,systemd=/var/lib/mesh/bundles/systemd/tools/systemd-tools,tautulli=/var/lib/mesh/bundles/tautulli/code/index.serve.mjs,tautulli=/var/lib/mesh/bundles/tautulli/code/tools/index.serve.mjs,time-sync=/var/lib/mesh/bundles/time-sync/tools/time-sync-tools,unifi=/var/lib/mesh/bundles/unifi/tools/tools/index.serve.mjs,youtube=/var/lib/mesh/bundles/youtube/tools/youtube,zsh=/var/lib/mesh/bundles/zsh/tools/tools/index.serve.mjs\"},\"id\":\"node-tools.runtime\",\"module\":\"node-tools\",\"name\":\"node-tools\",\"restart-on\":[\"avahi.bundle-tools\",\"baserow.bundle-tools\",\"bazarr.bundle-code\",\"bookshelf.bundle-code\",\"claude-code.bundle-tools\",\"dbus.bundle-tools-go\",\"disk-load.bundle-tools-go\",\"dnsmasq.bundle-tools\",\"docker.bundle-tools\",\"fail2ban.bundle-tools\",\"grafana.bundle-code\",\"home-assistant.bundle-code\",\"hostname.bundle-tools\",\"icecast.bundle-code\",\"influxdb.bundle-code\",\"jackett.bundle-code\",\"kernel.bundle-tools\",\"kometa.bundle-code\",\"letta.bundle-tools\",\"lidarr.bundle-tools-go\",\"localization.bundle-tools\",\"logrotate.bundle-tools\",\"media-importer.bundle-tools\",\"mesh-files.bundle-tools\",\"mosquitto.bundle-code\",\"mssql.bundle-code\",\"netcheck.bundle-tools-go\",\"netcheck.bundle-tools-typescript\",\"networkmanager.bundle-tools\",\"nfs-server.bundle-tools\",\"nftables.bundle-tools\",\"node-tools.needs-broker\",\"nodered.bundle-code\",\"nzbgeek.bundle-tools\",\"nzbget.bundle-code\",\"ombi.bundle-code\",\"pacman.bundle-tools\",\"plex.bundle-code\",\"postgres.bundle-code\",\"power.bundle-tools-go\",\"qbittorrent.bundle-code\",\"radarr.bundle-tools-go\",\"redis.bundle-code\",\"restic.bundle-tools\",\"route-proxy.bundle-provider\",\"searxng.bundle-tools\",\"sonarr.bundle-code\",\"ssh-client.bundle-tools\",\"sudo.bundle-tools\",\"systemd.bundle-tools\",\"tautulli.bundle-code\",\"time-sync.bundle-tools\",\"unifi.bundle-tools\",\"youtube.bundle-tools\",\"zsh.bundle-tools\",\"baserow.runtime-config\",\"bazarr.runtime-config\",\"claude-code.facts\",\"claude-code.settings\",\"grafana.runtime-config\",\"home-assistant.needs-token\",\"home-assistant.runtime-config\",\"icecast.runtime-config\",\"icecast.secret-admin\",\"influxdb.needs-admin-token\",\"influxdb.received-influxdb-api\",\"influxdb.runtime-config\",\"jackett.needs-api-key\",\"jackett.runtime-config\",\"kometa.settings\",\"letta.runtime-config\",\"lidarr.bound-plex-api\",\"lidarr.needs-api-key\",\"lidarr.secret-plex-api\",\"media-importer.settings\",\"mosquitto.needs-admin\",\"mosquitto.received-mqtt-topic\",\"mssql.needs-reader\",\"mssql.needs-sa\",\"mssql.received-mssql-database\",\"nodered.runtime-config\",\"nzbgeek.needs-api-key\",\"nzbgeek.settings\",\"nzbget.needs-password\",\"nzbget.runtime-config\",\"ombi.needs-api-key\",\"ombi.runtime-config\",\"pacman.keeper-config\",\"postgres.consumers\",\"postgres.needs-reader\",\"postgres.needs-superuser\",\"postgres.received-postgres-database\",\"qbittorrent.needs-password\",\"qbittorrent.runtime-config\",\"radarr.bound-plex-api\",\"radarr.needs-api-key\",\"radarr.secret-plex-api\",\"redis.received-redis-cache\",\"redis.secret-secret\",\"restic.data-declared\",\"restic.declared\",\"restic.needs-repository\",\"searxng.runtime-config\",\"sonarr.bound-plex-api\",\"sonarr.needs-api-key\",\"sonarr.secret-plex-api\",\"tautulli.runtime-config\",\"unifi.runtime-config\",\"youtube.settings\",\"zsh.tools-conf\"],\"run\":[\"./node-tools\"],\"source\":\"http://novox.internal:5100/v2/node-tools/runtime/blobs/sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a\",\"type\":\"process\",\"user\":\"ace\"}],\"v\":1}", wire: "{\"fp\":\"src2:6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f\",\"module\":\"node-tools\",\"node\":\"ace\",\"resources\":[{\"id\":\"node-tools.mesh-state\",\"mode\":\"0755\",\"module\":\"node-tools\",\"path\":\"/var/lib/mesh/node-tools\",\"type\":\"directory\"},{\"id\":\"node-tools.interpreter\",\"module\":\"node-tools\",\"package\":\"nodejs\",\"type\":\"package\"},{\"digest\":\"sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a\",\"env\":{\"MESH_AGENT_ACCOUNT\":\"ace\",\"MESH_AGENT_HOME\":\"/home/ace\",\"MESH_BROKER_FILE\":\"/var/lib/mesh/node-tools/broker\",\"MESH_OPERATOR_ACCOUNT\":\"ace\",\"MESH_OPERATOR_HOME\":\"/home/ace\",\"MESH_TOOL_ENV\":\"{\\\"baserow\\\":{\\\"MESH_BASEROW_CONFIG_FILE\\\":\\\"/var/lib/mesh/baserow/config.json\\\",\\\"MESH_BASEROW_URL\\\":\\\"http://127.0.0.1:20011\\\"},\\\"bazarr\\\":{\\\"MESH_BAZARR_CONFIG_DIR\\\":\\\"/var/lib/bazarr/config\\\",\\\"MESH_BAZARR_CONFIG_FILE\\\":\\\"/var/lib/bazarr/config.json\\\",\\\"MESH_BAZARR_CONTAINER\\\":\\\"bazarr\\\",\\\"MESH_BAZARR_URL\\\":\\\"http://127.0.0.1:20020\\\"},\\\"bookshelf\\\":{\\\"MESH_BOOKSHELF_CONFIG_DIR\\\":\\\"/var/lib/bookshelf/config\\\",\\\"MESH_BOOKSHELF_URL\\\":\\\"http://127.0.0.1:20021\\\"},\\\"claude-code\\\":{\\\"MESH_CLAUDE_CODE_FACTS\\\":\\\"/var/lib/claude-code/facts.json\\\",\\\"MESH_CLAUDE_CODE_SETTINGS\\\":\\\"/var/lib/claude-code/settings.json\\\",\\\"MESH_CLAUDE_CODE_STATE\\\":\\\"/var/lib/claude-code\\\"},\\\"grafana\\\":{\\\"MESH_GRAFANA_CONFIG_FILE\\\":\\\"/var/lib/mesh/grafana/config.json\\\",\\\"MESH_GRAFANA_URL\\\":\\\"http://127.0.0.1:20012\\\"},\\\"home-assistant\\\":{\\\"MESH_HOMEASSISTANT_CONFIG_FILE\\\":\\\"/var/lib/mesh/home-assistant/config.json\\\",\\\"MESH_HOMEASSISTANT_TOKEN_FILE\\\":\\\"/var/lib/mesh/home-assistant/token\\\",\\\"MESH_HOMEASSISTANT_URL\\\":\\\"http://127.0.0.1:8123\\\"},\\\"icecast\\\":{\\\"MESH_ICECAST_ADMIN_PASSWORD_FILE\\\":\\\"/var/lib/icecast/admin.secret\\\",\\\"MESH_ICECAST_CONFIG_FILE\\\":\\\"/var/lib/mesh/icecast/config.json\\\",\\\"MESH_ICECAST_URL\\\":\\\"http://127.0.0.1:20005\\\"},\\\"influxdb\\\":{\\\"MESH_INFLUXDB_CONFIG_FILE\\\":\\\"/var/lib/mesh/influxdb/config.json\\\",\\\"MESH_INFLUXDB_TOKEN_FILE\\\":\\\"/var/lib/influxdb/admin-token.secret\\\",\\\"MESH_INFLUXDB_URL\\\":\\\"http://127.0.0.1:8086\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/influxdb/grants/mesh.json\\\"},\\\"jackett\\\":{\\\"MESH_JACKETT_API_KEY_FILE\\\":\\\"/var/lib/mesh/jackett/api-key\\\",\\\"MESH_JACKETT_CONFIG_DIR\\\":\\\"/var/lib/jackett/config\\\",\\\"MESH_JACKETT_CONFIG_FILE\\\":\\\"/var/lib/jackett/config.json\\\",\\\"MESH_JACKETT_URL\\\":\\\"http://127.0.0.1:20004\\\"},\\\"kometa\\\":{\\\"MESH_KOMETA_CONFIG_DIR\\\":\\\"/var/lib/kometa/config\\\",\\\"MESH_KOMETA_CONFIG_FILE\\\":\\\"/var/lib/kometa/config.yml\\\"},\\\"letta\\\":{\\\"MESH_LETTA_CONFIG_FILE\\\":\\\"/var/lib/mesh/letta/config.json\\\",\\\"MESH_LETTA_URL\\\":\\\"http://127.0.0.1:20002\\\"},\\\"lidarr\\\":{\\\"MESH_LIDARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/lidarr/api-key\\\",\\\"MESH_LIDARR_CONFIG_DIR\\\":\\\"/var/lib/lidarr/config\\\",\\\"MESH_LIDARR_RECYCLE_BIN\\\":\\\"/recycle/\\\",\\\"MESH_LIDARR_RECYCLE_DAYS\\\":\\\"7\\\",\\\"MESH_LIDARR_URL\\\":\\\"http://127.0.0.1:20022\\\",\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/lidarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/lidarr/plex-api.secret\\\"},\\\"media-importer\\\":{\\\"MESH_MEDIA_IMPORTER_SETTINGS\\\":\\\"/var/lib/media-importer/settings.json\\\",\\\"MESH_MEDIA_IMPORTER_STATE\\\":\\\"/var/lib/media-importer\\\",\\\"MESH_MEDIA_LIBRARY\\\":\\\"/storage/media/poc\\\"},\\\"mesh-files\\\":{\\\"MESH_FILES_STATE\\\":\\\"/var/lib/mesh-files\\\",\\\"MESH_OWNED_PATHS\\\":\\\"{\\\\\\\"/etc/NetworkManager/conf.d/50-mesh.conf\\\\\\\":\\\\\\\"networkmanager (its own tools)\\\\\\\",\\\\\\\"/etc/claude-code\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/etc/claude-code/agent-account\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/etc/dnsmasq.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/docker/daemon.json\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/action.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/action.d/iptables-allports-dualchain.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/filter.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d/recidive.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.d/sshd.conf\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/jail.local\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/fail2ban/mesh-own-addresses.json\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/hostname\\\\\\\":\\\\\\\"hostname (node-hostname)\\\\\\\",\\\\\\\"/etc/hosts\\\\\\\":\\\\\\\"hostname (node-hostname)\\\\\\\",\\\\\\\"/etc/locale.conf\\\\\\\":\\\\\\\"localization (its own tools)\\\\\\\",\\\\\\\"/etc/logrotate.conf\\\\\\\":\\\\\\\"logrotate (its own tools)\\\\\\\",\\\\\\\"/etc/logrotate.d/fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/after-boot\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/after-wake\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/before-shutdown\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/before-sleep\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/on-battery\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-power/moments/on-mains\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-resolver/nodes.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/mesh-resolver/zones.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server/machines\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs-server/shares.conf\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/nfs.conf.d/50-mesh.conf\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/etc/pacman.conf\\\\\\\":\\\\\\\"pacman (its own tools)\\\\\\\",\\\\\\\"/etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/resolv.conf\\\\\\\":\\\\\\\"networkmanager (node-uplink)\\\\\\\",\\\\\\\"/etc/ssh/sshd_config.d/10-mesh.conf\\\\\\\":\\\\\\\"sshd (its own tools)\\\\\\\",\\\\\\\"/etc/sudoers.d/10-mesh-operator\\\\\\\":\\\\\\\"sudo (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/logind.conf.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/logind.conf.d/power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/dnsmasq.service.d\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/dnsmasq.service.d/mesh.conf\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/docker-prune.service\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/docker-prune.timer\\\\\\\":\\\\\\\"docker (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hibernate.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hibernate.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hybrid-sleep.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/hybrid-sleep.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-ca-trust.service\\\\\\\":\\\\\\\"ca-trust (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-filter.service\\\\\\\":\\\\\\\"nftables (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-kernel-boot-entries.service\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-kernel-huge-pages.service\\\\\\\":\\\\\\\"kernel (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-after-boot.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-after-wake.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-before-shutdown.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-before-sleep.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/mesh-power-supply.service\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/nftables.service.d/mesh.conf\\\\\\\":\\\\\\\"nftables (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/sleep.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/sleep.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend-then-hibernate.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend.target.d\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/system/suspend.target.d/mesh-power.conf\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/systemd/timesyncd.conf.d/50-mesh.conf\\\\\\\":\\\\\\\"time-sync (its own tools)\\\\\\\",\\\\\\\"/etc/udev/rules.d/90-mesh-power.rules\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/etc/vconsole.conf\\\\\\\":\\\\\\\"localization (its own tools)\\\\\\\",\\\\\\\"/etc/xdg/reflector/reflector.conf\\\\\\\":\\\\\\\"pacman (its own tools)\\\\\\\",\\\\\\\"/run/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/run/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/usr/lib/nox-mesh-host/launch\\\\\\\":\\\\\\\"mesh-host (its own tools)\\\\\\\",\\\\\\\"/usr/lib/nox-mesh-host/versions/4e121b6e88f1\\\\\\\":\\\\\\\"mesh-host (its own tools)\\\\\\\",\\\\\\\"/usr/local/bin\\\\\\\":\\\\\\\"mesh-cli (its own tools)\\\\\\\",\\\\\\\"/usr/local/bin/claude-agent\\\\\\\":\\\\\\\"claude-code (its own tools)\\\\\\\",\\\\\\\"/usr/local/lib/mesh-power\\\\\\\":\\\\\\\"power (its own tools)\\\\\\\",\\\\\\\"/var/lib/dnsmasq-watch\\\\\\\":\\\\\\\"dnsmasq (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-fail2ban/own-addresses\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/lib/mesh-store\\\\\\\":\\\\\\\"postgres (its own tools)\\\\\\\",\\\\\\\"/var/lib/nfs-server\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/var/lib/nfs-server/from-bus\\\\\\\":\\\\\\\"nfs-server (its own tools)\\\\\\\",\\\\\\\"/var/lib/postgres\\\\\\\":\\\\\\\"postgres (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/acme\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/ca\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/route-proxy/routes\\\\\\\":\\\\\\\"route-proxy (its own tools)\\\\\\\",\\\\\\\"/var/lib/systemd\\\\\\\":\\\\\\\"systemd (its own tools)\\\\\\\",\\\\\\\"/var/log/fail2ban.log\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\",\\\\\\\"/var/run/fail2ban\\\\\\\":\\\\\\\"fail2ban (its own tools)\\\\\\\"}\\\",\\\"MESH_SECRET_PLACES\\\":\\\"[\\\\\\\"/var/lib/baserow\\\\\\\",\\\\\\\"/var/lib/bazarr/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/bazarr/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/bookshelf/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/car-hunter\\\\\\\",\\\\\\\"/var/lib/claude-code/gitea-read.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/lidarr-api.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/mqtt-topic.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/home-assistant/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/icecast/admin.secret\\\\\\\",\\\\\\\"/var/lib/icecast/relay.secret\\\\\\\",\\\\\\\"/var/lib/icecast/source.secret\\\\\\\",\\\\\\\"/var/lib/influxdb\\\\\\\",\\\\\\\"/var/lib/kometa\\\\\\\",\\\\\\\"/var/lib/letta\\\\\\\",\\\\\\\"/var/lib/lidarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/lidarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/mesh-delivery-assigner\\\\\\\",\\\\\\\"/var/lib/mesh-provisioner-client\\\\\\\",\\\\\\\"/var/lib/mesh-reads\\\\\\\",\\\\\\\"/var/lib/mesh/dnsmasq\\\\\\\",\\\\\\\"/var/lib/mesh/grafana\\\\\\\",\\\\\\\"/var/lib/mesh/home-assistant\\\\\\\",\\\\\\\"/var/lib/mesh/jackett\\\\\\\",\\\\\\\"/var/lib/mesh/lidarr\\\\\\\",\\\\\\\"/var/lib/mesh/mosquitto\\\\\\\",\\\\\\\"/var/lib/mesh/node-tools\\\\\\\",\\\\\\\"/var/lib/mesh/nodered\\\\\\\",\\\\\\\"/var/lib/mesh/nzbget\\\\\\\",\\\\\\\"/var/lib/mesh/ombi\\\\\\\",\\\\\\\"/var/lib/mesh/pacman\\\\\\\",\\\\\\\"/var/lib/mesh/postgres\\\\\\\",\\\\\\\"/var/lib/mesh/qbittorrent\\\\\\\",\\\\\\\"/var/lib/mesh/radarr\\\\\\\",\\\\\\\"/var/lib/mesh/route-proxy\\\\\\\",\\\\\\\"/var/lib/mesh/searxng\\\\\\\",\\\\\\\"/var/lib/mesh/sonarr\\\\\\\",\\\\\\\"/var/lib/mesh/unifi\\\\\\\",\\\\\\\"/var/lib/mosquitto/grants\\\\\\\",\\\\\\\"/var/lib/mssql\\\\\\\",\\\\\\\"/var/lib/n8n/database.secret\\\\\\\",\\\\\\\"/var/lib/nodered/mqtt-topic.secret\\\\\\\",\\\\\\\"/var/lib/nzbgeek\\\\\\\",\\\\\\\"/var/lib/ombi/lidarr-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/radarr-api.secret\\\\\\\",\\\\\\\"/var/lib/ombi/sonarr-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/radarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/redis/default.secret\\\\\\\",\\\\\\\"/var/lib/redis/grants\\\\\\\",\\\\\\\"/var/lib/restic\\\\\\\",\\\\\\\"/var/lib/sonarr/jackett-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/nzbget-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/sonarr/qbittorrent-api.secret\\\\\\\",\\\\\\\"/var/lib/supabase\\\\\\\",\\\\\\\"/var/lib/tautulli/plex-api.secret\\\\\\\",\\\\\\\"/var/lib/txt-game/database.secret\\\\\\\"]\\\"},\\\"mosquitto\\\":{\\\"MESH_MQTT_CTRL_CONTAINER\\\":\\\"mosquitto\\\",\\\"MESH_PROVISION_ADMIN_USER\\\":\\\"mesh-admin\\\",\\\"MESH_PROVISION_MQTT\\\":\\\"127.0.0.1:1883\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mesh/mosquitto/admin\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mosquitto/grants/mesh.json\\\"},\\\"mssql\\\":{\\\"MESH_MSSQL_READER_PASSWORD_FILE\\\":\\\"/var/lib/mssql/reader.secret\\\",\\\"MESH_PROVISION_MSSQL\\\":\\\"mssql://sa@127.0.0.1:4848/master\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mssql/sa.secret\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mssql/grants/mesh.json\\\"},\\\"nodered\\\":{\\\"MESH_NODERED_CONFIG_FILE\\\":\\\"/var/lib/mesh/nodered/config.json\\\",\\\"MESH_NODERED_URL\\\":\\\"http://127.0.0.1:20006\\\"},\\\"nzbgeek\\\":{\\\"MESH_NZBGEEK_KEY_FILE\\\":\\\"/var/lib/nzbgeek/api-key.secret\\\",\\\"MESH_NZBGEEK_SETTINGS\\\":\\\"/var/lib/nzbgeek/settings.json\\\"},\\\"nzbget\\\":{\\\"MESH_NZBGET_CONFIG_DIR\\\":\\\"/var/lib/nzbget/config\\\",\\\"MESH_NZBGET_CONFIG_FILE\\\":\\\"/var/lib/nzbget/config.json\\\",\\\"MESH_NZBGET_PASSWORD_FILE\\\":\\\"/var/lib/mesh/nzbget/password\\\",\\\"MESH_NZBGET_SECRETS_DIR\\\":\\\"/var/lib/mesh/nzbget\\\",\\\"MESH_NZBGET_STATE\\\":\\\"/var/lib/nzbget\\\",\\\"MESH_NZBGET_URL\\\":\\\"http://127.0.0.1:20017\\\"},\\\"ombi\\\":{\\\"MESH_OMBI_API_KEY_FILE\\\":\\\"/var/lib/mesh/ombi/api-key\\\",\\\"MESH_OMBI_CONFIG_FILE\\\":\\\"/var/lib/mesh/ombi/config.json\\\",\\\"MESH_OMBI_URL\\\":\\\"http://127.0.0.1:20024\\\"},\\\"pacman\\\":{\\\"PACMAN_KEEPER_CONFIG\\\":\\\"/var/lib/mesh/pacman/keeper.json\\\"},\\\"plex\\\":{\\\"MESH_PLEX_CONFIG_DIR\\\":\\\"/mnt/plex/config\\\",\\\"MESH_PLEX_URL\\\":\\\"http://127.0.0.1:32400\\\"},\\\"postgres\\\":{\\\"MESH_CONSUMERS_FILE\\\":\\\"/var/lib/mesh/postgres/consumers.json\\\",\\\"MESH_POSTGRES_READER_PASSWORD_FILE\\\":\\\"/var/lib/mesh/postgres/reader.secret\\\",\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/mesh/postgres/superuser.secret\\\",\\\"MESH_PROVISION_POSTGRES\\\":\\\"postgres://postgres@127.0.0.1:5434/postgres?sslmode=disable\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/mesh/postgres/grants/mesh.json\\\"},\\\"qbittorrent\\\":{\\\"MESH_QBITTORRENT_CONFIG_DIR\\\":\\\"/var/lib/qbittorrent/config\\\",\\\"MESH_QBITTORRENT_CONFIG_FILE\\\":\\\"/var/lib/qbittorrent/config.json\\\",\\\"MESH_QBITTORRENT_PASSWORD_FILE\\\":\\\"/var/lib/mesh/qbittorrent/password\\\",\\\"MESH_QBITTORRENT_URL\\\":\\\"http://127.0.0.1:8112\\\"},\\\"radarr\\\":{\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/radarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/radarr/plex-api.secret\\\",\\\"MESH_RADARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/radarr/api-key\\\",\\\"MESH_RADARR_CONFIG_DIR\\\":\\\"/var/lib/radarr/config\\\",\\\"MESH_RADARR_URL\\\":\\\"http://127.0.0.1:20019\\\"},\\\"redis\\\":{\\\"MESH_PROVISION_PASSWORD_FILE\\\":\\\"/var/lib/redis/default.secret\\\",\\\"MESH_PROVISION_REDIS\\\":\\\"127.0.0.1:20003\\\",\\\"MESH_RECEIVES\\\":\\\"/var/lib/redis/grants/mesh.json\\\"},\\\"restic\\\":{\\\"MESH_BACKUP_DATA\\\":\\\"/var/lib/restic/data.conf\\\",\\\"MESH_BACKUP_DECLARED\\\":\\\"/var/lib/restic/backups.conf\\\",\\\"MESH_BACKUP_PASSWORD_FILE\\\":\\\"/var/lib/restic/repository.secret\\\",\\\"MESH_BACKUP_REPOSITORY\\\":\\\"/storage/restic\\\",\\\"MESH_BACKUP_STATE\\\":\\\"/var/lib/restic\\\"},\\\"searxng\\\":{\\\"MESH_SEARXNG_CONFIG_FILE\\\":\\\"/var/lib/mesh/searxng/config.json\\\",\\\"MESH_SEARXNG_URL\\\":\\\"http://127.0.0.1:20000\\\"},\\\"sonarr\\\":{\\\"MESH_PLEX_BIND_FILE\\\":\\\"/var/lib/sonarr/plex-api.json\\\",\\\"MESH_PLEX_TOKEN_FILE\\\":\\\"/var/lib/sonarr/plex-api.secret\\\",\\\"MESH_SONARR_API_KEY_FILE\\\":\\\"/var/lib/mesh/sonarr/api-key\\\",\\\"MESH_SONARR_CONFIG_DIR\\\":\\\"/var/lib/sonarr/config\\\",\\\"MESH_SONARR_RECYCLE_DIR\\\":\\\"/var/lib/sonarr/recycle-bin\\\",\\\"MESH_SONARR_URL\\\":\\\"http://127.0.0.1:20018\\\"},\\\"systemd\\\":{\\\"MESH_SYSTEMD_STATE\\\":\\\"/var/lib/mesh/systemd\\\"},\\\"tautulli\\\":{\\\"MESH_TAUTULLI_CONFIG_DIR\\\":\\\"/var/lib/tautulli/config\\\",\\\"MESH_TAUTULLI_CONFIG_FILE\\\":\\\"/var/lib/mesh/tautulli/config.json\\\",\\\"MESH_TAUTULLI_CONTAINER\\\":\\\"tautulli\\\",\\\"MESH_TAUTULLI_URL\\\":\\\"http://127.0.0.1:20023\\\"},\\\"unifi\\\":{\\\"MESH_UNIFI_CONFIG_FILE\\\":\\\"/var/lib/mesh/unifi/config.json\\\",\\\"MESH_UNIFI_URL\\\":\\\"https://127.0.0.1:8443\\\"},\\\"youtube\\\":{\\\"MESH_YOUTUBE_DOWNLOADS\\\":\\\"/storage/downloads/youtube\\\",\\\"MESH_YOUTUBE_SETTINGS\\\":\\\"/var/lib/youtube/settings.json\\\"},\\\"zsh\\\":{\\\"ZSH_TOOLS_CONF\\\":\\\"/var/lib/zsh/tools.conf\\\"}}\",\"MESH_TOOL_MODULES\":\"avahi=/var/lib/mesh/bundles/avahi/tools/avahi-tools,baserow=/var/lib/mesh/bundles/baserow/tools/tools/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/tools/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/tools/index.serve.mjs,claude-code=/var/lib/mesh/bundles/claude-code/tools/claude-code,dbus=/var/lib/mesh/bundles/dbus/tools-go/dbus-tools,disk-load=/var/lib/mesh/bundles/disk-load/tools-go/disk-load,dnsmasq=/var/lib/mesh/bundles/dnsmasq/tools/dnsmasq-tools,docker=/var/lib/mesh/bundles/docker/tools/docker-tools,fail2ban=/var/lib/mesh/bundles/fail2ban/tools/fail2ban-tools,grafana=/var/lib/mesh/bundles/grafana/code/index.serve.mjs,grafana=/var/lib/mesh/bundles/grafana/code/tools/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/tools/index.serve.mjs,hostname=/var/lib/mesh/bundles/hostname/tools/hostname-tools,icecast=/var/lib/mesh/bundles/icecast/code/index.serve.mjs,icecast=/var/lib/mesh/bundles/icecast/code/tools/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/provisioner/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/tools/index.serve.mjs,jackett=/var/lib/mesh/bundles/jackett/code/tools/index.serve.mjs,kernel=/var/lib/mesh/bundles/kernel/tools/kernel-tools,kometa=/var/lib/mesh/bundles/kometa/code/tools/index.serve.mjs,letta=/var/lib/mesh/bundles/letta/tools/tools/index.serve.mjs,lidarr=/var/lib/mesh/bundles/lidarr/tools-go/lidarr,localization=/var/lib/mesh/bundles/localization/tools/localization-tools,logrotate=/var/lib/mesh/bundles/logrotate/tools/logrotate-tools,media-importer=/var/lib/mesh/bundles/media-importer/tools/media-importer,mesh-files=/var/lib/mesh/bundles/mesh-files/tools/mesh-files,mosquitto=/var/lib/mesh/bundles/mosquitto/code/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/provisioner/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/tools/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/provisioner/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/tools/index.serve.mjs,netcheck=/var/lib/mesh/bundles/netcheck/tools-go/netcheck,netcheck=/var/lib/mesh/bundles/netcheck/tools-typescript/tools/index.serve.mjs,networkmanager=/var/lib/mesh/bundles/networkmanager/tools/uplink-tools,nfs-server=/var/lib/mesh/bundles/nfs-server/tools/nfs-server,nftables=/var/lib/mesh/bundles/nftables/tools/tools/index.serve.mjs,nodered=/var/lib/mesh/bundles/nodered/code/tools/index.serve.mjs,nzbgeek=/var/lib/mesh/bundles/nzbgeek/tools/nzbgeek,nzbget=/var/lib/mesh/bundles/nzbget/code/nzbget,ombi=/var/lib/mesh/bundles/ombi/code/index.serve.mjs,ombi=/var/lib/mesh/bundles/ombi/code/tools/index.serve.mjs,pacman=/var/lib/mesh/bundles/pacman/tools/pacman-tools,plex=/var/lib/mesh/bundles/plex/code/plex,postgres=/var/lib/mesh/bundles/postgres/code/postgres-provider,power=/var/lib/mesh/bundles/power/tools-go/power,qbittorrent=/var/lib/mesh/bundles/qbittorrent/code/qbittorrent,radarr=/var/lib/mesh/bundles/radarr/tools-go/radarr,redis=/var/lib/mesh/bundles/redis/code/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/provisioner/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/tools/index.serve.mjs,restic=/var/lib/mesh/bundles/restic/tools/restic-backups,route-proxy=/var/lib/mesh/bundles/route-proxy/provider/route-provider,searxng=/var/lib/mesh/bundles/searxng/tools/tools/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/tools/index.serve.mjs,ssh-client=/var/lib/mesh/bundles/ssh-client/tools/ssh-client-tools,sudo=/var/lib/mesh/bundles/sudo/tools/sudo-tools,systemd=/var/lib/mesh/bundles/systemd/tools/systemd-tools,tautulli=/var/lib/mesh/bundles/tautulli/code/index.serve.mjs,tautulli=/var/lib/mesh/bundles/tautulli/code/tools/index.serve.mjs,time-sync=/var/lib/mesh/bundles/time-sync/tools/time-sync-tools,unifi=/var/lib/mesh/bundles/unifi/tools/tools/index.serve.mjs,youtube=/var/lib/mesh/bundles/youtube/tools/youtube,zsh=/var/lib/mesh/bundles/zsh/tools/tools/index.serve.mjs\"},\"id\":\"node-tools.runtime\",\"module\":\"node-tools\",\"name\":\"node-tools\",\"restart-on\":[\"avahi.bundle-tools\",\"baserow.bundle-tools\",\"bazarr.bundle-code\",\"bookshelf.bundle-code\",\"claude-code.bundle-tools\",\"dbus.bundle-tools-go\",\"disk-load.bundle-tools-go\",\"dnsmasq.bundle-tools\",\"docker.bundle-tools\",\"fail2ban.bundle-tools\",\"grafana.bundle-code\",\"home-assistant.bundle-code\",\"hostname.bundle-tools\",\"icecast.bundle-code\",\"influxdb.bundle-code\",\"jackett.bundle-code\",\"kernel.bundle-tools\",\"kometa.bundle-code\",\"letta.bundle-tools\",\"lidarr.bundle-tools-go\",\"localization.bundle-tools\",\"logrotate.bundle-tools\",\"media-importer.bundle-tools\",\"mesh-files.bundle-tools\",\"mosquitto.bundle-code\",\"mssql.bundle-code\",\"netcheck.bundle-tools-go\",\"netcheck.bundle-tools-typescript\",\"networkmanager.bundle-tools\",\"nfs-server.bundle-tools\",\"nftables.bundle-tools\",\"node-tools.needs-broker\",\"nodered.bundle-code\",\"nzbgeek.bundle-tools\",\"nzbget.bundle-code\",\"ombi.bundle-code\",\"pacman.bundle-tools\",\"plex.bundle-code\",\"postgres.bundle-code\",\"power.bundle-tools-go\",\"qbittorrent.bundle-code\",\"radarr.bundle-tools-go\",\"redis.bundle-code\",\"restic.bundle-tools\",\"route-proxy.bundle-provider\",\"searxng.bundle-tools\",\"sonarr.bundle-code\",\"ssh-client.bundle-tools\",\"sudo.bundle-tools\",\"systemd.bundle-tools\",\"tautulli.bundle-code\",\"time-sync.bundle-tools\",\"unifi.bundle-tools\",\"youtube.bundle-tools\",\"zsh.bundle-tools\",\"baserow.runtime-config\",\"bazarr.runtime-config\",\"claude-code.facts\",\"claude-code.settings\",\"grafana.runtime-config\",\"home-assistant.needs-token\",\"home-assistant.runtime-config\",\"icecast.runtime-config\",\"icecast.secret-admin\",\"influxdb.needs-admin-token\",\"influxdb.received-influxdb-api\",\"influxdb.runtime-config\",\"jackett.needs-api-key\",\"jackett.runtime-config\",\"kometa.settings\",\"letta.runtime-config\",\"lidarr.bound-plex-api\",\"lidarr.needs-api-key\",\"lidarr.secret-plex-api\",\"media-importer.settings\",\"mosquitto.needs-admin\",\"mosquitto.received-mqtt-topic\",\"mssql.needs-reader\",\"mssql.needs-sa\",\"mssql.received-mssql-database\",\"nodered.runtime-config\",\"nzbgeek.needs-api-key\",\"nzbgeek.settings\",\"nzbget.needs-password\",\"nzbget.runtime-config\",\"ombi.needs-api-key\",\"ombi.runtime-config\",\"pacman.keeper-config\",\"postgres.consumers\",\"postgres.needs-reader\",\"postgres.needs-superuser\",\"postgres.received-postgres-database\",\"qbittorrent.needs-password\",\"qbittorrent.runtime-config\",\"radarr.bound-plex-api\",\"radarr.needs-api-key\",\"radarr.secret-plex-api\",\"redis.received-redis-cache\",\"redis.secret-secret\",\"restic.data-declared\",\"restic.declared\",\"restic.needs-repository\",\"searxng.runtime-config\",\"sonarr.bound-plex-api\",\"sonarr.needs-api-key\",\"sonarr.secret-plex-api\",\"tautulli.runtime-config\",\"unifi.runtime-config\",\"youtube.settings\",\"zsh.tools-conf\"],\"run\":[\"./node-tools\"],\"source\":\"http://novox.internal:5100/v2/node-tools/runtime/blobs/sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a\",\"type\":\"process\",\"user\":\"ace\"}],\"v\":1}", + decode: func(b []byte) (any, error) { + var v ComposedByNodeModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module the engine could not compose", subject: "", given: "{\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"module\":\"nzbget\",\"node\":\"ace\",\"refused\":\"a secret the vault does not hold\",\"resources\":[],\"v\":1}", wire: "{\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"module\":\"nzbget\",\"node\":\"ace\",\"refused\":\"a secret the vault does not hold\",\"resources\":[],\"v\":1}", + decode: func(b []byte) (any, error) { + var v ComposedByNodeModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "the node's own composition", subject: "", given: "{\"node\":\"ace\",\"own\":{\"adoption\":{\"taken\":[\"avahi\"],\"untaken\":{\"plex\":[\"plex.bundle-code\"]}},\"groups\":[{\"gid\":1002,\"name\":\"mesh-media\"}],\"identities\":[{\"gid\":1001,\"groups\":[1002],\"module\":\"plex\",\"name\":\"plex\",\"runner\":\"plex\",\"uid\":1001}],\"libraries\":[{\"gid\":1002,\"group\":\"mesh-media\",\"home\":false,\"library\":\"movies\",\"paths\":[\"/srv/media/movies\"],\"root\":\"/srv/media/movies\",\"taken\":true}],\"provisioner_account\":\"mesh-provisioner-client\"},\"resources\":[],\"v\":1}", wire: "{\"node\":\"ace\",\"own\":{\"adoption\":{\"taken\":[\"avahi\"],\"untaken\":{\"plex\":[\"plex.bundle-code\"]}},\"groups\":[{\"gid\":1002,\"name\":\"mesh-media\"}],\"identities\":[{\"gid\":1001,\"groups\":[1002],\"module\":\"plex\",\"name\":\"plex\",\"runner\":\"plex\",\"uid\":1001}],\"libraries\":[{\"gid\":1002,\"group\":\"mesh-media\",\"home\":false,\"library\":\"movies\",\"paths\":[\"/srv/media/movies\"],\"root\":\"/srv/media/movies\",\"taken\":true}],\"provisioner_account\":\"mesh-provisioner-client\"},\"resources\":[],\"v\":1}", + decode: func(b []byte) (any, error) { + var v ComposedByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module running (typed from the engine's own values; no read tool answers a have key)", subject: "", given: "{\"build\":\"b1\",\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"ok\",\"phase\":\"running\",\"sealed\":[\"web.secret\"],\"sealed-at\":{\"web.secret#sealed\":\"box1\"},\"v\":1}", wire: "{\"build\":\"b1\",\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"ok\",\"phase\":\"running\",\"sealed\":[\"web.secret\"],\"sealed-at\":{\"web.secret#sealed\":\"box1\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a failed apply waiting on a store's holder (typed from the engine's values)", subject: "", given: "{\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"detail\":\"the pull met a store that is not settled\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"failed\",\"phase\":\"failed\",\"pull-wait\":{\"began\":\"2026-11-05T12:00:00Z\",\"holder\":\"gitea\",\"image\":\"registry.example/web:1\",\"retried\":\"the holder settled\",\"retried-at\":\"2026-11-05T12:05:00Z\",\"seat\":\"container-image-registry\"},\"v\":1}", wire: "{\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"detail\":\"the pull met a store that is not settled\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"failed\",\"phase\":\"failed\",\"pull-wait\":{\"began\":\"2026-11-05T12:00:00Z\",\"holder\":\"gitea\",\"image\":\"registry.example/web:1\",\"retried\":\"the holder settled\",\"retried-at\":\"2026-11-05T12:05:00Z\",\"seat\":\"container-image-registry\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a move waiting for a person's yes", subject: "", given: "{\"build-alone\":true,\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"ok\",\"judgement\":\"recreates\",\"kept\":[\"tool runner not declared\"],\"phase\":\"waiting-yes\",\"v\":1,\"waits\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"}", wire: "{\"build-alone\":true,\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fp\":\"src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275\",\"health\":\"ok\",\"judgement\":\"recreates\",\"kept\":[\"tool runner not declared\"],\"phase\":\"waiting-yes\",\"v\":1,\"waits\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"}", + decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "the bus module's have carries the user list written (typed from the engine's values)", subject: "", given: "{\"health\":\"ok\",\"phase\":\"running\",\"user-list\":{\"at\":\"2026-11-05T12:00:00Z\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"follows\":\"grants\",\"handed\":{\"at\":\"2026-11-05T12:00:00Z\",\"box\":{\"ct\":\"c\",\"eph\":\"e\",\"iv\":\"i\",\"tag\":\"t\",\"v\":1},\"from\":\"ace\",\"to\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"users\":3},\"key\":\"-----BEGIN PUBLIC KEY-----\",\"revision\":7,\"source\":\"user-list\",\"users\":3,\"wanted\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"},\"v\":1}", wire: "{\"health\":\"ok\",\"phase\":\"running\",\"user-list\":{\"at\":\"2026-11-05T12:00:00Z\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"follows\":\"grants\",\"handed\":{\"at\":\"2026-11-05T12:00:00Z\",\"box\":{\"ct\":\"c\",\"eph\":\"e\",\"iv\":\"i\",\"tag\":\"t\",\"v\":1},\"from\":\"ace\",\"to\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"users\":3},\"key\":\"-----BEGIN PUBLIC KEY-----\",\"revision\":7,\"source\":\"user-list\",\"users\":3,\"wanted\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's own facts with its record and an answer to a candidate (typed from the engine's values)", subject: "", given: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}", wire: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}", + decode: func(b []byte) (any, error) { var v HaveByNodeNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a module's data items on a node (typed from the engine's values)", subject: "", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"items\":[{\"class\":\"database\",\"covered_by\":\"/var/lib/postgres\",\"item\":\"cluster\",\"measure\":\"dataset\",\"module\":\"postgres\",\"path\":\"/var/lib/postgres\",\"protection\":\"backup\"}],\"module\":\"postgres\",\"node\":\"ace\",\"v\":1}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"items\":[{\"class\":\"database\",\"covered_by\":\"/var/lib/postgres\",\"item\":\"cluster\",\"measure\":\"dataset\",\"module\":\"postgres\",\"path\":\"/var/lib/postgres\",\"protection\":\"backup\"}],\"module\":\"postgres\",\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { var v DataByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's health (typed from the engine's values)", subject: "", given: "{\"at\":\"2026-11-05T12:00:00Z\",\"contract\":3,\"network\":{\"parts\":[{\"part\":\"resolver\",\"since\":\"2026-11-05T10:00:00Z\",\"state\":\"running\"}],\"since\":\"2026-11-05T10:00:00Z\",\"state\":\"running\"},\"node\":\"ace\",\"peers\":[{\"addresses\":[\"198.51.100.1\"],\"key\":\"k\",\"seen\":true}],\"relogin\":[{\"account\":\"web\",\"reason\":\"password changed\",\"since\":\"2026-11-05T11:00:00Z\"}],\"units\":{\"failed\":[{\"result\":\"exit-code\",\"scope\":\"system\",\"since\":\"2026-11-05T11:00:00Z\",\"unit\":\"x.service\"}],\"state\":\"degraded\"},\"v\":1}", wire: "{\"at\":\"2026-11-05T12:00:00Z\",\"contract\":3,\"network\":{\"parts\":[{\"part\":\"resolver\",\"since\":\"2026-11-05T10:00:00Z\",\"state\":\"running\"}],\"since\":\"2026-11-05T10:00:00Z\",\"state\":\"running\"},\"node\":\"ace\",\"peers\":[{\"addresses\":[\"198.51.100.1\"],\"key\":\"k\",\"seen\":true}],\"relogin\":[{\"account\":\"web\",\"reason\":\"password changed\",\"since\":\"2026-11-05T11:00:00Z\"}],\"units\":{\"failed\":[{\"result\":\"exit-code\",\"scope\":\"system\",\"since\":\"2026-11-05T11:00:00Z\",\"unit\":\"x.service\"}],\"state\":\"degraded\"},\"v\":1}", + decode: func(b []byte) (any, error) { var v HealthByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json index aff5a04..35631e7 100644 --- a/seats/node-engine/fixtures/states.json +++ b/seats/node-engine/fixtures/states.json @@ -48,5 +48,472 @@ } ] } + }, + { + "name": "buildah composed on ace (resources captured from the declaration of 2026-10-24; the fingerprint stands in for the indexer's)", + "of": "state:composed/..", + "given": { + "v": 1, + "node": "ace", + "module": "buildah", + "fp": "src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275", + "resources": [ + { + "id": "buildah.buildah", + "module": "buildah", + "package": "buildah", + "type": "package" + } + ] + } + }, + { + "name": "node-tools composed with a filled file (captured; the fingerprint stands in)", + "of": "state:composed/..", + "given": { + "v": 1, + "node": "ace", + "module": "node-tools", + "fp": "src2:6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f64652d6e6f", + "resources": [ + { + "id": "node-tools.mesh-state", + "mode": "0755", + "module": "node-tools", + "path": "/var/lib/mesh/node-tools", + "type": "directory" + }, + { + "id": "node-tools.interpreter", + "module": "node-tools", + "package": "nodejs", + "type": "package" + }, + { + "digest": "sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a", + "env": { + "MESH_AGENT_ACCOUNT": "ace", + "MESH_AGENT_HOME": "/home/ace", + "MESH_BROKER_FILE": "/var/lib/mesh/node-tools/broker", + "MESH_OPERATOR_ACCOUNT": "ace", + "MESH_OPERATOR_HOME": "/home/ace", + "MESH_TOOL_ENV": "{\"baserow\":{\"MESH_BASEROW_CONFIG_FILE\":\"/var/lib/mesh/baserow/config.json\",\"MESH_BASEROW_URL\":\"http://127.0.0.1:20011\"},\"bazarr\":{\"MESH_BAZARR_CONFIG_DIR\":\"/var/lib/bazarr/config\",\"MESH_BAZARR_CONFIG_FILE\":\"/var/lib/bazarr/config.json\",\"MESH_BAZARR_CONTAINER\":\"bazarr\",\"MESH_BAZARR_URL\":\"http://127.0.0.1:20020\"},\"bookshelf\":{\"MESH_BOOKSHELF_CONFIG_DIR\":\"/var/lib/bookshelf/config\",\"MESH_BOOKSHELF_URL\":\"http://127.0.0.1:20021\"},\"claude-code\":{\"MESH_CLAUDE_CODE_FACTS\":\"/var/lib/claude-code/facts.json\",\"MESH_CLAUDE_CODE_SETTINGS\":\"/var/lib/claude-code/settings.json\",\"MESH_CLAUDE_CODE_STATE\":\"/var/lib/claude-code\"},\"grafana\":{\"MESH_GRAFANA_CONFIG_FILE\":\"/var/lib/mesh/grafana/config.json\",\"MESH_GRAFANA_URL\":\"http://127.0.0.1:20012\"},\"home-assistant\":{\"MESH_HOMEASSISTANT_CONFIG_FILE\":\"/var/lib/mesh/home-assistant/config.json\",\"MESH_HOMEASSISTANT_TOKEN_FILE\":\"/var/lib/mesh/home-assistant/token\",\"MESH_HOMEASSISTANT_URL\":\"http://127.0.0.1:8123\"},\"icecast\":{\"MESH_ICECAST_ADMIN_PASSWORD_FILE\":\"/var/lib/icecast/admin.secret\",\"MESH_ICECAST_CONFIG_FILE\":\"/var/lib/mesh/icecast/config.json\",\"MESH_ICECAST_URL\":\"http://127.0.0.1:20005\"},\"influxdb\":{\"MESH_INFLUXDB_CONFIG_FILE\":\"/var/lib/mesh/influxdb/config.json\",\"MESH_INFLUXDB_TOKEN_FILE\":\"/var/lib/influxdb/admin-token.secret\",\"MESH_INFLUXDB_URL\":\"http://127.0.0.1:8086\",\"MESH_RECEIVES\":\"/var/lib/influxdb/grants/mesh.json\"},\"jackett\":{\"MESH_JACKETT_API_KEY_FILE\":\"/var/lib/mesh/jackett/api-key\",\"MESH_JACKETT_CONFIG_DIR\":\"/var/lib/jackett/config\",\"MESH_JACKETT_CONFIG_FILE\":\"/var/lib/jackett/config.json\",\"MESH_JACKETT_URL\":\"http://127.0.0.1:20004\"},\"kometa\":{\"MESH_KOMETA_CONFIG_DIR\":\"/var/lib/kometa/config\",\"MESH_KOMETA_CONFIG_FILE\":\"/var/lib/kometa/config.yml\"},\"letta\":{\"MESH_LETTA_CONFIG_FILE\":\"/var/lib/mesh/letta/config.json\",\"MESH_LETTA_URL\":\"http://127.0.0.1:20002\"},\"lidarr\":{\"MESH_LIDARR_API_KEY_FILE\":\"/var/lib/mesh/lidarr/api-key\",\"MESH_LIDARR_CONFIG_DIR\":\"/var/lib/lidarr/config\",\"MESH_LIDARR_RECYCLE_BIN\":\"/recycle/\",\"MESH_LIDARR_RECYCLE_DAYS\":\"7\",\"MESH_LIDARR_URL\":\"http://127.0.0.1:20022\",\"MESH_PLEX_BIND_FILE\":\"/var/lib/lidarr/plex-api.json\",\"MESH_PLEX_TOKEN_FILE\":\"/var/lib/lidarr/plex-api.secret\"},\"media-importer\":{\"MESH_MEDIA_IMPORTER_SETTINGS\":\"/var/lib/media-importer/settings.json\",\"MESH_MEDIA_IMPORTER_STATE\":\"/var/lib/media-importer\",\"MESH_MEDIA_LIBRARY\":\"/storage/media/poc\"},\"mesh-files\":{\"MESH_FILES_STATE\":\"/var/lib/mesh-files\",\"MESH_OWNED_PATHS\":\"{\\\"/etc/NetworkManager/conf.d/50-mesh.conf\\\":\\\"networkmanager (its own tools)\\\",\\\"/etc/claude-code\\\":\\\"claude-code (its own tools)\\\",\\\"/etc/claude-code/agent-account\\\":\\\"claude-code (its own tools)\\\",\\\"/etc/dnsmasq.conf\\\":\\\"dnsmasq (its own tools)\\\",\\\"/etc/docker/daemon.json\\\":\\\"docker (its own tools)\\\",\\\"/etc/fail2ban/action.d\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/action.d/iptables-allports-dualchain.conf\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/filter.d\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/jail.d\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/jail.d/recidive.conf\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/jail.d/sshd.conf\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/jail.local\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/fail2ban/mesh-own-addresses.json\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/hostname\\\":\\\"hostname (node-hostname)\\\",\\\"/etc/hosts\\\":\\\"hostname (node-hostname)\\\",\\\"/etc/locale.conf\\\":\\\"localization (its own tools)\\\",\\\"/etc/logrotate.conf\\\":\\\"logrotate (its own tools)\\\",\\\"/etc/logrotate.d/fail2ban\\\":\\\"fail2ban (its own tools)\\\",\\\"/etc/mesh-power\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/after-boot\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/after-wake\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/before-shutdown\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/before-sleep\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/on-battery\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-power/moments/on-mains\\\":\\\"power (its own tools)\\\",\\\"/etc/mesh-resolver/nodes.conf\\\":\\\"dnsmasq (its own tools)\\\",\\\"/etc/mesh-resolver/zones.conf\\\":\\\"dnsmasq (its own tools)\\\",\\\"/etc/nfs-server\\\":\\\"nfs-server (its own tools)\\\",\\\"/etc/nfs-server/machines\\\":\\\"nfs-server (its own tools)\\\",\\\"/etc/nfs-server/shares.conf\\\":\\\"nfs-server (its own tools)\\\",\\\"/etc/nfs.conf.d/50-mesh.conf\\\":\\\"nfs-server (its own tools)\\\",\\\"/etc/pacman.conf\\\":\\\"pacman (its own tools)\\\",\\\"/etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook\\\":\\\"kernel (its own tools)\\\",\\\"/etc/resolv.conf\\\":\\\"networkmanager (node-uplink)\\\",\\\"/etc/ssh/sshd_config.d/10-mesh.conf\\\":\\\"sshd (its own tools)\\\",\\\"/etc/sudoers.d/10-mesh-operator\\\":\\\"sudo (its own tools)\\\",\\\"/etc/systemd/logind.conf.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/logind.conf.d/power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/dnsmasq.service.d\\\":\\\"dnsmasq (its own tools)\\\",\\\"/etc/systemd/system/dnsmasq.service.d/mesh.conf\\\":\\\"dnsmasq (its own tools)\\\",\\\"/etc/systemd/system/docker-prune.service\\\":\\\"docker (its own tools)\\\",\\\"/etc/systemd/system/docker-prune.timer\\\":\\\"docker (its own tools)\\\",\\\"/etc/systemd/system/hibernate.target.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/hibernate.target.d/mesh-power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/hybrid-sleep.target.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/hybrid-sleep.target.d/mesh-power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/mesh-ca-trust.service\\\":\\\"ca-trust (its own tools)\\\",\\\"/etc/systemd/system/mesh-filter.service\\\":\\\"nftables (its own tools)\\\",\\\"/etc/systemd/system/mesh-kernel-boot-entries.service\\\":\\\"kernel (its own tools)\\\",\\\"/etc/systemd/system/mesh-kernel-huge-pages.service\\\":\\\"kernel (its own tools)\\\",\\\"/etc/systemd/system/mesh-power-after-boot.service\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/mesh-power-after-wake.service\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/mesh-power-before-shutdown.service\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/mesh-power-before-sleep.service\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/mesh-power-supply.service\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/nftables.service.d/mesh.conf\\\":\\\"nftables (its own tools)\\\",\\\"/etc/systemd/system/sleep.target.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/sleep.target.d/mesh-power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/suspend-then-hibernate.target.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/suspend.target.d\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/system/suspend.target.d/mesh-power.conf\\\":\\\"power (its own tools)\\\",\\\"/etc/systemd/timesyncd.conf.d/50-mesh.conf\\\":\\\"time-sync (its own tools)\\\",\\\"/etc/udev/rules.d/90-mesh-power.rules\\\":\\\"power (its own tools)\\\",\\\"/etc/vconsole.conf\\\":\\\"localization (its own tools)\\\",\\\"/etc/xdg/reflector/reflector.conf\\\":\\\"pacman (its own tools)\\\",\\\"/run/mesh-power\\\":\\\"power (its own tools)\\\",\\\"/run/nfs-server\\\":\\\"nfs-server (its own tools)\\\",\\\"/usr/lib/nox-mesh-host/launch\\\":\\\"mesh-host (its own tools)\\\",\\\"/usr/lib/nox-mesh-host/versions/4e121b6e88f1\\\":\\\"mesh-host (its own tools)\\\",\\\"/usr/local/bin\\\":\\\"mesh-cli (its own tools)\\\",\\\"/usr/local/bin/claude-agent\\\":\\\"claude-code (its own tools)\\\",\\\"/usr/local/lib/mesh-power\\\":\\\"power (its own tools)\\\",\\\"/var/lib/dnsmasq-watch\\\":\\\"dnsmasq (its own tools)\\\",\\\"/var/lib/mesh-fail2ban\\\":\\\"fail2ban (its own tools)\\\",\\\"/var/lib/mesh-fail2ban/own-addresses\\\":\\\"fail2ban (its own tools)\\\",\\\"/var/lib/mesh-store\\\":\\\"postgres (its own tools)\\\",\\\"/var/lib/nfs-server\\\":\\\"nfs-server (its own tools)\\\",\\\"/var/lib/nfs-server/from-bus\\\":\\\"nfs-server (its own tools)\\\",\\\"/var/lib/postgres\\\":\\\"postgres (its own tools)\\\",\\\"/var/lib/route-proxy/acme\\\":\\\"route-proxy (its own tools)\\\",\\\"/var/lib/route-proxy/ca\\\":\\\"route-proxy (its own tools)\\\",\\\"/var/lib/route-proxy/routes\\\":\\\"route-proxy (its own tools)\\\",\\\"/var/lib/systemd\\\":\\\"systemd (its own tools)\\\",\\\"/var/log/fail2ban.log\\\":\\\"fail2ban (its own tools)\\\",\\\"/var/run/fail2ban\\\":\\\"fail2ban (its own tools)\\\"}\",\"MESH_SECRET_PLACES\":\"[\\\"/var/lib/baserow\\\",\\\"/var/lib/bazarr/radarr-api.secret\\\",\\\"/var/lib/bazarr/sonarr-api.secret\\\",\\\"/var/lib/bookshelf/jackett-api.secret\\\",\\\"/var/lib/bookshelf/nzbget-api.secret\\\",\\\"/var/lib/bookshelf/qbittorrent-api.secret\\\",\\\"/var/lib/car-hunter\\\",\\\"/var/lib/claude-code/gitea-read.secret\\\",\\\"/var/lib/home-assistant/lidarr-api.secret\\\",\\\"/var/lib/home-assistant/mqtt-topic.secret\\\",\\\"/var/lib/home-assistant/radarr-api.secret\\\",\\\"/var/lib/home-assistant/sonarr-api.secret\\\",\\\"/var/lib/icecast/admin.secret\\\",\\\"/var/lib/icecast/relay.secret\\\",\\\"/var/lib/icecast/source.secret\\\",\\\"/var/lib/influxdb\\\",\\\"/var/lib/kometa\\\",\\\"/var/lib/letta\\\",\\\"/var/lib/lidarr/jackett-api.secret\\\",\\\"/var/lib/lidarr/nzbget-api.secret\\\",\\\"/var/lib/lidarr/plex-api.secret\\\",\\\"/var/lib/lidarr/qbittorrent-api.secret\\\",\\\"/var/lib/mesh-delivery-assigner\\\",\\\"/var/lib/mesh-provisioner-client\\\",\\\"/var/lib/mesh-reads\\\",\\\"/var/lib/mesh/dnsmasq\\\",\\\"/var/lib/mesh/grafana\\\",\\\"/var/lib/mesh/home-assistant\\\",\\\"/var/lib/mesh/jackett\\\",\\\"/var/lib/mesh/lidarr\\\",\\\"/var/lib/mesh/mosquitto\\\",\\\"/var/lib/mesh/node-tools\\\",\\\"/var/lib/mesh/nodered\\\",\\\"/var/lib/mesh/nzbget\\\",\\\"/var/lib/mesh/ombi\\\",\\\"/var/lib/mesh/pacman\\\",\\\"/var/lib/mesh/postgres\\\",\\\"/var/lib/mesh/qbittorrent\\\",\\\"/var/lib/mesh/radarr\\\",\\\"/var/lib/mesh/route-proxy\\\",\\\"/var/lib/mesh/searxng\\\",\\\"/var/lib/mesh/sonarr\\\",\\\"/var/lib/mesh/unifi\\\",\\\"/var/lib/mosquitto/grants\\\",\\\"/var/lib/mssql\\\",\\\"/var/lib/n8n/database.secret\\\",\\\"/var/lib/nodered/mqtt-topic.secret\\\",\\\"/var/lib/nzbgeek\\\",\\\"/var/lib/ombi/lidarr-api.secret\\\",\\\"/var/lib/ombi/plex-api.secret\\\",\\\"/var/lib/ombi/radarr-api.secret\\\",\\\"/var/lib/ombi/sonarr-api.secret\\\",\\\"/var/lib/radarr/jackett-api.secret\\\",\\\"/var/lib/radarr/nzbget-api.secret\\\",\\\"/var/lib/radarr/plex-api.secret\\\",\\\"/var/lib/radarr/qbittorrent-api.secret\\\",\\\"/var/lib/redis/default.secret\\\",\\\"/var/lib/redis/grants\\\",\\\"/var/lib/restic\\\",\\\"/var/lib/sonarr/jackett-api.secret\\\",\\\"/var/lib/sonarr/nzbget-api.secret\\\",\\\"/var/lib/sonarr/plex-api.secret\\\",\\\"/var/lib/sonarr/qbittorrent-api.secret\\\",\\\"/var/lib/supabase\\\",\\\"/var/lib/tautulli/plex-api.secret\\\",\\\"/var/lib/txt-game/database.secret\\\"]\"},\"mosquitto\":{\"MESH_MQTT_CTRL_CONTAINER\":\"mosquitto\",\"MESH_PROVISION_ADMIN_USER\":\"mesh-admin\",\"MESH_PROVISION_MQTT\":\"127.0.0.1:1883\",\"MESH_PROVISION_PASSWORD_FILE\":\"/var/lib/mesh/mosquitto/admin\",\"MESH_RECEIVES\":\"/var/lib/mosquitto/grants/mesh.json\"},\"mssql\":{\"MESH_MSSQL_READER_PASSWORD_FILE\":\"/var/lib/mssql/reader.secret\",\"MESH_PROVISION_MSSQL\":\"mssql://sa@127.0.0.1:4848/master\",\"MESH_PROVISION_PASSWORD_FILE\":\"/var/lib/mssql/sa.secret\",\"MESH_RECEIVES\":\"/var/lib/mssql/grants/mesh.json\"},\"nodered\":{\"MESH_NODERED_CONFIG_FILE\":\"/var/lib/mesh/nodered/config.json\",\"MESH_NODERED_URL\":\"http://127.0.0.1:20006\"},\"nzbgeek\":{\"MESH_NZBGEEK_KEY_FILE\":\"/var/lib/nzbgeek/api-key.secret\",\"MESH_NZBGEEK_SETTINGS\":\"/var/lib/nzbgeek/settings.json\"},\"nzbget\":{\"MESH_NZBGET_CONFIG_DIR\":\"/var/lib/nzbget/config\",\"MESH_NZBGET_CONFIG_FILE\":\"/var/lib/nzbget/config.json\",\"MESH_NZBGET_PASSWORD_FILE\":\"/var/lib/mesh/nzbget/password\",\"MESH_NZBGET_SECRETS_DIR\":\"/var/lib/mesh/nzbget\",\"MESH_NZBGET_STATE\":\"/var/lib/nzbget\",\"MESH_NZBGET_URL\":\"http://127.0.0.1:20017\"},\"ombi\":{\"MESH_OMBI_API_KEY_FILE\":\"/var/lib/mesh/ombi/api-key\",\"MESH_OMBI_CONFIG_FILE\":\"/var/lib/mesh/ombi/config.json\",\"MESH_OMBI_URL\":\"http://127.0.0.1:20024\"},\"pacman\":{\"PACMAN_KEEPER_CONFIG\":\"/var/lib/mesh/pacman/keeper.json\"},\"plex\":{\"MESH_PLEX_CONFIG_DIR\":\"/mnt/plex/config\",\"MESH_PLEX_URL\":\"http://127.0.0.1:32400\"},\"postgres\":{\"MESH_CONSUMERS_FILE\":\"/var/lib/mesh/postgres/consumers.json\",\"MESH_POSTGRES_READER_PASSWORD_FILE\":\"/var/lib/mesh/postgres/reader.secret\",\"MESH_PROVISION_PASSWORD_FILE\":\"/var/lib/mesh/postgres/superuser.secret\",\"MESH_PROVISION_POSTGRES\":\"postgres://postgres@127.0.0.1:5434/postgres?sslmode=disable\",\"MESH_RECEIVES\":\"/var/lib/mesh/postgres/grants/mesh.json\"},\"qbittorrent\":{\"MESH_QBITTORRENT_CONFIG_DIR\":\"/var/lib/qbittorrent/config\",\"MESH_QBITTORRENT_CONFIG_FILE\":\"/var/lib/qbittorrent/config.json\",\"MESH_QBITTORRENT_PASSWORD_FILE\":\"/var/lib/mesh/qbittorrent/password\",\"MESH_QBITTORRENT_URL\":\"http://127.0.0.1:8112\"},\"radarr\":{\"MESH_PLEX_BIND_FILE\":\"/var/lib/radarr/plex-api.json\",\"MESH_PLEX_TOKEN_FILE\":\"/var/lib/radarr/plex-api.secret\",\"MESH_RADARR_API_KEY_FILE\":\"/var/lib/mesh/radarr/api-key\",\"MESH_RADARR_CONFIG_DIR\":\"/var/lib/radarr/config\",\"MESH_RADARR_URL\":\"http://127.0.0.1:20019\"},\"redis\":{\"MESH_PROVISION_PASSWORD_FILE\":\"/var/lib/redis/default.secret\",\"MESH_PROVISION_REDIS\":\"127.0.0.1:20003\",\"MESH_RECEIVES\":\"/var/lib/redis/grants/mesh.json\"},\"restic\":{\"MESH_BACKUP_DATA\":\"/var/lib/restic/data.conf\",\"MESH_BACKUP_DECLARED\":\"/var/lib/restic/backups.conf\",\"MESH_BACKUP_PASSWORD_FILE\":\"/var/lib/restic/repository.secret\",\"MESH_BACKUP_REPOSITORY\":\"/storage/restic\",\"MESH_BACKUP_STATE\":\"/var/lib/restic\"},\"searxng\":{\"MESH_SEARXNG_CONFIG_FILE\":\"/var/lib/mesh/searxng/config.json\",\"MESH_SEARXNG_URL\":\"http://127.0.0.1:20000\"},\"sonarr\":{\"MESH_PLEX_BIND_FILE\":\"/var/lib/sonarr/plex-api.json\",\"MESH_PLEX_TOKEN_FILE\":\"/var/lib/sonarr/plex-api.secret\",\"MESH_SONARR_API_KEY_FILE\":\"/var/lib/mesh/sonarr/api-key\",\"MESH_SONARR_CONFIG_DIR\":\"/var/lib/sonarr/config\",\"MESH_SONARR_RECYCLE_DIR\":\"/var/lib/sonarr/recycle-bin\",\"MESH_SONARR_URL\":\"http://127.0.0.1:20018\"},\"systemd\":{\"MESH_SYSTEMD_STATE\":\"/var/lib/mesh/systemd\"},\"tautulli\":{\"MESH_TAUTULLI_CONFIG_DIR\":\"/var/lib/tautulli/config\",\"MESH_TAUTULLI_CONFIG_FILE\":\"/var/lib/mesh/tautulli/config.json\",\"MESH_TAUTULLI_CONTAINER\":\"tautulli\",\"MESH_TAUTULLI_URL\":\"http://127.0.0.1:20023\"},\"unifi\":{\"MESH_UNIFI_CONFIG_FILE\":\"/var/lib/mesh/unifi/config.json\",\"MESH_UNIFI_URL\":\"https://127.0.0.1:8443\"},\"youtube\":{\"MESH_YOUTUBE_DOWNLOADS\":\"/storage/downloads/youtube\",\"MESH_YOUTUBE_SETTINGS\":\"/var/lib/youtube/settings.json\"},\"zsh\":{\"ZSH_TOOLS_CONF\":\"/var/lib/zsh/tools.conf\"}}", + "MESH_TOOL_MODULES": "avahi=/var/lib/mesh/bundles/avahi/tools/avahi-tools,baserow=/var/lib/mesh/bundles/baserow/tools/tools/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/index.serve.mjs,bazarr=/var/lib/mesh/bundles/bazarr/code/tools/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/index.serve.mjs,bookshelf=/var/lib/mesh/bundles/bookshelf/code/tools/index.serve.mjs,claude-code=/var/lib/mesh/bundles/claude-code/tools/claude-code,dbus=/var/lib/mesh/bundles/dbus/tools-go/dbus-tools,disk-load=/var/lib/mesh/bundles/disk-load/tools-go/disk-load,dnsmasq=/var/lib/mesh/bundles/dnsmasq/tools/dnsmasq-tools,docker=/var/lib/mesh/bundles/docker/tools/docker-tools,fail2ban=/var/lib/mesh/bundles/fail2ban/tools/fail2ban-tools,grafana=/var/lib/mesh/bundles/grafana/code/index.serve.mjs,grafana=/var/lib/mesh/bundles/grafana/code/tools/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/index.serve.mjs,home-assistant=/var/lib/mesh/bundles/home-assistant/code/tools/index.serve.mjs,hostname=/var/lib/mesh/bundles/hostname/tools/hostname-tools,icecast=/var/lib/mesh/bundles/icecast/code/index.serve.mjs,icecast=/var/lib/mesh/bundles/icecast/code/tools/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/provisioner/index.serve.mjs,influxdb=/var/lib/mesh/bundles/influxdb/code/tools/index.serve.mjs,jackett=/var/lib/mesh/bundles/jackett/code/tools/index.serve.mjs,kernel=/var/lib/mesh/bundles/kernel/tools/kernel-tools,kometa=/var/lib/mesh/bundles/kometa/code/tools/index.serve.mjs,letta=/var/lib/mesh/bundles/letta/tools/tools/index.serve.mjs,lidarr=/var/lib/mesh/bundles/lidarr/tools-go/lidarr,localization=/var/lib/mesh/bundles/localization/tools/localization-tools,logrotate=/var/lib/mesh/bundles/logrotate/tools/logrotate-tools,media-importer=/var/lib/mesh/bundles/media-importer/tools/media-importer,mesh-files=/var/lib/mesh/bundles/mesh-files/tools/mesh-files,mosquitto=/var/lib/mesh/bundles/mosquitto/code/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/provisioner/index.serve.mjs,mosquitto=/var/lib/mesh/bundles/mosquitto/code/tools/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/provisioner/index.serve.mjs,mssql=/var/lib/mesh/bundles/mssql/code/tools/index.serve.mjs,netcheck=/var/lib/mesh/bundles/netcheck/tools-go/netcheck,netcheck=/var/lib/mesh/bundles/netcheck/tools-typescript/tools/index.serve.mjs,networkmanager=/var/lib/mesh/bundles/networkmanager/tools/uplink-tools,nfs-server=/var/lib/mesh/bundles/nfs-server/tools/nfs-server,nftables=/var/lib/mesh/bundles/nftables/tools/tools/index.serve.mjs,nodered=/var/lib/mesh/bundles/nodered/code/tools/index.serve.mjs,nzbgeek=/var/lib/mesh/bundles/nzbgeek/tools/nzbgeek,nzbget=/var/lib/mesh/bundles/nzbget/code/nzbget,ombi=/var/lib/mesh/bundles/ombi/code/index.serve.mjs,ombi=/var/lib/mesh/bundles/ombi/code/tools/index.serve.mjs,pacman=/var/lib/mesh/bundles/pacman/tools/pacman-tools,plex=/var/lib/mesh/bundles/plex/code/plex,postgres=/var/lib/mesh/bundles/postgres/code/postgres-provider,power=/var/lib/mesh/bundles/power/tools-go/power,qbittorrent=/var/lib/mesh/bundles/qbittorrent/code/qbittorrent,radarr=/var/lib/mesh/bundles/radarr/tools-go/radarr,redis=/var/lib/mesh/bundles/redis/code/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/provisioner/index.serve.mjs,redis=/var/lib/mesh/bundles/redis/code/tools/index.serve.mjs,restic=/var/lib/mesh/bundles/restic/tools/restic-backups,route-proxy=/var/lib/mesh/bundles/route-proxy/provider/route-provider,searxng=/var/lib/mesh/bundles/searxng/tools/tools/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/index.serve.mjs,sonarr=/var/lib/mesh/bundles/sonarr/code/tools/index.serve.mjs,ssh-client=/var/lib/mesh/bundles/ssh-client/tools/ssh-client-tools,sudo=/var/lib/mesh/bundles/sudo/tools/sudo-tools,systemd=/var/lib/mesh/bundles/systemd/tools/systemd-tools,tautulli=/var/lib/mesh/bundles/tautulli/code/index.serve.mjs,tautulli=/var/lib/mesh/bundles/tautulli/code/tools/index.serve.mjs,time-sync=/var/lib/mesh/bundles/time-sync/tools/time-sync-tools,unifi=/var/lib/mesh/bundles/unifi/tools/tools/index.serve.mjs,youtube=/var/lib/mesh/bundles/youtube/tools/youtube,zsh=/var/lib/mesh/bundles/zsh/tools/tools/index.serve.mjs" + }, + "id": "node-tools.runtime", + "module": "node-tools", + "name": "node-tools", + "restart-on": [ + "avahi.bundle-tools", + "baserow.bundle-tools", + "bazarr.bundle-code", + "bookshelf.bundle-code", + "claude-code.bundle-tools", + "dbus.bundle-tools-go", + "disk-load.bundle-tools-go", + "dnsmasq.bundle-tools", + "docker.bundle-tools", + "fail2ban.bundle-tools", + "grafana.bundle-code", + "home-assistant.bundle-code", + "hostname.bundle-tools", + "icecast.bundle-code", + "influxdb.bundle-code", + "jackett.bundle-code", + "kernel.bundle-tools", + "kometa.bundle-code", + "letta.bundle-tools", + "lidarr.bundle-tools-go", + "localization.bundle-tools", + "logrotate.bundle-tools", + "media-importer.bundle-tools", + "mesh-files.bundle-tools", + "mosquitto.bundle-code", + "mssql.bundle-code", + "netcheck.bundle-tools-go", + "netcheck.bundle-tools-typescript", + "networkmanager.bundle-tools", + "nfs-server.bundle-tools", + "nftables.bundle-tools", + "node-tools.needs-broker", + "nodered.bundle-code", + "nzbgeek.bundle-tools", + "nzbget.bundle-code", + "ombi.bundle-code", + "pacman.bundle-tools", + "plex.bundle-code", + "postgres.bundle-code", + "power.bundle-tools-go", + "qbittorrent.bundle-code", + "radarr.bundle-tools-go", + "redis.bundle-code", + "restic.bundle-tools", + "route-proxy.bundle-provider", + "searxng.bundle-tools", + "sonarr.bundle-code", + "ssh-client.bundle-tools", + "sudo.bundle-tools", + "systemd.bundle-tools", + "tautulli.bundle-code", + "time-sync.bundle-tools", + "unifi.bundle-tools", + "youtube.bundle-tools", + "zsh.bundle-tools", + "baserow.runtime-config", + "bazarr.runtime-config", + "claude-code.facts", + "claude-code.settings", + "grafana.runtime-config", + "home-assistant.needs-token", + "home-assistant.runtime-config", + "icecast.runtime-config", + "icecast.secret-admin", + "influxdb.needs-admin-token", + "influxdb.received-influxdb-api", + "influxdb.runtime-config", + "jackett.needs-api-key", + "jackett.runtime-config", + "kometa.settings", + "letta.runtime-config", + "lidarr.bound-plex-api", + "lidarr.needs-api-key", + "lidarr.secret-plex-api", + "media-importer.settings", + "mosquitto.needs-admin", + "mosquitto.received-mqtt-topic", + "mssql.needs-reader", + "mssql.needs-sa", + "mssql.received-mssql-database", + "nodered.runtime-config", + "nzbgeek.needs-api-key", + "nzbgeek.settings", + "nzbget.needs-password", + "nzbget.runtime-config", + "ombi.needs-api-key", + "ombi.runtime-config", + "pacman.keeper-config", + "postgres.consumers", + "postgres.needs-reader", + "postgres.needs-superuser", + "postgres.received-postgres-database", + "qbittorrent.needs-password", + "qbittorrent.runtime-config", + "radarr.bound-plex-api", + "radarr.needs-api-key", + "radarr.secret-plex-api", + "redis.received-redis-cache", + "redis.secret-secret", + "restic.data-declared", + "restic.declared", + "restic.needs-repository", + "searxng.runtime-config", + "sonarr.bound-plex-api", + "sonarr.needs-api-key", + "sonarr.secret-plex-api", + "tautulli.runtime-config", + "unifi.runtime-config", + "youtube.settings", + "zsh.tools-conf" + ], + "run": [ + "./node-tools" + ], + "source": "http://novox.internal:5100/v2/node-tools/runtime/blobs/sha256:ef790876783d817903b337b8418b1eb21fb49d34128a390bbf7c3d08a4b2be8a", + "type": "process", + "user": "ace" + } + ] + } + }, + { + "name": "a module the engine could not compose", + "of": "state:composed/..", + "given": { + "v": 1, + "node": "ace", + "module": "nzbget", + "fp": "src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275", + "resources": [], + "refused": "a secret the vault does not hold" + } + }, + { + "name": "the node's own composition", + "of": "state:composed/.node", + "given": { + "v": 1, + "node": "ace", + "resources": [], + "own": { + "adoption": { + "taken": [ + "avahi" + ], + "untaken": { + "plex": [ + "plex.bundle-code" + ] + } + }, + "identities": [ + { + "module": "plex", + "name": "plex", + "uid": 1001, + "gid": 1001, + "groups": [ + 1002 + ], + "runner": "plex" + } + ], + "groups": [ + { + "name": "mesh-media", + "gid": 1002 + } + ], + "libraries": [ + { + "library": "movies", + "root": "/srv/media/movies", + "group": "mesh-media", + "gid": 1002, + "home": false, + "taken": true, + "paths": [ + "/srv/media/movies" + ] + } + ], + "provisioner_account": "mesh-provisioner-client" + } + } + }, + { + "name": "a module running (typed from the engine's own values; no read tool answers a have key)", + "of": "state:have/.", + "given": { + "v": 1, + "fp": "src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275", + "composed": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "build": "b1", + "phase": "running", + "health": "ok", + "sealed": [ + "web.secret" + ], + "sealed-at": { + "web.secret#sealed": "box1" + } + } + }, + { + "name": "a failed apply waiting on a store's holder (typed from the engine's values)", + "of": "state:have/.", + "given": { + "v": 1, + "fp": "src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275", + "composed": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "phase": "failed", + "health": "failed", + "detail": "the pull met a store that is not settled", + "pull-wait": { + "image": "registry.example/web:1", + "seat": "container-image-registry", + "holder": "gitea", + "began": "2026-11-05T12:00:00Z", + "retried": "the holder settled", + "retried-at": "2026-11-05T12:05:00Z" + } + } + }, + { + "name": "a move waiting for a person's yes", + "of": "state:have/.", + "given": { + "v": 1, + "fp": "src2:62756c6c6462756c6c6462756c6c6462756c6c6462756c6c6462756c6c646275", + "composed": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "phase": "waiting-yes", + "health": "ok", + "judgement": "recreates", + "waits": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "build-alone": true, + "kept": [ + "tool runner not declared" + ] + } + }, + { + "name": "the bus module's have carries the user list written (typed from the engine's values)", + "of": "state:have/.", + "given": { + "v": 1, + "phase": "running", + "health": "ok", + "user-list": { + "source": "user-list", + "revision": 7, + "wanted": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "users": 3, + "at": "2026-11-05T12:00:00Z", + "follows": "grants", + "key": "-----BEGIN PUBLIC KEY-----", + "handed": { + "from": "ace", + "to": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "users": 3, + "box": { + "v": 1, + "eph": "e", + "iv": "i", + "tag": "t", + "ct": "c" + }, + "at": "2026-11-05T12:00:00Z" + } + } + } + }, + { + "name": "a node's own facts with its record and an answer to a candidate (typed from the engine's values)", + "of": "state:have/.node", + "given": { + "v": 1, + "composed": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "record": { + "v": 1, + "node": "ace", + "tunnel-key": "k", + "capabilities": [ + "docker" + ], + "networks": [ + { + "link": "eth0", + "network": "192.0.2.0/24" + } + ], + "networks-said": true, + "contract": 3 + }, + "composes": "own", + "candidates": { + "p1": { + "proposal": "p1", + "revision": 4, + "verdict": "differs", + "same": [ + "web" + ], + "differs": [ + { + "module": "api", + "composed": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "running": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "why": "a file differs", + "bodies": [ + { + "id": "api.cfg", + "why": "content", + "paths": [ + "/etc/api.conf" + ] + } + ] + } + ], + "added": [ + { + "module": "db", + "unissued": [ + "db.secret" + ] + } + ], + "removed": [ + "old" + ], + "refusals": [ + { + "module": "mail", + "why": "a value is missing", + "today": "runs" + } + ] + } + } + } + }, + { + "name": "a module's data items on a node (typed from the engine's values)", + "of": "state:data/.", + "given": { + "v": 1, + "node": "ace", + "module": "postgres", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "items": [ + { + "module": "postgres", + "item": "cluster", + "class": "database", + "path": "/var/lib/postgres", + "covered_by": "/var/lib/postgres", + "protection": "backup", + "measure": "dataset" + } + ] + } + }, + { + "name": "a node's health (typed from the engine's values)", + "of": "state:health/", + "given": { + "v": 1, + "node": "ace", + "network": { + "state": "running", + "since": "2026-11-05T10:00:00Z", + "parts": [ + { + "part": "resolver", + "state": "running", + "since": "2026-11-05T10:00:00Z" + } + ] + }, + "peers": [ + { + "key": "k", + "addresses": [ + "198.51.100.1" + ], + "seen": true + } + ], + "units": { + "state": "degraded", + "failed": [ + { + "unit": "x.service", + "scope": "system", + "result": "exit-code", + "since": "2026-11-05T11:00:00Z" + } + ] + }, + "relogin": [ + { + "account": "web", + "reason": "password changed", + "since": "2026-11-05T11:00:00Z" + } + ], + "contract": 3, + "at": "2026-11-05T12:00:00Z" + } } ] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index fec0fbb..237a651 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -754,58 +754,84 @@ }, "composed": { "type": "string", - "description": "the composition's digest" + "description": "the digest of the composition the node runs" }, "build": { "type": "string" }, "phase": { "type": "string", - "description": "how far it has come" + "description": "how far it has come", + "enum": [ + "backing-up", + "applying", + "checking", + "running", + "failed", + "removed", + "held", + "waiting-yes" + ] }, "health": { "type": "string", - "description": "how it is" + "description": "what the module's own check says", + "enum": [ + "ok", + "failed", + "waits", + "unknown" + ] }, "detail": { "type": "string" }, "user-list": { - "type": "object", - "description": "the user list written" + "$ref": "#/defs/UserListWritten" }, "judgement": { - "type": "string" + "type": "string", + "description": "in phase waiting-yes, the judgement of the move", + "enum": [ + "recreates", + "moves-data", + "cannot-judge" + ] }, "waits": { "type": "string", - "description": "what it waits for" + "description": "in phase waiting-yes, the digest of the composition that waits" }, "build-alone": { - "type": "boolean" + "type": "boolean", + "description": "the move changes the build alone" }, "kept": { "type": "array", "items": { "type": "string" - } + }, + "description": "what the node keeps of the module as it runs" }, "sealed": { "type": "array", "items": { "type": "string" - } + }, + "description": "each value sealed to the node's key" }, "sealed-at": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "the sealed values by slot" }, "pull-wait": { - "type": "object", - "description": "a pull the engine waits for" + "$ref": "#/defs/PullWait" }, "handed": { - "type": "object", - "description": "the accounts handed" + "$ref": "#/defs/AccountsHanded" } }, "required": [ @@ -826,7 +852,8 @@ "type": "integer" }, "composed": { - "type": "string" + "type": "string", + "description": "the digest of the node's own composition it runs" }, "windows": { "type": "object" @@ -844,8 +871,7 @@ "type": "object" }, "record": { - "type": "object", - "description": "the node's record: its tunnel and certificate" + "$ref": "#/defs/NodeRecord" }, "sealed": { "type": "array", @@ -854,17 +880,21 @@ } }, "sealed-at": { - "type": "object" + "type": "object", + "additionalProperties": { + "type": "string" + } }, "candidates": { "type": "object", "additionalProperties": { - "type": "object" - } + "$ref": "#/defs/CandidateAnswer" + }, + "description": "the node's answer to each placement candidate naming it, by proposal id" }, "composes": { "type": "string", - "description": "own, or the controller's" + "description": "own, or none (empty)" }, "composes-why": { "type": "string" @@ -890,53 +920,96 @@ "type": "object", "description": "one composition of a module on a node under a fingerprint", "properties": { - "V": { + "v": { "type": "integer" }, - "Node": { + "node": { "type": "string" }, - "Module": { + "module": { "type": "string" }, - "FP": { + "fp": { "type": "string" }, - "Data": { - "type": "boolean" + "data": { + "type": "boolean", + "description": "the module declares data" }, - "Resources": { + "resources": { "type": "array", "items": { "type": "object" - } + }, + "description": "the declaration resources, each saying its module" }, - "Filled": { + "filled_files": { "type": "array", "items": { "type": "string" - } + }, + "description": "the files the provisioner fills" }, - "Own": { + "plan": { "type": "object", - "description": "the node's own adoption, identities, groups, libraries and provisioner account" + "description": "the module's whole provisioning plan, catalogue.Plan, kept whole: it is the manifest layer's type" }, - "Plan": { - "type": "object", - "description": "the module's plan" - }, - "Refused": { + "refused": { "type": "string", - "description": "why the composition was refused" + "description": "why the module cannot be composed at this fingerprint" } }, "required": [ - "Node", - "Module", - "FP" + "v", + "node", + "module", + "fp", + "resources" ] }, "description": "one composition" + }, + { + "key": ".node", + "value": { + "type": "object", + "description": "the node's own composition: its resources and parts", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "data": { + "type": "boolean", + "description": "the module declares data" + }, + "resources": { + "type": "array", + "items": { + "type": "object" + }, + "description": "the declaration resources, each saying its module" + }, + "filled_files": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the files the provisioner fills" + }, + "own": { + "$ref": "#/defs/NodeOwn" + } + }, + "required": [ + "v", + "node", + "resources" + ] + }, + "description": "the node's own composition" } ], "description": "the compositions of one node, kept in a bucket per node named composed-" @@ -966,11 +1039,12 @@ "items": { "type": "array", "items": { - "type": "object" + "$ref": "#/defs/PlacedDataItem" } } }, "required": [ + "v", "node", "module", "digest", @@ -1048,36 +1122,36 @@ "type": "string" }, "network": { - "type": "object" + "$ref": "#/defs/NetworkHealth" }, "peers": { "type": "array", "items": { - "type": "object" + "$ref": "#/defs/PeerSeen" } }, "units": { - "type": "object" + "$ref": "#/defs/UnitsHealth" }, "relogin": { "type": "array", "items": { - "type": "object" + "$ref": "#/defs/NodeAccount" } }, "found": { "type": "array", "items": { - "type": "object" + "$ref": "#/defs/NodeDirectory" } }, "fallback": { - "type": "object" + "$ref": "#/defs/LoginFallback" }, "root": { "type": "array", "items": { - "type": "object" + "$ref": "#/defs/RootAccountVerdict" } }, "contract": { @@ -1085,6 +1159,7 @@ }, "at": { "type": "string", + "description": "when", "format": "date-time" } }, @@ -1703,6 +1778,874 @@ "at", "said" ] + }, + "UserListWritten": { + "type": "object", + "description": "the bus's user list as the engine of the bus node wrote it to the bus module's file", + "properties": { + "source": { + "type": "string", + "description": "the key the list was taken from" + }, + "revision": { + "type": "integer" + }, + "wanted": { + "type": "string", + "description": "the digest of the list the source holds, sha256:" + }, + "digest": { + "type": "string", + "description": "the digest of the file on the machine after the write" + }, + "users": { + "type": "integer" + }, + "at": { + "type": "string", + "description": "when", + "format": "date-time" + }, + "error": { + "type": "string", + "description": "why a write failed" + }, + "follows": { + "type": "string", + "description": "which list the node follows now" + }, + "key": { + "type": "string", + "description": "the bus node's sealing key, its public half" + }, + "unwatched": { + "type": "string", + "description": "why the bus node does not watch the grants part's key now" + }, + "handed": { + "$ref": "#/defs/AccountsHanded" + } + }, + "required": [ + "source", + "revision", + "wanted", + "users", + "at" + ] + }, + "AccountsHanded": { + "type": "object", + "description": "the hashes of the users of the list, sealed to the vault", + "properties": { + "from": { + "type": "string" + }, + "to": { + "type": "string", + "description": "the fingerprint of the vault's key" + }, + "users": { + "type": "integer" + }, + "box": { + "$ref": "#/defs/SealedBox" + }, + "at": { + "type": "string", + "description": "when", + "format": "date-time" + } + }, + "required": [ + "from", + "to", + "users", + "box", + "at" + ] + }, + "SealedBox": { + "type": "object", + "description": "a value sealed to a key", + "properties": { + "v": { + "type": "integer" + }, + "eph": { + "type": "string" + }, + "iv": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "ct": { + "type": "string" + } + }, + "required": [ + "v", + "eph", + "iv", + "tag", + "ct" + ] + }, + "PullWait": { + "type": "object", + "description": "the pull from a store a failed apply waits on", + "properties": { + "image": { + "type": "string" + }, + "seat": { + "type": "string", + "description": "the seat the pull came through" + }, + "holder": { + "type": "string", + "description": "that seat's holder, whose settling the apply waits for" + }, + "began": { + "type": "string", + "description": "when the pull began", + "format": "date-time" + }, + "retried": { + "type": "string", + "description": "what triggered the last try again, in words" + }, + "retried-at": { + "type": "string", + "description": "when the apply was last tried again", + "format": "date-time" + } + }, + "required": [ + "image", + "holder", + "began" + ] + }, + "NodeRecord": { + "type": "object", + "description": "what the machine observes of itself, for every node and composing part to read", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "tunnel-key": { + "type": "string" + }, + "capabilities": { + "type": "array", + "items": { + "type": "string" + } + }, + "sealing-key": { + "type": "string" + }, + "serving-key": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "outward-links": { + "type": "array", + "items": { + "type": "string" + } + }, + "networks": { + "type": "array", + "items": { + "$ref": "#/defs/AttachedNetwork" + } + }, + "networks-said": { + "type": "boolean" + }, + "contract": { + "type": "integer" + }, + "authority": { + "type": "string" + }, + "trusts": { + "type": "array", + "items": { + "type": "string" + } + }, + "manifest-reads": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "v", + "node" + ] + }, + "AttachedNetwork": { + "type": "object", + "description": "a network attached to a link", + "properties": { + "link": { + "type": "string" + }, + "network": { + "type": "string" + } + }, + "required": [ + "link", + "network" + ] + }, + "CandidateAnswer": { + "type": "object", + "description": "the node's answer to one placement candidate naming it", + "properties": { + "proposal": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "verdict": { + "type": "string", + "description": "same, differs or refused", + "enum": [ + "same", + "differs", + "refused" + ] + }, + "same": { + "type": "array", + "items": { + "type": "string" + }, + "description": "modules whose composition is the one running" + }, + "differs": { + "type": "array", + "items": { + "$ref": "#/defs/CandidateDiffers" + } + }, + "added": { + "type": "array", + "items": { + "$ref": "#/defs/CandidateAdded" + } + }, + "removed": { + "type": "array", + "items": { + "type": "string" + }, + "description": "modules the candidate removes" + }, + "refusals": { + "type": "array", + "items": { + "$ref": "#/defs/CandidateRefusal" + } + }, + "held": { + "type": "array", + "items": { + "$ref": "#/defs/CandidateRefusal" + } + }, + "refused": { + "type": "string", + "description": "why the node cannot compose with the candidate at all" + }, + "returns": { + "type": "string" + }, + "note": { + "type": "string" + }, + "values_differ": { + "type": "integer" + } + }, + "required": [ + "proposal", + "revision", + "verdict" + ] + }, + "CandidateDiffers": { + "type": "object", + "description": "a module whose composition with the candidate differs from the one running", + "properties": { + "module": { + "type": "string" + }, + "composed": { + "type": "string" + }, + "running": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "why": { + "type": "string" + }, + "bodies": { + "type": "array", + "items": { + "$ref": "#/defs/CandidateBodies" + } + } + }, + "required": [ + "module", + "why" + ] + }, + "CandidateBodies": { + "type": "object", + "description": "the bodies of one resource that differ", + "properties": { + "id": { + "type": "string" + }, + "why": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "composed": { + "type": "string" + }, + "running": { + "type": "string" + } + }, + "required": [ + "id", + "why" + ] + }, + "CandidateAdded": { + "type": "object", + "description": "a module the candidate adds", + "properties": { + "module": { + "type": "string" + }, + "composed": { + "type": "string" + }, + "unissued": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "module" + ] + }, + "CandidateRefusal": { + "type": "object", + "description": "a module refused or held with the candidate", + "properties": { + "module": { + "type": "string" + }, + "why": { + "type": "string" + }, + "today": { + "type": "string" + } + }, + "required": [ + "module", + "why" + ] + }, + "NodeOwn": { + "type": "object", + "description": "what a node's own composition carries beside its resources", + "properties": { + "adoption": { + "$ref": "#/defs/Adoption" + }, + "identities": { + "type": "array", + "items": { + "$ref": "#/defs/DeclaredIdentity" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/defs/DeclaredGroup" + } + }, + "libraries": { + "type": "array", + "items": { + "$ref": "#/defs/DeclaredLibrary" + } + }, + "provisioner_account": { + "type": "string", + "description": "the account the provisioner's client runs as on the node" + } + } + }, + "Adoption": { + "type": "object", + "description": "the node is adopted and which of its modules are taken", + "properties": { + "taken": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the modules taken" + }, + "untaken": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": "by module, the resources left untaken" + } + }, + "required": [ + "taken" + ] + }, + "DeclaredIdentity": { + "type": "object", + "description": "the identity of a module assigned to the node", + "properties": { + "module": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uid": { + "type": "integer" + }, + "gid": { + "type": "integer" + }, + "groups": { + "type": "array", + "items": { + "type": "integer" + } + }, + "runner": { + "type": "string" + } + }, + "required": [ + "module", + "name", + "uid", + "gid" + ] + }, + "DeclaredGroup": { + "type": "object", + "description": "a group", + "properties": { + "name": { + "type": "string" + }, + "gid": { + "type": "integer" + } + }, + "required": [ + "name", + "gid" + ] + }, + "DeclaredLibrary": { + "type": "object", + "description": "a library the node is reached by", + "properties": { + "library": { + "type": "string" + }, + "root": { + "type": "string" + }, + "group": { + "type": "string" + }, + "gid": { + "type": "integer" + }, + "home": { + "type": "boolean" + }, + "taken": { + "type": "boolean" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "runner": { + "type": "string" + } + }, + "required": [ + "library", + "root", + "group", + "gid", + "home" + ] + }, + "PlacedDataItem": { + "type": "object", + "description": "one data item a module declares, as placed on a node", + "properties": { + "module": { + "type": "string" + }, + "item": { + "type": "string" + }, + "class": { + "type": "string" + }, + "path": { + "type": "string", + "description": "the item's place on the node" + }, + "covered_by": { + "type": "string", + "description": "the path whose snapshot keeps it, empty when it is not backed up" + }, + "protection": { + "type": "string", + "description": "backup, redundancy, both or none" + }, + "measure": { + "type": "string", + "description": "walk, dataset or shallow" + } + }, + "required": [ + "module", + "item", + "class", + "path" + ] + }, + "NetworkHealth": { + "type": "object", + "description": "the node's network", + "properties": { + "state": { + "type": "string", + "description": "running, degraded or unknown" + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/defs/NetworkPart" + } + } + }, + "required": [ + "state", + "since", + "parts" + ] + }, + "NetworkPart": { + "type": "object", + "description": "one part of the node's network", + "properties": { + "part": { + "type": "string" + }, + "state": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "said": { + "type": "string" + }, + "writer": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "toward": { + "type": "array", + "items": { + "type": "string" + } + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + }, + "streak": { + "type": "integer" + } + }, + "required": [ + "part", + "state", + "since" + ] + }, + "UnitsHealth": { + "type": "object", + "description": "the node's units", + "properties": { + "state": { + "type": "string" + }, + "failed": { + "type": "array", + "items": { + "$ref": "#/defs/FailedUnit" + } + }, + "unread": { + "type": "array", + "items": { + "type": "string" + } + }, + "away": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "state", + "failed" + ] + }, + "FailedUnit": { + "type": "object", + "description": "a unit that failed", + "properties": { + "unit": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "load": { + "type": "string" + }, + "result": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + } + }, + "required": [ + "unit", + "scope", + "since" + ] + }, + "PeerSeen": { + "type": "object", + "description": "a tunnel peer and whether it is seen", + "properties": { + "key": { + "type": "string" + }, + "addresses": { + "type": "array", + "items": { + "type": "string" + } + }, + "seen": { + "type": "boolean" + } + }, + "required": [ + "key", + "seen" + ] + }, + "NodeAccount": { + "type": "object", + "description": "an account the node needs to log in again", + "properties": { + "account": { + "type": "string" + }, + "module": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + } + }, + "required": [ + "account", + "reason", + "since" + ] + }, + "NodeDirectory": { + "type": "object", + "description": "a directory found where the node expected none", + "properties": { + "module": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "path": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + } + }, + "required": [ + "module", + "resource", + "path", + "reason", + "since" + ] + }, + "LoginFallback": { + "type": "object", + "description": "a login that fell back to another node", + "properties": { + "node": { + "type": "string" + }, + "from": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "description": "when", + "format": "date-time" + } + }, + "required": [ + "node", + "from", + "why", + "at" + ] + }, + "RootAccountVerdict": { + "type": "object", + "description": "the engine's verdict of whether an account's resource is root-free, as the node's health lists it", + "properties": { + "account": { + "type": "string" + }, + "module": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "state": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "since": { + "type": "string", + "description": "since when", + "format": "date-time" + } + }, + "required": [ + "account", + "module", + "resource", + "state", + "since" + ] } } } diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index a03d9ab..dbc0378 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -112,6 +112,22 @@ export function parseComposedByNodeModuleFingerprintKey(key: string): { node: st return { node, module, fingerprint }; } +/** The key of one value of the state composed under the form ".node". */ +export function composedByNodeNodeKey(node: string): string { + return node + ".node"; +} + +/** The parts of a key of the form ".node", or undefined for a key of another form. */ +export function parseComposedByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + const at0 = rest.indexOf(".node"); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 5); + if (rest !== "") return undefined; + return { node }; +} + /** The key of one value of the state data under the form ".". */ export function dataByNodeModuleKey(node: string, module: string): string { return node + "." + module; @@ -176,6 +192,17 @@ export function parseFindingsByKeyKey(key: string): { key: string } | undefined return { key }; } +/** the hashes of the users of the list, sealed to the vault */ +export interface AccountsHanded { + /** when */ + "at": string; + "box": SealedBox; + "from": string; + /** the fingerprint of the vault's key */ + "to": string; + "users": number; +} + /** one act asked through the seat, as acts reads it (ADR 0563 §9) */ export interface Act { /** the act: hand-over, access-act, judge-directories or setuid-search */ @@ -242,6 +269,14 @@ export interface Acts { "node": string; } +/** the node is adopted and which of its modules are taken */ +export interface Adoption { + /** the modules taken */ + "taken": string[]; + /** by module, the resources left untaken */ + "untaken"?: Record; +} + /** the answer of an act the operator is asked */ export interface Asked { /** the ask's id: acts shows where it stands */ @@ -252,6 +287,105 @@ export interface Asked { "says": string; } +/** a network attached to a link */ +export interface AttachedNetwork { + "link": string; + "network": string; +} + +/** a module the candidate adds */ +export interface CandidateAdded { + "composed"?: string; + "module": string; + "unissued"?: string[]; +} + +/** the node's answer to one placement candidate naming it */ +export interface CandidateAnswer { + "added"?: CandidateAdded[]; + "differs"?: CandidateDiffers[]; + "held"?: CandidateRefusal[]; + "note"?: string; + "proposal": string; + "refusals"?: CandidateRefusal[]; + /** why the node cannot compose with the candidate at all */ + "refused"?: string; + /** modules the candidate removes */ + "removed"?: string[]; + "returns"?: string; + "revision": number; + /** modules whose composition is the one running */ + "same"?: string[]; + "values_differ"?: number; + /** same, differs or refused */ + "verdict": "same" | "differs" | "refused"; +} + +/** the bodies of one resource that differ */ +export interface CandidateBodies { + "composed"?: string; + "id": string; + "paths"?: string[]; + "running"?: string; + "why": string; +} + +/** a module whose composition with the candidate differs from the one running */ +export interface CandidateDiffers { + "bodies"?: CandidateBodies[]; + "composed"?: string; + "module": string; + "resource"?: string; + "running"?: string; + "why": string; +} + +/** a module refused or held with the candidate */ +export interface CandidateRefusal { + "module": string; + "today"?: string; + "why": string; +} + +/** a group */ +export interface DeclaredGroup { + "gid": number; + "name": string; +} + +/** the identity of a module assigned to the node */ +export interface DeclaredIdentity { + "gid": number; + "groups"?: number[]; + "module": string; + "name": string; + "runner"?: string; + "uid": number; +} + +/** a library the node is reached by */ +export interface DeclaredLibrary { + "gid": number; + "group": string; + "home": boolean; + "library": string; + "paths"?: string[]; + "root": string; + "runner"?: string; + "taken"?: boolean; +} + +/** a unit that failed */ +export interface FailedUnit { + "load"?: string; + "resource"?: string; + "result"?: string; + "scope": string; + /** since when */ + "since": string; + "unit": string; +} + /** one answer the operator may give from a notification */ export interface FindingAction { "arguments"?: Record; @@ -280,6 +414,15 @@ export interface FindingSubject { "scope": string; } +/** a login that fell back to another node */ +export interface LoginFallback { + /** when */ + "at": string; + "from": string; + "node": string; + "why": string; +} + /** what a node-engine says when what it reads changed or its node returned (ADR 0618 §4) */ export interface ManifestReadsChanged { /** when it says it, RFC 3339 */ @@ -310,6 +453,115 @@ export interface Module { "words"?: string; } +/** the node's network */ +export interface NetworkHealth { + "parts": NetworkPart[]; + /** since when */ + "since": string; + /** running, degraded or unknown */ + "state": string; +} + +/** one part of the node's network */ +export interface NetworkPart { + "owner"?: string; + "part": string; + "reason"?: string; + "said"?: string; + /** since when */ + "since": string; + "state": string; + "streak"?: number; + "targets"?: string[]; + "toward"?: string[]; + "writer"?: string; +} + +/** an account the node needs to log in again */ +export interface NodeAccount { + "account": string; + "module"?: string; + "reason": string; + "resource"?: string; + /** since when */ + "since": string; +} + +/** a directory found where the node expected none */ +export interface NodeDirectory { + "module": string; + "path": string; + "reason": string; + "resource": string; + /** since when */ + "since": string; +} + +/** what a node's own composition carries beside its resources */ +export interface NodeOwn { + "adoption"?: Adoption; + "groups"?: DeclaredGroup[]; + "identities"?: DeclaredIdentity[]; + "libraries"?: DeclaredLibrary[]; + /** the account the provisioner's client runs as on the node */ + "provisioner_account"?: string; +} + +/** what the machine observes of itself, for every node and composing part to read */ +export interface NodeRecord { + "authority"?: string; + "capabilities"?: string[]; + "certificate"?: string; + "contract"?: number; + "manifest-reads"?: string[]; + "networks"?: AttachedNetwork[]; + "networks-said"?: boolean; + "node": string; + "outward-links"?: string[]; + "sealing-key"?: string; + "serving-key"?: string; + "trusts"?: string[]; + "tunnel-key"?: string; + "v": number; +} + +/** a tunnel peer and whether it is seen */ +export interface PeerSeen { + "addresses"?: string[]; + "key": string; + "seen": boolean; +} + +/** one data item a module declares, as placed on a node */ +export interface PlacedDataItem { + "class": string; + /** the path whose snapshot keeps it, empty when it is not backed up */ + "covered_by"?: string; + "item": string; + /** walk, dataset or shallow */ + "measure"?: string; + "module": string; + /** the item's place on the node */ + "path": string; + /** backup, redundancy, both or none */ + "protection"?: string; +} + +/** the pull from a store a failed apply waits on */ +export interface PullWait { + /** when the pull began */ + "began": string; + /** that seat's holder, whose settling the apply waits for */ + "holder": string; + "image": string; + /** what triggered the last try again, in words */ + "retried"?: string; + /** when the apply was last tried again */ + "retried-at"?: string; + /** the seat the pull came through */ + "seat"?: string; +} + /** one revision of a composition key */ export interface Revision { /** its digest */ @@ -326,6 +578,17 @@ export interface Revision { "revision"?: number; } +/** the engine's verdict of whether an account's resource is root-free, as the node's health lists it */ +export interface RootAccountVerdict { + "account": string; + "module": string; + "reason"?: string; + "resource": string; + /** since when */ + "since": string; + "state": string; +} + /** the engine's root-free verdict (ADR 0563 §6) */ export interface RootVerdict { /** the machine is root-free */ @@ -346,6 +609,15 @@ export interface Said { "said": string; } +/** a value sealed to a key */ +export interface SealedBox { + "ct": string; + "eph": string; + "iv": string; + "tag": string; + "v": number; +} + export interface SeatListStatus { /** the seat mesh-catalog has answered */ "heard": boolean; @@ -374,6 +646,37 @@ export interface SeatListHeld { "source": "trunk" | "bootstrap"; } +/** the node's units */ +export interface UnitsHealth { + "away"?: string[]; + "failed": FailedUnit[]; + "state": string; + "unread"?: string[]; +} + +/** the bus's user list as the engine of the bus node wrote it to the bus module's file */ +export interface UserListWritten { + /** when */ + "at": string; + /** the digest of the file on the machine after the write */ + "digest"?: string; + /** why a write failed */ + "error"?: string; + /** which list the node follows now */ + "follows"?: string; + "handed"?: AccountsHanded; + /** the bus node's sealing key, its public half */ + "key"?: string; + "revision": number; + /** the key the list was taken from */ + "source": string; + /** why the bus node does not watch the grants part's key now */ + "unwatched"?: string; + "users": number; + /** the digest of the list the source holds, sha256: */ + "wanted": string; +} + /** is what hand-over takes. */ export interface HandOverInput { /** the directory's absolute path, as the engine states it */ @@ -711,45 +1014,48 @@ export type ManifestReadsChangedEvent = ManifestReadsChanged; /** is one value of the state have under the key .. */ export interface HaveByNodeModuleValue { "build"?: string; + /** the move changes the build alone */ "build-alone"?: boolean; - /** the composition's digest */ + /** the digest of the composition the node runs */ "composed"?: string; "detail"?: string; /** the fingerprint held */ "fp"?: string; - /** the accounts handed */ - "handed"?: Record; - /** how it is */ - "health": string; - "judgement"?: string; + "handed"?: AccountsHanded; + /** what the module's own check says */ + "health": "ok" | "failed" | "waits" | "unknown"; + /** in phase waiting-yes, the judgement of the move */ + "judgement"?: "recreates" | "moves-data" | "cannot-judge"; + /** what the node keeps of the module as it runs */ "kept"?: string[]; /** how far it has come */ - "phase": string; - /** a pull the engine waits for */ - "pull-wait"?: Record; + "phase": "backing-up" | "applying" | "checking" | "running" | "failed" | "removed" | "held" | "waiting-yes"; + "pull-wait"?: PullWait; + /** each value sealed to the node's key */ "sealed"?: string[]; - "sealed-at"?: Record; - /** the user list written */ - "user-list"?: Record; + /** the sealed values by slot */ + "sealed-at"?: Record; + "user-list"?: UserListWritten; "v": number; - /** what it waits for */ + /** in phase waiting-yes, the digest of the composition that waits */ "waits"?: string; } /** is one value of the state have under the key .node. */ export interface HaveByNodeNodeValue { "busy"?: Record; - "candidates"?: Record>; + /** the node's answer to each placement candidate naming it, by proposal id */ + "candidates"?: Record; + /** the digest of the node's own composition it runs */ "composed"?: string; - /** own, or the controller's */ + /** own, or none (empty) */ "composes"?: string; "composes-why"?: string; "filters"?: Record; "profile"?: Record; - /** the node's record: its tunnel and certificate */ - "record"?: Record; + "record"?: NodeRecord; "sealed"?: string[]; - "sealed-at"?: Record; + "sealed-at"?: Record; "strays"?: Record; "v": number; "windows"?: Record; @@ -757,28 +1063,42 @@ export interface HaveByNodeNodeValue { /** is one value of the state composed under the key ... */ export interface ComposedByNodeModuleFingerprintValue { - "Data"?: boolean; - "FP": string; - "Filled"?: string[]; - "Module": string; - "Node": string; - /** the node's own adoption, identities, groups, libraries and provisioner account */ - "Own"?: Record; - /** the module's plan */ - "Plan"?: Record; - /** why the composition was refused */ - "Refused"?: string; - "Resources"?: Record[]; - "V"?: number; + /** the module declares data */ + "data"?: boolean; + /** the files the provisioner fills */ + "filled_files"?: string[]; + "fp": string; + "module": string; + "node": string; + /** the module's whole provisioning plan, catalogue.Plan, kept whole: it is the manifest layer's type */ + "plan"?: Record; + /** why the module cannot be composed at this fingerprint */ + "refused"?: string; + /** the declaration resources, each saying its module */ + "resources": Record[]; + "v": number; +} + +/** is one value of the state composed under the key .node. */ +export interface ComposedByNodeNodeValue { + /** the module declares data */ + "data"?: boolean; + /** the files the provisioner fills */ + "filled_files"?: string[]; + "node": string; + "own"?: NodeOwn; + /** the declaration resources, each saying its module */ + "resources": Record[]; + "v": number; } /** is one value of the state data under the key .. */ export interface DataByNodeModuleValue { "digest": string; - "items": Record[]; + "items": PlacedDataItem[]; "module": string; "node": string; - "v"?: number; + "v": number; } export interface PackagesByNodeValuePackagesItem { @@ -795,16 +1115,17 @@ export interface PackagesByNodeValue { /** is one value of the state health under the key . */ export interface HealthByNodeValue { + /** when */ "at": string; "contract"?: number; - "fallback"?: Record; - "found"?: Record[]; - "network"?: Record; + "fallback"?: LoginFallback; + "found"?: NodeDirectory[]; + "network"?: NetworkHealth; "node": string; - "peers"?: Record[]; - "relogin"?: Record[]; - "root"?: Record[]; - "units"?: Record; + "peers"?: PeerSeen[]; + "relogin"?: NodeAccount[]; + "root"?: RootAccountVerdict[]; + "units"?: UnitsHealth; "v": number; } -- 2.54.0 From ce9d39d3f0d631a5ae3eb2359fdcb62fd2ad92d4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:51:46 +0100 Subject: [PATCH 62/94] =?UTF-8?q?node-engine=20client:=20the=20codec,=20th?= =?UTF-8?q?e=20digest=20and=20the=20comparison=20of=20a=20composition,=20i?= =?UTF-8?q?n=20the=20field=20order=20its=20first=20writer=20used=20(ADR=20?= =?UTF-8?q?0634=20=C2=A71,=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/composed.go | 323 ++++++++++++++++++ go/clients/nodeengineclient/composed_test.go | 130 +++++++ .../testdata/captured-ace.json | 63 ++++ 3 files changed, 516 insertions(+) create mode 100644 go/clients/nodeengineclient/composed.go create mode 100644 go/clients/nodeengineclient/composed_test.go create mode 100644 go/clients/nodeengineclient/testdata/captured-ace.json diff --git a/go/clients/nodeengineclient/composed.go b/go/clients/nodeengineclient/composed.go new file mode 100644 index 0000000..b8f5356 --- /dev/null +++ b/go/clients/nodeengineclient/composed.go @@ -0,0 +1,323 @@ +package nodeengineclient + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "strings" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/declaration" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The compositions (novox/hq ADR 0539 clauses 1 to 4), the node-engine's state composed: its layout is the seat's contract +// (seat.ComposedByNodeModuleFingerprintValue, seat.ComposedByNodeNodeValue); this file is the codec and the digest the +// engine, the assigner, the grants part and the provisioner share. A composition's digest is a function of its content +// alone, so the encoding keeps the field order its first writer used, which the contract's generated type (fields sorted) +// does not: the wire structs below hold it. + +// SourcePrefix is the start of a fingerprint of the new form, `src2:` and 64 lowercase hex digits. +const SourcePrefix = "src2:" + +// FingerprintKeyPart is a new-form fingerprint as the key part of ComposedKey: its 64 hex digits without the prefix. +func FingerprintKeyPart(fp string) (string, error) { + part, ok := strings.CutPrefix(fp, SourcePrefix) + if !ok || !keys.IsHex64(part) { + return "", fmt.Errorf("node-engine: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", fp, SourcePrefix) + } + return part, nil +} + +// Composition is one module's resources composed for one node at one fingerprint, or, with Module and FP empty, the node's +// own resources and parts (Own). Each resource is a declaration resource, kept as JSON and written canonically (keys +// sorted, spacing dropped). Plan is the module's whole provisioning plan, kept as JSON too: part of the encoding, not of +// the digest. Refused says the module cannot be composed at this fingerprint; such a composition holds nothing else. +type Composition struct { + V int + Node string + Module string + FP string + Data bool + Resources []json.RawMessage + Filled []string + Own seat.NodeOwn + Plan json.RawMessage + Refused string +} + +type adoptionWire struct { + Taken []string `json:"taken"` + Untaken map[string][]string `json:"untaken,omitempty"` +} + +type identityWire struct { + Module string `json:"module"` + Name string `json:"name"` + UID int64 `json:"uid"` + GID int64 `json:"gid"` + Groups []int64 `json:"groups,omitempty"` + Runner string `json:"runner,omitempty"` +} + +type groupWire struct { + Name string `json:"name"` + GID int64 `json:"gid"` +} + +type libraryWire struct { + Library string `json:"library"` + Root string `json:"root"` + Group string `json:"group"` + GID int64 `json:"gid"` + Home bool `json:"home"` + Taken bool `json:"taken,omitempty"` + Paths []string `json:"paths,omitempty"` + Runner string `json:"runner,omitempty"` +} + +type ownWire struct { + Adoption *adoptionWire `json:"adoption,omitempty"` + Identities []identityWire `json:"identities,omitempty"` + Groups []groupWire `json:"groups,omitempty"` + Libraries []libraryWire `json:"libraries,omitempty"` + Provisioner string `json:"provisioner_account,omitempty"` +} + +func ownIsEmpty(o seat.NodeOwn) bool { + return o.Adoption == nil && len(o.Identities) == 0 && len(o.Groups) == 0 && len(o.Libraries) == 0 && o.ProvisionerAccount == "" +} + +func ownToWire(o seat.NodeOwn) *ownWire { + w := ownWire{Provisioner: o.ProvisionerAccount} + if o.Adoption != nil { + w.Adoption = &adoptionWire{Taken: o.Adoption.Taken, Untaken: o.Adoption.Untaken} + } + for _, i := range o.Identities { + w.Identities = append(w.Identities, identityWire{Module: i.Module, Name: i.Name, UID: i.Uid, GID: i.Gid, Groups: i.Groups, Runner: i.Runner}) + } + for _, g := range o.Groups { + w.Groups = append(w.Groups, groupWire{Name: g.Name, GID: g.Gid}) + } + for _, l := range o.Libraries { + w.Libraries = append(w.Libraries, libraryWire{Library: l.Library, Root: l.Root, Group: l.Group, GID: l.Gid, Home: l.Home, Taken: l.Taken, Paths: l.Paths, Runner: l.Runner}) + } + return &w +} + +func ownFromWire(w ownWire) seat.NodeOwn { + o := seat.NodeOwn{ProvisionerAccount: w.Provisioner} + if w.Adoption != nil { + o.Adoption = &seat.Adoption{Taken: w.Adoption.Taken, Untaken: w.Adoption.Untaken} + } + for _, i := range w.Identities { + o.Identities = append(o.Identities, seat.DeclaredIdentity{Module: i.Module, Name: i.Name, Uid: i.UID, Gid: i.GID, Groups: i.Groups, Runner: i.Runner}) + } + for _, g := range w.Groups { + o.Groups = append(o.Groups, seat.DeclaredGroup{Name: g.Name, Gid: g.GID}) + } + for _, l := range w.Libraries { + o.Libraries = append(o.Libraries, seat.DeclaredLibrary{Library: l.Library, Root: l.Root, Group: l.Group, Gid: l.GID, Home: l.Home, Taken: l.Taken, Paths: l.Paths, Runner: l.Runner}) + } + return o +} + +// composedWire is a composition as it is written. +type composedWire struct { + V int `json:"v"` + Node string `json:"node"` + Module string `json:"module,omitempty"` + FP string `json:"fp,omitempty"` + Data bool `json:"data,omitempty"` + Resources []json.RawMessage `json:"resources"` + Filled []string `json:"filled_files,omitempty"` + Own *ownWire `json:"own,omitempty"` + Plan json.RawMessage `json:"plan,omitempty"` + Refused string `json:"refused,omitempty"` + // Signature is read and dropped: a composition is unsigned (ADR 0593 §6), and a value the ended controller signed + // still decodes. Nothing writes it. + Signature json.RawMessage `json:"signature,omitempty"` +} + +// canonicalJSON is raw with its object keys sorted and its spacing dropped, its numbers kept as written. +func canonicalJSON(raw json.RawMessage) (json.RawMessage, error) { + var v any + d := json.NewDecoder(bytes.NewReader(raw)) + d.UseNumber() + if err := d.Decode(&v); err != nil { + return nil, err + } + if _, err := d.Token(); err != io.EOF { + return nil, fmt.Errorf("more after the value") + } + return json.Marshal(v) +} + +func (c Composition) check() error { + if err := keys.CheckName("node", c.Node); err != nil { + return err + } + if c.Module == "" { + if c.FP != "" { + return fmt.Errorf("node-engine: the node's own composition names no fingerprint, and this one names %s", c.FP) + } + if c.Data { + return fmt.Errorf("node-engine: the node's own composition holds no module's data") + } + if len(c.Plan) > 0 || c.Refused != "" { + return fmt.Errorf("node-engine: the node's own composition carries no plan and no refusal: only a module's does") + } + } else { + part, err := FingerprintKeyPart(c.FP) + if err != nil { + return err + } + if _, err := ComposedKey(c.Node, c.Module, part); err != nil { + return err + } + if !ownIsEmpty(c.Own) { + return fmt.Errorf("node-engine: the composition of %s carries the node's own parts, which only .node carries", c.Module) + } + } + if c.Refused != "" && (len(c.Resources) > 0 || len(c.Filled) > 0 || c.Data || len(c.Plan) > 0) { + return fmt.Errorf("node-engine: the composition of %s on %s is refused and holds resources, filled files, data or a plan", c.Module, c.Node) + } + seen := map[string]bool{} + files := map[string][]*declaration.File{} + for _, raw := range c.Resources { + r, err := declaration.ParseResource(raw) + if err != nil { + return fmt.Errorf("node-engine: the composition of %q on %s: %w", c.Module, c.Node, err) + } + if seen[r.Identity()] { + return fmt.Errorf("node-engine: the composition of %q on %s holds the resource %q twice", c.Module, c.Node, r.Identity()) + } + seen[r.Identity()] = true + if r.DeclaredModule() != c.Module { + return fmt.Errorf("node-engine: the resource %q is of %q, and this is the composition of %q on %s", + r.Identity(), r.DeclaredModule(), c.Module, c.Node) + } + if f, ok := r.(*declaration.File); ok { + if f.TravelsByDigest() { + return fmt.Errorf("node-engine: the file %q is over %d bytes and travels by digest, not whole", + f.ID, declaration.FileReferenceThreshold) + } + files[f.Path] = append(files[f.Path], f) + } + } + // A file the provisioner fills says no content, so a composition that does not say it is filled would have the + // engine write it empty (issue 1945): refused, naming it. + filledHere := make(map[string]bool, len(c.Filled)) + for _, path := range c.Filled { + filledHere[path] = true + } + for path, fs := range files { + for _, f := range fs { + if f.OwnerAtRuntime == declaration.RuntimeOwned && !filledHere[path] && !saysContent(f) { + return fmt.Errorf("node-engine: the file %q says no content and the composition of %q on %s does not say "+ + "the provisioner fills %s: the engine would write it empty", f.ID, c.Module, c.Node, path) + } + } + } + for _, path := range c.Filled { + if len(files[path]) == 0 { + return fmt.Errorf("node-engine: the composition of %q fills %q, and declares no file there", c.Module, path) + } + for _, f := range files[path] { + if f.OwnerAtRuntime == "" { + return fmt.Errorf("node-engine: the file %q is filled by the provisioner and says no owner-at-runtime", f.ID) + } + } + } + return nil +} + +func saysContent(f *declaration.File) bool { + return f.Content != "" || f.Bytes != "" || f.Digest != "" || f.Sealed != "" || f.VaultSealed != nil +} + +func (c Composition) wire() (composedWire, error) { + if err := c.check(); err != nil { + return composedWire{}, err + } + w := composedWire{V: keys.Version, Node: c.Node, Module: c.Module, FP: c.FP, Data: c.Data, Filled: c.Filled, + Refused: c.Refused, Resources: make([]json.RawMessage, len(c.Resources))} + for i, r := range c.Resources { + canon, err := canonicalJSON(r) + if err != nil { + return composedWire{}, fmt.Errorf("node-engine: a resource of %q is not JSON: %w", c.Module, err) + } + w.Resources[i] = canon + } + if len(c.Plan) > 0 { + canon, err := canonicalJSON(c.Plan) + if err != nil { + return composedWire{}, fmt.Errorf("node-engine: the plan of %q is not JSON: %w", c.Module, err) + } + w.Plan = canon + } + if !ownIsEmpty(c.Own) { + w.Own = ownToWire(c.Own) + } + return w, nil +} + +// EncodeComposition is c as JSON at the version, each resource canonical. It refuses a composition whose node or +// fingerprint a key refuses, a resource its kind refuses, a resource of another module, a module's composition carrying +// the node's own parts, a file over 64 KiB carried whole, and a filled path no file declares. +func EncodeComposition(c Composition) ([]byte, error) { + w, err := c.wire() + if err != nil { + return nil, err + } + return json.Marshal(w) +} + +// DecodeComposition reads a composition; it refuses an unknown version (keys.ErrUnknownVersion), a field it does not +// know, and what EncodeComposition refuses. +func DecodeComposition(data []byte) (Composition, error) { + if err := keys.CheckVersion(data); err != nil { + return Composition{}, err + } + var w composedWire + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(&w); err != nil { + return Composition{}, fmt.Errorf("node-engine: a composition does not fit its shape: %w", err) + } + c := Composition{V: w.V, Node: w.Node, Module: w.Module, FP: w.FP, Data: w.Data, Resources: w.Resources, + Filled: w.Filled, Plan: w.Plan, Refused: w.Refused} + if w.Own != nil { + c.Own = ownFromWire(*w.Own) + } + if err := c.check(); err != nil { + return Composition{}, err + } + return c, nil +} + +// ComposedDigest is a composition's digest, "sha256:", of its encoding without its plan. A want names it (ADR 0539 +// clause 1), and the node's engine writes it beside the composition. A refused composition has no digest. +func ComposedDigest(c Composition) (string, error) { + if c.Refused != "" { + return "", fmt.Errorf("node-engine: the composition of %s on %s is refused (%s) and has no digest", c.Module, c.Node, c.Refused) + } + c.Plan = nil + raw, err := EncodeComposition(c) + if err != nil { + return "", err + } + sum := sha256.Sum256(raw) + return "sha256:" + hex.EncodeToString(sum[:]), nil +} + +// SameComposition says whether two compositions are the same value, their plans included, so the engine writes only +// when they differ. +func SameComposition(a, b Composition) bool { + ra, errA := EncodeComposition(a) + rb, errB := EncodeComposition(b) + return errA == nil && errB == nil && bytes.Equal(ra, rb) +} diff --git a/go/clients/nodeengineclient/composed_test.go b/go/clients/nodeengineclient/composed_test.go new file mode 100644 index 0000000..725b824 --- /dev/null +++ b/go/clients/nodeengineclient/composed_test.go @@ -0,0 +1,130 @@ +package nodeengineclient + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "testing" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" +) + +// captured is resources of ace's declaration of 2026-10-24 (the controller's `plan` verb answered it; testdata/captured-ace.json +// keeps three modules of it, never typed by hand), composed here by module under a stand-in fingerprint. +func captured(t *testing.T) (account string, byModule map[string][]json.RawMessage) { + t.Helper() + raw, err := os.ReadFile("testdata/captured-ace.json") + if err != nil { + t.Fatal(err) + } + var c struct { + Account string `json:"provisioner_account"` + Resources []json.RawMessage `json:"resources"` + } + if err := json.Unmarshal(raw, &c); err != nil { + t.Fatal(err) + } + byModule = map[string][]json.RawMessage{} + for _, r := range c.Resources { + var h struct { + Module string `json:"module"` + } + _ = json.Unmarshal(r, &h) + byModule[h.Module] = append(byModule[h.Module], r) + } + return c.Account, byModule +} + +func fp(n int) string { return SourcePrefix + fmt.Sprintf("%064x", n) } + +// TestCompositionDigests: a composition's digest is the one the first writer computed (the values are the digests +// mesh-sdk's delivery package gave for the same compositions when it moved here), whatever order the contract's generated +// types sort their fields in, and the plan is no part of it. +func TestCompositionDigests(t *testing.T) { + account, by := captured(t) + own := seat.NodeOwn{ + Adoption: &seat.Adoption{Taken: []string{"avahi"}, Untaken: map[string][]string{"plex": {"plex.bundle-code"}}}, + Identities: []seat.DeclaredIdentity{{Module: "plex", Name: "plex", Uid: 1001, Gid: 1001, Groups: []int64{1002}, Runner: "plex"}}, + Groups: []seat.DeclaredGroup{{Name: "mesh-media", Gid: 1002}}, + Libraries: []seat.DeclaredLibrary{{Library: "movies", Root: "/srv/media/movies", Group: "mesh-media", Gid: 1002, Taken: true, Paths: []string{"/srv/media/movies"}}}, + ProvisionerAccount: "mesh-provisioner-client", + } + cases := []struct { + name string + c Composition + digest string + }{ + {"a node's own, the account only", Composition{Node: "ace", Own: seat.NodeOwn{ProvisionerAccount: account}}, "sha256:17fde3a215d522619c18bc80b8c236975e25b2d2980a984aebd7c18376b095a2"}, + {"a node's own, every part", Composition{Node: "ace", Own: own}, "sha256:b8ac7ace490a3c483c3ae18a9cda58e607aa6b69167fa274c062949c7b34fdbf"}, + {"one package", Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: by["buildah"]}, "sha256:84e480a7a62333960b3e4be21a7548bbaa09572ef73ca8fb9dc6dfd1228aeb6b"}, + {"a service and a file", Composition{Node: "ace", Module: "mesh-wireguard", FP: fp(14), Resources: by["mesh-wireguard"]}, "sha256:d6edec2474f606503e55d4ee2e0b4f5d1ffc25d016e4bc595e86ea67fe205b8c"}, + {"a plan beside the resources", Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: by["hostname"], Plan: json.RawMessage(`{"files":[{"path":"/etc/hostname"}]}`)}, "sha256:49e17d6c471ee65cdef745ca3ac5cea4d37b6a100e11d8434dc64a6e2949e57e"}, + } + for _, c := range cases { + got, err := ComposedDigest(c.c) + if err != nil || got != c.digest { + t.Errorf("%s: digest %s, %v; want %s", c.name, got, err, c.digest) + } + raw, err := EncodeComposition(c.c) + if err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + back, err := DecodeComposition(raw) + if err != nil { + t.Errorf("%s: %v", c.name, err) + continue + } + if !SameComposition(c.c, back) { + t.Errorf("%s: reads back as another composition", c.name) + } + again, _ := ComposedDigest(back) + if again != c.digest { + t.Errorf("%s: read back, its digest is %s", c.name, again) + } + } + a := Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: by["hostname"], Plan: json.RawMessage(`{"a":1}`)} + b := a + b.Plan = json.RawMessage(`{"a":2}`) + da, _ := ComposedDigest(a) + db, _ := ComposedDigest(b) + if da != db || SameComposition(a, b) { + t.Error("a plan changes the composition's encoding and not its digest") + } +} + +// TestCompositionRefusals: what the codec refuses. +func TestCompositionRefusals(t *testing.T) { + _, by := captured(t) + ok := Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: by["buildah"]} + cases := []struct { + name string + c Composition + want string + }{ + {"an old-form fingerprint", Composition{Node: "ace", Module: "buildah", FP: "src1:abc", Resources: ok.Resources}, "new form"}, + {"a resource of another module", Composition{Node: "ace", Module: "hostname", FP: fp(8), Resources: ok.Resources}, "is of"}, + {"the node's own parts on a module", Composition{Node: "ace", Module: "buildah", FP: fp(7), Own: seat.NodeOwn{ProvisionerAccount: "x"}}, "node's own parts"}, + {"a refusal that holds resources", Composition{Node: "ace", Module: "buildah", FP: fp(7), Resources: ok.Resources, Refused: "no secret"}, "refused and holds"}, + {"the node's own with a fingerprint", Composition{Node: "ace", FP: fp(7)}, "names no fingerprint"}, + {"a node name with a dot", Composition{Node: "a.ce"}, "node"}, + } + for _, c := range cases { + if _, err := EncodeComposition(c.c); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: %v, want an error saying %q", c.name, err, c.want) + } + } + if _, err := ComposedDigest(Composition{Node: "ace", Module: "buildah", FP: fp(7), Refused: "no secret"}); err == nil { + t.Error("a refused composition has a digest") + } + if _, err := DecodeComposition([]byte(`{"v":2,"node":"ace","resources":[]}`)); err == nil { + t.Error("a composition of version 2 is read") + } + if _, err := DecodeComposition([]byte(`{"v":1,"node":"ace","resources":[],"extra":1}`)); err == nil { + t.Error("a composition with a field it does not know is read") + } + if _, err := DecodeComposition([]byte(`{"v":1,"node":"ace","resources":[],"signature":"x"}`)); err != nil { + t.Errorf("a signed composition of the ended controller is refused: %v", err) + } +} diff --git a/go/clients/nodeengineclient/testdata/captured-ace.json b/go/clients/nodeengineclient/testdata/captured-ace.json new file mode 100644 index 0000000..d727d90 --- /dev/null +++ b/go/clients/nodeengineclient/testdata/captured-ace.json @@ -0,0 +1,63 @@ +{ + "provisioner_account": "mesh-provisioner-client", + "resources": [ + { + "id": "buildah.buildah", + "module": "buildah", + "package": "buildah", + "type": "package" + }, + { + "id": "mesh-wireguard.overlay-tools", + "module": "mesh-wireguard", + "package": "wireguard-tools", + "type": "package" + }, + { + "content": "# Generated by the mesh. Do not edit \u2014 this file is replaced whenever the\n# peer graph changes, and an edit would survive until the next change and\n# then vanish, which is worse than not being applied at all.\n#\n# node ace, at home, not dialable \u2014 it opens every path itself\n\n[Interface]\nAddress = 10.10.0.2/32\nPostUp = wg set %i private-key /var/lib/mesh-host/overlay.key\n\n# novox \u2014 the hub \u2014 everything not at this site\n[Peer]\nPublicKey = xW0ETQwyR9rHi2XSfOJrKkairE+Fp70FPNNcirqMixY=\nAllowedIPs = 10.10.0.0/24\nEndpoint = 142.132.152.141:51820\nPersistentKeepalive = 25\n", + "id": "mesh-wireguard.overlay-config", + "mode": "0600", + "module": "mesh-wireguard", + "path": "/etc/wireguard/mesh0.conf", + "type": "file" + }, + { + "boot": "enabled", + "id": "mesh-wireguard.overlay-up", + "module": "mesh-wireguard", + "restart-on": [ + "mesh-wireguard.overlay-config" + ], + "state": "running", + "type": "service", + "unit": "wg-quick@mesh0" + }, + { + "digest": "sha256:71ed8f5b4e7ac73ab82b7b559b08e5951a82553197d153651cc8be36ff35bcda", + "id": "hostname.bundle-tools", + "module": "hostname", + "path": "/var/lib/mesh/bundles/hostname/tools", + "source": "http://novox.internal:5100/v2/hostname/tools/blobs/sha256:71ed8f5b4e7ac73ab82b7b559b08e5951a82553197d153651cc8be36ff35bcda", + "type": "archive" + }, + { + "at": "start", + "content": "# The machine's own names (module hostname, novox/hq ADR 0199, ADR 0223). Every line outside this\n# block is the operator's: kept across every push, changed through the node-hostname verbs add and\n# remove, and given back when this module goes. The mesh's names are not here: the mesh's resolver\n# answers them.\n127.0.0.1\tlocalhost\n::1\tlocalhost\n127.0.1.1\tace\n", + "id": "hostname.own", + "into": "block", + "mode": "0644", + "module": "hostname", + "path": "/etc/hosts", + "type": "file" + }, + { + "content": "Ace\n", + "id": "hostname.name", + "mode": "0644", + "module": "hostname", + "path": "/etc/hostname", + "type": "file" + } + ], + "filled_files": [] +} \ No newline at end of file -- 2.54.0 From 35aa61f4d9e74e75cd45c880e37c4ddd98316f89 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:53:24 +0100 Subject: [PATCH 63/94] =?UTF-8?q?operator-channel:=20the=20state=20asks=20?= =?UTF-8?q?typed=20to=20the=20router's=20projection;=20mesh-node-enroller:?= =?UTF-8?q?=20redeem's=20answer=20typed,=20with=20a=20fixture=20and=20a=20?= =?UTF-8?q?client=20test=20(ADR=200634=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshnodeenrollerclient/enrolment_test.go | 28 ++++++++++ .../go/seats/meshnodeenroller/contract.go | 15 +++++- .../seats/meshnodeenroller/contract_test.go | 2 + .../mesh-node-enroller/fixtures/verbs.json | 14 +++++ .../seats/mesh-node-enroller/seat.json | 35 ++++++++++++- .../ts/seats/meshnodeenroller/src/index.ts | 15 +++++- .../go/seats/operatorchannel/contract.go | 21 +++++++- .../seats/operator-channel/seat.json | 51 ++++++++++++++++++- .../ts/seats/operatorchannel/src/index.ts | 21 +++++++- 9 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go index 41098fe..fc61d8b 100644 --- a/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go +++ b/identity-and-access/go/clients/meshnodeenrollerclient/enrolment_test.go @@ -1,8 +1,13 @@ package meshnodeenrollerclient import ( + "bytes" + "encoding/json" + "reflect" "testing" "time" + + seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller" ) func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) { @@ -28,3 +33,26 @@ func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) { t.Errorf("SignedBytes = %s", got) } } + +// TestTheRedeemAnswerIsTheContractsWire: the client's answer reads into the contract's type with no field dropped, and +// the contract's reads back as the client's wire. +func TestTheRedeemAnswerIsTheContractsWire(t *testing.T) { + mine := RedeemAnswer{Node: "g14", Login: "g14-login", Account: "YQ==", Certificate: "pem", Signer: "dg==", Signature: "cw=="} + wire, _ := json.Marshal(mine) + var theirs seat.RedeemAnswer + d := json.NewDecoder(bytes.NewReader(wire)) + d.DisallowUnknownFields() + if err := d.Decode(&theirs); err != nil { + t.Fatal(err) + } + if theirs.Node != "g14" || theirs.Signer != "dg==" || theirs.Signature != "cw==" { + t.Errorf("the contract reads %+v", theirs) + } + back, _ := json.Marshal(theirs) + var a, b map[string]string + _ = json.Unmarshal(wire, &a) + _ = json.Unmarshal(back, &b) + if len(a) != 6 || !reflect.DeepEqual(a, b) { + t.Errorf("the contract reads %s back as %s", wire, back) + } +} diff --git a/identity-and-access/go/seats/meshnodeenroller/contract.go b/identity-and-access/go/seats/meshnodeenroller/contract.go index 9f1cc08..3cdab36 100644 --- a/identity-and-access/go/seats/meshnodeenroller/contract.go +++ b/identity-and-access/go/seats/meshnodeenroller/contract.go @@ -187,7 +187,20 @@ type RedeemInput struct { } // RedeemAnswer is what redeem answers. -type RedeemAnswer = map[string]any +type RedeemAnswer struct { + // its account sealed to the sealing key it presented, base64 + Account string `json:"account"` + // its certificate, PEM + Certificate string `json:"certificate"` + // the login it dials as next + Login string `json:"login"` + // the node + Node string `json:"node"` + // the vault's signature over the answer without it, under the context of the enrolment kind, base64 + Signature string `json:"signature,omitempty"` + // the vault's public signing half, base64 + Signer string `json:"signer"` +} // ConfirmInput is what confirm takes. type ConfirmInput struct { diff --git a/identity-and-access/go/seats/meshnodeenroller/contract_test.go b/identity-and-access/go/seats/meshnodeenroller/contract_test.go index 6e1d2bf..9fa2469 100644 --- a/identity-and-access/go/seats/meshnodeenroller/contract_test.go +++ b/identity-and-access/go/seats/meshnodeenroller/contract_test.go @@ -31,6 +31,8 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "redeem answers the account sealed to the node, signed by the vault", subject: "", given: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}", wire: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}", + decode: func(b []byte) (any, error) { var v RedeemAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json b/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json new file mode 100644 index 0000000..d594ec3 --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/verbs.json @@ -0,0 +1,14 @@ +[ + { + "name": "redeem answers the account sealed to the node, signed by the vault", + "of": "verb:redeem.answer", + "given": { + "node": "g14", + "login": "g14-login", + "account": "c2VhbGVk", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n", + "signer": "dmF1bHQ=", + "signature": "c2ln" + } + } +] diff --git a/identity-and-access/seats/mesh-node-enroller/seat.json b/identity-and-access/seats/mesh-node-enroller/seat.json index ecb4170..4d21cd0 100644 --- a/identity-and-access/seats/mesh-node-enroller/seat.json +++ b/identity-and-access/seats/mesh-node-enroller/seat.json @@ -90,7 +90,40 @@ }, "answer": { "type": "object", - "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault" + "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault", + "properties": { + "node": { + "type": "string", + "description": "the node" + }, + "login": { + "type": "string", + "description": "the login it dials as next" + }, + "account": { + "type": "string", + "description": "its account sealed to the sealing key it presented, base64" + }, + "certificate": { + "type": "string", + "description": "its certificate, PEM" + }, + "signer": { + "type": "string", + "description": "the vault's public signing half, base64" + }, + "signature": { + "type": "string", + "description": "the vault's signature over the answer without it, under the context of the enrolment kind, base64" + } + }, + "required": [ + "node", + "login", + "account", + "certificate", + "signer" + ] } }, { diff --git a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts index 4068a1d..3ca423d 100644 --- a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts +++ b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts @@ -123,7 +123,20 @@ export interface RedeemInput { } /** is what redeem answers. */ -export type RedeemAnswer = Record; +export interface RedeemAnswer { + /** its account sealed to the sealing key it presented, base64 */ + "account": string; + /** its certificate, PEM */ + "certificate": string; + /** the login it dials as next */ + "login": string; + /** the node */ + "node": string; + /** the vault's signature over the answer without it, under the context of the enrolment kind, base64 */ + "signature"?: string; + /** the vault's public signing half, base64 */ + "signer": string; +} /** is what confirm takes. */ export interface ConfirmInput { diff --git a/operator-and-conversation/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go index f17556f..6d30be7 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -477,4 +477,23 @@ const ( ) // AsksByAskerIDValue is one value of the state asks under the key .. -type AsksByAskerIDValue = map[string]any +type AsksByAskerIDValue struct { + // how it ended, in words + Answer string `json:"answer,omitempty"` + // its asker + Asker string `json:"asker"` + // its asker has been told how it ended + AskerTold bool `json:"asker_told,omitempty"` + // when it ends unanswered, RFC 3339 + Expires string `json:"expires"` + // its headline + Headline string `json:"headline"` + // . + Key string `json:"key"` + // the channels it was offered on + OfferedOn []string `json:"offered_on,omitempty"` + // when it was opened, RFC 3339 + Opened string `json:"opened"` + // how it stands: open, chosen, expired, cancelled, replaced or refused + State string `json:"state"` +} diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index b40e63e..a9a5365 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -182,7 +182,56 @@ "key": ".", "value": { "type": "object", - "description": "one ask the router holds and how it stands" + "description": "one ask the router holds and how it stands, as the router's tool states it", + "properties": { + "key": { + "type": "string", + "description": "." + }, + "asker": { + "type": "string", + "description": "its asker" + }, + "headline": { + "type": "string", + "description": "its headline" + }, + "state": { + "type": "string", + "description": "how it stands: open, chosen, expired, cancelled, replaced or refused" + }, + "opened": { + "type": "string", + "description": "when it was opened, RFC 3339" + }, + "expires": { + "type": "string", + "description": "when it ends unanswered, RFC 3339" + }, + "offered_on": { + "type": "array", + "items": { + "type": "string" + }, + "description": "the channels it was offered on" + }, + "asker_told": { + "type": "boolean", + "description": "its asker has been told how it ended" + }, + "answer": { + "type": "string", + "description": "how it ended, in words" + } + }, + "required": [ + "key", + "asker", + "headline", + "state", + "opened", + "expires" + ] }, "description": "one ask the router holds and how it stands" } diff --git a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts index fbffec0..4b2837b 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -406,5 +406,24 @@ export interface ClosedHolderEvent { } /** is one value of the state asks under the key .. */ -export type AsksByAskerIDValue = Record; +export interface AsksByAskerIDValue { + /** how it ended, in words */ + "answer"?: string; + /** its asker */ + "asker": string; + /** its asker has been told how it ended */ + "asker_told"?: boolean; + /** when it ends unanswered, RFC 3339 */ + "expires": string; + /** its headline */ + "headline": string; + /** . */ + "key": string; + /** the channels it was offered on */ + "offered_on"?: string[]; + /** when it was opened, RFC 3339 */ + "opened": string; + /** how it stands: open, chosen, expired, cancelled, replaced or refused */ + "state": string; +} -- 2.54.0 From 29dc84a38b88956dca4469a6de21cd6b137a1b62 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:56:40 +0100 Subject: [PATCH 64/94] =?UTF-8?q?operatorchannelclient:=20the=20bench=20su?= =?UTF-8?q?bjects=20and=20words=20the=20router=20and=20the=20channels=20sh?= =?UTF-8?q?are=20(B5d;=20ADR=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/clients/operatorchannelclient/bench.go | 46 +++++++++++++++++++ .../operatorchannelclient/bench_test.go | 18 ++++++++ 2 files changed, 64 insertions(+) create mode 100644 operator-and-conversation/go/clients/operatorchannelclient/bench.go diff --git a/operator-and-conversation/go/clients/operatorchannelclient/bench.go b/operator-and-conversation/go/clients/operatorchannelclient/bench.go new file mode 100644 index 0000000..28fe858 --- /dev/null +++ b/operator-and-conversation/go/clients/operatorchannelclient/bench.go @@ -0,0 +1,46 @@ +package operatorchannelclient + +// The subjects and words of the two kinded benches a channel holds, `channel` (to show and say) and `intake` (to say +// what was chosen, and to ask a proof): the router and every channel share them (novox/hq ADR 0259 §3, ADR 0634 §1). +// The shapes carried are the seat's types (Message, Choice, LinkAsked, Failed, Standing, Code, ProofAnswer). + +// The benches. +const ( + // ChannelSeat is the kinded bench a channel holds to show and say: its accepts carry the kind. + ChannelSeat = "channel" + // IntakeSeat is the kinded bench a channel holds to say what was chosen: its events and proofs carry the kind. + IntakeSeat = "intake" +) + +// The work a channel takes, on ChannelSubject. +const ( + Show = "show" // a message offering answers + Edit = "edit" // a message shown before, replaced + Send = "send" // a message offering nothing +) + +// ChannelSubject is where the router sends a channel of a kind its work. +func ChannelSubject(verb, kind string) string { + return "mesh.seat." + ChannelSeat + ".accept." + verb + "." + kind +} + +// What a channel says, on IntakeSubject. +const ( + Chosen = "choice" // a button tapped + Link = "link" // somebody asked to be linked as the operator + FailedWhat = "failed" // a message it could not deliver + StandingWhat = "standing" // what it says of itself +) + +// IntakeSubject is where a channel of a kind says what arrived. +func IntakeSubject(what, kind string) string { + return "mesh.seat." + IntakeSeat + ".event." + what + "." + kind +} + +// CodeProof is the one proof verb: a code the operator typed, carried by request and reply, never kept. +const CodeProof = "code" + +// ProofSubject is where a channel of a kind asks a proof. +func ProofSubject(verb, kind string) string { + return "mesh.seat." + IntakeSeat + ".proof." + verb + "." + kind +} diff --git a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go index f8a7512..d720639 100644 --- a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go @@ -56,3 +56,21 @@ func jsonEqual(a, b any) bool { y, _ := json.Marshal(b) return bytes.Equal(x, y) } + +// TestTheBenchSubjectsAreTheChannelsOwn: the subjects and words are mesh-sdk channel's, character for character. +func TestTheBenchSubjectsAreTheChannelsOwn(t *testing.T) { + for _, c := range [][2]string{ + {ChannelSubject(Show, "telegram"), channel.ChannelSubject(channel.Show, "telegram")}, + {ChannelSubject(Edit, "desk"), channel.ChannelSubject(channel.Edit, "desk")}, + {ChannelSubject(Send, "desk"), channel.ChannelSubject(channel.Send, "desk")}, + {IntakeSubject(Chosen, "telegram"), channel.IntakeSubject(channel.Chosen, "telegram")}, + {IntakeSubject(Link, "telegram"), channel.IntakeSubject(channel.Link, "telegram")}, + {IntakeSubject(FailedWhat, "desk"), channel.IntakeSubject(channel.FailedWhat, "desk")}, + {IntakeSubject(StandingWhat, "desk"), channel.IntakeSubject(channel.StandingWhat, "desk")}, + {ProofSubject(CodeProof, "telegram"), channel.ProofSubject(channel.CodeProof, "telegram")}, + } { + if c[0] != c[1] { + t.Errorf("subject %q, want %q", c[0], c[1]) + } + } +} -- 2.54.0 From cd2615f09404500958ef027ee705d8aa2433ade8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:01:57 +0100 Subject: [PATCH 65/94] operator-channel: the times of the bench events are date-time, so the generated types are time.Time (B5d; ADR 0634; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/seats/operatorchannel/contract.go | 9 +++++---- .../seats/operator-channel/seat.json | 12 ++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/operator-and-conversation/go/seats/operatorchannel/contract.go b/operator-and-conversation/go/seats/operatorchannel/contract.go index 6d30be7..8fb2a22 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -160,7 +161,7 @@ type Button struct { // Choice is a button chosen on a channel, said on the bench intake as the event `choice`. type Choice struct { // when, RFC 3339 - At string `json:"at"` + At time.Time `json:"at"` // the message's handle Handle string `json:"handle,omitempty"` // the channel's own for this arrival, unique, so the router acts on it once @@ -230,7 +231,7 @@ type Condition struct { // Failed is a message a channel could not deliver, said on the bench intake as the event `failed`. type Failed struct { // when, RFC 3339 - At string `json:"at"` + At time.Time `json:"at"` // the message's handle Handle string `json:"handle"` // trying again cannot help @@ -242,7 +243,7 @@ type Failed struct { // LinkAsked is somebody on a channel asking to be linked as the operator, said on the bench intake as the event `link`. type LinkAsked struct { // when, RFC 3339 - At string `json:"at"` + At time.Time `json:"at"` // the channel's own for this arrival ID string `json:"id"` Sender Sender `json:"sender"` @@ -321,7 +322,7 @@ type Sender struct { // Standing is what a channel says of itself at least every five minutes and whenever it changes, on the bench intake as the event `standing`. type Standing struct { // when, RFC 3339 - At string `json:"at"` + At time.Time `json:"at"` // its edits notify nobody EditsSilently bool `json:"edits-silently,omitempty"` // it can send now diff --git a/operator-and-conversation/seats/operator-channel/seat.json b/operator-and-conversation/seats/operator-channel/seat.json index a9a5365..3f33532 100644 --- a/operator-and-conversation/seats/operator-channel/seat.json +++ b/operator-and-conversation/seats/operator-channel/seat.json @@ -728,7 +728,8 @@ }, "at": { "type": "string", - "description": "when, RFC 3339" + "description": "when, RFC 3339", + "format": "date-time" } }, "required": [ @@ -751,7 +752,8 @@ }, "at": { "type": "string", - "description": "when, RFC 3339" + "description": "when, RFC 3339", + "format": "date-time" } }, "required": [ @@ -778,7 +780,8 @@ }, "at": { "type": "string", - "description": "when, RFC 3339" + "description": "when, RFC 3339", + "format": "date-time" } }, "required": [ @@ -805,7 +808,8 @@ }, "at": { "type": "string", - "description": "when, RFC 3339" + "description": "when, RFC 3339", + "format": "date-time" } }, "required": [ -- 2.54.0 From 25da1f887993e7f5bbe411b3650aee9fed075048 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:05:08 +0100 Subject: [PATCH 66/94] =?UTF-8?q?node-engine=20client:=20the=20exported=20?= =?UTF-8?q?fingerprint=20key=20part=20is=20mesh-catalog's=20client's,=20th?= =?UTF-8?q?e=20codec=20checks=20the=20field's=20form=20itself=20(a=20clien?= =?UTF-8?q?t=20imports=20no=20other=20seat's=20client;=20ADR=200625=20?= =?UTF-8?q?=C2=A74,=20ADR=200634=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/composed.go | 21 ++++++++++---------- go/clients/nodeengineclient/composed_test.go | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/go/clients/nodeengineclient/composed.go b/go/clients/nodeengineclient/composed.go index b8f5356..aded503 100644 --- a/go/clients/nodeengineclient/composed.go +++ b/go/clients/nodeengineclient/composed.go @@ -20,16 +20,18 @@ import ( // alone, so the encoding keeps the field order its first writer used, which the contract's generated type (fields sorted) // does not: the wire structs below hold it. -// SourcePrefix is the start of a fingerprint of the new form, `src2:` and 64 lowercase hex digits. -const SourcePrefix = "src2:" +// sourcePrefix opens a source fingerprint of the form mesh-catalog's client defines (meshcatalogclient.SourcePrefix2, +// KeyPart); a client imports no other seat's client (ADR 0625 §4), so a composition's field is checked here for that form +// only, and a caller makes the key part with meshcatalogclient.KeyPart. +const sourcePrefix = "src2:" -// FingerprintKeyPart is a new-form fingerprint as the key part of ComposedKey: its 64 hex digits without the prefix. -func FingerprintKeyPart(fp string) (string, error) { - part, ok := strings.CutPrefix(fp, SourcePrefix) +// checkFingerprintForm refuses a fingerprint that is not `src2:` and 64 lowercase hex digits, naming it. +func checkFingerprintForm(fp string) error { + part, ok := strings.CutPrefix(fp, sourcePrefix) if !ok || !keys.IsHex64(part) { - return "", fmt.Errorf("node-engine: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", fp, SourcePrefix) + return fmt.Errorf("node-engine: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", fp, sourcePrefix) } - return part, nil + return nil } // Composition is one module's resources composed for one node at one fingerprint, or, with Module and FP empty, the node's @@ -171,11 +173,10 @@ func (c Composition) check() error { return fmt.Errorf("node-engine: the node's own composition carries no plan and no refusal: only a module's does") } } else { - part, err := FingerprintKeyPart(c.FP) - if err != nil { + if err := checkFingerprintForm(c.FP); err != nil { return err } - if _, err := ComposedKey(c.Node, c.Module, part); err != nil { + if _, err := ComposedKey(c.Node, c.Module, strings.TrimPrefix(c.FP, sourcePrefix)); err != nil { return err } if !ownIsEmpty(c.Own) { diff --git a/go/clients/nodeengineclient/composed_test.go b/go/clients/nodeengineclient/composed_test.go index 725b824..0fe22fa 100644 --- a/go/clients/nodeengineclient/composed_test.go +++ b/go/clients/nodeengineclient/composed_test.go @@ -36,7 +36,7 @@ func captured(t *testing.T) (account string, byModule map[string][]json.RawMessa return c.Account, byModule } -func fp(n int) string { return SourcePrefix + fmt.Sprintf("%064x", n) } +func fp(n int) string { return sourcePrefix + fmt.Sprintf("%064x", n) } // TestCompositionDigests: a composition's digest is the one the first writer computed (the values are the digests // mesh-sdk's delivery package gave for the same compositions when it moved here), whatever order the contract's generated -- 2.54.0 From 181441e91bb96f43d6448045028cd7719b101a63 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:06:14 +0100 Subject: [PATCH 67/94] The generated contracts write date-time as time.Time (seatgen of B5d); the conditions and vault clients follow (ADR 0634; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/seats/nodeengine/contract.go | 51 +++++----- .../go/clients/meshconditionsclient/client.go | 8 +- .../meshconditionsclient/client_test.go | 16 ++- .../go/seats/meshconditions/contract.go | 99 ++++++++++--------- .../go/seats/meshhandacts/contract.go | 23 ++--- .../go/seats/meshhealers/contract.go | 53 +++++----- .../go/seats/meshselfcheck/contract.go | 6 +- .../go/seats/meshwatchdogs/contract.go | 7 +- .../go/clients/meshvaultclient/client_test.go | 4 +- .../go/clients/meshvaultclient/signers.go | 2 +- .../go/seats/meshnodeenroller/contract.go | 25 ++--- .../go/seats/meshvault/contract.go | 19 ++-- .../go/seats/meshmoduleassigner/contract.go | 33 ++++--- .../go/seats/meshprovisioner/contract.go | 21 ++-- 14 files changed, 194 insertions(+), 173 deletions(-) diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index f8267e8..b0d565a 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -348,7 +349,7 @@ func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) { // AccountsHanded is the hashes of the users of the list, sealed to the vault. type AccountsHanded struct { // when - At string `json:"at"` + At time.Time `json:"at"` Box SealedBox `json:"box"` From string `json:"from"` // the fingerprint of the vault's key @@ -557,8 +558,8 @@ type FailedUnit struct { Result string `json:"result,omitempty"` Scope string `json:"scope"` // since when - Since string `json:"since"` - Unit string `json:"unit"` + Since time.Time `json:"since"` + Unit string `json:"unit"` } // FindingAction is one answer the operator may give from a notification. @@ -579,8 +580,8 @@ const ( // FindingEvidence is one observation, as it was said. type FindingEvidence struct { - At string `json:"at"` - Said string `json:"said"` + At time.Time `json:"at"` + Said string `json:"said"` } // FindingSubject is what the condition is about. @@ -598,10 +599,10 @@ type FindingSubject struct { // LoginFallback is a login that fell back to another node. type LoginFallback struct { // when - At string `json:"at"` - From string `json:"from"` - Node string `json:"node"` - Why string `json:"why"` + At time.Time `json:"at"` + From string `json:"from"` + Node string `json:"node"` + Why string `json:"why"` } // ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4). @@ -651,7 +652,7 @@ const ( type NetworkHealth struct { Parts []NetworkPart `json:"parts"` // since when - Since string `json:"since"` + Since time.Time `json:"since"` // running, degraded or unknown State string `json:"state"` } @@ -663,12 +664,12 @@ type NetworkPart struct { Reason string `json:"reason,omitempty"` Said string `json:"said,omitempty"` // since when - Since string `json:"since"` - State string `json:"state"` - Streak int64 `json:"streak,omitempty"` - Targets []string `json:"targets,omitempty"` - Toward []string `json:"toward,omitempty"` - Writer string `json:"writer,omitempty"` + Since time.Time `json:"since"` + State string `json:"state"` + Streak int64 `json:"streak,omitempty"` + Targets []string `json:"targets,omitempty"` + Toward []string `json:"toward,omitempty"` + Writer string `json:"writer,omitempty"` } // NodeAccount is an account the node needs to log in again. @@ -678,7 +679,7 @@ type NodeAccount struct { Reason string `json:"reason"` Resource string `json:"resource,omitempty"` // since when - Since string `json:"since"` + Since time.Time `json:"since"` } // NodeDirectory is a directory found where the node expected none. @@ -688,7 +689,7 @@ type NodeDirectory struct { Reason string `json:"reason"` Resource string `json:"resource"` // since when - Since string `json:"since"` + Since time.Time `json:"since"` } // NodeOwn is what a node's own composition carries beside its resources. @@ -744,14 +745,14 @@ type PlacedDataItem struct { // PullWait is the pull from a store a failed apply waits on. type PullWait struct { // when the pull began - Began string `json:"began"` + Began time.Time `json:"began"` // that seat's holder, whose settling the apply waits for Holder string `json:"holder"` Image string `json:"image"` // what triggered the last try again, in words Retried string `json:"retried,omitempty"` // when the apply was last tried again - RetriedAt string `json:"retried-at,omitempty"` + RetriedAt *time.Time `json:"retried-at,omitempty"` // the seat the pull came through Seat string `json:"seat,omitempty"` } @@ -779,8 +780,8 @@ type RootAccountVerdict struct { Reason string `json:"reason,omitempty"` Resource string `json:"resource"` // since when - Since string `json:"since"` - State string `json:"state"` + Since time.Time `json:"since"` + State string `json:"state"` } // RootVerdict is the engine's root-free verdict (ADR 0563 §6). @@ -859,7 +860,7 @@ type UnitsHealth struct { // UserListWritten is the bus's user list as the engine of the bus node wrote it to the bus module's file. type UserListWritten struct { // when - At string `json:"at"` + At time.Time `json:"at"` // the digest of the file on the machine after the write Digest string `json:"digest,omitempty"` // why a write failed @@ -1361,7 +1362,7 @@ type PackagesByNodeValue struct { // HealthByNodeValue is one value of the state health under the key . type HealthByNodeValue struct { // when - At string `json:"at"` + At time.Time `json:"at"` Contract int64 `json:"contract,omitempty"` Fallback *LoginFallback `json:"fallback,omitempty"` Found []NodeDirectory `json:"found,omitempty"` @@ -1377,7 +1378,7 @@ type HealthByNodeValue struct { // FindingsByKeyValue is one value of the state findings under the key . type FindingsByKeyValue struct { Actions []FindingAction `json:"actions,omitempty"` - At string `json:"at"` + At time.Time `json:"at"` // newest first, at most 10 Evidence []FindingEvidence `json:"evidence,omitempty"` Explanation string `json:"explanation,omitempty"` diff --git a/health-and-repair/go/clients/meshconditionsclient/client.go b/health-and-repair/go/clients/meshconditionsclient/client.go index 8e3a014..2931dc8 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client.go +++ b/health-and-repair/go/clients/meshconditionsclient/client.go @@ -146,7 +146,7 @@ func CheckFinding(f seat.FindingsByKeyValue) error { // Order sorts conditions as a person reads them: urgent first, then oldest raised, then by key. func Order(list []seat.ConditionsByKeyValue) { - raised := func(c seat.ConditionsByKeyValue) time.Time { t, _ := time.Parse(time.RFC3339Nano, c.Raised); return t } + raised := func(c seat.ConditionsByKeyValue) time.Time { return c.Raised } sort.SliceStable(list, func(i, j int) bool { if list[i].Severity != list[j].Severity { return list[i].Severity == Urgent @@ -163,8 +163,8 @@ func SilencedAt(c seat.ConditionsByKeyValue, now time.Time) bool { if c.Silenced == nil { return false } - until, err := time.Parse(time.RFC3339Nano, c.Silenced.Until) - return err == nil && now.Before(until) + until := c.Silenced.Until + return until != nil && now.Before(*until) } // Escalated says the condition waits on the operator after healers tried. @@ -191,7 +191,7 @@ var _ views.FindingWriter = FindingWriter{} // FindingFor is the value and key of the seat's finding for f. func FindingFor(f views.Finding) seat.FindingsByKeyValue { - at := f.At.UTC().Format(time.RFC3339Nano) + at := f.At.UTC() return seat.FindingsByKeyValue{V: FindingVersion, Key: Key(ScopeModule, f.ID, f.Kind), Kind: f.Kind, Subject: seat.Subject{Scope: ScopeModule, ID: f.ID}, Severity: Warning, Summary: f.Summary, Headline: f.Headline, Explanation: f.Explanation, Evidence: []seat.Evidence{{At: at, Said: f.Summary}}, Source: f.Observer + "." + f.Kind, diff --git a/health-and-repair/go/clients/meshconditionsclient/client_test.go b/health-and-repair/go/clients/meshconditionsclient/client_test.go index 746368a..c9ba6df 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -41,9 +41,9 @@ func TestAFindingsKeyAndTheOrderOfConditions(t *testing.T) { } } list := []seat.ConditionsByKeyValue{ - {Key: "b", Severity: Warning, Raised: "2026-11-01T10:00:00Z"}, - {Key: "c", Severity: Urgent, Raised: "2026-11-02T10:00:00Z"}, - {Key: "a", Severity: Urgent, Raised: "2026-11-01T10:00:00.5Z", Silenced: &seat.ConditionsByKeyValueSilenced{Until: "2026-11-03T00:00:00Z"}}, + {Key: "b", Severity: Warning, Raised: when("2026-11-01T10:00:00Z")}, + {Key: "c", Severity: Urgent, Raised: when("2026-11-02T10:00:00Z")}, + {Key: "a", Severity: Urgent, Raised: when("2026-11-01T10:00:00.5Z"), Silenced: &seat.ConditionsByKeyValueSilenced{Until: ptr(when("2026-11-03T00:00:00Z"))}}, } Order(list) if got := list[0].Key + list[1].Key + list[2].Key; got != "acb" { @@ -99,3 +99,13 @@ func TestTheFindingWriterWritesAndClearsAModulesFindingInTheSeatsLayout(t *testi t.Error("a finding that says nothing was written") } } + +func when(s string) time.Time { + t, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + panic(err) + } + return t +} + +func ptr(t time.Time) *time.Time { return &t } diff --git a/health-and-repair/go/seats/meshconditions/contract.go b/health-and-repair/go/seats/meshconditions/contract.go index eddd19d..0757680 100644 --- a/health-and-repair/go/seats/meshconditions/contract.go +++ b/health-and-repair/go/seats/meshconditions/contract.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "errors" + "time" ) // Seat is the seat's name. @@ -134,50 +135,50 @@ const ( // ConditionEventGapsItem is a type of the contract. type ConditionEventGapsItem struct { - Cleared string `json:"cleared,omitempty"` - Reopened string `json:"reopened,omitempty"` + Cleared *time.Time `json:"cleared,omitempty"` + Reopened *time.Time `json:"reopened,omitempty"` } // ConditionEventSilenced is a type of the contract. type ConditionEventSilenced struct { - By string `json:"by,omitempty"` - Since string `json:"since,omitempty"` - Until string `json:"until,omitempty"` - Why string `json:"why,omitempty"` + By string `json:"by,omitempty"` + Since *time.Time `json:"since,omitempty"` + Until *time.Time `json:"until,omitempty"` + Why string `json:"why,omitempty"` } // ConditionEventTriedItem is a type of the contract. type ConditionEventTriedItem struct { - At string `json:"at,omitempty"` - By string `json:"by,omitempty"` - Outcome string `json:"outcome,omitempty"` - What string `json:"what,omitempty"` + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + Outcome string `json:"outcome,omitempty"` + What string `json:"what,omitempty"` } // ConditionEvent is a condition and what changed. type ConditionEvent struct { - Actions []Action `json:"actions,omitempty"` - At string `json:"at"` - Change string `json:"change"` - Cleared string `json:"cleared,omitempty"` - Count int64 `json:"count,omitempty"` - Epoch int64 `json:"epoch,omitempty"` - Event string `json:"event"` + Actions []Action `json:"actions,omitempty"` + At time.Time `json:"at"` + Change string `json:"change"` + Cleared *time.Time `json:"cleared,omitempty"` + Count int64 `json:"count,omitempty"` + Epoch int64 `json:"epoch,omitempty"` + Event string `json:"event"` // newest first, at most 10 Evidence []Evidence `json:"evidence,omitempty"` Explanation string `json:"explanation,omitempty"` - First string `json:"first,omitempty"` + First *time.Time `json:"first,omitempty"` Gaps []ConditionEventGapsItem `json:"gaps,omitempty"` Headline string `json:"headline,omitempty"` // scope.id.token - Key string `json:"key"` - Kind string `json:"kind,omitempty"` - LastObserved string `json:"last-observed,omitempty"` - LooksUnread string `json:"looks-unread,omitempty"` - Needs string `json:"needs,omitempty"` - Observations int64 `json:"observations,omitempty"` - Raised string `json:"raised,omitempty"` - Resolved string `json:"resolved,omitempty"` + Key string `json:"key"` + Kind string `json:"kind,omitempty"` + LastObserved *time.Time `json:"last-observed,omitempty"` + LooksUnread string `json:"looks-unread,omitempty"` + Needs string `json:"needs,omitempty"` + Observations int64 `json:"observations,omitempty"` + Raised *time.Time `json:"raised,omitempty"` + Resolved string `json:"resolved,omitempty"` // self, operator, agent or healer: Resolver string `json:"resolver,omitempty"` Severity string `json:"severity,omitempty"` @@ -214,8 +215,8 @@ const ( // Evidence is one observation, as it was said. type Evidence struct { - At string `json:"at"` - Said string `json:"said"` + At time.Time `json:"at"` + Said string `json:"said"` } // Subject is what the condition is about. @@ -288,24 +289,24 @@ type ConditionClearedEvent = ConditionEvent // ConditionsByKeyValueGapsItem is a type of the contract. type ConditionsByKeyValueGapsItem struct { - Cleared string `json:"cleared,omitempty"` - Reopened string `json:"reopened,omitempty"` + Cleared *time.Time `json:"cleared,omitempty"` + Reopened *time.Time `json:"reopened,omitempty"` } // ConditionsByKeyValueSilenced is a type of the contract. type ConditionsByKeyValueSilenced struct { - By string `json:"by,omitempty"` - Since string `json:"since,omitempty"` - Until string `json:"until,omitempty"` - Why string `json:"why,omitempty"` + By string `json:"by,omitempty"` + Since *time.Time `json:"since,omitempty"` + Until *time.Time `json:"until,omitempty"` + Why string `json:"why,omitempty"` } // ConditionsByKeyValueTriedItem is a type of the contract. type ConditionsByKeyValueTriedItem struct { - At string `json:"at,omitempty"` - By string `json:"by,omitempty"` - Outcome string `json:"outcome,omitempty"` - What string `json:"what,omitempty"` + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + Outcome string `json:"outcome,omitempty"` + What string `json:"what,omitempty"` } // ConditionsByKeyValue is one value of the state conditions under the key . @@ -316,18 +317,18 @@ type ConditionsByKeyValue struct { // newest first, at most 10 Evidence []Evidence `json:"evidence,omitempty"` Explanation string `json:"explanation,omitempty"` - First string `json:"first,omitempty"` + First *time.Time `json:"first,omitempty"` Gaps []ConditionsByKeyValueGapsItem `json:"gaps,omitempty"` Headline string `json:"headline,omitempty"` // scope.id.token - Key string `json:"key"` - Kind string `json:"kind"` - LastObserved string `json:"last-observed"` - LooksUnread string `json:"looks-unread,omitempty"` - Needs string `json:"needs,omitempty"` - Observations int64 `json:"observations,omitempty"` - Raised string `json:"raised"` - Resolved string `json:"resolved,omitempty"` + Key string `json:"key"` + Kind string `json:"kind"` + LastObserved time.Time `json:"last-observed"` + LooksUnread string `json:"looks-unread,omitempty"` + Needs string `json:"needs,omitempty"` + Observations int64 `json:"observations,omitempty"` + Raised time.Time `json:"raised"` + Resolved string `json:"resolved,omitempty"` // self, operator, agent or healer: Resolver string `json:"resolver,omitempty"` Severity string `json:"severity"` @@ -348,8 +349,8 @@ const ( // FindingsByKeyValue is one value of the state findings under the key . type FindingsByKeyValue struct { - Actions []Action `json:"actions,omitempty"` - At string `json:"at"` + Actions []Action `json:"actions,omitempty"` + At time.Time `json:"at"` // newest first, at most 10 Evidence []Evidence `json:"evidence,omitempty"` Explanation string `json:"explanation,omitempty"` diff --git a/health-and-repair/go/seats/meshhandacts/contract.go b/health-and-repair/go/seats/meshhandacts/contract.go index dfe00ce..12c84db 100644 --- a/health-and-repair/go/seats/meshhandacts/contract.go +++ b/health-and-repair/go/seats/meshhandacts/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -116,9 +117,9 @@ func DecodeWarrantsByAskerAskValue(raw []byte) (WarrantsByAskerAskValue, error) // Act is one act done by hand. type Act struct { - Arguments []string `json:"arguments,omitempty"` - Ask string `json:"ask,omitempty"` - At string `json:"at"` + Arguments []string `json:"arguments,omitempty"` + Ask string `json:"ask,omitempty"` + At time.Time `json:"at"` // the caller the runtime proved By string `json:"by"` Carried []string `json:"carried,omitempty"` @@ -191,9 +192,9 @@ type ActRecordedEvent = Act // ActsByIDValue is one value of the state acts under the key . type ActsByIDValue struct { - Arguments []string `json:"arguments,omitempty"` - Ask string `json:"ask,omitempty"` - At string `json:"at"` + Arguments []string `json:"arguments,omitempty"` + Ask string `json:"ask,omitempty"` + At time.Time `json:"at"` // the caller the runtime proved By string `json:"by"` Carried []string `json:"carried,omitempty"` @@ -214,9 +215,9 @@ type ActsByIDValue struct { // WarrantsByAskerAskValue is one value of the state warrants under the key .. type WarrantsByAskerAskValue struct { - Ask string `json:"ask"` - Asker string `json:"asker"` - At string `json:"at,omitempty"` - By string `json:"by,omitempty"` - Option string `json:"option,omitempty"` + Ask string `json:"ask"` + Asker string `json:"asker"` + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` } diff --git a/health-and-repair/go/seats/meshhealers/contract.go b/health-and-repair/go/seats/meshhealers/contract.go index b26eaaf..4733ebc 100644 --- a/health-and-repair/go/seats/meshhealers/contract.go +++ b/health-and-repair/go/seats/meshhealers/contract.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "errors" + "time" ) // Seat is the seat's name. @@ -143,17 +144,17 @@ type DrillAnswer = map[string]any // HealerActedEvent is the body of the event healer-acted. type HealerActedEvent struct { - Act string `json:"act,omitempty"` - At string `json:"at"` - Budget string `json:"budget,omitempty"` - By string `json:"by,omitempty"` - Condition string `json:"condition"` - Event string `json:"event"` - Healer string `json:"healer"` - Kind string `json:"kind,omitempty"` - Outcome string `json:"outcome"` - Said string `json:"said,omitempty"` - Show string `json:"show,omitempty"` + Act string `json:"act,omitempty"` + At time.Time `json:"at"` + Budget string `json:"budget,omitempty"` + By string `json:"by,omitempty"` + Condition string `json:"condition"` + Event string `json:"event"` + Healer string `json:"healer"` + Kind string `json:"kind,omitempty"` + Outcome string `json:"outcome"` + Said string `json:"said,omitempty"` + Show string `json:"show,omitempty"` } // The values of HealerActedEvent's outcome. @@ -166,16 +167,16 @@ const ( // AttemptsByAttemptValue is one value of the state attempts under the key . type AttemptsByAttemptValue struct { - Act string `json:"act"` - Begun string `json:"begun"` + Act string `json:"act"` + Begun time.Time `json:"begun"` // what the budget counts by: the condition's key unless the healer says another - BudgetKey string `json:"budget-key"` - Condition string `json:"condition"` - Finished string `json:"finished,omitempty"` - Healer string `json:"healer"` - Kind string `json:"kind"` - Outcome string `json:"outcome"` - Said string `json:"said,omitempty"` + BudgetKey string `json:"budget-key"` + Condition string `json:"condition"` + Finished *time.Time `json:"finished,omitempty"` + Healer string `json:"healer"` + Kind string `json:"kind"` + Outcome string `json:"outcome"` + Said string `json:"said,omitempty"` } // The values of AttemptsByAttemptValue's outcome. @@ -188,10 +189,10 @@ const ( // WouldByWouldValue is one value of the state would under the key . type WouldByWouldValue struct { - Act string `json:"act"` - At string `json:"at"` - Condition string `json:"condition"` - Healer string `json:"healer"` - Kind string `json:"kind,omitempty"` - Why string `json:"why,omitempty"` + Act string `json:"act"` + At time.Time `json:"at"` + Condition string `json:"condition"` + Healer string `json:"healer"` + Kind string `json:"kind,omitempty"` + Why string `json:"why,omitempty"` } diff --git a/health-and-repair/go/seats/meshselfcheck/contract.go b/health-and-repair/go/seats/meshselfcheck/contract.go index 2c67551..ad3b5cb 100644 --- a/health-and-repair/go/seats/meshselfcheck/contract.go +++ b/health-and-repair/go/seats/meshselfcheck/contract.go @@ -3,6 +3,8 @@ // Package meshselfcheck is the contract of the seat "mesh-self-check", defined whole (seats/mesh-self-check/seat.json): The mesh checking itself (ADR 0564): probes run every five minutes, each failure a finding the seat's conditions raise. package meshselfcheck +import "time" + // Seat is the seat's name. const Seat = "mesh-self-check" @@ -88,13 +90,13 @@ type CheckedEventCounts struct { // CheckedEvent is the body of the event checked. type CheckedEvent struct { - At string `json:"at"` + At time.Time `json:"at"` Controller string `json:"controller,omitempty"` Counts *CheckedEventCounts `json:"counts,omitempty"` IntervalSeconds int64 `json:"interval-seconds,omitempty"` Probes []ProbeVerdict `json:"probes"` Run string `json:"run"` - Started string `json:"started,omitempty"` + Started *time.Time `json:"started,omitempty"` Took string `json:"took,omitempty"` Unsaid int64 `json:"unsaid,omitempty"` Why string `json:"why,omitempty"` diff --git a/health-and-repair/go/seats/meshwatchdogs/contract.go b/health-and-repair/go/seats/meshwatchdogs/contract.go index 05c76ca..286d7dd 100644 --- a/health-and-repair/go/seats/meshwatchdogs/contract.go +++ b/health-and-repair/go/seats/meshwatchdogs/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -258,9 +259,9 @@ type StuckByBucketKeyValue struct { Bucket string `json:"bucket"` Key string `json:"key"` // it has moved again - Moved bool `json:"moved,omitempty"` - Since string `json:"since"` - V int64 `json:"v,omitempty"` + Moved bool `json:"moved,omitempty"` + Since time.Time `json:"since"` + V int64 `json:"v,omitempty"` // what it waited for, in words Waited string `json:"waited,omitempty"` } diff --git a/identity-and-access/go/clients/meshvaultclient/client_test.go b/identity-and-access/go/clients/meshvaultclient/client_test.go index 2c00bf5..77dba63 100644 --- a/identity-and-access/go/clients/meshvaultclient/client_test.go +++ b/identity-and-access/go/clients/meshvaultclient/client_test.go @@ -36,7 +36,7 @@ func TestSigningNodeLoginsAndOwnDrift(t *testing.T) { } // A node's two logins: the first, the second, a rotation to the other only, a stage only with a rotation. - since := "2026-11-02T01:00:00Z" + since := time.Date(2026, 11, 2, 1, 0, 0, 0, time.UTC) for name, c := range map[string]struct { l seat.NodeLoginsByNodeValue ok bool @@ -56,7 +56,7 @@ func TestSigningNodeLoginsAndOwnDrift(t *testing.T) { } else if back, err := DecodeNodeLogin(raw); err != nil || Rotates(back) { t.Errorf("round trip: %+v %v", back, err) } - if _, err := DecodeNodeLogin([]byte(`{"v":9,"node":"g14","login":"node.g14","since":"` + since + `"}`)); err == nil { + if _, err := DecodeNodeLogin([]byte(`{"v":9,"node":"g14","login":"node.g14","since":"` + since.Format(time.RFC3339) + `"}`)); err == nil { t.Error("version 9 was taken") } if n, ok := LoginNode("node.g14--b"); !ok || n != "g14" { diff --git a/identity-and-access/go/clients/meshvaultclient/signers.go b/identity-and-access/go/clients/meshvaultclient/signers.go index 1ef79ce..476b4e6 100644 --- a/identity-and-access/go/clients/meshvaultclient/signers.go +++ b/identity-and-access/go/clients/meshvaultclient/signers.go @@ -87,7 +87,7 @@ func ParseCertificate(text string) (*x509.Certificate, error) { // NewSignerKey is the value of `key` for pub. func NewSignerKey(pub ed25519.PublicKey, made time.Time) seat.SignersByKeyValue { - return seat.SignersByKeyValue{V: 1, Key: base64.StdEncoding.EncodeToString(pub), Fingerprint: SignerFingerprint(pub), Made: made.UTC().Format(time.RFC3339Nano)} + return seat.SignersByKeyValue{V: 1, Key: base64.StdEncoding.EncodeToString(pub), Fingerprint: SignerFingerprint(pub), Made: made.UTC()} } // PublicKey is k's key, checked: an ed25519 public key whose fingerprint is the one k names. diff --git a/identity-and-access/go/seats/meshnodeenroller/contract.go b/identity-and-access/go/seats/meshnodeenroller/contract.go index 3cdab36..fc7f677 100644 --- a/identity-and-access/go/seats/meshnodeenroller/contract.go +++ b/identity-and-access/go/seats/meshnodeenroller/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -137,14 +138,14 @@ func DecodeEnrolmentsByAddressAddressValue(raw []byte) (EnrolmentsByAddressAddre // Enrolment is one open enrolment. type Enrolment struct { - Address string `json:"address"` - Ask string `json:"ask,omitempty"` - Caller string `json:"caller,omitempty"` - Expires string `json:"expires,omitempty"` - Issued string `json:"issued,omitempty"` - KeysDigest string `json:"keys-digest,omitempty"` - Node string `json:"node"` - OverlayKey string `json:"overlay-key,omitempty"` + Address string `json:"address"` + Ask string `json:"ask,omitempty"` + Caller string `json:"caller,omitempty"` + Expires *time.Time `json:"expires,omitempty"` + Issued *time.Time `json:"issued,omitempty"` + KeysDigest string `json:"keys-digest,omitempty"` + Node string `json:"node"` + OverlayKey string `json:"overlay-key,omitempty"` // asked, live or redeeming Stage string `json:"stage"` V int64 `json:"v,omitempty"` @@ -266,8 +267,8 @@ type EnrolmentsByNodeNodeValue = Enrolment // EnrolmentsByAddressAddressValue is one value of the state enrolments under the key address.
. type EnrolmentsByAddressAddressValue struct { - Ask string `json:"ask,omitempty"` - Issued string `json:"issued,omitempty"` - Node string `json:"node"` - V int64 `json:"v,omitempty"` + Ask string `json:"ask,omitempty"` + Issued *time.Time `json:"issued,omitempty"` + Node string `json:"node"` + V int64 `json:"v,omitempty"` } diff --git a/identity-and-access/go/seats/meshvault/contract.go b/identity-and-access/go/seats/meshvault/contract.go index 2f3fe11..9d51257 100644 --- a/identity-and-access/go/seats/meshvault/contract.go +++ b/identity-and-access/go/seats/meshvault/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -539,18 +540,18 @@ type OwnRetiredHolderEvent struct { type SignersByKeyValue struct { Fingerprint string `json:"fingerprint"` // the signing key's public half, standard base64 - Key string `json:"key"` - Made string `json:"made"` - V int64 `json:"v,omitempty"` + Key string `json:"key"` + Made time.Time `json:"made"` + V int64 `json:"v,omitempty"` } // SignersByAuthorityValue is one value of the state signers under the key authority. type SignersByAuthorityValue struct { // the authority certificate, PEM - Certificate string `json:"certificate"` - Fingerprint string `json:"fingerprint"` - Made string `json:"made"` - V int64 `json:"v,omitempty"` + Certificate string `json:"certificate"` + Fingerprint string `json:"fingerprint"` + Made time.Time `json:"made"` + V int64 `json:"v,omitempty"` } // NodeLoginsByNodeValue is one value of the state node-logins under the key . @@ -562,8 +563,8 @@ type NodeLoginsByNodeValue struct { Login string `json:"login"` Node string `json:"node"` // the login rotating to, empty when none - Rotating string `json:"rotating,omitempty"` - Since string `json:"since"` + Rotating string `json:"rotating,omitempty"` + Since time.Time `json:"since"` // begun, ready or confirmed; empty when no rotation Stage string `json:"stage,omitempty"` V int64 `json:"v,omitempty"` diff --git a/placement/go/seats/meshmoduleassigner/contract.go b/placement/go/seats/meshmoduleassigner/contract.go index 843340f..85f9ec8 100644 --- a/placement/go/seats/meshmoduleassigner/contract.go +++ b/placement/go/seats/meshmoduleassigner/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -403,9 +404,9 @@ type Placement struct { // PlacementEventWarrant is who approved it, through which channel, when. type PlacementEventWarrant struct { - At string `json:"at"` - Warrant string `json:"warrant"` - Words string `json:"words"` + At time.Time `json:"at"` + Warrant string `json:"warrant"` + Words string `json:"words"` } // PlacementEvent is what each placement event carries. @@ -421,9 +422,9 @@ type PlacementEvent struct { // Proposal is an open proposal. type Proposal struct { - Again string `json:"again,omitempty"` - Ask string `json:"ask,omitempty"` - At string `json:"at"` + Again *time.Time `json:"again,omitempty"` + Ask string `json:"ask,omitempty"` + At time.Time `json:"at"` // by node whose key it changes, the revision of the key it was laid over (0: none) Base map[string]int64 `json:"base"` By string `json:"by"` @@ -434,14 +435,14 @@ type Proposal struct { Must []string `json:"must,omitempty"` Seed bool `json:"seed,omitempty"` // answering, failed or asked - State string `json:"state"` - V int64 `json:"v,omitempty"` - Verdict *Verdict `json:"verdict,omitempty"` - Warrant *Warrant `json:"warrant,omitempty"` - Why string `json:"why"` - Withdrawn string `json:"withdrawn,omitempty"` - Words string `json:"words,omitempty"` - Written string `json:"written,omitempty"` + State string `json:"state"` + V int64 `json:"v,omitempty"` + Verdict *Verdict `json:"verdict,omitempty"` + Warrant *Warrant `json:"warrant,omitempty"` + Why string `json:"why"` + Withdrawn string `json:"withdrawn,omitempty"` + Words string `json:"words,omitempty"` + Written *time.Time `json:"written,omitempty"` } // ProposalChangeKeys is the public halves of a joining node's keys. @@ -507,7 +508,7 @@ type SeatHolders struct { type Verdict struct { // every node's answer to its candidate, by node Answers map[string]map[string]any `json:"answers,omitempty"` - At string `json:"at"` + At time.Time `json:"at"` Carried map[string][]string `json:"carried,omitempty"` Fails []string `json:"fails,omitempty"` Intended map[string][]string `json:"intended,omitempty"` @@ -516,7 +517,7 @@ type Verdict struct { // waits, pass or fail State string `json:"state"` Unintended map[string][]string `json:"unintended,omitempty"` - Until string `json:"until,omitempty"` + Until *time.Time `json:"until,omitempty"` } // Warrant is the enrolment's warrant an add-node carries. diff --git a/provisioning/go/seats/meshprovisioner/contract.go b/provisioning/go/seats/meshprovisioner/contract.go index 3d54da9..7e2325e 100644 --- a/provisioning/go/seats/meshprovisioner/contract.go +++ b/provisioning/go/seats/meshprovisioner/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -217,16 +218,16 @@ type ProvisionsByNodeConsumerProvisionValueReference struct { // ProvisionsByNodeConsumerProvisionValueWarrant is a type of the contract. type ProvisionsByNodeConsumerProvisionValueWarrant struct { - Ask string `json:"ask,omitempty"` - At string `json:"at,omitempty"` - By string `json:"by,omitempty"` - Option string `json:"option,omitempty"` + Ask string `json:"ask,omitempty"` + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` } // ProvisionsByNodeConsumerProvisionValue is one value of the state provisions under the key ... type ProvisionsByNodeConsumerProvisionValue struct { Ask string `json:"ask,omitempty"` - At string `json:"at"` + At time.Time `json:"at"` Consumer ProvisionsByNodeConsumerProvisionValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` @@ -289,16 +290,16 @@ type ProvisionsByNodeConsumerProvisionLocalValueReference struct { // ProvisionsByNodeConsumerProvisionLocalValueWarrant is a type of the contract. type ProvisionsByNodeConsumerProvisionLocalValueWarrant struct { - Ask string `json:"ask,omitempty"` - At string `json:"at,omitempty"` - By string `json:"by,omitempty"` - Option string `json:"option,omitempty"` + Ask string `json:"ask,omitempty"` + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + Option string `json:"option,omitempty"` } // ProvisionsByNodeConsumerProvisionLocalValue is one value of the state provisions under the key .... type ProvisionsByNodeConsumerProvisionLocalValue struct { Ask string `json:"ask,omitempty"` - At string `json:"at"` + At time.Time `json:"at"` Consumer ProvisionsByNodeConsumerProvisionLocalValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` -- 2.54.0 From db2a6da4713309f5f37da24617a59c98afcac9c9 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:14:16 +0100 Subject: [PATCH 68/94] =?UTF-8?q?node-engine=20client:=20the=20codec=20and?= =?UTF-8?q?=20the=20comparison=20of=20the=20have=20and=20the=20node's=20fa?= =?UTF-8?q?cts,=20and=20the=20phases=20and=20healths=20a=20have=20names=20?= =?UTF-8?q?(ADR=200634=20=C2=A71,=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../containerimageregistryclient/client.go | 63 --- .../client_test.go | 57 --- .../containerimageregistryclient/go.mod | 17 - .../containerimageregistryclient/go.sum | 18 - go/clients/gopackageregistryclient/client.go | 63 --- .../gopackageregistryclient/client_test.go | 57 --- go/clients/gopackageregistryclient/go.mod | 17 - go/clients/gopackageregistryclient/go.sum | 18 - go/clients/nodeengineclient/have.go | 206 +++++++++ go/clients/nodeengineclient/have_test.go | 107 +++++ go/clients/npmpackageregistryclient/client.go | 63 --- .../npmpackageregistryclient/client_test.go | 57 --- go/clients/npmpackageregistryclient/go.mod | 17 - go/clients/npmpackageregistryclient/go.sum | 18 - .../ociartifactregistryclient/client.go | 63 --- .../ociartifactregistryclient/client_test.go | 57 --- go/clients/ociartifactregistryclient/go.mod | 17 - go/clients/ociartifactregistryclient/go.sum | 18 - go/seats/containerimageregistry/contract.go | 403 ------------------ .../containerimageregistry/contract_test.go | 74 ---- go/seats/containerimageregistry/go.mod | 3 - go/seats/gopackageregistry/contract.go | 262 ------------ go/seats/gopackageregistry/contract_test.go | 66 --- go/seats/gopackageregistry/go.mod | 3 - go/seats/meshdeliverychecker/contract.go | 131 ------ go/seats/meshdeliverychecker/contract_test.go | 52 --- go/seats/meshdeliverychecker/go.mod | 3 - go/seats/npmpackageregistry/contract.go | 262 ------------ go/seats/npmpackageregistry/contract_test.go | 66 --- go/seats/npmpackageregistry/go.mod | 3 - go/seats/ociartifactregistry/contract.go | 361 ---------------- go/seats/ociartifactregistry/contract_test.go | 74 ---- go/seats/ociartifactregistry/go.mod | 3 - .../fixtures/events.json | 98 ----- .../fixtures/verbs.json | 149 ------- seats/container-image-registry/seat.json | 345 --------------- seats/git/seat.json | 9 - .../go-package-registry/fixtures/events.json | 73 ---- seats/go-package-registry/fixtures/verbs.json | 110 ----- seats/go-package-registry/seat.json | 35 -- seats/mesh-artifact-store/seat.json | 8 - seats/mesh-artifact-sweep/seat.json | 104 ----- seats/mesh-catalog/seat.json | 70 --- .../fixtures/events.json | 44 -- seats/mesh-delivery-checker/seat.json | 246 ----------- seats/mesh-delivery/seat.json | 227 ---------- seats/node-build-go/seat.json | 47 -- seats/node-build-image/seat.json | 47 -- seats/node-build-node/seat.json | 47 -- .../npm-package-registry/fixtures/events.json | 72 ---- .../npm-package-registry/fixtures/verbs.json | 110 ----- seats/npm-package-registry/seat.json | 35 -- .../fixtures/events.json | 98 ----- .../oci-artifact-registry/fixtures/verbs.json | 138 ------ seats/oci-artifact-registry/seat.json | 248 ----------- .../containerimageregistry/package-lock.json | 12 - ts/seats/containerimageregistry/package.json | 13 - ts/seats/containerimageregistry/src/index.ts | 190 --------- .../src/parts/registry.ts | 187 -------- ts/seats/containerimageregistry/tsconfig.json | 12 - ts/seats/gopackageregistry/package-lock.json | 12 - ts/seats/gopackageregistry/package.json | 13 - ts/seats/gopackageregistry/src/index.ts | 54 --- .../gopackageregistry/src/parts/registry.ts | 187 -------- ts/seats/gopackageregistry/tsconfig.json | 12 - .../meshdeliverychecker/package-lock.json | 12 - ts/seats/meshdeliverychecker/package.json | 13 - ts/seats/meshdeliverychecker/src/index.ts | 122 ------ ts/seats/meshdeliverychecker/tsconfig.json | 12 - ts/seats/npmpackageregistry/package-lock.json | 12 - ts/seats/npmpackageregistry/package.json | 13 - ts/seats/npmpackageregistry/src/index.ts | 54 --- .../npmpackageregistry/src/parts/registry.ts | 187 -------- ts/seats/npmpackageregistry/tsconfig.json | 12 - .../ociartifactregistry/package-lock.json | 12 - ts/seats/ociartifactregistry/package.json | 13 - ts/seats/ociartifactregistry/src/index.ts | 149 ------- .../ociartifactregistry/src/parts/registry.ts | 187 -------- ts/seats/ociartifactregistry/tsconfig.json | 12 - 79 files changed, 313 insertions(+), 6248 deletions(-) delete mode 100644 go/clients/containerimageregistryclient/client.go delete mode 100644 go/clients/containerimageregistryclient/client_test.go delete mode 100644 go/clients/containerimageregistryclient/go.mod delete mode 100644 go/clients/containerimageregistryclient/go.sum delete mode 100644 go/clients/gopackageregistryclient/client.go delete mode 100644 go/clients/gopackageregistryclient/client_test.go delete mode 100644 go/clients/gopackageregistryclient/go.mod delete mode 100644 go/clients/gopackageregistryclient/go.sum create mode 100644 go/clients/nodeengineclient/have.go create mode 100644 go/clients/nodeengineclient/have_test.go delete mode 100644 go/clients/npmpackageregistryclient/client.go delete mode 100644 go/clients/npmpackageregistryclient/client_test.go delete mode 100644 go/clients/npmpackageregistryclient/go.mod delete mode 100644 go/clients/npmpackageregistryclient/go.sum delete mode 100644 go/clients/ociartifactregistryclient/client.go delete mode 100644 go/clients/ociartifactregistryclient/client_test.go delete mode 100644 go/clients/ociartifactregistryclient/go.mod delete mode 100644 go/clients/ociartifactregistryclient/go.sum delete mode 100644 go/seats/containerimageregistry/contract.go delete mode 100644 go/seats/containerimageregistry/contract_test.go delete mode 100644 go/seats/containerimageregistry/go.mod delete mode 100644 go/seats/gopackageregistry/contract.go delete mode 100644 go/seats/gopackageregistry/contract_test.go delete mode 100644 go/seats/gopackageregistry/go.mod delete mode 100644 go/seats/meshdeliverychecker/contract.go delete mode 100644 go/seats/meshdeliverychecker/contract_test.go delete mode 100644 go/seats/meshdeliverychecker/go.mod delete mode 100644 go/seats/npmpackageregistry/contract.go delete mode 100644 go/seats/npmpackageregistry/contract_test.go delete mode 100644 go/seats/npmpackageregistry/go.mod delete mode 100644 go/seats/ociartifactregistry/contract.go delete mode 100644 go/seats/ociartifactregistry/contract_test.go delete mode 100644 go/seats/ociartifactregistry/go.mod delete mode 100644 seats/container-image-registry/fixtures/events.json delete mode 100644 seats/container-image-registry/fixtures/verbs.json delete mode 100644 seats/container-image-registry/seat.json delete mode 100644 seats/git/seat.json delete mode 100644 seats/go-package-registry/fixtures/events.json delete mode 100644 seats/go-package-registry/fixtures/verbs.json delete mode 100644 seats/go-package-registry/seat.json delete mode 100644 seats/mesh-artifact-store/seat.json delete mode 100644 seats/mesh-artifact-sweep/seat.json delete mode 100644 seats/mesh-catalog/seat.json delete mode 100644 seats/mesh-delivery-checker/fixtures/events.json delete mode 100644 seats/mesh-delivery-checker/seat.json delete mode 100644 seats/mesh-delivery/seat.json delete mode 100644 seats/node-build-go/seat.json delete mode 100644 seats/node-build-image/seat.json delete mode 100644 seats/node-build-node/seat.json delete mode 100644 seats/npm-package-registry/fixtures/events.json delete mode 100644 seats/npm-package-registry/fixtures/verbs.json delete mode 100644 seats/npm-package-registry/seat.json delete mode 100644 seats/oci-artifact-registry/fixtures/events.json delete mode 100644 seats/oci-artifact-registry/fixtures/verbs.json delete mode 100644 seats/oci-artifact-registry/seat.json delete mode 100644 ts/seats/containerimageregistry/package-lock.json delete mode 100644 ts/seats/containerimageregistry/package.json delete mode 100644 ts/seats/containerimageregistry/src/index.ts delete mode 100644 ts/seats/containerimageregistry/src/parts/registry.ts delete mode 100644 ts/seats/containerimageregistry/tsconfig.json delete mode 100644 ts/seats/gopackageregistry/package-lock.json delete mode 100644 ts/seats/gopackageregistry/package.json delete mode 100644 ts/seats/gopackageregistry/src/index.ts delete mode 100644 ts/seats/gopackageregistry/src/parts/registry.ts delete mode 100644 ts/seats/gopackageregistry/tsconfig.json delete mode 100644 ts/seats/meshdeliverychecker/package-lock.json delete mode 100644 ts/seats/meshdeliverychecker/package.json delete mode 100644 ts/seats/meshdeliverychecker/src/index.ts delete mode 100644 ts/seats/meshdeliverychecker/tsconfig.json delete mode 100644 ts/seats/npmpackageregistry/package-lock.json delete mode 100644 ts/seats/npmpackageregistry/package.json delete mode 100644 ts/seats/npmpackageregistry/src/index.ts delete mode 100644 ts/seats/npmpackageregistry/src/parts/registry.ts delete mode 100644 ts/seats/npmpackageregistry/tsconfig.json delete mode 100644 ts/seats/ociartifactregistry/package-lock.json delete mode 100644 ts/seats/ociartifactregistry/package.json delete mode 100644 ts/seats/ociartifactregistry/src/index.ts delete mode 100644 ts/seats/ociartifactregistry/src/parts/registry.ts delete mode 100644 ts/seats/ociartifactregistry/tsconfig.json diff --git a/go/clients/containerimageregistryclient/client.go b/go/clients/containerimageregistryclient/client.go deleted file mode 100644 index 4add3df..0000000 --- a/go/clients/containerimageregistryclient/client.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package containerimageregistryclient is the container-image-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 -// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, -// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a -// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is -// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the -// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. -package containerimageregistryclient - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - - seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. -type Call func(key string, body any) (json.RawMessage, error) - -// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were -// sent, and the chunks they took. -type Sent struct { - Transfer string - parts.Sent -} - -// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in -// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes -// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. -// Its outcome, published or refused, is the holder's event, which the caller waits for. -func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { - open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { - in.Parts = make([]seat.Part, 0, len(ps)) - for _, p := range ps { - in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) - } - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) - if err != nil { - return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) - } - var a seat.PublishAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) - } - return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil - } - chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, - Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) - if err != nil { - return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) - } - var a seat.PublishPartAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) - } - return nil - } - sent, err := parts.Send(ctx, open, chunk, ps) - return Sent{Transfer: in.Transfer, Sent: sent}, err -} diff --git a/go/clients/containerimageregistryclient/client_test.go b/go/clients/containerimageregistryclient/client_test.go deleted file mode 100644 index 8ef827a..0000000 --- a/go/clients/containerimageregistryclient/client_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package containerimageregistryclient - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "io" - "testing" - - seat "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the -// holder lacks travels by publish-part, its bytes whole, the last chunk marked. -func TestPublishTalksToItsOwnSeat(t *testing.T) { - held, lacked := []byte("held"), []byte("lacked") - part := func(b []byte) parts.Part { - sum := sha256.Sum256(b) - return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), - Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} - } - ps := []parts.Part{part(held), part(lacked)} - var asked []string - var got []byte - call := func(key string, body any) (json.RawMessage, error) { - asked = append(asked, key) - raw, _ := json.Marshal(body) - switch key { - case "seat:container-image-registry.publish": - var in seat.PublishInput - _ = json.Unmarshal(raw, &in) - if len(in.Parts) != 2 { - t.Errorf("publish names %d parts, want 2", len(in.Parts)) - } - return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) - case "seat:container-image-registry.publish-part": - var in seat.PublishPartInput - _ = json.Unmarshal(raw, &in) - data, _ := base64.StdEncoding.DecodeString(in.Data) - if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { - t.Errorf("publish-part %+v", in) - } - got = append(got, data...) - return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) - } - t.Fatalf("asked %s", key) - return nil, nil - } - sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) - if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { - t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) - } -} diff --git a/go/clients/containerimageregistryclient/go.mod b/go/clients/containerimageregistryclient/go.mod deleted file mode 100644 index 6e28d16..0000000 --- a/go/clients/containerimageregistryclient/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/clients/containerimageregistryclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef -) - -require ( - github.com/klauspost/compress v1.20.1 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect - github.com/nats-io/nkeys v0.4.16 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/sys v0.48.0 // indirect -) diff --git a/go/clients/containerimageregistryclient/go.sum b/go/clients/containerimageregistryclient/go.sum deleted file mode 100644 index a93fc2b..0000000 --- a/go/clients/containerimageregistryclient/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a h1:BTqpa7dCCTQH9m08Jle8KVFwyIHiHPSUbwWxuqSg2yA= -git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:pPxwhTMb3hIk7UUrBicJnGQCqfTEoBGJvNFspEK2U18= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= -github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/gopackageregistryclient/client.go b/go/clients/gopackageregistryclient/client.go deleted file mode 100644 index e339065..0000000 --- a/go/clients/gopackageregistryclient/client.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package gopackageregistryclient is the go-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 -// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, -// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a -// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is -// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the -// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. -package gopackageregistryclient - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - - seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. -type Call func(key string, body any) (json.RawMessage, error) - -// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were -// sent, and the chunks they took. -type Sent struct { - Transfer string - parts.Sent -} - -// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in -// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes -// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. -// Its outcome, published or refused, is the holder's event, which the caller waits for. -func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { - open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { - in.Parts = make([]seat.Part, 0, len(ps)) - for _, p := range ps { - in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) - } - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) - if err != nil { - return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) - } - var a seat.PublishAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) - } - return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil - } - chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, - Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) - if err != nil { - return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) - } - var a seat.PublishPartAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) - } - return nil - } - sent, err := parts.Send(ctx, open, chunk, ps) - return Sent{Transfer: in.Transfer, Sent: sent}, err -} diff --git a/go/clients/gopackageregistryclient/client_test.go b/go/clients/gopackageregistryclient/client_test.go deleted file mode 100644 index 3180a68..0000000 --- a/go/clients/gopackageregistryclient/client_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package gopackageregistryclient - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "io" - "testing" - - seat "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the -// holder lacks travels by publish-part, its bytes whole, the last chunk marked. -func TestPublishTalksToItsOwnSeat(t *testing.T) { - held, lacked := []byte("held"), []byte("lacked") - part := func(b []byte) parts.Part { - sum := sha256.Sum256(b) - return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), - Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} - } - ps := []parts.Part{part(held), part(lacked)} - var asked []string - var got []byte - call := func(key string, body any) (json.RawMessage, error) { - asked = append(asked, key) - raw, _ := json.Marshal(body) - switch key { - case "seat:go-package-registry.publish": - var in seat.PublishInput - _ = json.Unmarshal(raw, &in) - if len(in.Parts) != 2 { - t.Errorf("publish names %d parts, want 2", len(in.Parts)) - } - return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) - case "seat:go-package-registry.publish-part": - var in seat.PublishPartInput - _ = json.Unmarshal(raw, &in) - data, _ := base64.StdEncoding.DecodeString(in.Data) - if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { - t.Errorf("publish-part %+v", in) - } - got = append(got, data...) - return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) - } - t.Fatalf("asked %s", key) - return nil, nil - } - sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) - if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { - t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) - } -} diff --git a/go/clients/gopackageregistryclient/go.mod b/go/clients/gopackageregistryclient/go.mod deleted file mode 100644 index ab2eab4..0000000 --- a/go/clients/gopackageregistryclient/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/clients/gopackageregistryclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef -) - -require ( - github.com/klauspost/compress v1.20.1 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect - github.com/nats-io/nkeys v0.4.16 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/sys v0.48.0 // indirect -) diff --git a/go/clients/gopackageregistryclient/go.sum b/go/clients/gopackageregistryclient/go.sum deleted file mode 100644 index 0234cfd..0000000 --- a/go/clients/gopackageregistryclient/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a h1:Zl6CzMg9AZtk7RU/DSJxSGGoUg5fSW3t7Yf8zyfXbVY= -git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:Y5kgBh0hBbL/hgc+abCF9Vbhov+vCsB0ckpRzUw1D+0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= -github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/nodeengineclient/have.go b/go/clients/nodeengineclient/have.go new file mode 100644 index 0000000..7babbbe --- /dev/null +++ b/go/clients/nodeengineclient/have.go @@ -0,0 +1,206 @@ +package nodeengineclient + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The state have (novox/hq ADR 0539 clause 9): a module's have on a node under `.` and the node's own facts +// under `.node`; their layouts are the seat's contract (seat.HaveByNodeModuleValue, seat.HaveByNodeNodeValue). This +// file is the codec the node-engine, the assigner, the watchman and every reader share: it writes the version, and refuses +// a phase, a health, a judgement or a digest outside the lists. + +// The phases of a module's change on a node, as the have states them (the seat's constants, short). +const ( + PhaseBackingUp = seat.HaveByNodeModuleValuePhaseBackingUp + PhaseApplying = seat.HaveByNodeModuleValuePhaseApplying + PhaseChecking = seat.HaveByNodeModuleValuePhaseChecking + PhaseRunning = seat.HaveByNodeModuleValuePhaseRunning + PhaseFailed = seat.HaveByNodeModuleValuePhaseFailed + PhaseRemoved = seat.HaveByNodeModuleValuePhaseRemoved + PhaseHeld = seat.HaveByNodeModuleValuePhaseHeld + PhaseWaitingYes = seat.HaveByNodeModuleValuePhaseWaitingYes +) + +// The healths a have carries. +const ( + HealthOK = seat.HaveByNodeModuleValueHealthOk + HealthFailed = seat.HaveByNodeModuleValueHealthFailed + HealthWaits = seat.HaveByNodeModuleValueHealthWaits + HealthUnknown = seat.HaveByNodeModuleValueHealthUnknown +) + +// The judgements of a move that waits for a yes. +const ( + JudgedRecreates = seat.HaveByNodeModuleValueJudgementRecreates + JudgedMovesData = seat.HaveByNodeModuleValueJudgementMovesData + JudgedCannot = seat.HaveByNodeModuleValueJudgementCannotJudge +) + +// ComposesOwn is a node applying its own compositions (the node facts' composes). +const ComposesOwn = "own" + +// composedByTheEndedController is what an engine from before the controller ended wrote in composes: read, never written. +const composedByTheEndedController = "controller" + +var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) + +func checkDigest(digest, of string) error { + if !digestForm.MatchString(digest) { + return fmt.Errorf("node-engine: %s %q is not sha256:<64 lowercase hex digits>", of, digest) + } + return nil +} + +func checkHave(h seat.HaveByNodeModuleValue) error { + switch h.Phase { + case PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed, PhaseRemoved, PhaseHeld: + if h.Judgement != "" || h.Waits != "" || h.BuildAlone { + return fmt.Errorf("node-engine: only a have waiting for a yes names a judgement, the composition waiting and whether it changes the build alone") + } + if h.PullWait != nil && h.Phase != PhaseFailed { + return fmt.Errorf("node-engine: only a failed have names the pull it waits on, not one %s", h.Phase) + } + case PhaseWaitingYes: + if h.Judgement != JudgedRecreates && h.Judgement != JudgedMovesData && h.Judgement != JudgedCannot { + return fmt.Errorf("node-engine: a have waiting for a yes names the judgement %q, not recreates, moves-data or cannot-judge", h.Judgement) + } + if err := checkDigest(h.Waits, "the composition a have waits a yes for"); err != nil { + return err + } + default: + return fmt.Errorf("node-engine: phase %q is not backing-up, applying, checking, running, failed, removed, held or waiting-yes", h.Phase) + } + if w := h.PullWait; w != nil && (w.Image == "" || w.Holder == "") { + return fmt.Errorf("node-engine: a have's pull wait names no image or no holder") + } + if h.Composed != "" { + if err := checkDigest(h.Composed, "the have's composition digest"); err != nil { + return err + } + } + switch h.Health { + case HealthOK, HealthFailed, HealthWaits, HealthUnknown: + default: + return fmt.Errorf("node-engine: health %q is not ok, failed, waits or unknown", h.Health) + } + return nil +} + +// EncodeHave is h as JSON at the current version; it refuses a phase or health outside the lists. +func EncodeHave(h seat.HaveByNodeModuleValue) ([]byte, error) { + if err := checkHave(h); err != nil { + return nil, err + } + h.V = keys.Version + return json.Marshal(h) +} + +// DecodeHave reads a have; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does not know and +// what EncodeHave refuses. +func DecodeHave(data []byte) (seat.HaveByNodeModuleValue, error) { + if err := keys.CheckVersion(data); err != nil { + return seat.HaveByNodeModuleValue{}, err + } + h, err := seat.DecodeHaveByNodeModuleValue(data) + if err != nil { + return seat.HaveByNodeModuleValue{}, fmt.Errorf("node-engine: value does not fit its shape: %w", err) + } + if err := checkHave(h); err != nil { + return seat.HaveByNodeModuleValue{}, err + } + return h, nil +} + +func checkHaveNode(n seat.HaveByNodeNodeValue) error { + switch n.Composes { + case "", ComposesOwn, composedByTheEndedController: + default: + return fmt.Errorf("node-engine: a node composes %q, not %s", n.Composes, ComposesOwn) + } + for group, a := range n.Candidates { + if group == "" { + return fmt.Errorf("node-engine: a node's candidate answer names no group") + } + switch a.Verdict { + case seat.CandidateAnswerVerdictSame, seat.CandidateAnswerVerdictDiffers, seat.CandidateAnswerVerdictRefused: + default: + return fmt.Errorf("node-engine: the answer to %s's candidate has the verdict %q", group, a.Verdict) + } + } + return nil +} + +// EncodeHaveNode is n as JSON at the current version; it refuses a candidate answer with a verdict outside the lists. +func EncodeHaveNode(n seat.HaveByNodeNodeValue) ([]byte, error) { + if err := checkHaveNode(n); err != nil { + return nil, err + } + n.V = keys.Version + return json.Marshal(n) +} + +// DecodeHaveNode reads a node's facts; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does +// not know and what EncodeHaveNode refuses. +func DecodeHaveNode(data []byte) (seat.HaveByNodeNodeValue, error) { + if err := keys.CheckVersion(data); err != nil { + return seat.HaveByNodeNodeValue{}, err + } + n, err := seat.DecodeHaveByNodeNodeValue(data) + if err != nil { + return seat.HaveByNodeNodeValue{}, fmt.Errorf("node-engine: value does not fit its shape: %w", err) + } + if err := checkHaveNode(n); err != nil { + return seat.HaveByNodeNodeValue{}, err + } + return n, nil +} + +// SameHave says whether two haves are the same value, so a writer writes only when they differ: times compare as +// instants, the user list's and the handed accounts' time as well. +func SameHave(a, b seat.HaveByNodeModuleValue) bool { + return sameJSON(utcHave(a), utcHave(b)) +} + +// SameHaveNode says whether two node facts are the same value, compared as their JSON (maps sorted, spacing dropped). +func SameHaveNode(a, b seat.HaveByNodeNodeValue) bool { return sameJSON(a, b) } + +// SameNodeRecord says whether two records are the same value but for their version. +func SameNodeRecord(a, b seat.NodeRecord) bool { + a.V, b.V = 0, 0 + return sameJSON(a, b) +} + +func utcHave(h seat.HaveByNodeModuleValue) seat.HaveByNodeModuleValue { + if w := h.PullWait; w != nil { + c := *w + c.Began = c.Began.UTC() + if c.RetriedAt != nil { + t := c.RetriedAt.UTC() + c.RetriedAt = &t + } + h.PullWait = &c + } + if u := h.UserList; u != nil { + c := *u + c.At = c.At.UTC() + if c.Handed != nil { + hd := *c.Handed + hd.At = hd.At.UTC() + c.Handed = &hd + } + h.UserList = &c + } + return h +} + +func sameJSON(a, b any) bool { + ra, errA := json.Marshal(a) + rb, errB := json.Marshal(b) + return errA == nil && errB == nil && bytes.Equal(ra, rb) +} diff --git a/go/clients/nodeengineclient/have_test.go b/go/clients/nodeengineclient/have_test.go new file mode 100644 index 0000000..e91d135 --- /dev/null +++ b/go/clients/nodeengineclient/have_test.go @@ -0,0 +1,107 @@ +package nodeengineclient + +import ( + "errors" + "strings" + "testing" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +const digestA = "sha256:0000000000000000000000000000000000000000000000000000000000000001" + +func TestHaveCodec(t *testing.T) { + good := seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Composed: digestA, Fp: "src2:x"} + waiting := seat.HaveByNodeModuleValue{Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedRecreates, Waits: digestA} + pull := &seat.PullWait{Image: "i", Holder: "h"} + tests := map[string]struct { + have seat.HaveByNodeModuleValue + bad string + }{ + "running": {good, ""}, + "waiting for a yes": {waiting, ""}, + "unknown phase": {seat.HaveByNodeModuleValue{Phase: "gone", Health: HealthOK}, "phase"}, + "unknown health": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: "fine"}, "health"}, + "judgement outside a yes": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Judgement: JudgedCannot}, "judgement"}, + "a yes without a digest": {seat.HaveByNodeModuleValue{Phase: PhaseWaitingYes, Health: HealthOK, Judgement: JudgedCannot, Waits: "x"}, "sha256"}, + "pull wait on a running": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, PullWait: pull}, "pull"}, + "pull wait without holder": {seat.HaveByNodeModuleValue{Phase: PhaseFailed, Health: HealthFailed, PullWait: &seat.PullWait{Image: "i"}}, "holder"}, + "bad composed digest": {seat.HaveByNodeModuleValue{Phase: PhaseRunning, Health: HealthOK, Composed: "abc"}, "sha256"}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + raw, err := EncodeHave(tc.have) + if tc.bad != "" { + if err == nil || !strings.Contains(err.Error(), tc.bad) { + t.Fatalf("encode: %v, want it to name %q", err, tc.bad) + } + return + } + if err != nil { + t.Fatal(err) + } + back, err := DecodeHave(raw) + if err != nil { + t.Fatal(err) + } + tc.have.V = keys.Version + if !SameHave(back, tc.have) { + t.Fatalf("round trip differs: %+v", back) + } + }) + } + if _, err := DecodeHave([]byte(`{"v":2,"phase":"running","health":"ok"}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Fatalf("version 2: %v", err) + } + if _, err := DecodeHave([]byte(`{"v":1,"phase":"running","health":"ok","extra":1}`)); err == nil { + t.Fatal("an unknown field is read") + } +} + +func TestSameHaveComparesTimesAsInstants(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + zone := time.FixedZone("x", 3600) + mk := func(tm time.Time) seat.HaveByNodeModuleValue { + return seat.HaveByNodeModuleValue{Phase: PhaseFailed, Health: HealthFailed, + PullWait: &seat.PullWait{Image: "i", Holder: "h", Began: tm}, + UserList: &seat.UserListWritten{Source: "s", At: tm}} + } + if !SameHave(mk(at), mk(at.In(zone))) { + t.Fatal("the same instant in another zone differs") + } + if SameHave(mk(at), mk(at.Add(time.Second))) { + t.Fatal("another instant is the same") + } +} + +func TestHaveNodeCodec(t *testing.T) { + n := seat.HaveByNodeNodeValue{Composed: digestA, Composes: ComposesOwn, + Candidates: map[string]seat.CandidateAnswer{"g": {Proposal: "p", Verdict: seat.CandidateAnswerVerdictSame}}} + raw, err := EncodeHaveNode(n) + if err != nil { + t.Fatal(err) + } + back, err := DecodeHaveNode(raw) + if err != nil { + t.Fatal(err) + } + n.V = keys.Version + if !SameHaveNode(back, n) { + t.Fatalf("round trip differs: %+v", back) + } + for name, bad := range map[string]seat.HaveByNodeNodeValue{ + "composes": {Composes: "other"}, + "no group": {Candidates: map[string]seat.CandidateAnswer{"": {Verdict: "same"}}}, + "a verdict": {Candidates: map[string]seat.CandidateAnswer{"g": {Verdict: "maybe"}}}, + } { + if _, err := EncodeHaveNode(bad); err == nil { + t.Errorf("%s: encoded", name) + } + } + a, b := seat.NodeRecord{Node: "n", V: 1}, seat.NodeRecord{Node: "n", V: 2} + if !SameNodeRecord(a, b) { + t.Error("a record differs by its version") + } +} diff --git a/go/clients/npmpackageregistryclient/client.go b/go/clients/npmpackageregistryclient/client.go deleted file mode 100644 index f72a8cf..0000000 --- a/go/clients/npmpackageregistryclient/client.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package npmpackageregistryclient is the npm-package-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 -// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, -// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a -// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is -// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the -// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. -package npmpackageregistryclient - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - - seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. -type Call func(key string, body any) (json.RawMessage, error) - -// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were -// sent, and the chunks they took. -type Sent struct { - Transfer string - parts.Sent -} - -// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in -// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes -// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. -// Its outcome, published or refused, is the holder's event, which the caller waits for. -func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { - open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { - in.Parts = make([]seat.Part, 0, len(ps)) - for _, p := range ps { - in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) - } - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) - if err != nil { - return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) - } - var a seat.PublishAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) - } - return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil - } - chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, - Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) - if err != nil { - return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) - } - var a seat.PublishPartAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) - } - return nil - } - sent, err := parts.Send(ctx, open, chunk, ps) - return Sent{Transfer: in.Transfer, Sent: sent}, err -} diff --git a/go/clients/npmpackageregistryclient/client_test.go b/go/clients/npmpackageregistryclient/client_test.go deleted file mode 100644 index 4433025..0000000 --- a/go/clients/npmpackageregistryclient/client_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package npmpackageregistryclient - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "io" - "testing" - - seat "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the -// holder lacks travels by publish-part, its bytes whole, the last chunk marked. -func TestPublishTalksToItsOwnSeat(t *testing.T) { - held, lacked := []byte("held"), []byte("lacked") - part := func(b []byte) parts.Part { - sum := sha256.Sum256(b) - return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), - Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} - } - ps := []parts.Part{part(held), part(lacked)} - var asked []string - var got []byte - call := func(key string, body any) (json.RawMessage, error) { - asked = append(asked, key) - raw, _ := json.Marshal(body) - switch key { - case "seat:npm-package-registry.publish": - var in seat.PublishInput - _ = json.Unmarshal(raw, &in) - if len(in.Parts) != 2 { - t.Errorf("publish names %d parts, want 2", len(in.Parts)) - } - return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) - case "seat:npm-package-registry.publish-part": - var in seat.PublishPartInput - _ = json.Unmarshal(raw, &in) - data, _ := base64.StdEncoding.DecodeString(in.Data) - if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { - t.Errorf("publish-part %+v", in) - } - got = append(got, data...) - return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) - } - t.Fatalf("asked %s", key) - return nil, nil - } - sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) - if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { - t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) - } -} diff --git a/go/clients/npmpackageregistryclient/go.mod b/go/clients/npmpackageregistryclient/go.mod deleted file mode 100644 index 7a07bb9..0000000 --- a/go/clients/npmpackageregistryclient/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/clients/npmpackageregistryclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef -) - -require ( - github.com/klauspost/compress v1.20.1 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect - github.com/nats-io/nkeys v0.4.16 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/sys v0.48.0 // indirect -) diff --git a/go/clients/npmpackageregistryclient/go.sum b/go/clients/npmpackageregistryclient/go.sum deleted file mode 100644 index aeb3cba..0000000 --- a/go/clients/npmpackageregistryclient/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a h1:JdEqNAv+s0ut6JEcCtXN5RIoGpuYg0jIVteFZSJJ36s= -git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:JRtTjIjMTkzn59IN4vCYei9ZwJGFnN1Puwame4V/sD0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= -github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/clients/ociartifactregistryclient/client.go b/go/clients/ociartifactregistryclient/client.go deleted file mode 100644 index 3f9a894..0000000 --- a/go/clients/ociartifactregistryclient/client.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package ociartifactregistryclient is the oci-artifact-registry seat's client (novox/hq ADR 0627 §1, §2; ADR 0611, design 51 -// *Publishing*): it publishes a file in parts to the seat's holder. `publish` opens the transfer naming every part, -// the holder answers the parts it lacks, and only those travel by `publish-part`, in chunks a bus message carries; a -// cut transfer goes on from what the holder answers when the same transfer is opened again. The sending in parts is -// mesh-sdk's seat-neutral runtime (parts); this client binds it to this seat's verbs and types alone. The call is the -// caller's own (a bundle's stdio.Ask, a process's call of a seat verb), so it holds no transport. -package ociartifactregistryclient - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - - seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// Call asks one verb of the seat, `seat:.`, with a body, and answers its JSON. -type Call func(key string, body any) (json.RawMessage, error) - -// Sent is what a Publish did: the transfer, whether the holder held its digest already, the parts it lacked and were -// sent, and the chunks they took. -type Sent struct { - Transfer string - parts.Sent -} - -// Publish opens (or opens again) the transfer in on the seat and sends the parts the holder answers it lacks, each in -// chunks of at most parts.MaxChunk, in sequence, the last marked. in.Parts is written from parts. A part whose bytes -// do not hash to its digest is refused before anything of it is sent; a part the holder does not name is never read. -// Its outcome, published or refused, is the holder's event, which the caller waits for. -func Publish(ctx context.Context, call Call, in seat.PublishInput, ps []parts.Part) (Sent, error) { - open := func(_ context.Context, ps []parts.Part) (parts.Opened, error) { - in.Parts = make([]seat.Part, 0, len(ps)) - for _, p := range ps { - in.Parts = append(in.Parts, seat.Part{Digest: p.Digest, Size: p.Size}) - } - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublish, in) - if err != nil { - return parts.Opened{}, fmt.Errorf("%s's publish of %s: %w", seat.Seat, in.Name, err) - } - var a seat.PublishAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return parts.Opened{}, fmt.Errorf("%s's answer to publish: %w", seat.Seat, err) - } - return parts.Opened{Held: a.Held, Lacks: a.Lacks}, nil - } - chunk := func(_ context.Context, part string, sequence int, last bool, data []byte) error { - raw, err := call("seat:"+seat.Seat+"."+seat.VerbPublishPart, seat.PublishPartInput{Transfer: in.Transfer, Part: part, - Sequence: int64(sequence), Last: last, Data: base64.StdEncoding.EncodeToString(data)}) - if err != nil { - return fmt.Errorf("%s's publish-part: %w", seat.Seat, err) - } - var a seat.PublishPartAnswer - if err := json.Unmarshal(raw, &a); err != nil { - return fmt.Errorf("%s's answer to publish-part: %w", seat.Seat, err) - } - return nil - } - sent, err := parts.Send(ctx, open, chunk, ps) - return Sent{Transfer: in.Transfer, Sent: sent}, err -} diff --git a/go/clients/ociartifactregistryclient/client_test.go b/go/clients/ociartifactregistryclient/client_test.go deleted file mode 100644 index 0f64e6f..0000000 --- a/go/clients/ociartifactregistryclient/client_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package ociartifactregistryclient - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "io" - "testing" - - seat "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" - "git.novox.be/novox/mesh-sdk/go/parts" -) - -// The client publishes over its own seat's verbs alone (ADR 0627 §1): publish names every part, and only the part the -// holder lacks travels by publish-part, its bytes whole, the last chunk marked. -func TestPublishTalksToItsOwnSeat(t *testing.T) { - held, lacked := []byte("held"), []byte("lacked") - part := func(b []byte) parts.Part { - sum := sha256.Sum256(b) - return parts.Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)), - Open: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }} - } - ps := []parts.Part{part(held), part(lacked)} - var asked []string - var got []byte - call := func(key string, body any) (json.RawMessage, error) { - asked = append(asked, key) - raw, _ := json.Marshal(body) - switch key { - case "seat:oci-artifact-registry.publish": - var in seat.PublishInput - _ = json.Unmarshal(raw, &in) - if len(in.Parts) != 2 { - t.Errorf("publish names %d parts, want 2", len(in.Parts)) - } - return json.Marshal(seat.PublishAnswer{Transfer: in.Transfer, Lacks: []string{ps[1].Digest}}) - case "seat:oci-artifact-registry.publish-part": - var in seat.PublishPartInput - _ = json.Unmarshal(raw, &in) - data, _ := base64.StdEncoding.DecodeString(in.Data) - if in.Part != ps[1].Digest || !in.Last || in.Transfer != "t1" { - t.Errorf("publish-part %+v", in) - } - got = append(got, data...) - return json.Marshal(seat.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part}) - } - t.Fatalf("asked %s", key) - return nil, nil - } - sent, err := Publish(context.Background(), call, seat.PublishInput{Transfer: "t1", Name: "x"}, ps) - if err != nil || sent.Transfer != "t1" || sent.Chunks != 1 || len(sent.Parts) != 1 || !bytes.Equal(got, lacked) || len(asked) != 2 { - t.Errorf("sent %+v, %q arrived, asked %v, %v", sent, got, asked, err) - } -} diff --git a/go/clients/ociartifactregistryclient/go.mod b/go/clients/ociartifactregistryclient/go.mod deleted file mode 100644 index c3f364d..0000000 --- a/go/clients/ociartifactregistryclient/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/clients/ociartifactregistryclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef -) - -require ( - github.com/klauspost/compress v1.20.1 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect - github.com/nats-io/nkeys v0.4.16 // indirect - github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/sys v0.48.0 // indirect -) diff --git a/go/clients/ociartifactregistryclient/go.sum b/go/clients/ociartifactregistryclient/go.sum deleted file mode 100644 index 737e62d..0000000 --- a/go/clients/ociartifactregistryclient/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a h1:fhQ+q40fnPwuBBU+7Bpv13ktzoPxHtQiO6mpHR9oVoo= -git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104172218-347a3053580a/go.mod h1:ooQa2CiP8JOvhP0bdQvwurV5wskRE7Cm68ecURBbCwU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= -github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= -github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/go/seats/containerimageregistry/contract.go b/go/seats/containerimageregistry/contract.go deleted file mode 100644 index 6eb1f99..0000000 --- a/go/seats/containerimageregistry/contract.go +++ /dev/null @@ -1,403 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package containerimageregistry is the contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. -package containerimageregistry - -// Seat is the seat's name. -const Seat = "container-image-registry" - -const ( - VerbPublish = "publish" - VerbPublishPart = "publish-part" - VerbVersions = "versions" - VerbRemove = "remove" - VerbMirror = "mirror" - VerbTags = "tags" - VerbHold = "hold" - VerbRelease = "release" - VerbList = "list" -) - -const ( - EventPublished = "published" - EventRefused = "refused" - EventRemoved = "removed" - EventKept = "kept" - EventHeld = "held" - EventReleased = "released" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbMirror, VerbTags, VerbHold, VerbRelease, VerbList} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "container-image-registry.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPublished is where published is published. -const SubjectPublished = "mesh.seat.container-image-registry.event.published" - -// SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.container-image-registry.event.refused" - -// SubjectRemoved is where removed is published. -const SubjectRemoved = "mesh.seat.container-image-registry.event.removed" - -// SubjectKept is where kept is published. -const SubjectKept = "mesh.seat.container-image-registry.event.kept" - -// SubjectHeld is where held is published. -const SubjectHeld = "mesh.seat.container-image-registry.event.held" - -// SubjectReleased is where released is published. -const SubjectReleased = "mesh.seat.container-image-registry.event.released" - -// Artifact is one published thing, as every registry event and answer names it. -type Artifact struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Artifact's kind. -const ( - ArtifactKindPackage = "package" - ArtifactKindImage = "image" - ArtifactKindBundle = "bundle" - ArtifactKindArchive = "archive" - ArtifactKindUpstream = "upstream" -) - -// Hold is the body of held and released. -type Hold struct { - // when, RFC 3339 - At string `json:"at,omitempty"` - // the caller the holder proved - By string `json:"by,omitempty"` - // the digest - Digest string `json:"digest"` - // the module holding the seat - Holder string `json:"holder"` - // the name - Name string `json:"name"` - // why - Reason string `json:"reason,omitempty"` -} - -// Listed is a type of the contract. -type Listed struct { - // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by - Artifact string `json:"artifact,omitempty"` - // the manifest's digest, sha256: - Digest string `json:"digest"` - // a hold keeps it from retention - Held bool `json:"held"` - // it is an index, naming a manifest per platform - Index bool `json:"index"` - // for an index, the digests of the manifests it names - Platforms []string `json:"platforms,omitempty"` - // its bytes: its own content and every blob it names - Size int64 `json:"size"` - // the tags naming it, none for an untagged one - Tags []string `json:"tags,omitempty"` -} - -// Outcome is the body of published, refused and removed. -type Outcome struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // the module holding the seat that said it - Holder string `json:"holder"` - // what it is - Kind string `json:"kind"` - // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact - Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) - Reason string `json:"reason,omitempty"` - // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) - Reference string `json:"reference,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // for kept: the other tags still naming its digest - Tags []string `json:"tags,omitempty"` - // the transfer it ends, when it ended one - Transfer string `json:"transfer,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Outcome's kind. -const ( - OutcomeKindPackage = "package" - OutcomeKindImage = "image" - OutcomeKindBundle = "bundle" - OutcomeKindArchive = "archive" - OutcomeKindUpstream = "upstream" -) - -// Part is one part of a transfer. -type Part struct { - // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob - Digest string `json:"digest"` - // its size in bytes - Size int64 `json:"size"` -} - -// Repository is a type of the contract. -type Repository struct { - // each manifest it holds, by digest - Manifests []Listed `json:"manifests"` - // the repository - Name string `json:"name"` -} - -// PublishInput is what publish takes. -type PublishInput struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // every part's digest and size; none when the digest is already held - Parts []Part `json:"parts,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer's name, the caller's own; the same name opens the same transfer again - Transfer string `json:"transfer"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of PublishInput's kind. -const ( - PublishInputKindPackage = "package" - PublishInputKindImage = "image" - PublishInputKindBundle = "bundle" - PublishInputKindArchive = "archive" - PublishInputKindUpstream = "upstream" -) - -// PublishAnswer is what publish answers. -type PublishAnswer struct { - // the digest is held already: nothing to send; the outcome follows as an event - Held bool `json:"held,omitempty"` - // the digests of the parts the holder lacks, to send by publish-part - Lacks []string `json:"lacks,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartInput is what publish-part takes. -type PublishPartInput struct { - // the chunk's bytes, base64 (at most 760 KiB before encoding) - Data string `json:"data"` - // this chunk is the part's last - Last bool `json:"last"` - // the part's digest - Part string `json:"part"` - // the chunk's place in its part, from 0 - Sequence int64 `json:"sequence"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartAnswer is what publish-part answers. -type PublishPartAnswer struct { - // the part is whole and its bytes match its digest - Complete bool `json:"complete,omitempty"` - // the part - Part string `json:"part"` - // the chunks of the part held so far - Received int64 `json:"received,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// VersionsInput is what versions takes. -type VersionsInput struct { - // narrowed to one kind (optional) - Kind string `json:"kind,omitempty"` - // the name - Name string `json:"name"` -} - -// VersionsAnswer is what versions answers. -type VersionsAnswer struct { - // the name - Name string `json:"name"` - // newest first - Versions []Artifact `json:"versions"` -} - -// RemoveInput is what remove takes. -type RemoveInput struct { - // the digest (for a bundle, archive or copied base) - Digest string `json:"digest,omitempty"` - // the name - Name string `json:"name"` - // why it goes - Reason string `json:"reason"` - // the tag (for a tagged version) - Tag string `json:"tag,omitempty"` - // the version - Version string `json:"version,omitempty"` -} - -// The values of RemoveInput's reason. -const ( - RemoveInputReasonTagGone = "tag-gone" - RemoveInputReasonRetention = "retention" -) - -// RemoveAnswer is what remove answers. -type RemoveAnswer struct { - Artifact *Artifact `json:"artifact,omitempty"` - // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) - Kept bool `json:"kept,omitempty"` - // why it was kept: shared-digest, another tag names its digest - Reason string `json:"reason,omitempty"` - // it is gone - Removed bool `json:"removed"` - // the other tags still naming its digest, when kept - Tags []string `json:"tags,omitempty"` -} - -// The values of RemoveAnswer's reason. -const ( - RemoveAnswerReasonSharedDigest = "shared-digest" -) - -// MirrorInput is what mirror takes. -type MirrorInput struct { - // its pinned digest, sha256: - Digest string `json:"digest"` - // the name it is kept under - Name string `json:"name"` - // the outside image's reference - Source string `json:"source"` -} - -// MirrorAnswer is what mirror answers. -type MirrorAnswer struct { - // the digest stored - Digest string `json:"digest"` - // the reference the seat answers - Reference string `json:"reference"` -} - -// TagsInput is what tags takes. -type TagsInput struct { - // the image - Name string `json:"name"` -} - -// TagsAnswerTagsItem is a type of the contract. -type TagsAnswerTagsItem struct { - // its digest - Digest string `json:"digest"` - // the tag - Tag string `json:"tag"` -} - -// TagsAnswer is what tags answers. -type TagsAnswer struct { - // the image - Name string `json:"name"` - // every tag - Tags []TagsAnswerTagsItem `json:"tags"` -} - -// HoldInput is what hold takes. -type HoldInput struct { - // the digest - Digest string `json:"digest"` - // the name - Name string `json:"name"` - // which of the sweep's rules keeps it - Reason string `json:"reason"` -} - -// HoldAnswer is what hold answers. -type HoldAnswer struct { - // it is held - Held bool `json:"held"` - // when the hold began, RFC 3339 - Since string `json:"since,omitempty"` -} - -// ReleaseInput is what release takes. -type ReleaseInput struct { - // the digest - Digest string `json:"digest"` - // the name - Name string `json:"name"` - // why it is let go - Reason string `json:"reason"` -} - -// ReleaseAnswer is what release answers. -type ReleaseAnswer struct { - // a hold was let go; false when there was none - Released bool `json:"released"` -} - -// ListInput is what list takes. -type ListInput struct { - // the repository, /; every repository of the seat when not given - Name string `json:"name,omitempty"` -} - -// ListAnswer is what list answers. -type ListAnswer struct { - // each repository named, in order of its name - Repositories []Repository `json:"repositories"` -} - -// PublishedEvent is the body of the event published. -type PublishedEvent = Outcome - -// RefusedEvent is the body of the event refused. -type RefusedEvent = Outcome - -// RemovedEvent is the body of the event removed. -type RemovedEvent = Outcome - -// KeptEvent is the body of the event kept. -type KeptEvent = Outcome - -// HeldEvent is the body of the event held. -type HeldEvent = Hold - -// ReleasedEvent is the body of the event released. -type ReleasedEvent = Hold diff --git a/go/seats/containerimageregistry/contract_test.go b/go/seats/containerimageregistry/contract_test.go deleted file mode 100644 index 8f4d543..0000000 --- a/go/seats/containerimageregistry/contract_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package containerimageregistry - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/container-image-registry/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "published on the seat's subject", subject: "mesh.seat.container-image-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.container-image-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.container-image-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.removed$")}, - {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.container-image-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", - decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.kept$")}, - {name: "held on the seat's subject", subject: "mesh.seat.container-image-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", - decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.held$")}, - {name: "released on the seat's subject", subject: "mesh.seat.container-image-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", - decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.container-image-registry\\.event\\.released$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"image\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", - decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", - decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", - decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "list answers an index with its platforms, its tag and its hold", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":false,\"index\":true,\"platforms\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"size\":2048,\"tags\":[\"3\"]},{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"held\":false,\"index\":false,\"size\":1024}],\"name\":\"mail/server\"}]}", - decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/go/seats/containerimageregistry/go.mod b/go/seats/containerimageregistry/go.mod deleted file mode 100644 index 3ee2145..0000000 --- a/go/seats/containerimageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry - -go 1.26.0 diff --git a/go/seats/gopackageregistry/contract.go b/go/seats/gopackageregistry/contract.go deleted file mode 100644 index eb53461..0000000 --- a/go/seats/gopackageregistry/contract.go +++ /dev/null @@ -1,262 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package gopackageregistry is the contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. -package gopackageregistry - -// Seat is the seat's name. -const Seat = "go-package-registry" - -const ( - VerbPublish = "publish" - VerbPublishPart = "publish-part" - VerbVersions = "versions" - VerbRemove = "remove" -) - -const ( - EventPublished = "published" - EventRefused = "refused" - EventRemoved = "removed" - EventKept = "kept" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventKept} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "go-package-registry.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPublished is where published is published. -const SubjectPublished = "mesh.seat.go-package-registry.event.published" - -// SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.go-package-registry.event.refused" - -// SubjectRemoved is where removed is published. -const SubjectRemoved = "mesh.seat.go-package-registry.event.removed" - -// SubjectKept is where kept is published. -const SubjectKept = "mesh.seat.go-package-registry.event.kept" - -// Artifact is one published thing, as every registry event and answer names it. -type Artifact struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Artifact's kind. -const ( - ArtifactKindPackage = "package" - ArtifactKindImage = "image" - ArtifactKindBundle = "bundle" - ArtifactKindArchive = "archive" - ArtifactKindUpstream = "upstream" -) - -// Outcome is the body of published, refused and removed. -type Outcome struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // the module holding the seat that said it - Holder string `json:"holder"` - // what it is - Kind string `json:"kind"` - // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact - Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) - Reason string `json:"reason,omitempty"` - // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) - Reference string `json:"reference,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // for kept: the other tags still naming its digest - Tags []string `json:"tags,omitempty"` - // the transfer it ends, when it ended one - Transfer string `json:"transfer,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Outcome's kind. -const ( - OutcomeKindPackage = "package" - OutcomeKindImage = "image" - OutcomeKindBundle = "bundle" - OutcomeKindArchive = "archive" - OutcomeKindUpstream = "upstream" -) - -// Part is one part of a transfer. -type Part struct { - // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob - Digest string `json:"digest"` - // its size in bytes - Size int64 `json:"size"` -} - -// PublishInput is what publish takes. -type PublishInput struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // every part's digest and size; none when the digest is already held - Parts []Part `json:"parts,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer's name, the caller's own; the same name opens the same transfer again - Transfer string `json:"transfer"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of PublishInput's kind. -const ( - PublishInputKindPackage = "package" - PublishInputKindImage = "image" - PublishInputKindBundle = "bundle" - PublishInputKindArchive = "archive" - PublishInputKindUpstream = "upstream" -) - -// PublishAnswer is what publish answers. -type PublishAnswer struct { - // the digest is held already: nothing to send; the outcome follows as an event - Held bool `json:"held,omitempty"` - // the digests of the parts the holder lacks, to send by publish-part - Lacks []string `json:"lacks,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartInput is what publish-part takes. -type PublishPartInput struct { - // the chunk's bytes, base64 (at most 760 KiB before encoding) - Data string `json:"data"` - // this chunk is the part's last - Last bool `json:"last"` - // the part's digest - Part string `json:"part"` - // the chunk's place in its part, from 0 - Sequence int64 `json:"sequence"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartAnswer is what publish-part answers. -type PublishPartAnswer struct { - // the part is whole and its bytes match its digest - Complete bool `json:"complete,omitempty"` - // the part - Part string `json:"part"` - // the chunks of the part held so far - Received int64 `json:"received,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// VersionsInput is what versions takes. -type VersionsInput struct { - // narrowed to one kind (optional) - Kind string `json:"kind,omitempty"` - // the name - Name string `json:"name"` -} - -// VersionsAnswer is what versions answers. -type VersionsAnswer struct { - // the name - Name string `json:"name"` - // newest first - Versions []Artifact `json:"versions"` -} - -// RemoveInput is what remove takes. -type RemoveInput struct { - // the digest (for a bundle, archive or copied base) - Digest string `json:"digest,omitempty"` - // the name - Name string `json:"name"` - // why it goes - Reason string `json:"reason"` - // the tag (for a tagged version) - Tag string `json:"tag,omitempty"` - // the version - Version string `json:"version,omitempty"` -} - -// The values of RemoveInput's reason. -const ( - RemoveInputReasonTagGone = "tag-gone" - RemoveInputReasonRetention = "retention" -) - -// RemoveAnswer is what remove answers. -type RemoveAnswer struct { - Artifact *Artifact `json:"artifact,omitempty"` - // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) - Kept bool `json:"kept,omitempty"` - // why it was kept: shared-digest, another tag names its digest - Reason string `json:"reason,omitempty"` - // it is gone - Removed bool `json:"removed"` - // the other tags still naming its digest, when kept - Tags []string `json:"tags,omitempty"` -} - -// The values of RemoveAnswer's reason. -const ( - RemoveAnswerReasonSharedDigest = "shared-digest" -) - -// PublishedEvent is the body of the event published. -type PublishedEvent = Outcome - -// RefusedEvent is the body of the event refused. -type RefusedEvent = Outcome - -// RemovedEvent is the body of the event removed. -type RemovedEvent = Outcome - -// KeptEvent is the body of the event kept. -type KeptEvent = Outcome - -// GoPackageRegistryForm is the form of the provision go-package-registry. -type GoPackageRegistryForm struct { - // the read side's address - URL string `json:"url"` -} diff --git a/go/seats/gopackageregistry/contract_test.go b/go/seats/gopackageregistry/contract_test.go deleted file mode 100644 index e5f69ec..0000000 --- a/go/seats/gopackageregistry/contract_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package gopackageregistry - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/go-package-registry/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "published on the seat's subject", subject: "mesh.seat.go-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"module\":\"git.novox.be/novox/mesh-sdk/go\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.go-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.go-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"retention\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.removed$")}, - {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.go-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"reason\":\"shared-digest\",\"reference\":\"git.novox.be/novox/mesh-sdk/go@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", - decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.go-package-registry\\.event\\.kept$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"git.novox.be/novox/mesh-sdk/go\",\"version\":\"0.1.19\"}]}", - decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", - decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/go/seats/gopackageregistry/go.mod b/go/seats/gopackageregistry/go.mod deleted file mode 100644 index da0340e..0000000 --- a/go/seats/gopackageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry - -go 1.26.0 diff --git a/go/seats/meshdeliverychecker/contract.go b/go/seats/meshdeliverychecker/contract.go deleted file mode 100644 index 024999d..0000000 --- a/go/seats/meshdeliverychecker/contract.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package meshdeliverychecker is the contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). -package meshdeliverychecker - -// Seat is the seat's name. -const Seat = "mesh-delivery-checker" - -const ( - VerbTry = "try" - VerbTryRead = "try-read" - VerbTryEnd = "try-end" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbTry, VerbTryRead, VerbTryEnd} -} - -// The holder's own events, listed beside the seat and published under the holder's subjects (ADR 0620 §2). -const ( - HolderEventVerdict = "verdict" - HolderEventTagWanted = "tag-wanted" - HolderEventPreviewWritten = "preview.written" -) - -// HolderSubjectVerdict is where the holder publishes verdict, kept in checker.asks. -func HolderSubjectVerdict(holder string) string { return "mesh.stream." + holder + ".event.verdict" } - -// HolderSubjectTagWanted is where the holder publishes tag-wanted, kept in checker.asks. -func HolderSubjectTagWanted(holder string) string { - return "mesh.stream." + holder + ".event.tag-wanted" -} - -// HolderSubjectPreviewWritten is where the holder publishes preview.written, kept in checks. -func HolderSubjectPreviewWritten(holder string) string { - return "mesh.stream." + holder + ".event.preview.written" -} - -// TryInput is what try takes. -type TryInput struct { - // the full commit to try, 40 or 64 hex digits - Head string `json:"head"` - // unit or integration: only this level; both when absent - Level string `json:"level,omitempty"` - // / - Repository string `json:"repository"` - // a go test -run pattern: only those tests run, and nothing is kept - Tests string `json:"tests,omitempty"` - // the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent - Trunk string `json:"trunk,omitempty"` -} - -// TryAnswer is what try answers. -type TryAnswer struct { - // the try's id - ID string `json:"id"` - // its state - State string `json:"state,omitempty"` -} - -// TryReadInput is what try-read takes. -type TryReadInput struct { - // the try's id, try- - ID string `json:"id"` -} - -// TryReadAnswer is what try-read answers. -type TryReadAnswer = map[string]any - -// TryEndInput is what try-end takes. -type TryEndInput struct { - // the try's id, try- - ID string `json:"id"` - // why it is ended, in words - Why string `json:"why"` -} - -// TryEndAnswer is what try-end answers. -type TryEndAnswer struct { - // it is ended - Ended bool `json:"ended"` - // the try - ID string `json:"id"` -} - -// VerdictHolderEventSeriesItem is a type of the contract. -type VerdictHolderEventSeriesItem struct { - // its artifact, or the package's ecosystem name as the module's packages name it - Artifact string `json:"artifact"` - // the series on trunk - From string `json:"from"` - // the module whose artifact's series changes - Module string `json:"module"` - // the series the checked head gives it - To string `json:"to"` -} - -// VerdictHolderEvent is the body of the holder's event verdict. -type VerdictHolderEvent struct { - // the commit checked - Head string `json:"head"` - // the generated files out of step with their seat definitions, each path (design 26) - OutOfStep []string `json:"out-of-step,omitempty"` - // the pull request's number, for a pull request's check - Pull int64 `json:"pull,omitempty"` - // owner/repo - Repository string `json:"repository"` - // each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none - Series []VerdictHolderEventSeriesItem `json:"series,omitempty"` - // pass or fail - Verdict string `json:"verdict"` -} - -// TagWantedHolderEvent is the body of the holder's event tag-wanted. -type TagWantedHolderEvent struct { - // the pull request's branch: its tag is removed when the branch ends - Branch string `json:"branch"` - // its head - Head string `json:"head"` - // the group member - Module string `json:"module"` - // the package it declares - Package string `json:"package"` - // the pull request - Pull int64 `json:"pull,omitempty"` - // owner/repo - Repository string `json:"repository"` - // the tag wanted - Tag string `json:"tag,omitempty"` -} diff --git a/go/seats/meshdeliverychecker/contract_test.go b/go/seats/meshdeliverychecker/contract_test.go deleted file mode 100644 index 1b17187..0000000 --- a/go/seats/meshdeliverychecker/contract_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package meshdeliverychecker - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/mesh-delivery-checker/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "verdict on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.verdict", given: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", wire: "{\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"out-of-step\":[\"go/seats/meshbroker/contract.go\"],\"pull\":3,\"repository\":\"novox/mesh-contracts\",\"series\":[{\"artifact\":\"go\",\"from\":\"0.1\",\"module\":\"mesh-contracts\",\"to\":\"0.2\"}],\"verdict\":\"fail\"}", - decode: func(b []byte) (any, error) { var v VerdictHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.verdict$")}, - {name: "tag-wanted on the checker's own family", subject: "mesh.stream.mesh-delivery-checker.event.tag-wanted", given: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", wire: "{\"branch\":\"feat/registry\",\"head\":\"cccccccccccccccccccccccccccccccccccccccc\",\"module\":\"mesh-sdk\",\"package\":\"go\",\"pull\":170,\"repository\":\"novox/mesh-sdk\",\"tag\":\"registry-1966\"}", - decode: func(b []byte) (any, error) { var v TagWantedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.tag-wanted$")}, - {name: "preview.written on the checker's stream", subject: "mesh.stream.mesh-delivery-checker.event.preview.written", given: "{}", wire: "{}", - decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.preview\\.written$")}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/go/seats/meshdeliverychecker/go.mod b/go/seats/meshdeliverychecker/go.mod deleted file mode 100644 index c0318ae..0000000 --- a/go/seats/meshdeliverychecker/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker - -go 1.26.0 diff --git a/go/seats/npmpackageregistry/contract.go b/go/seats/npmpackageregistry/contract.go deleted file mode 100644 index 5f39cb8..0000000 --- a/go/seats/npmpackageregistry/contract.go +++ /dev/null @@ -1,262 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package npmpackageregistry is the contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. -package npmpackageregistry - -// Seat is the seat's name. -const Seat = "npm-package-registry" - -const ( - VerbPublish = "publish" - VerbPublishPart = "publish-part" - VerbVersions = "versions" - VerbRemove = "remove" -) - -const ( - EventPublished = "published" - EventRefused = "refused" - EventRemoved = "removed" - EventKept = "kept" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventKept} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "npm-package-registry.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPublished is where published is published. -const SubjectPublished = "mesh.seat.npm-package-registry.event.published" - -// SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.npm-package-registry.event.refused" - -// SubjectRemoved is where removed is published. -const SubjectRemoved = "mesh.seat.npm-package-registry.event.removed" - -// SubjectKept is where kept is published. -const SubjectKept = "mesh.seat.npm-package-registry.event.kept" - -// Artifact is one published thing, as every registry event and answer names it. -type Artifact struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Artifact's kind. -const ( - ArtifactKindPackage = "package" - ArtifactKindImage = "image" - ArtifactKindBundle = "bundle" - ArtifactKindArchive = "archive" - ArtifactKindUpstream = "upstream" -) - -// Outcome is the body of published, refused and removed. -type Outcome struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // the module holding the seat that said it - Holder string `json:"holder"` - // what it is - Kind string `json:"kind"` - // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact - Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) - Reason string `json:"reason,omitempty"` - // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) - Reference string `json:"reference,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // for kept: the other tags still naming its digest - Tags []string `json:"tags,omitempty"` - // the transfer it ends, when it ended one - Transfer string `json:"transfer,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Outcome's kind. -const ( - OutcomeKindPackage = "package" - OutcomeKindImage = "image" - OutcomeKindBundle = "bundle" - OutcomeKindArchive = "archive" - OutcomeKindUpstream = "upstream" -) - -// Part is one part of a transfer. -type Part struct { - // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob - Digest string `json:"digest"` - // its size in bytes - Size int64 `json:"size"` -} - -// PublishInput is what publish takes. -type PublishInput struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // every part's digest and size; none when the digest is already held - Parts []Part `json:"parts,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer's name, the caller's own; the same name opens the same transfer again - Transfer string `json:"transfer"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of PublishInput's kind. -const ( - PublishInputKindPackage = "package" - PublishInputKindImage = "image" - PublishInputKindBundle = "bundle" - PublishInputKindArchive = "archive" - PublishInputKindUpstream = "upstream" -) - -// PublishAnswer is what publish answers. -type PublishAnswer struct { - // the digest is held already: nothing to send; the outcome follows as an event - Held bool `json:"held,omitempty"` - // the digests of the parts the holder lacks, to send by publish-part - Lacks []string `json:"lacks,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartInput is what publish-part takes. -type PublishPartInput struct { - // the chunk's bytes, base64 (at most 760 KiB before encoding) - Data string `json:"data"` - // this chunk is the part's last - Last bool `json:"last"` - // the part's digest - Part string `json:"part"` - // the chunk's place in its part, from 0 - Sequence int64 `json:"sequence"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartAnswer is what publish-part answers. -type PublishPartAnswer struct { - // the part is whole and its bytes match its digest - Complete bool `json:"complete,omitempty"` - // the part - Part string `json:"part"` - // the chunks of the part held so far - Received int64 `json:"received,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// VersionsInput is what versions takes. -type VersionsInput struct { - // narrowed to one kind (optional) - Kind string `json:"kind,omitempty"` - // the name - Name string `json:"name"` -} - -// VersionsAnswer is what versions answers. -type VersionsAnswer struct { - // the name - Name string `json:"name"` - // newest first - Versions []Artifact `json:"versions"` -} - -// RemoveInput is what remove takes. -type RemoveInput struct { - // the digest (for a bundle, archive or copied base) - Digest string `json:"digest,omitempty"` - // the name - Name string `json:"name"` - // why it goes - Reason string `json:"reason"` - // the tag (for a tagged version) - Tag string `json:"tag,omitempty"` - // the version - Version string `json:"version,omitempty"` -} - -// The values of RemoveInput's reason. -const ( - RemoveInputReasonTagGone = "tag-gone" - RemoveInputReasonRetention = "retention" -) - -// RemoveAnswer is what remove answers. -type RemoveAnswer struct { - Artifact *Artifact `json:"artifact,omitempty"` - // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) - Kept bool `json:"kept,omitempty"` - // why it was kept: shared-digest, another tag names its digest - Reason string `json:"reason,omitempty"` - // it is gone - Removed bool `json:"removed"` - // the other tags still naming its digest, when kept - Tags []string `json:"tags,omitempty"` -} - -// The values of RemoveAnswer's reason. -const ( - RemoveAnswerReasonSharedDigest = "shared-digest" -) - -// PublishedEvent is the body of the event published. -type PublishedEvent = Outcome - -// RefusedEvent is the body of the event refused. -type RefusedEvent = Outcome - -// RemovedEvent is the body of the event removed. -type RemovedEvent = Outcome - -// KeptEvent is the body of the event kept. -type KeptEvent = Outcome - -// NpmPackageRegistryForm is the form of the provision npm-package-registry. -type NpmPackageRegistryForm struct { - // the read side's address - URL string `json:"url"` -} diff --git a/go/seats/npmpackageregistry/contract_test.go b/go/seats/npmpackageregistry/contract_test.go deleted file mode 100644 index 3092b37..0000000 --- a/go/seats/npmpackageregistry/contract_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package npmpackageregistry - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/npm-package-registry/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "published on the seat's subject", subject: "mesh.seat.npm-package-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.npm-package-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.npm-package-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"retention\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.removed$")}, - {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.npm-package-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"gitea\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"@novox/mesh-sdk@0.1.19\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", - decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.npm-package-registry\\.event\\.kept$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"later\":{\"x\":1},\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"@novox/mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.19\"}]}", - decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", - decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/go/seats/npmpackageregistry/go.mod b/go/seats/npmpackageregistry/go.mod deleted file mode 100644 index 208b96c..0000000 --- a/go/seats/npmpackageregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry - -go 1.26.0 diff --git a/go/seats/ociartifactregistry/contract.go b/go/seats/ociartifactregistry/contract.go deleted file mode 100644 index edade5c..0000000 --- a/go/seats/ociartifactregistry/contract.go +++ /dev/null @@ -1,361 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package ociartifactregistry is the contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. -package ociartifactregistry - -// Seat is the seat's name. -const Seat = "oci-artifact-registry" - -const ( - VerbPublish = "publish" - VerbPublishPart = "publish-part" - VerbVersions = "versions" - VerbRemove = "remove" - VerbHold = "hold" - VerbRelease = "release" - VerbList = "list" -) - -const ( - EventPublished = "published" - EventRefused = "refused" - EventRemoved = "removed" - EventKept = "kept" - EventHeld = "held" - EventReleased = "released" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbPublish, VerbPublishPart, VerbVersions, VerbRemove, VerbHold, VerbRelease, VerbList} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPublished, EventRefused, EventRemoved, EventKept, EventHeld, EventReleased} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "oci-artifact-registry.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPublished is where published is published. -const SubjectPublished = "mesh.seat.oci-artifact-registry.event.published" - -// SubjectRefused is where refused is published. -const SubjectRefused = "mesh.seat.oci-artifact-registry.event.refused" - -// SubjectRemoved is where removed is published. -const SubjectRemoved = "mesh.seat.oci-artifact-registry.event.removed" - -// SubjectKept is where kept is published. -const SubjectKept = "mesh.seat.oci-artifact-registry.event.kept" - -// SubjectHeld is where held is published. -const SubjectHeld = "mesh.seat.oci-artifact-registry.event.held" - -// SubjectReleased is where released is published. -const SubjectReleased = "mesh.seat.oci-artifact-registry.event.released" - -// Artifact is one published thing, as every registry event and answer names it. -type Artifact struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Artifact's kind. -const ( - ArtifactKindPackage = "package" - ArtifactKindImage = "image" - ArtifactKindBundle = "bundle" - ArtifactKindArchive = "archive" - ArtifactKindUpstream = "upstream" -) - -// Hold is the body of held and released. -type Hold struct { - // when, RFC 3339 - At string `json:"at,omitempty"` - // the caller the holder proved - By string `json:"by,omitempty"` - // the digest - Digest string `json:"digest"` - // the module holding the seat - Holder string `json:"holder"` - // the name - Name string `json:"name"` - // why - Reason string `json:"reason,omitempty"` -} - -// Listed is a type of the contract. -type Listed struct { - // for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by - Artifact string `json:"artifact,omitempty"` - // the manifest's digest, sha256: - Digest string `json:"digest"` - // a hold keeps it from retention - Held bool `json:"held"` - // it is an index, naming a manifest per platform - Index bool `json:"index"` - // for an index, the digests of the manifests it names - Platforms []string `json:"platforms,omitempty"` - // its bytes: its own content and every blob it names - Size int64 `json:"size"` - // the tags naming it, none for an untagged one - Tags []string `json:"tags,omitempty"` -} - -// Outcome is the body of published, refused and removed. -type Outcome struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // the module holding the seat that said it - Holder string `json:"holder"` - // what it is - Kind string `json:"kind"` - // the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact - Module string `json:"module,omitempty"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) - Reason string `json:"reason,omitempty"` - // the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) - Reference string `json:"reference,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // for kept: the other tags still naming its digest - Tags []string `json:"tags,omitempty"` - // the transfer it ends, when it ended one - Transfer string `json:"transfer,omitempty"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of Outcome's kind. -const ( - OutcomeKindPackage = "package" - OutcomeKindImage = "image" - OutcomeKindBundle = "bundle" - OutcomeKindArchive = "archive" - OutcomeKindUpstream = "upstream" -) - -// Part is one part of a transfer. -type Part struct { - // the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob - Digest string `json:"digest"` - // its size in bytes - Size int64 `json:"size"` -} - -// Repository is a type of the contract. -type Repository struct { - // each manifest it holds, by digest - Manifests []Listed `json:"manifests"` - // the repository - Name string `json:"name"` -} - -// PublishInput is what publish takes. -type PublishInput struct { - // the source commit it was built from - Commit string `json:"commit,omitempty"` - // its digest, sha256: (a Go package's h1: hash beside it in the version's record) - Digest string `json:"digest"` - // the build's fingerprint - Fingerprint string `json:"fingerprint,omitempty"` - // what it is - Kind string `json:"kind"` - // the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) - Name string `json:"name"` - // every part's digest and size; none when the digest is already held - Parts []Part `json:"parts,omitempty"` - // the tag it carries, for a tagged version - Tag string `json:"tag,omitempty"` - // the transfer's name, the caller's own; the same name opens the same transfer again - Transfer string `json:"transfer"` - // the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone - Version string `json:"version,omitempty"` -} - -// The values of PublishInput's kind. -const ( - PublishInputKindPackage = "package" - PublishInputKindImage = "image" - PublishInputKindBundle = "bundle" - PublishInputKindArchive = "archive" - PublishInputKindUpstream = "upstream" -) - -// PublishAnswer is what publish answers. -type PublishAnswer struct { - // the digest is held already: nothing to send; the outcome follows as an event - Held bool `json:"held,omitempty"` - // the digests of the parts the holder lacks, to send by publish-part - Lacks []string `json:"lacks,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartInput is what publish-part takes. -type PublishPartInput struct { - // the chunk's bytes, base64 (at most 760 KiB before encoding) - Data string `json:"data"` - // this chunk is the part's last - Last bool `json:"last"` - // the part's digest - Part string `json:"part"` - // the chunk's place in its part, from 0 - Sequence int64 `json:"sequence"` - // the transfer - Transfer string `json:"transfer"` -} - -// PublishPartAnswer is what publish-part answers. -type PublishPartAnswer struct { - // the part is whole and its bytes match its digest - Complete bool `json:"complete,omitempty"` - // the part - Part string `json:"part"` - // the chunks of the part held so far - Received int64 `json:"received,omitempty"` - // the transfer - Transfer string `json:"transfer"` -} - -// VersionsInput is what versions takes. -type VersionsInput struct { - // narrowed to one kind (optional) - Kind string `json:"kind,omitempty"` - // the name - Name string `json:"name"` -} - -// VersionsAnswer is what versions answers. -type VersionsAnswer struct { - // the name - Name string `json:"name"` - // newest first - Versions []Artifact `json:"versions"` -} - -// RemoveInput is what remove takes. -type RemoveInput struct { - // the digest (for a bundle, archive or copied base) - Digest string `json:"digest,omitempty"` - // the name - Name string `json:"name"` - // why it goes - Reason string `json:"reason"` - // the tag (for a tagged version) - Tag string `json:"tag,omitempty"` - // the version - Version string `json:"version,omitempty"` -} - -// The values of RemoveInput's reason. -const ( - RemoveInputReasonTagGone = "tag-gone" - RemoveInputReasonRetention = "retention" -) - -// RemoveAnswer is what remove answers. -type RemoveAnswer struct { - Artifact *Artifact `json:"artifact,omitempty"` - // it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) - Kept bool `json:"kept,omitempty"` - // why it was kept: shared-digest, another tag names its digest - Reason string `json:"reason,omitempty"` - // it is gone - Removed bool `json:"removed"` - // the other tags still naming its digest, when kept - Tags []string `json:"tags,omitempty"` -} - -// The values of RemoveAnswer's reason. -const ( - RemoveAnswerReasonSharedDigest = "shared-digest" -) - -// HoldInput is what hold takes. -type HoldInput struct { - // the digest - Digest string `json:"digest"` - // the name - Name string `json:"name"` - // which of the sweep's rules keeps it - Reason string `json:"reason"` -} - -// HoldAnswer is what hold answers. -type HoldAnswer struct { - // it is held - Held bool `json:"held"` - // when the hold began, RFC 3339 - Since string `json:"since,omitempty"` -} - -// ReleaseInput is what release takes. -type ReleaseInput struct { - // the digest - Digest string `json:"digest"` - // the name - Name string `json:"name"` - // why it is let go - Reason string `json:"reason"` -} - -// ReleaseAnswer is what release answers. -type ReleaseAnswer struct { - // a hold was let go; false when there was none - Released bool `json:"released"` -} - -// ListInput is what list takes. -type ListInput struct { - // the repository, /; every repository of the seat when not given - Name string `json:"name,omitempty"` -} - -// ListAnswer is what list answers. -type ListAnswer struct { - // each repository named, in order of its name - Repositories []Repository `json:"repositories"` -} - -// PublishedEvent is the body of the event published. -type PublishedEvent = Outcome - -// RefusedEvent is the body of the event refused. -type RefusedEvent = Outcome - -// RemovedEvent is the body of the event removed. -type RemovedEvent = Outcome - -// KeptEvent is the body of the event kept. -type KeptEvent = Outcome - -// HeldEvent is the body of the event held. -type HeldEvent = Hold - -// ReleasedEvent is the body of the event released. -type ReleasedEvent = Hold diff --git a/go/seats/ociartifactregistry/contract_test.go b/go/seats/ociartifactregistry/contract_test.go deleted file mode 100644 index f6d90b3..0000000 --- a/go/seats/ociartifactregistry/contract_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package ociartifactregistry - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/oci-artifact-registry/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "published on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.published", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.published$")}, - {name: "refused on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.refused", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RefusedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.refused$")}, - {name: "removed on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.removed", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"retention\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v RemovedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.removed$")}, - {name: "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", subject: "mesh.seat.oci-artifact-registry.event.kept", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"holder\":\"distribution\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"reason\":\"shared-digest\",\"reference\":\"oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"tag\":\"0.1.19-feat-x.2\",\"tags\":[\"0.1.19\"],\"version\":\"0.1.19-feat-x.2\"}", - decode: func(b []byte) (any, error) { var v KeptEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.kept$")}, - {name: "held on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.held", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", - decode: func(b []byte) (any, error) { var v HeldEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.held$")}, - {name: "released on the seat's subject", subject: "mesh.seat.oci-artifact-registry.event.released", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"by\":\"mesh-artifact-sweep\",\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"holder\":\"distribution\",\"name\":\"mesh-sdk\",\"reason\":\"kept by the sweep's rule\"}", - decode: func(b []byte) (any, error) { var v ReleasedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.oci-artifact-registry\\.event\\.released$")}, - {name: "publish opens a transfer", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a field the reader does not know is dropped", subject: "", given: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"later\":{\"x\":1},\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", wire: "{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"bundle\",\"name\":\"mesh-sdk\",\"parts\":[{\"digest\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"size\":1048576}],\"transfer\":\"t1\",\"version\":\"0.1.19\"}", - decode: func(b []byte) (any, error) { var v PublishInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "publish answers what it lacks", subject: "", given: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", wire: "{\"lacks\":[\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a chunk of a part", subject: "", given: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", wire: "{\"data\":\"aGVsbG8=\",\"last\":true,\"part\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"sequence\":0,\"transfer\":\"t1\"}", - decode: func(b []byte) (any, error) { var v PublishPartInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "the versions held", subject: "", given: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", wire: "{\"name\":\"mesh-sdk\",\"versions\":[{\"commit\":\"cccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"fingerprint\":\"f1\",\"kind\":\"package\",\"name\":\"mesh-sdk\",\"version\":\"0.1.19\"}]}", - decode: func(b []byte) (any, error) { var v VersionsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a release of what is not held answers released false", subject: "", given: "{\"released\":false}", wire: "{\"released\":false}", - decode: func(b []byte) (any, error) { var v ReleaseAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "a tagged version sharing its digest with another tag is kept, not removed", subject: "", given: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", wire: "{\"kept\":true,\"reason\":\"shared-digest\",\"removed\":false,\"tags\":[\"0.1.19\"]}", - decode: func(b []byte) (any, error) { var v RemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - {name: "list answers an untagged archive by the blob it keeps", subject: "", given: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", wire: "{\"repositories\":[{\"manifests\":[{\"artifact\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"held\":true,\"index\":false,\"size\":4096}],\"name\":\"mail/files\"}]}", - decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/go/seats/ociartifactregistry/go.mod b/go/seats/ociartifactregistry/go.mod deleted file mode 100644 index 993f3bc..0000000 --- a/go/seats/ociartifactregistry/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry - -go 1.26.0 diff --git a/seats/container-image-registry/fixtures/events.json b/seats/container-image-registry/fixtures/events.json deleted file mode 100644 index 65e98e5..0000000 --- a/seats/container-image-registry/fixtures/events.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "name": "published on the seat's subject", - "of": "event:published", - "subject": "mesh.seat.container-image-registry.event.published", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - }, - { - "name": "refused on the seat's subject", - "of": "event:refused", - "subject": "mesh.seat.container-image-registry.event.refused", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "retention" - } - }, - { - "name": "removed on the seat's subject", - "of": "event:removed", - "subject": "mesh.seat.container-image-registry.event.removed", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "retention" - } - }, - { - "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", - "of": "event:kept", - "subject": "mesh.seat.container-image-registry.event.kept", - "given": { - "name": "mesh-sdk", - "version": "0.1.19-feat-x.2", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "reference": "container-image-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "shared-digest", - "tag": "0.1.19-feat-x.2", - "tags": [ - "0.1.19" - ] - } - }, - { - "name": "held on the seat's subject", - "of": "event:held", - "subject": "mesh.seat.container-image-registry.event.held", - "given": { - "name": "mesh-sdk", - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "reason": "kept by the sweep's rule", - "holder": "distribution", - "by": "mesh-artifact-sweep", - "at": "2026-11-04T12:00:00Z" - } - }, - { - "name": "released on the seat's subject", - "of": "event:released", - "subject": "mesh.seat.container-image-registry.event.released", - "given": { - "name": "mesh-sdk", - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "reason": "kept by the sweep's rule", - "holder": "distribution", - "by": "mesh-artifact-sweep", - "at": "2026-11-04T12:00:00Z" - } - } -] diff --git a/seats/container-image-registry/fixtures/verbs.json b/seats/container-image-registry/fixtures/verbs.json deleted file mode 100644 index e024a81..0000000 --- a/seats/container-image-registry/fixtures/verbs.json +++ /dev/null @@ -1,149 +0,0 @@ -[ - { - "name": "publish opens a transfer", - "of": "verb:publish.input", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "a field the reader does not know is dropped", - "of": "verb:publish.input", - "why": "ADR 0608 §6: a reader ignores a field it does not know", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ], - "later": { - "x": 1 - } - }, - "wire": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "image", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "publish answers what it lacks", - "of": "verb:publish.answer", - "given": { - "transfer": "t1", - "lacks": [ - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ] - } - }, - { - "name": "a chunk of a part", - "of": "verb:publish-part.input", - "given": { - "transfer": "t1", - "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "sequence": 0, - "last": true, - "data": "aGVsbG8=" - } - }, - { - "name": "the versions held", - "of": "verb:versions.answer", - "given": { - "name": "mesh-sdk", - "versions": [ - { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc" - } - ] - } - }, - { - "name": "a release of what is not held answers released false", - "of": "verb:release.answer", - "why": "A3t: the outcome asked holds already, so it is no error", - "given": { - "released": false - } - }, - { - "name": "a tagged version sharing its digest with another tag is kept, not removed", - "of": "verb:remove.answer", - "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", - "given": { - "removed": false, - "kept": true, - "reason": "shared-digest", - "tags": [ - "0.1.19" - ] - } - }, - { - "name": "list answers an index with its platforms, its tag and its hold", - "of": "verb:list.answer", - "given": { - "repositories": [ - { - "name": "mail/server", - "manifests": [ - { - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": true, - "platforms": [ - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ], - "tags": [ - "3" - ], - "size": 2048, - "held": false - }, - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "index": false, - "size": 1024, - "held": false - } - ] - } - ] - } - } -] diff --git a/seats/container-image-registry/seat.json b/seats/container-image-registry/seat.json deleted file mode 100644 index 3de19d8..0000000 --- a/seats/container-image-registry/seat.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "format": 1, - "seat": "container-image-registry", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0605", - "description": "The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image.", - "includes": [ - "registry" - ], - "verbs": [ - { - "name": "mirror", - "description": "Copy an outside image into the store by its pinned digest; emits published or refused.", - "callers": "mesh-build-image's holder of the image build seat", - "input": { - "type": "object", - "properties": { - "source": { - "type": "string", - "description": "the outside image's reference" - }, - "digest": { - "type": "string", - "description": "its pinned digest, sha256:" - }, - "name": { - "type": "string", - "description": "the name it is kept under" - } - }, - "required": [ - "source", - "digest", - "name" - ] - }, - "answer": { - "type": "object", - "properties": { - "reference": { - "type": "string", - "description": "the reference the seat answers" - }, - "digest": { - "type": "string", - "description": "the digest stored" - } - }, - "required": [ - "reference", - "digest" - ] - }, - "optional": true - }, - { - "name": "tags", - "description": "The tags of one image and the digest each names.", - "callers": "any caller whose role grants it", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the image" - } - }, - "required": [ - "name" - ] - }, - "answer": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the image" - }, - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "tag": { - "type": "string", - "description": "the tag" - }, - "digest": { - "type": "string", - "description": "its digest" - } - }, - "required": [ - "tag", - "digest" - ] - }, - "description": "every tag" - } - }, - "required": [ - "name", - "tags" - ] - }, - "optional": true - }, - { - "name": "hold", - "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", - "callers": "the module that runs the artifact sweep alone", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "which of the sweep's rules keeps it" - } - }, - "required": [ - "name", - "digest", - "reason" - ] - }, - "answer": { - "type": "object", - "properties": { - "held": { - "type": "boolean", - "description": "it is held" - }, - "since": { - "type": "string", - "description": "when the hold began, RFC 3339" - } - }, - "required": [ - "held" - ] - }, - "optional": true - }, - { - "name": "release", - "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", - "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "why it is let go" - } - }, - "required": [ - "name", - "digest", - "reason" - ] - }, - "answer": { - "type": "object", - "properties": { - "released": { - "type": "boolean", - "description": "a hold was let go; false when there was none" - } - }, - "required": [ - "released" - ] - }, - "optional": true - }, - { - "name": "list", - "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every image manifest and index, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", - "callers": "any caller whose role grants it", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the repository, /; every repository of the seat when not given" - } - } - }, - "answer": { - "type": "object", - "properties": { - "repositories": { - "type": "array", - "items": { - "$ref": "#/defs/Repository" - }, - "description": "each repository named, in order of its name" - } - }, - "required": [ - "repositories" - ] - }, - "optional": true - } - ], - "events": [ - { - "name": "held", - "body": { - "$ref": "#/defs/Hold" - } - }, - { - "name": "released", - "body": { - "$ref": "#/defs/Hold" - } - } - ], - "family": { - "name": "container-image-registry.events", - "max-age-days": 7 - }, - "defs": { - "Hold": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "why" - }, - "holder": { - "type": "string", - "description": "the module holding the seat" - }, - "by": { - "type": "string", - "description": "the caller the holder proved" - }, - "at": { - "type": "string", - "description": "when, RFC 3339" - } - }, - "required": [ - "name", - "digest", - "holder" - ], - "description": "the body of held and released" - }, - "Repository": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the repository" - }, - "manifests": { - "type": "array", - "items": { - "$ref": "#/defs/Listed" - }, - "description": "each manifest it holds, by digest" - } - }, - "required": [ - "name", - "manifests" - ] - }, - "Listed": { - "type": "object", - "properties": { - "digest": { - "type": "string", - "description": "the manifest's digest, sha256:" - }, - "index": { - "type": "boolean", - "description": "it is an index, naming a manifest per platform" - }, - "platforms": { - "type": "array", - "items": { - "type": "string" - }, - "description": "for an index, the digests of the manifests it names" - }, - "artifact": { - "type": "string", - "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "the tags naming it, none for an untagged one" - }, - "size": { - "type": "integer", - "description": "its bytes: its own content and every blob it names" - }, - "held": { - "type": "boolean", - "description": "a hold keeps it from retention" - } - }, - "required": [ - "digest", - "index", - "size", - "held" - ] - } - } -} diff --git a/seats/git/seat.json b/seats/git/seat.json deleted file mode 100644 index b7c55a1..0000000 --- a/seats/git/seat.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "format": 1, - "seat": "git", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0111", - "delivers": "git" -} diff --git a/seats/go-package-registry/fixtures/events.json b/seats/go-package-registry/fixtures/events.json deleted file mode 100644 index c3a8b16..0000000 --- a/seats/go-package-registry/fixtures/events.json +++ /dev/null @@ -1,73 +0,0 @@ -[ - { - "name": "published on the seat's subject", - "of": "event:published", - "subject": "mesh.seat.go-package-registry.event.published", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "module": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19" - } - }, - { - "name": "refused on the seat's subject", - "of": "event:refused", - "subject": "mesh.seat.go-package-registry.event.refused", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", - "reason": "retention" - } - }, - { - "name": "removed on the seat's subject", - "of": "event:removed", - "subject": "mesh.seat.go-package-registry.event.removed", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", - "reason": "retention" - } - }, - { - "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", - "of": "event:kept", - "subject": "mesh.seat.go-package-registry.event.kept", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19-feat-x.2", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "reference": "git.novox.be/novox/mesh-sdk/go@0.1.19", - "reason": "shared-digest", - "tag": "0.1.19-feat-x.2", - "tags": [ - "0.1.19" - ] - } - } -] diff --git a/seats/go-package-registry/fixtures/verbs.json b/seats/go-package-registry/fixtures/verbs.json deleted file mode 100644 index be6a53a..0000000 --- a/seats/go-package-registry/fixtures/verbs.json +++ /dev/null @@ -1,110 +0,0 @@ -[ - { - "name": "publish opens a transfer", - "of": "verb:publish.input", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "a field the reader does not know is dropped", - "of": "verb:publish.input", - "why": "ADR 0608 §6: a reader ignores a field it does not know", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ], - "later": { - "x": 1 - } - }, - "wire": { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "publish answers what it lacks", - "of": "verb:publish.answer", - "given": { - "transfer": "t1", - "lacks": [ - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ] - } - }, - { - "name": "a chunk of a part", - "of": "verb:publish-part.input", - "given": { - "transfer": "t1", - "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "sequence": 0, - "last": true, - "data": "aGVsbG8=" - } - }, - { - "name": "the versions held", - "of": "verb:versions.answer", - "given": { - "name": "git.novox.be/novox/mesh-sdk/go", - "versions": [ - { - "name": "git.novox.be/novox/mesh-sdk/go", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc" - } - ] - } - }, - { - "name": "a tagged version sharing its digest with another tag is kept, not removed", - "of": "verb:remove.answer", - "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", - "given": { - "removed": false, - "kept": true, - "reason": "shared-digest", - "tags": [ - "0.1.19" - ] - } - } -] diff --git a/seats/go-package-registry/seat.json b/seats/go-package-registry/seat.json deleted file mode 100644 index 9d42636..0000000 --- a/seats/go-package-registry/seat.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "format": 1, - "seat": "go-package-registry", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0605", - "description": "The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus.", - "delivers": "go-package-registry", - "includes": [ - "registry" - ], - "family": { - "name": "go-package-registry.events", - "max-age-days": 7 - }, - "provisions": [ - { - "name": "go-package-registry", - "form": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "the read side's address" - } - }, - "required": [ - "url" - ], - "description": "form none: the Go module proxy's read side, answered anonymously, with no credential (design 51)" - } - } - ] -} diff --git a/seats/mesh-artifact-store/seat.json b/seats/mesh-artifact-store/seat.json deleted file mode 100644 index d82ab33..0000000 --- a/seats/mesh-artifact-store/seat.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "format": 1, - "seat": "mesh-artifact-store", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0075" -} diff --git a/seats/mesh-artifact-sweep/seat.json b/seats/mesh-artifact-sweep/seat.json deleted file mode 100644 index 2e5b673..0000000 --- a/seats/mesh-artifact-sweep/seat.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "format": 1, - "seat": "mesh-artifact-sweep", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0571", - "verbs": [ - { - "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": { - "properties": { - "collected": { - "description": "\"true\": list the collected references too (counted always)", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "repository": { - "description": "only this repository's references, as \u003cmodule\u003e/\u003cartifact\u003e (the counts stay whole)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": { - "references": { - "description": "the artifacts' references, artifact-store://\u003cmodule\u003e/\u003cartifact\u003e/blobs/sha256:\u003chex\u003e: a list, or one text separated by commas", - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "references" - ], - "type": "object" - } - }, - { - "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": { - "properties": { - "confirm": { - "description": "\"true\": let go of what is eligible (needs why); without it nothing is done", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "most": { - "description": "let go of at most this many (500 by default, at most 5000)", - "type": "string" - }, - "why": { - "description": "with confirm: why, required and recorded", - "type": "string" - } - }, - "type": "object" - } - }, - { - "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": { - "properties": {}, - "type": "object" - } - }, - { - "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": { - "properties": { - "node": { - "description": "the machine's name", - "type": "string" - } - }, - "required": [ - "node" - ], - "type": "object" - } - } - ], - "events": [ - { - "name": "artifacts-gone" - } - ] -} diff --git a/seats/mesh-catalog/seat.json b/seats/mesh-catalog/seat.json deleted file mode 100644 index 5f71e23..0000000 --- a/seats/mesh-catalog/seat.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "format": 1, - "seat": "mesh-catalog", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0604", - "verbs": [ - { - "name": "modules", - "description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "module", - "description": "One module whole: its repository, directory, commit and fingerprint, and its manifest as the trunk holds it. Older versions are in its repository's history. Reads only.", - "optional": true, - "input": { - "properties": { - "module": { - "description": "the module's name", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - } - }, - { - "name": "provides", - "description": "Which modules provide a provision (a module provides its own name too) and which require it, from the module graph's provision edges. Reads only.", - "optional": true, - "input": { - "properties": { - "provision": { - "description": "the provision, such as postgres-database", - "type": "string" - } - }, - "required": [ - "provision" - ], - "type": "object" - } - }, - { - "name": "dependents", - "description": "The modules whose build edges reach a module, directly or through others (a base, a mesh package it publishes, the build module that builds them): what a change of it builds again, from the module graph. Reads only.", - "optional": true, - "input": { - "properties": { - "module": { - "description": "the module's name", - "type": "string" - } - }, - "required": [ - "module" - ], - "type": "object" - } - } - ] -} diff --git a/seats/mesh-delivery-checker/fixtures/events.json b/seats/mesh-delivery-checker/fixtures/events.json deleted file mode 100644 index 7df46f6..0000000 --- a/seats/mesh-delivery-checker/fixtures/events.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "name": "verdict on the checker's own family", - "of": "holder-event:verdict", - "subject": "mesh.stream.mesh-delivery-checker.event.verdict", - "given": { - "repository": "novox/mesh-contracts", - "pull": 3, - "head": "cccccccccccccccccccccccccccccccccccccccc", - "verdict": "fail", - "out-of-step": [ - "go/seats/meshbroker/contract.go" - ], - "series": [ - { - "module": "mesh-contracts", - "artifact": "go", - "from": "0.1", - "to": "0.2" - } - ] - } - }, - { - "name": "tag-wanted on the checker's own family", - "of": "holder-event:tag-wanted", - "subject": "mesh.stream.mesh-delivery-checker.event.tag-wanted", - "given": { - "repository": "novox/mesh-sdk", - "pull": 170, - "head": "cccccccccccccccccccccccccccccccccccccccc", - "branch": "feat/registry", - "module": "mesh-sdk", - "package": "go", - "tag": "registry-1966" - } - }, - { - "name": "preview.written on the checker's stream", - "of": "holder-event:preview.written", - "subject": "mesh.stream.mesh-delivery-checker.event.preview.written", - "given": {} - } -] diff --git a/seats/mesh-delivery-checker/seat.json b/seats/mesh-delivery-checker/seat.json deleted file mode 100644 index 808b3e9..0000000 --- a/seats/mesh-delivery-checker/seat.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "format": 1, - "seat": "mesh-delivery-checker", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0547", - "verbs": [ - { - "name": "try", - "description": "Try a pushed head before its pull request is opened: the checker judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most 4 wait or run at once, mesh-wide.", - "optional": true, - "replaces": [ - "sh merge-check.sh", - "docker run postgres" - ], - "input": { - "properties": { - "head": { - "description": "the full commit to try, 40 or 64 hex digits", - "type": "string" - }, - "level": { - "description": "unit or integration: only this level; both when absent", - "type": "string" - }, - "repository": { - "description": "/", - "type": "string" - }, - "tests": { - "description": "a go test -run pattern: only those tests run, and nothing is kept", - "type": "string" - }, - "trunk": { - "description": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", - "type": "string" - } - }, - "required": [ - "repository", - "head" - ], - "type": "object" - }, - "answer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "the try's id" - }, - "state": { - "type": "string", - "description": "its state" - } - }, - "required": [ - "id" - ] - } - }, - { - "name": "try-read", - "description": "Where a try stands, as stored: queued, running, or its end (pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit it was merged onto, the time it took and what failed first. Read-only.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the try's id, try-", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - }, - "answer": { - "type": "object", - "description": "the try: its state, its steps and its verdict when it has one" - } - }, - { - "name": "try-end", - "description": "End a try that waits or runs, as error, by hand: say why (kept in what it says). Its place among the tries is free at once and the next try of its head asks anew with a new id. A try that ended already is refused.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the try's id, try-", - "type": "string" - }, - "why": { - "description": "why it is ended, in words", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - }, - "answer": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "the try" - }, - "ended": { - "type": "boolean", - "description": "it is ended" - } - }, - "required": [ - "id", - "ended" - ] - } - } - ], - "description": "The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2).", - "holder-events": [ - { - "name": "verdict", - "subject": "mesh.stream..event.verdict", - "family": "checker.asks", - "body": { - "type": "object", - "properties": { - "repository": { - "type": "string", - "description": "owner/repo" - }, - "pull": { - "type": "integer", - "description": "the pull request's number, for a pull request's check" - }, - "head": { - "type": "string", - "description": "the commit checked" - }, - "verdict": { - "type": "string", - "description": "pass or fail" - }, - "out-of-step": { - "type": "array", - "items": { - "type": "string" - }, - "description": "the generated files out of step with their seat definitions, each path (design 26)" - }, - "series": { - "type": "array", - "items": { - "type": "object", - "properties": { - "module": { - "type": "string", - "description": "the module whose artifact's series changes" - }, - "artifact": { - "type": "string", - "description": "its artifact, or the package's ecosystem name as the module's packages name it" - }, - "from": { - "type": "string", - "description": "the series on trunk" - }, - "to": { - "type": "string", - "description": "the series the checked head gives it" - } - }, - "required": [ - "module", - "artifact", - "from", - "to" - ] - }, - "description": "each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none" - } - }, - "required": [ - "repository", - "head", - "verdict" - ] - } - }, - { - "name": "tag-wanted", - "subject": "mesh.stream..event.tag-wanted", - "family": "checker.asks", - "body": { - "type": "object", - "properties": { - "repository": { - "type": "string", - "description": "owner/repo" - }, - "pull": { - "type": "integer", - "description": "the pull request" - }, - "head": { - "type": "string", - "description": "its head" - }, - "branch": { - "type": "string", - "description": "the pull request's branch: its tag is removed when the branch ends" - }, - "module": { - "type": "string", - "description": "the group member" - }, - "package": { - "type": "string", - "description": "the package it declares" - }, - "tag": { - "type": "string", - "description": "the tag wanted" - } - }, - "required": [ - "repository", - "head", - "branch", - "module", - "package" - ] - } - }, - { - "name": "preview.written", - "subject": "mesh.stream..event.preview.written", - "family": "checks" - } - ] -} diff --git a/seats/mesh-delivery/seat.json b/seats/mesh-delivery/seat.json deleted file mode 100644 index 43ba193..0000000 --- a/seats/mesh-delivery/seat.json +++ /dev/null @@ -1,227 +0,0 @@ -{ - "format": 1, - "seat": "mesh-delivery", - "scope": "mesh", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0239", - "verbs": [ - { - "name": "deliveries", - "description": "Every delivery not final, and those that ended in the last day, one line each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed by state, repository or group.", - "input": { - "properties": { - "all": { - "description": "\"true\": the final ones of the last thirty days too", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "group": { - "description": "a group's id (its branch name)", - "type": "string" - }, - "repository": { - "description": "owner/repository", - "type": "string" - }, - "state": { - "description": "one state, e.g. delivering or held", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "times", - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the delivery view (nox-delivery): each machine's want and have, and since when", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "show", - "description": "One delivery or group whole: its delivery plan (what it builds, what each machine receives, what is not an ordinary send), every transition with when and why, the machine steps of its walk, its group and its order.", - "input": { - "properties": { - "id": { - "description": "a delivery's id, or a group's", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - } - }, - { - "name": "groups", - "description": "Every delivery group: its members in order, why each pair is ordered (declared, built by, version skew, engine before controller, by name), its composed check and its state, derived from its members.", - "input": { - "properties": { - "all": { - "description": "\"true\": the groups that ended too", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "what-if", - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the merge check's composition, which composes every machine a change touches", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "checks", - "description": "What the mesh's checks said of a pull request's head or of one commit: each of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, mesh/delivery, …) with its state, description and when it was set; the merge check's full verdict as the controller said it — each layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's protection would let it merge, every required status being success.", - "optional": true, - "input": { - "properties": { - "commit": { - "description": "a commit's sha, or the start of one, instead of a pull request", - "type": "string" - }, - "number": { - "description": "a pull request's number: its head is read", - "type": "string" - }, - "repository": { - "description": "owner/repository", - "type": "string" - } - }, - "required": [ - "repository" - ], - "type": "object" - } - }, - { - "name": "table", - "description": "The state table every delivery runs by: each transition with its guard, each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "stalled", - "description": "Every delivery held past its state's bound, with the transition the table lets healer H2 take for it, or none.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "recheck", - "description": "Check a rejected or ready delivery again: it goes back to proposed and the controller is asked for its check. With why.", - "input": { - "properties": { - "id": { - "description": "the delivery's id", - "type": "string" - }, - "why": { - "description": "why, kept with the transition", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - } - }, - { - "name": "release", - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: the assigner's asks (hold, restore, backup, record), answered on the phone or in the delivery view", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "stop", - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: a bad fingerprint is held and its canary set back by the assigner (ADR 0429 §6, ADR 0539 clause 18)", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "close", - "description": "Healer H2's verb: take the one transition the table names for a delivery held past its bound. Refused for any other.", - "input": { - "properties": { - "id": { - "description": "the delivery's id", - "type": "string" - }, - "why": { - "description": "the condition it answers", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - } - }, - { - "name": "retire-history", - "description": "Removed in R6 (ADR 0539): refused by name, naming its replacement: nothing to retire: the gate keeps no delivery after its merge", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "settle", - "description": "Ask the operator to settle deliveries held because their merge reached the trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another reason, naming why.", - "optional": true, - "input": { - "additionalProperties": false, - "properties": { - "ids": { - "description": "the held deliveries' ids (owner/repository@commit)", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" - }, - "why": { - "description": "why they may be settled, shown to the operator", - "type": "string" - } - }, - "required": [ - "ids", - "why" - ], - "type": "object" - } - } - ] -} diff --git a/seats/node-build-go/seat.json b/seats/node-build-go/seat.json deleted file mode 100644 index ccb5664..0000000 --- a/seats/node-build-go/seat.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "format": 1, - "seat": "node-build-go", - "scope": "node", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "pulled-by-kind": true, - "yields": true, - "verbs": [ - { - "name": "current", - "description": "The Go asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "pause", - "description": "Take no new Go ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "resume", - "description": "Take Go asks again on this node.", - "input": { - "properties": {}, - "type": "object" - } - } - ], - "accepts": [ - { - "name": "delivery" - }, - { - "name": "check" - }, - { - "name": "recheck" - } - ] -} diff --git a/seats/node-build-image/seat.json b/seats/node-build-image/seat.json deleted file mode 100644 index b275f85..0000000 --- a/seats/node-build-image/seat.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "format": 1, - "seat": "node-build-image", - "scope": "node", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "pulled-by-kind": true, - "yields": true, - "verbs": [ - { - "name": "current", - "description": "The image asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "pause", - "description": "Take no new image ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "resume", - "description": "Take image asks again on this node.", - "input": { - "properties": {}, - "type": "object" - } - } - ], - "accepts": [ - { - "name": "delivery" - }, - { - "name": "check" - }, - { - "name": "recheck" - } - ] -} diff --git a/seats/node-build-node/seat.json b/seats/node-build-node/seat.json deleted file mode 100644 index aa7f182..0000000 --- a/seats/node-build-node/seat.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "format": 1, - "seat": "node-build-node", - "scope": "node", - "grain": "seat-list", - "domain": "Delivery", - "decision": "novox/hq ADR 0525, ADR 0536, ADR 0553", - "pulled-by-kind": true, - "yields": true, - "verbs": [ - { - "name": "current", - "description": "The Node asks this node runs now (each ask's id, kind, module and when it started), its build capacity, and whether it is paused, taking no new ask.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "pause", - "description": "Take no new Node ask on this node until resumed; an ask running here ends in its place. Kept across a restart of the build module (its pause key). A capacity of 0 is the lasting way to keep a node from building.", - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "resume", - "description": "Take Node asks again on this node.", - "input": { - "properties": {}, - "type": "object" - } - } - ], - "accepts": [ - { - "name": "delivery" - }, - { - "name": "check" - }, - { - "name": "recheck" - } - ] -} diff --git a/seats/npm-package-registry/fixtures/events.json b/seats/npm-package-registry/fixtures/events.json deleted file mode 100644 index 4cadc30..0000000 --- a/seats/npm-package-registry/fixtures/events.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "published on the seat's subject", - "of": "event:published", - "subject": "mesh.seat.npm-package-registry.event.published", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "@novox/mesh-sdk@0.1.19" - } - }, - { - "name": "refused on the seat's subject", - "of": "event:refused", - "subject": "mesh.seat.npm-package-registry.event.refused", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "@novox/mesh-sdk@0.1.19", - "reason": "retention" - } - }, - { - "name": "removed on the seat's subject", - "of": "event:removed", - "subject": "mesh.seat.npm-package-registry.event.removed", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "transfer": "t1", - "reference": "@novox/mesh-sdk@0.1.19", - "reason": "retention" - } - }, - { - "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", - "of": "event:kept", - "subject": "mesh.seat.npm-package-registry.event.kept", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19-feat-x.2", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "gitea", - "reference": "@novox/mesh-sdk@0.1.19", - "reason": "shared-digest", - "tag": "0.1.19-feat-x.2", - "tags": [ - "0.1.19" - ] - } - } -] diff --git a/seats/npm-package-registry/fixtures/verbs.json b/seats/npm-package-registry/fixtures/verbs.json deleted file mode 100644 index 0b88f08..0000000 --- a/seats/npm-package-registry/fixtures/verbs.json +++ /dev/null @@ -1,110 +0,0 @@ -[ - { - "name": "publish opens a transfer", - "of": "verb:publish.input", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "a field the reader does not know is dropped", - "of": "verb:publish.input", - "why": "ADR 0608 §6: a reader ignores a field it does not know", - "given": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ], - "later": { - "x": 1 - } - }, - "wire": { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "publish answers what it lacks", - "of": "verb:publish.answer", - "given": { - "transfer": "t1", - "lacks": [ - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ] - } - }, - { - "name": "a chunk of a part", - "of": "verb:publish-part.input", - "given": { - "transfer": "t1", - "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "sequence": 0, - "last": true, - "data": "aGVsbG8=" - } - }, - { - "name": "the versions held", - "of": "verb:versions.answer", - "given": { - "name": "@novox/mesh-sdk", - "versions": [ - { - "name": "@novox/mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc" - } - ] - } - }, - { - "name": "a tagged version sharing its digest with another tag is kept, not removed", - "of": "verb:remove.answer", - "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", - "given": { - "removed": false, - "kept": true, - "reason": "shared-digest", - "tags": [ - "0.1.19" - ] - } - } -] diff --git a/seats/npm-package-registry/seat.json b/seats/npm-package-registry/seat.json deleted file mode 100644 index ae2ab3d..0000000 --- a/seats/npm-package-registry/seat.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "format": 1, - "seat": "npm-package-registry", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0109", - "description": "The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus.", - "delivers": "npm-package-registry", - "includes": [ - "registry" - ], - "family": { - "name": "npm-package-registry.events", - "max-age-days": 7 - }, - "provisions": [ - { - "name": "npm-package-registry", - "form": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "the read side's address" - } - }, - "required": [ - "url" - ], - "description": "form none: the npm registry's read side, answered anonymously, with no credential (design 51)" - } - } - ] -} diff --git a/seats/oci-artifact-registry/fixtures/events.json b/seats/oci-artifact-registry/fixtures/events.json deleted file mode 100644 index e0a8cde..0000000 --- a/seats/oci-artifact-registry/fixtures/events.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "name": "published on the seat's subject", - "of": "event:published", - "subject": "mesh.seat.oci-artifact-registry.event.published", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - } - }, - { - "name": "refused on the seat's subject", - "of": "event:refused", - "subject": "mesh.seat.oci-artifact-registry.event.refused", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "retention" - } - }, - { - "name": "removed on the seat's subject", - "of": "event:removed", - "subject": "mesh.seat.oci-artifact-registry.event.removed", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "transfer": "t1", - "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "retention" - } - }, - { - "name": "kept on the seat's subject: a tagged version sharing its digest stays, naming the tags on it", - "of": "event:kept", - "subject": "mesh.seat.oci-artifact-registry.event.kept", - "given": { - "name": "mesh-sdk", - "version": "0.1.19-feat-x.2", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "holder": "distribution", - "reference": "oci-artifact-registry/mesh-sdk/lib@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "reason": "shared-digest", - "tag": "0.1.19-feat-x.2", - "tags": [ - "0.1.19" - ] - } - }, - { - "name": "held on the seat's subject", - "of": "event:held", - "subject": "mesh.seat.oci-artifact-registry.event.held", - "given": { - "name": "mesh-sdk", - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "reason": "kept by the sweep's rule", - "holder": "distribution", - "by": "mesh-artifact-sweep", - "at": "2026-11-04T12:00:00Z" - } - }, - { - "name": "released on the seat's subject", - "of": "event:released", - "subject": "mesh.seat.oci-artifact-registry.event.released", - "given": { - "name": "mesh-sdk", - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "reason": "kept by the sweep's rule", - "holder": "distribution", - "by": "mesh-artifact-sweep", - "at": "2026-11-04T12:00:00Z" - } - } -] diff --git a/seats/oci-artifact-registry/fixtures/verbs.json b/seats/oci-artifact-registry/fixtures/verbs.json deleted file mode 100644 index 039c38b..0000000 --- a/seats/oci-artifact-registry/fixtures/verbs.json +++ /dev/null @@ -1,138 +0,0 @@ -[ - { - "name": "publish opens a transfer", - "of": "verb:publish.input", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "a field the reader does not know is dropped", - "of": "verb:publish.input", - "why": "ADR 0608 §6: a reader ignores a field it does not know", - "given": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ], - "later": { - "x": 1 - } - }, - "wire": { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "bundle", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc", - "transfer": "t1", - "parts": [ - { - "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "size": 1048576 - } - ] - } - }, - { - "name": "publish answers what it lacks", - "of": "verb:publish.answer", - "given": { - "transfer": "t1", - "lacks": [ - "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ] - } - }, - { - "name": "a chunk of a part", - "of": "verb:publish-part.input", - "given": { - "transfer": "t1", - "part": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "sequence": 0, - "last": true, - "data": "aGVsbG8=" - } - }, - { - "name": "the versions held", - "of": "verb:versions.answer", - "given": { - "name": "mesh-sdk", - "versions": [ - { - "name": "mesh-sdk", - "version": "0.1.19", - "kind": "package", - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "fingerprint": "f1", - "commit": "cccccccccccccccccccccccccccccccccccccccc" - } - ] - } - }, - { - "name": "a release of what is not held answers released false", - "of": "verb:release.answer", - "why": "A3t: the outcome asked holds already, so it is no error", - "given": { - "released": false - } - }, - { - "name": "a tagged version sharing its digest with another tag is kept, not removed", - "of": "verb:remove.answer", - "why": "G-sdk-contracts 3: the store cannot remove the tag alone, so the answer says what holds", - "given": { - "removed": false, - "kept": true, - "reason": "shared-digest", - "tags": [ - "0.1.19" - ] - } - }, - { - "name": "list answers an untagged archive by the blob it keeps", - "of": "verb:list.answer", - "given": { - "repositories": [ - { - "name": "mail/files", - "manifests": [ - { - "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": false, - "artifact": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "size": 4096, - "held": true - } - ] - } - ] - } - } -] diff --git a/seats/oci-artifact-registry/seat.json b/seats/oci-artifact-registry/seat.json deleted file mode 100644 index a9d969c..0000000 --- a/seats/oci-artifact-registry/seat.json +++ /dev/null @@ -1,248 +0,0 @@ -{ - "format": 1, - "seat": "oci-artifact-registry", - "scope": "mesh", - "grain": "whole", - "domain": "Delivery", - "decision": "novox/hq ADR 0605", - "description": "The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side.", - "includes": [ - "registry" - ], - "verbs": [ - { - "name": "hold", - "description": "Keep one untagged bundle, archive or copied base from removal by retention, as the artifact sweep's rules keep it (ADR 0617 §2); never an image of the holder's own. Emits held.", - "callers": "the module that runs the artifact sweep alone", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "which of the sweep's rules keeps it" - } - }, - "required": [ - "name", - "digest", - "reason" - ] - }, - "answer": { - "type": "object", - "properties": { - "held": { - "type": "boolean", - "description": "it is held" - }, - "since": { - "type": "string", - "description": "when the hold began, RFC 3339" - } - }, - "required": [ - "held" - ] - }, - "optional": true - }, - { - "name": "release", - "description": "Let a hold go; emits released. A release of what is not held answers released false and emits nothing, never an error: the outcome asked holds already. By hand only through the holder's own tool, the operator's session as the proved caller, recorded as a hand act.", - "callers": "the module that runs the artifact sweep; the operator's session through the holder's tool", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "why it is let go" - } - }, - "required": [ - "name", - "digest", - "reason" - ] - }, - "answer": { - "type": "object", - "properties": { - "released": { - "type": "boolean", - "description": "a hold was let go; false when there was none" - } - }, - "required": [ - "released" - ] - }, - "optional": true - }, - { - "name": "list", - "description": "What the store holds of one repository, or of every repository the seat holds when none is named: every manifest keeping one bundle or archive, each with its digest, whether it is an index (and the manifests an index names), the tags naming it, its size and whether it is held. Read from the store itself, so a manifest no tag names is listed too; it reads only (ADR 0632 §3).", - "callers": "any caller whose role grants it", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the repository, /; every repository of the seat when not given" - } - } - }, - "answer": { - "type": "object", - "properties": { - "repositories": { - "type": "array", - "items": { - "$ref": "#/defs/Repository" - }, - "description": "each repository named, in order of its name" - } - }, - "required": [ - "repositories" - ] - }, - "optional": true - } - ], - "events": [ - { - "name": "held", - "body": { - "$ref": "#/defs/Hold" - } - }, - { - "name": "released", - "body": { - "$ref": "#/defs/Hold" - } - } - ], - "family": { - "name": "oci-artifact-registry.events", - "max-age-days": 7 - }, - "defs": { - "Hold": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "digest": { - "type": "string", - "description": "the digest" - }, - "reason": { - "type": "string", - "description": "why" - }, - "holder": { - "type": "string", - "description": "the module holding the seat" - }, - "by": { - "type": "string", - "description": "the caller the holder proved" - }, - "at": { - "type": "string", - "description": "when, RFC 3339" - } - }, - "required": [ - "name", - "digest", - "holder" - ], - "description": "the body of held and released" - }, - "Repository": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the repository" - }, - "manifests": { - "type": "array", - "items": { - "$ref": "#/defs/Listed" - }, - "description": "each manifest it holds, by digest" - } - }, - "required": [ - "name", - "manifests" - ] - }, - "Listed": { - "type": "object", - "properties": { - "digest": { - "type": "string", - "description": "the manifest's digest, sha256:" - }, - "index": { - "type": "boolean", - "description": "it is an index, naming a manifest per platform" - }, - "platforms": { - "type": "array", - "items": { - "type": "string" - }, - "description": "for an index, the digests of the manifests it names" - }, - "artifact": { - "type": "string", - "description": "for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "the tags naming it, none for an untagged one" - }, - "size": { - "type": "integer", - "description": "its bytes: its own content and every blob it names" - }, - "held": { - "type": "boolean", - "description": "a hold keeps it from retention" - } - }, - "required": [ - "digest", - "index", - "size", - "held" - ] - } - } -} diff --git a/ts/seats/containerimageregistry/package-lock.json b/ts/seats/containerimageregistry/package-lock.json deleted file mode 100644 index acf5337..0000000 --- a/ts/seats/containerimageregistry/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-contracts-container-image-registry", - "packages": { - "": { - "name": "@novox/mesh-contracts-container-image-registry", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/ts/seats/containerimageregistry/package.json b/ts/seats/containerimageregistry/package.json deleted file mode 100644 index 2becd87..0000000 --- a/ts/seats/containerimageregistry/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat container-image-registry, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-contracts-container-image-registry", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/ts/seats/containerimageregistry/src/index.ts b/ts/seats/containerimageregistry/src/index.ts deleted file mode 100644 index 941ed44..0000000 --- a/ts/seats/containerimageregistry/src/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "container-image-registry", defined whole (seats/container-image-registry/seat.json): The mesh's container images, in the one artifact store: the OCI distribution protocol's read side, an image's tags and mirroring an outside image. - -import type * as registry from "./parts/registry.js"; - -export const seat = "container-image-registry"; - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", - mirror: "mirror", - tags: "tags", - hold: "hold", - release: "release", - list: "list", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", - held: "held", - released: "released", -} as const; - -export const family = { name: "container-image-registry.events", maxAgeDays: 7 } as const; - -export const publishedSubject = "mesh.seat.container-image-registry.event.published"; - -export const refusedSubject = "mesh.seat.container-image-registry.event.refused"; - -export const removedSubject = "mesh.seat.container-image-registry.event.removed"; - -export const keptSubject = "mesh.seat.container-image-registry.event.kept"; - -export const heldSubject = "mesh.seat.container-image-registry.event.held"; - -export const releasedSubject = "mesh.seat.container-image-registry.event.released"; - -export type Artifact = registry.Artifact; -export type KeptEvent = registry.KeptEvent; -export type Outcome = registry.Outcome; -export type Part = registry.Part; -export type PublishAnswer = registry.PublishAnswer; -export type PublishInput = registry.PublishInput; -export type PublishPartAnswer = registry.PublishPartAnswer; -export type PublishPartInput = registry.PublishPartInput; -export type PublishedEvent = registry.PublishedEvent; -export type RefusedEvent = registry.RefusedEvent; -export type RemoveAnswer = registry.RemoveAnswer; -export type RemoveInput = registry.RemoveInput; -export type RemovedEvent = registry.RemovedEvent; -export type VersionsAnswer = registry.VersionsAnswer; -export type VersionsInput = registry.VersionsInput; - -/** the body of held and released */ -export interface Hold { - /** when, RFC 3339 */ - "at"?: string; - /** the caller the holder proved */ - "by"?: string; - /** the digest */ - "digest": string; - /** the module holding the seat */ - "holder": string; - /** the name */ - "name": string; - /** why */ - "reason"?: string; -} - -export interface Listed { - /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ - "artifact"?: string; - /** the manifest's digest, sha256: */ - "digest": string; - /** a hold keeps it from retention */ - "held": boolean; - /** it is an index, naming a manifest per platform */ - "index": boolean; - /** for an index, the digests of the manifests it names */ - "platforms"?: string[]; - /** its bytes: its own content and every blob it names */ - "size": number; - /** the tags naming it, none for an untagged one */ - "tags"?: string[]; -} - -export interface Repository { - /** each manifest it holds, by digest */ - "manifests": Listed[]; - /** the repository */ - "name": string; -} - -/** is what mirror takes. */ -export interface MirrorInput { - /** its pinned digest, sha256: */ - "digest": string; - /** the name it is kept under */ - "name": string; - /** the outside image's reference */ - "source": string; -} - -/** is what mirror answers. */ -export interface MirrorAnswer { - /** the digest stored */ - "digest": string; - /** the reference the seat answers */ - "reference": string; -} - -/** is what tags takes. */ -export interface TagsInput { - /** the image */ - "name": string; -} - -export interface TagsAnswerTagsItem { - /** its digest */ - "digest": string; - /** the tag */ - "tag": string; -} - -/** is what tags answers. */ -export interface TagsAnswer { - /** the image */ - "name": string; - /** every tag */ - "tags": TagsAnswerTagsItem[]; -} - -/** is what hold takes. */ -export interface HoldInput { - /** the digest */ - "digest": string; - /** the name */ - "name": string; - /** which of the sweep's rules keeps it */ - "reason": string; -} - -/** is what hold answers. */ -export interface HoldAnswer { - /** it is held */ - "held": boolean; - /** when the hold began, RFC 3339 */ - "since"?: string; -} - -/** is what release takes. */ -export interface ReleaseInput { - /** the digest */ - "digest": string; - /** the name */ - "name": string; - /** why it is let go */ - "reason": string; -} - -/** is what release answers. */ -export interface ReleaseAnswer { - /** a hold was let go; false when there was none */ - "released": boolean; -} - -/** is what list takes. */ -export interface ListInput { - /** the repository, /; every repository of the seat when not given */ - "name"?: string; -} - -/** is what list answers. */ -export interface ListAnswer { - /** each repository named, in order of its name */ - "repositories": Repository[]; -} - -/** is the body of the event held. */ -export type HeldEvent = Hold; - -/** is the body of the event released. */ -export type ReleasedEvent = Hold; - diff --git a/ts/seats/containerimageregistry/src/parts/registry.ts b/ts/seats/containerimageregistry/src/parts/registry.ts deleted file mode 100644 index 17a8de4..0000000 --- a/ts/seats/containerimageregistry/src/parts/registry.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -/** one published thing, as every registry event and answer names it */ -export interface Artifact { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** the body of published, refused and removed */ -export interface Outcome { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** the module holding the seat that said it */ - "holder": string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ - "module"?: string; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ - "reason"?: string; - /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ - "reference"?: string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** for kept: the other tags still naming its digest */ - "tags"?: string[]; - /** the transfer it ends, when it ended one */ - "transfer"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** one part of a transfer */ -export interface Part { - /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ - "digest": string; - /** its size in bytes */ - "size": number; -} - -/** is what publish takes. */ -export interface PublishInput { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** every part's digest and size; none when the digest is already held */ - "parts"?: Part[]; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the transfer's name, the caller's own; the same name opens the same transfer again */ - "transfer": string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** is what publish answers. */ -export interface PublishAnswer { - /** the digest is held already: nothing to send; the outcome follows as an event */ - "held"?: boolean; - /** the digests of the parts the holder lacks, to send by publish-part */ - "lacks"?: string[]; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part takes. */ -export interface PublishPartInput { - /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ - "data": string; - /** this chunk is the part's last */ - "last": boolean; - /** the part's digest */ - "part": string; - /** the chunk's place in its part, from 0 */ - "sequence": number; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part answers. */ -export interface PublishPartAnswer { - /** the part is whole and its bytes match its digest */ - "complete"?: boolean; - /** the part */ - "part": string; - /** the chunks of the part held so far */ - "received"?: number; - /** the transfer */ - "transfer": string; -} - -/** is what versions takes. */ -export interface VersionsInput { - /** narrowed to one kind (optional) */ - "kind"?: string; - /** the name */ - "name": string; -} - -/** is what versions answers. */ -export interface VersionsAnswer { - /** the name */ - "name": string; - /** newest first */ - "versions": Artifact[]; -} - -/** is what remove takes. */ -export interface RemoveInput { - /** the digest (for a bundle, archive or copied base) */ - "digest"?: string; - /** the name */ - "name": string; - /** why it goes */ - "reason": "tag-gone" | "retention"; - /** the tag (for a tagged version) */ - "tag"?: string; - /** the version */ - "version"?: string; -} - -/** is what remove answers. */ -export interface RemoveAnswer { - "artifact"?: Artifact; - /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ - "kept"?: boolean; - /** why it was kept: shared-digest, another tag names its digest */ - "reason"?: "shared-digest"; - /** it is gone */ - "removed": boolean; - /** the other tags still naming its digest, when kept */ - "tags"?: string[]; -} - -/** is the body of the event published. */ -export type PublishedEvent = Outcome; - -/** is the body of the event refused. */ -export type RefusedEvent = Outcome; - -/** is the body of the event removed. */ -export type RemovedEvent = Outcome; - -/** is the body of the event kept. */ -export type KeptEvent = Outcome; - diff --git a/ts/seats/containerimageregistry/tsconfig.json b/ts/seats/containerimageregistry/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/ts/seats/containerimageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/ts/seats/gopackageregistry/package-lock.json b/ts/seats/gopackageregistry/package-lock.json deleted file mode 100644 index 20966bb..0000000 --- a/ts/seats/gopackageregistry/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-contracts-go-package-registry", - "packages": { - "": { - "name": "@novox/mesh-contracts-go-package-registry", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/ts/seats/gopackageregistry/package.json b/ts/seats/gopackageregistry/package.json deleted file mode 100644 index 03317fe..0000000 --- a/ts/seats/gopackageregistry/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat go-package-registry, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-contracts-go-package-registry", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/ts/seats/gopackageregistry/src/index.ts b/ts/seats/gopackageregistry/src/index.ts deleted file mode 100644 index f072090..0000000 --- a/ts/seats/gopackageregistry/src/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "go-package-registry", defined whole (seats/go-package-registry/seat.json): The mesh's Go modules: Go's module proxy protocol's read side, and publishing over the bus. - -import type * as registry from "./parts/registry.js"; - -export const seat = "go-package-registry"; - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -export const family = { name: "go-package-registry.events", maxAgeDays: 7 } as const; - -export const publishedSubject = "mesh.seat.go-package-registry.event.published"; - -export const refusedSubject = "mesh.seat.go-package-registry.event.refused"; - -export const removedSubject = "mesh.seat.go-package-registry.event.removed"; - -export const keptSubject = "mesh.seat.go-package-registry.event.kept"; - -export type Artifact = registry.Artifact; -export type KeptEvent = registry.KeptEvent; -export type Outcome = registry.Outcome; -export type Part = registry.Part; -export type PublishAnswer = registry.PublishAnswer; -export type PublishInput = registry.PublishInput; -export type PublishPartAnswer = registry.PublishPartAnswer; -export type PublishPartInput = registry.PublishPartInput; -export type PublishedEvent = registry.PublishedEvent; -export type RefusedEvent = registry.RefusedEvent; -export type RemoveAnswer = registry.RemoveAnswer; -export type RemoveInput = registry.RemoveInput; -export type RemovedEvent = registry.RemovedEvent; -export type VersionsAnswer = registry.VersionsAnswer; -export type VersionsInput = registry.VersionsInput; - -/** is the form of the provision go-package-registry. */ -export interface GoPackageRegistryForm { - /** the read side's address */ - "url": string; -} - diff --git a/ts/seats/gopackageregistry/src/parts/registry.ts b/ts/seats/gopackageregistry/src/parts/registry.ts deleted file mode 100644 index 17a8de4..0000000 --- a/ts/seats/gopackageregistry/src/parts/registry.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -/** one published thing, as every registry event and answer names it */ -export interface Artifact { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** the body of published, refused and removed */ -export interface Outcome { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** the module holding the seat that said it */ - "holder": string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ - "module"?: string; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ - "reason"?: string; - /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ - "reference"?: string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** for kept: the other tags still naming its digest */ - "tags"?: string[]; - /** the transfer it ends, when it ended one */ - "transfer"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** one part of a transfer */ -export interface Part { - /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ - "digest": string; - /** its size in bytes */ - "size": number; -} - -/** is what publish takes. */ -export interface PublishInput { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** every part's digest and size; none when the digest is already held */ - "parts"?: Part[]; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the transfer's name, the caller's own; the same name opens the same transfer again */ - "transfer": string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** is what publish answers. */ -export interface PublishAnswer { - /** the digest is held already: nothing to send; the outcome follows as an event */ - "held"?: boolean; - /** the digests of the parts the holder lacks, to send by publish-part */ - "lacks"?: string[]; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part takes. */ -export interface PublishPartInput { - /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ - "data": string; - /** this chunk is the part's last */ - "last": boolean; - /** the part's digest */ - "part": string; - /** the chunk's place in its part, from 0 */ - "sequence": number; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part answers. */ -export interface PublishPartAnswer { - /** the part is whole and its bytes match its digest */ - "complete"?: boolean; - /** the part */ - "part": string; - /** the chunks of the part held so far */ - "received"?: number; - /** the transfer */ - "transfer": string; -} - -/** is what versions takes. */ -export interface VersionsInput { - /** narrowed to one kind (optional) */ - "kind"?: string; - /** the name */ - "name": string; -} - -/** is what versions answers. */ -export interface VersionsAnswer { - /** the name */ - "name": string; - /** newest first */ - "versions": Artifact[]; -} - -/** is what remove takes. */ -export interface RemoveInput { - /** the digest (for a bundle, archive or copied base) */ - "digest"?: string; - /** the name */ - "name": string; - /** why it goes */ - "reason": "tag-gone" | "retention"; - /** the tag (for a tagged version) */ - "tag"?: string; - /** the version */ - "version"?: string; -} - -/** is what remove answers. */ -export interface RemoveAnswer { - "artifact"?: Artifact; - /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ - "kept"?: boolean; - /** why it was kept: shared-digest, another tag names its digest */ - "reason"?: "shared-digest"; - /** it is gone */ - "removed": boolean; - /** the other tags still naming its digest, when kept */ - "tags"?: string[]; -} - -/** is the body of the event published. */ -export type PublishedEvent = Outcome; - -/** is the body of the event refused. */ -export type RefusedEvent = Outcome; - -/** is the body of the event removed. */ -export type RemovedEvent = Outcome; - -/** is the body of the event kept. */ -export type KeptEvent = Outcome; - diff --git a/ts/seats/gopackageregistry/tsconfig.json b/ts/seats/gopackageregistry/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/ts/seats/gopackageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/ts/seats/meshdeliverychecker/package-lock.json b/ts/seats/meshdeliverychecker/package-lock.json deleted file mode 100644 index 9caec88..0000000 --- a/ts/seats/meshdeliverychecker/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-contracts-mesh-delivery-checker", - "packages": { - "": { - "name": "@novox/mesh-contracts-mesh-delivery-checker", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/ts/seats/meshdeliverychecker/package.json b/ts/seats/meshdeliverychecker/package.json deleted file mode 100644 index d732a5b..0000000 --- a/ts/seats/meshdeliverychecker/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat mesh-delivery-checker, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-contracts-mesh-delivery-checker", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/ts/seats/meshdeliverychecker/src/index.ts b/ts/seats/meshdeliverychecker/src/index.ts deleted file mode 100644 index bdd9486..0000000 --- a/ts/seats/meshdeliverychecker/src/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "mesh-delivery-checker", defined whole (seats/mesh-delivery-checker/seat.json): The merge check's tries: start one, read one, end one. The checker's own events stay under its own subjects (ADR 0620 §2). - -export const seat = "mesh-delivery-checker"; - -export const verbs = { - try: "try", - tryRead: "try-read", - tryEnd: "try-end", -} as const; - -export const holderEvents = { - verdict: "verdict", - tagWanted: "tag-wanted", - previewWritten: "preview.written", -} as const; - -export function verdictHolderSubject(holder: string): string { - return "mesh.stream." + holder + ".event.verdict"; -} - -export function tagWantedHolderSubject(holder: string): string { - return "mesh.stream." + holder + ".event.tag-wanted"; -} - -export function previewWrittenHolderSubject(holder: string): string { - return "mesh.stream." + holder + ".event.preview.written"; -} - -/** is what try takes. */ -export interface TryInput { - /** the full commit to try, 40 or 64 hex digits */ - "head": string; - /** unit or integration: only this level; both when absent */ - "level"?: string; - /** / */ - "repository": string; - /** a go test -run pattern: only those tests run, and nothing is kept */ - "tests"?: string; - /** the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent */ - "trunk"?: string; -} - -/** is what try answers. */ -export interface TryAnswer { - /** the try's id */ - "id": string; - /** its state */ - "state"?: string; -} - -/** is what try-read takes. */ -export interface TryReadInput { - /** the try's id, try- */ - "id": string; -} - -/** is what try-read answers. */ -export type TryReadAnswer = Record; - -/** is what try-end takes. */ -export interface TryEndInput { - /** the try's id, try- */ - "id": string; - /** why it is ended, in words */ - "why": string; -} - -/** is what try-end answers. */ -export interface TryEndAnswer { - /** it is ended */ - "ended": boolean; - /** the try */ - "id": string; -} - -export interface VerdictHolderEventSeriesItem { - /** its artifact, or the package's ecosystem name as the module's packages name it */ - "artifact": string; - /** the series on trunk */ - "from": string; - /** the module whose artifact's series changes */ - "module": string; - /** the series the checked head gives it */ - "to": string; -} - -/** is the body of the holder's event verdict. */ -export interface VerdictHolderEvent { - /** the commit checked */ - "head": string; - /** the generated files out of step with their seat definitions, each path (design 26) */ - "out-of-step"?: string[]; - /** the pull request's number, for a pull request's check */ - "pull"?: number; - /** owner/repo */ - "repository": string; - /** each change of an artifact's series the checked head makes against trunk's manifests, once each (design 32); absent when it changes none */ - "series"?: VerdictHolderEventSeriesItem[]; - /** pass or fail */ - "verdict": string; -} - -/** is the body of the holder's event tag-wanted. */ -export interface TagWantedHolderEvent { - /** the pull request's branch: its tag is removed when the branch ends */ - "branch": string; - /** its head */ - "head": string; - /** the group member */ - "module": string; - /** the package it declares */ - "package": string; - /** the pull request */ - "pull"?: number; - /** owner/repo */ - "repository": string; - /** the tag wanted */ - "tag"?: string; -} - diff --git a/ts/seats/meshdeliverychecker/tsconfig.json b/ts/seats/meshdeliverychecker/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/ts/seats/meshdeliverychecker/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/ts/seats/npmpackageregistry/package-lock.json b/ts/seats/npmpackageregistry/package-lock.json deleted file mode 100644 index 642a03f..0000000 --- a/ts/seats/npmpackageregistry/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-contracts-npm-package-registry", - "packages": { - "": { - "name": "@novox/mesh-contracts-npm-package-registry", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/ts/seats/npmpackageregistry/package.json b/ts/seats/npmpackageregistry/package.json deleted file mode 100644 index d864fb3..0000000 --- a/ts/seats/npmpackageregistry/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat npm-package-registry, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-contracts-npm-package-registry", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/ts/seats/npmpackageregistry/src/index.ts b/ts/seats/npmpackageregistry/src/index.ts deleted file mode 100644 index a704e8b..0000000 --- a/ts/seats/npmpackageregistry/src/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "npm-package-registry", defined whole (seats/npm-package-registry/seat.json): The mesh's npm packages: the npm registry protocol's read side, and publishing over the bus. - -import type * as registry from "./parts/registry.js"; - -export const seat = "npm-package-registry"; - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -export const family = { name: "npm-package-registry.events", maxAgeDays: 7 } as const; - -export const publishedSubject = "mesh.seat.npm-package-registry.event.published"; - -export const refusedSubject = "mesh.seat.npm-package-registry.event.refused"; - -export const removedSubject = "mesh.seat.npm-package-registry.event.removed"; - -export const keptSubject = "mesh.seat.npm-package-registry.event.kept"; - -export type Artifact = registry.Artifact; -export type KeptEvent = registry.KeptEvent; -export type Outcome = registry.Outcome; -export type Part = registry.Part; -export type PublishAnswer = registry.PublishAnswer; -export type PublishInput = registry.PublishInput; -export type PublishPartAnswer = registry.PublishPartAnswer; -export type PublishPartInput = registry.PublishPartInput; -export type PublishedEvent = registry.PublishedEvent; -export type RefusedEvent = registry.RefusedEvent; -export type RemoveAnswer = registry.RemoveAnswer; -export type RemoveInput = registry.RemoveInput; -export type RemovedEvent = registry.RemovedEvent; -export type VersionsAnswer = registry.VersionsAnswer; -export type VersionsInput = registry.VersionsInput; - -/** is the form of the provision npm-package-registry. */ -export interface NpmPackageRegistryForm { - /** the read side's address */ - "url": string; -} - diff --git a/ts/seats/npmpackageregistry/src/parts/registry.ts b/ts/seats/npmpackageregistry/src/parts/registry.ts deleted file mode 100644 index 17a8de4..0000000 --- a/ts/seats/npmpackageregistry/src/parts/registry.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -/** one published thing, as every registry event and answer names it */ -export interface Artifact { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** the body of published, refused and removed */ -export interface Outcome { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** the module holding the seat that said it */ - "holder": string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ - "module"?: string; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ - "reason"?: string; - /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ - "reference"?: string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** for kept: the other tags still naming its digest */ - "tags"?: string[]; - /** the transfer it ends, when it ended one */ - "transfer"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** one part of a transfer */ -export interface Part { - /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ - "digest": string; - /** its size in bytes */ - "size": number; -} - -/** is what publish takes. */ -export interface PublishInput { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** every part's digest and size; none when the digest is already held */ - "parts"?: Part[]; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the transfer's name, the caller's own; the same name opens the same transfer again */ - "transfer": string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** is what publish answers. */ -export interface PublishAnswer { - /** the digest is held already: nothing to send; the outcome follows as an event */ - "held"?: boolean; - /** the digests of the parts the holder lacks, to send by publish-part */ - "lacks"?: string[]; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part takes. */ -export interface PublishPartInput { - /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ - "data": string; - /** this chunk is the part's last */ - "last": boolean; - /** the part's digest */ - "part": string; - /** the chunk's place in its part, from 0 */ - "sequence": number; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part answers. */ -export interface PublishPartAnswer { - /** the part is whole and its bytes match its digest */ - "complete"?: boolean; - /** the part */ - "part": string; - /** the chunks of the part held so far */ - "received"?: number; - /** the transfer */ - "transfer": string; -} - -/** is what versions takes. */ -export interface VersionsInput { - /** narrowed to one kind (optional) */ - "kind"?: string; - /** the name */ - "name": string; -} - -/** is what versions answers. */ -export interface VersionsAnswer { - /** the name */ - "name": string; - /** newest first */ - "versions": Artifact[]; -} - -/** is what remove takes. */ -export interface RemoveInput { - /** the digest (for a bundle, archive or copied base) */ - "digest"?: string; - /** the name */ - "name": string; - /** why it goes */ - "reason": "tag-gone" | "retention"; - /** the tag (for a tagged version) */ - "tag"?: string; - /** the version */ - "version"?: string; -} - -/** is what remove answers. */ -export interface RemoveAnswer { - "artifact"?: Artifact; - /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ - "kept"?: boolean; - /** why it was kept: shared-digest, another tag names its digest */ - "reason"?: "shared-digest"; - /** it is gone */ - "removed": boolean; - /** the other tags still naming its digest, when kept */ - "tags"?: string[]; -} - -/** is the body of the event published. */ -export type PublishedEvent = Outcome; - -/** is the body of the event refused. */ -export type RefusedEvent = Outcome; - -/** is the body of the event removed. */ -export type RemovedEvent = Outcome; - -/** is the body of the event kept. */ -export type KeptEvent = Outcome; - diff --git a/ts/seats/npmpackageregistry/tsconfig.json b/ts/seats/npmpackageregistry/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/ts/seats/npmpackageregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/ts/seats/ociartifactregistry/package-lock.json b/ts/seats/ociartifactregistry/package-lock.json deleted file mode 100644 index a1f63b3..0000000 --- a/ts/seats/ociartifactregistry/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-contracts-oci-artifact-registry", - "packages": { - "": { - "name": "@novox/mesh-contracts-oci-artifact-registry", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/ts/seats/ociartifactregistry/package.json b/ts/seats/ociartifactregistry/package.json deleted file mode 100644 index e8c251f..0000000 --- a/ts/seats/ociartifactregistry/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat oci-artifact-registry, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-contracts-oci-artifact-registry", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/ts/seats/ociartifactregistry/src/index.ts b/ts/seats/ociartifactregistry/src/index.ts deleted file mode 100644 index ebf6242..0000000 --- a/ts/seats/ociartifactregistry/src/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "oci-artifact-registry", defined whole (seats/oci-artifact-registry/seat.json): The mesh's bundles and archives, by digest, in the one artifact store: the OCI distribution protocol's read side. - -import type * as registry from "./parts/registry.js"; - -export const seat = "oci-artifact-registry"; - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", - hold: "hold", - release: "release", - list: "list", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", - held: "held", - released: "released", -} as const; - -export const family = { name: "oci-artifact-registry.events", maxAgeDays: 7 } as const; - -export const publishedSubject = "mesh.seat.oci-artifact-registry.event.published"; - -export const refusedSubject = "mesh.seat.oci-artifact-registry.event.refused"; - -export const removedSubject = "mesh.seat.oci-artifact-registry.event.removed"; - -export const keptSubject = "mesh.seat.oci-artifact-registry.event.kept"; - -export const heldSubject = "mesh.seat.oci-artifact-registry.event.held"; - -export const releasedSubject = "mesh.seat.oci-artifact-registry.event.released"; - -export type Artifact = registry.Artifact; -export type KeptEvent = registry.KeptEvent; -export type Outcome = registry.Outcome; -export type Part = registry.Part; -export type PublishAnswer = registry.PublishAnswer; -export type PublishInput = registry.PublishInput; -export type PublishPartAnswer = registry.PublishPartAnswer; -export type PublishPartInput = registry.PublishPartInput; -export type PublishedEvent = registry.PublishedEvent; -export type RefusedEvent = registry.RefusedEvent; -export type RemoveAnswer = registry.RemoveAnswer; -export type RemoveInput = registry.RemoveInput; -export type RemovedEvent = registry.RemovedEvent; -export type VersionsAnswer = registry.VersionsAnswer; -export type VersionsInput = registry.VersionsInput; - -/** the body of held and released */ -export interface Hold { - /** when, RFC 3339 */ - "at"?: string; - /** the caller the holder proved */ - "by"?: string; - /** the digest */ - "digest": string; - /** the module holding the seat */ - "holder": string; - /** the name */ - "name": string; - /** why */ - "reason"?: string; -} - -export interface Listed { - /** for a manifest keeping one bundle or archive, the digest of that blob: what a reference names it by */ - "artifact"?: string; - /** the manifest's digest, sha256: */ - "digest": string; - /** a hold keeps it from retention */ - "held": boolean; - /** it is an index, naming a manifest per platform */ - "index": boolean; - /** for an index, the digests of the manifests it names */ - "platforms"?: string[]; - /** its bytes: its own content and every blob it names */ - "size": number; - /** the tags naming it, none for an untagged one */ - "tags"?: string[]; -} - -export interface Repository { - /** each manifest it holds, by digest */ - "manifests": Listed[]; - /** the repository */ - "name": string; -} - -/** is what hold takes. */ -export interface HoldInput { - /** the digest */ - "digest": string; - /** the name */ - "name": string; - /** which of the sweep's rules keeps it */ - "reason": string; -} - -/** is what hold answers. */ -export interface HoldAnswer { - /** it is held */ - "held": boolean; - /** when the hold began, RFC 3339 */ - "since"?: string; -} - -/** is what release takes. */ -export interface ReleaseInput { - /** the digest */ - "digest": string; - /** the name */ - "name": string; - /** why it is let go */ - "reason": string; -} - -/** is what release answers. */ -export interface ReleaseAnswer { - /** a hold was let go; false when there was none */ - "released": boolean; -} - -/** is what list takes. */ -export interface ListInput { - /** the repository, /; every repository of the seat when not given */ - "name"?: string; -} - -/** is what list answers. */ -export interface ListAnswer { - /** each repository named, in order of its name */ - "repositories": Repository[]; -} - -/** is the body of the event held. */ -export type HeldEvent = Hold; - -/** is the body of the event released. */ -export type ReleasedEvent = Hold; - diff --git a/ts/seats/ociartifactregistry/src/parts/registry.ts b/ts/seats/ociartifactregistry/src/parts/registry.ts deleted file mode 100644 index 17a8de4..0000000 --- a/ts/seats/ociartifactregistry/src/parts/registry.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The part "registry" several seats include whole: The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved. - -export const verbs = { - publish: "publish", - publishPart: "publish-part", - versions: "versions", - remove: "remove", -} as const; - -export const events = { - published: "published", - refused: "refused", - removed: "removed", - kept: "kept", -} as const; - -/** one published thing, as every registry event and answer names it */ -export interface Artifact { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** the body of published, refused and removed */ -export interface Outcome { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** the module holding the seat that said it */ - "holder": string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact */ - "module"?: string; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest) */ - "reason"?: string; - /** the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9) */ - "reference"?: string; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** for kept: the other tags still naming its digest */ - "tags"?: string[]; - /** the transfer it ends, when it ended one */ - "transfer"?: string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** one part of a transfer */ -export interface Part { - /** the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob */ - "digest": string; - /** its size in bytes */ - "size": number; -} - -/** is what publish takes. */ -export interface PublishInput { - /** the source commit it was built from */ - "commit"?: string; - /** its digest, sha256: (a Go package's h1: hash beside it in the version's record) */ - "digest": string; - /** the build's fingerprint */ - "fingerprint"?: string; - /** what it is */ - "kind": "package" | "image" | "bundle" | "archive" | "upstream"; - /** the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk) */ - "name": string; - /** every part's digest and size; none when the digest is already held */ - "parts"?: Part[]; - /** the tag it carries, for a tagged version */ - "tag"?: string; - /** the transfer's name, the caller's own; the same name opens the same transfer again */ - "transfer": string; - /** the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone */ - "version"?: string; -} - -/** is what publish answers. */ -export interface PublishAnswer { - /** the digest is held already: nothing to send; the outcome follows as an event */ - "held"?: boolean; - /** the digests of the parts the holder lacks, to send by publish-part */ - "lacks"?: string[]; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part takes. */ -export interface PublishPartInput { - /** the chunk's bytes, base64 (at most 760 KiB before encoding) */ - "data": string; - /** this chunk is the part's last */ - "last": boolean; - /** the part's digest */ - "part": string; - /** the chunk's place in its part, from 0 */ - "sequence": number; - /** the transfer */ - "transfer": string; -} - -/** is what publish-part answers. */ -export interface PublishPartAnswer { - /** the part is whole and its bytes match its digest */ - "complete"?: boolean; - /** the part */ - "part": string; - /** the chunks of the part held so far */ - "received"?: number; - /** the transfer */ - "transfer": string; -} - -/** is what versions takes. */ -export interface VersionsInput { - /** narrowed to one kind (optional) */ - "kind"?: string; - /** the name */ - "name": string; -} - -/** is what versions answers. */ -export interface VersionsAnswer { - /** the name */ - "name": string; - /** newest first */ - "versions": Artifact[]; -} - -/** is what remove takes. */ -export interface RemoveInput { - /** the digest (for a bundle, archive or copied base) */ - "digest"?: string; - /** the name */ - "name": string; - /** why it goes */ - "reason": "tag-gone" | "retention"; - /** the tag (for a tagged version) */ - "tag"?: string; - /** the version */ - "version"?: string; -} - -/** is what remove answers. */ -export interface RemoveAnswer { - "artifact"?: Artifact; - /** it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags) */ - "kept"?: boolean; - /** why it was kept: shared-digest, another tag names its digest */ - "reason"?: "shared-digest"; - /** it is gone */ - "removed": boolean; - /** the other tags still naming its digest, when kept */ - "tags"?: string[]; -} - -/** is the body of the event published. */ -export type PublishedEvent = Outcome; - -/** is the body of the event refused. */ -export type RefusedEvent = Outcome; - -/** is the body of the event removed. */ -export type RemovedEvent = Outcome; - -/** is the body of the event kept. */ -export type KeptEvent = Outcome; - diff --git a/ts/seats/ociartifactregistry/tsconfig.json b/ts/seats/ociartifactregistry/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/ts/seats/ociartifactregistry/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} -- 2.54.0 From ed7cf776597efc6b1b4c7514bdafc394b8ffb322 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:14:57 +0100 Subject: [PATCH 69/94] =?UTF-8?q?The=20Delivery=20domain's=20seats=20leave?= =?UTF-8?q?=20mesh-contracts=20for=20mesh-delivery-contracts:=20the=2013?= =?UTF-8?q?=20definitions,=20the=20registry=20part,=20the=20four=20registr?= =?UTF-8?q?y=20clients,=20the=20checker's=20generated=20packages=20(ADR=20?= =?UTF-8?q?0636=20=C2=A75;=20ADR=200634;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- README.md | 4 +- module.json | 136 -------------- parts/registry.json | 436 -------------------------------------------- seats/index.json | 13 -- 4 files changed, 2 insertions(+), 587 deletions(-) delete mode 100644 parts/registry.json diff --git a/README.md b/README.md index 5d494ab..ec0b10a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ implements its contract, and no holder's code lives here. ## Where a seat's code goes - **The definition**, written by hand: `seats//seat.json`, its fixtures in `seats//fixtures/`, and the - parts several seats include whole in `parts/` (the four registry seats' verbs and events are `parts/registry.json`). + parts several seats include whole in `parts/` (none here now: the registry seats' part is mesh-delivery-contracts'). mesh-sdk's `seatdef` is the one reader of the format; the indexer and the generators use it. - **A state**, in the definition's `states` (ADR 0608 §1, ADR 0634 §6): `{name, template, writer, key-forms, history, max-age-days, description}`. `template` is one of ADR 0595's six (`state`, `events`, `work-queue`, `log`, `objects`, @@ -27,7 +27,7 @@ implements its contract, and no holder's code lives here. - **A seat's client**, written by hand: `go/clients/client`, a Go module of its own, only where two or more callers share logic over the seat (ADR 0625 §4, ADR 0627 §1): operator-channel's (checking an ask, its digest, a warrant's, and an asking module's keeping of its asks), node-engine's (checking compose-candidate's ask - and answer) and the four registry seats' (publishing a file in parts). A client imports its own seat's contract and mesh-sdk's runtime, never another seat's client, and names + and answer). A client imports its own seat's contract and mesh-sdk's runtime, never another seat's client, and names no module's subject or tool. - **Not here**: the value layouts of seats not yet defined whole (they stay in mesh-sdk until their domain moves, ADR 0625 §9), and a role only one module could ever play (that module's own tools and events, ADR 0625 §8). diff --git a/module.json b/module.json index ca42cfb..ca90d2c 100644 --- a/module.json +++ b/module.json @@ -4,24 +4,6 @@ "slug": "contracts", "build": { "artifacts": [ - { - "name": "go-containerimageregistryclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/containerimageregistryclient" - ], - "series": "0.1" - }, - { - "name": "go-gopackageregistryclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/gopackageregistryclient" - ], - "series": "0.1" - }, { "name": "go-nodeengineclient", "kind": "package", @@ -31,44 +13,6 @@ ], "series": "0.1" }, - { - "name": "go-npmpackageregistryclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/npmpackageregistryclient" - ], - "series": "0.1" - }, - { - "name": "go-ociartifactregistryclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/ociartifactregistryclient" - ], - "series": "0.1" - }, - { - "name": "go-containerimageregistry", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/containerimageregistry" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "go-gopackageregistry", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/gopackageregistry" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "go-meshbroker", "kind": "package", @@ -89,16 +33,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "go-meshdeliverychecker", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/meshdeliverychecker" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "go-nodeengine", "kind": "package", @@ -109,46 +43,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "go-npmpackageregistry", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/npmpackageregistry" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "go-ociartifactregistry", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/ociartifactregistry" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "ts-containerimageregistry", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/containerimageregistry" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "ts-gopackageregistry", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/gopackageregistry" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "ts-meshbroker", "kind": "package", @@ -169,16 +63,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "ts-meshdeliverychecker", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/meshdeliverychecker" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "ts-nodeengine", "kind": "package", @@ -188,26 +72,6 @@ ], "series": "0.1", "generated-from": "." - }, - { - "name": "ts-npmpackageregistry", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/npmpackageregistry" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "ts-ociartifactregistry", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/ociartifactregistry" - ], - "series": "0.1", - "generated-from": "." } ] }, diff --git a/parts/registry.json b/parts/registry.json deleted file mode 100644 index ca0022f..0000000 --- a/parts/registry.json +++ /dev/null @@ -1,436 +0,0 @@ -{ - "format": 1, - "part": "registry", - "description": "The verbs and events every registry seat shares, written once so the four cannot drift (design 51, design 26). Who may call each verb is the holder's trusted config value listing its callers (ADR 0363's form), checked against the caller the tool runner proved.", - "verbs": [ - { - "name": "publish", - "description": "Open a transfer, naming the artifact and the digest of every part; answers the digests the holder lacks, which the caller then sends by publish-part. A transfer already open answers the parts held, so a cut transfer goes on from there. With no parts and a version, the held digest is published under that version and nothing is stored (the trunk's claim).", - "callers": "the build modules", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" - }, - "version": { - "type": "string", - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" - }, - "kind": { - "type": "string", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "description": "what it is" - }, - "tag": { - "type": "string", - "description": "the tag it carries, for a tagged version" - }, - "digest": { - "type": "string", - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" - }, - "fingerprint": { - "type": "string", - "description": "the build's fingerprint" - }, - "commit": { - "type": "string", - "description": "the source commit it was built from" - }, - "transfer": { - "type": "string", - "description": "the transfer's name, the caller's own; the same name opens the same transfer again" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/defs/Part" - }, - "description": "every part's digest and size; none when the digest is already held" - } - }, - "required": [ - "transfer", - "name", - "kind", - "digest" - ] - }, - "answer": { - "type": "object", - "properties": { - "transfer": { - "type": "string", - "description": "the transfer" - }, - "lacks": { - "type": "array", - "items": { - "type": "string" - }, - "description": "the digests of the parts the holder lacks, to send by publish-part" - }, - "held": { - "type": "boolean", - "description": "the digest is held already: nothing to send; the outcome follows as an event" - } - }, - "required": [ - "transfer" - ] - }, - "optional": true - }, - { - "name": "publish-part", - "description": "Send one chunk of one part of an open transfer; the last chunk of the last part ends the transfer, and the holder hashes what it received, asks builder_versions, stores it and says published or refused. A chunk's message stays under the bus's largest message (1 MiB), so at most 760 KiB of data, base64.", - "callers": "the build modules, and only the proved caller that opened the transfer", - "input": { - "type": "object", - "properties": { - "transfer": { - "type": "string", - "description": "the transfer" - }, - "part": { - "type": "string", - "description": "the part's digest" - }, - "sequence": { - "type": "integer", - "description": "the chunk's place in its part, from 0" - }, - "last": { - "type": "boolean", - "description": "this chunk is the part's last" - }, - "data": { - "type": "string", - "description": "the chunk's bytes, base64 (at most 760 KiB before encoding)" - } - }, - "required": [ - "transfer", - "part", - "sequence", - "last", - "data" - ] - }, - "answer": { - "type": "object", - "properties": { - "transfer": { - "type": "string", - "description": "the transfer" - }, - "part": { - "type": "string", - "description": "the part" - }, - "received": { - "type": "integer", - "description": "the chunks of the part held so far" - }, - "complete": { - "type": "boolean", - "description": "the part is whole and its bytes match its digest" - } - }, - "required": [ - "transfer", - "part" - ] - }, - "optional": true - }, - { - "name": "versions", - "description": "The versions the seat holds of one name, newest first: each one's version, tag, digest, kind and when it was published.", - "callers": "any caller whose role grants it", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "kind": { - "type": "string", - "description": "narrowed to one kind (optional)" - } - }, - "required": [ - "name" - ] - }, - "answer": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "versions": { - "type": "array", - "items": { - "$ref": "#/defs/Artifact" - }, - "description": "newest first" - } - }, - "required": [ - "name", - "versions" - ] - }, - "optional": true - }, - { - "name": "remove", - "description": "Remove one tagged version; the holder refuses an untagged one but for the retention removal of a bundle, archive or copied base by the artifact sweep's holder. Every removal emits removed with its reason. A tagged version the store cannot remove alone, because another tag names its digest, stays whole, its tag and bytes kept: the answer says kept with the reason shared-digest and the tags still naming the digest, removed false, and kept is emitted, never removed; the caller ends there without a retry.", - "callers": "the builder, for a tagged version; the artifact sweep's holder, for retention", - "input": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the name" - }, - "version": { - "type": "string", - "description": "the version" - }, - "tag": { - "type": "string", - "description": "the tag (for a tagged version)" - }, - "digest": { - "type": "string", - "description": "the digest (for a bundle, archive or copied base)" - }, - "reason": { - "type": "string", - "enum": [ - "tag-gone", - "retention" - ], - "description": "why it goes" - } - }, - "required": [ - "name", - "reason" - ] - }, - "answer": { - "type": "object", - "properties": { - "removed": { - "type": "boolean", - "description": "it is gone" - }, - "kept": { - "type": "boolean", - "description": "it stays whole, its tag and its bytes: the store cannot remove the tag alone while another tag names its digest (reason, tags)" - }, - "reason": { - "type": "string", - "enum": [ - "shared-digest" - ], - "description": "why it was kept: shared-digest, another tag names its digest" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "the other tags still naming its digest, when kept" - }, - "artifact": { - "$ref": "#/defs/Artifact" - } - }, - "required": [ - "removed" - ] - }, - "optional": true - } - ], - "events": [ - { - "name": "published", - "body": { - "$ref": "#/defs/Outcome" - } - }, - { - "name": "refused", - "body": { - "$ref": "#/defs/Outcome" - } - }, - { - "name": "removed", - "body": { - "$ref": "#/defs/Outcome" - } - }, - { - "name": "kept", - "body": { - "$ref": "#/defs/Outcome" - } - } - ], - "defs": { - "Artifact": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" - }, - "version": { - "type": "string", - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" - }, - "kind": { - "type": "string", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "description": "what it is" - }, - "tag": { - "type": "string", - "description": "the tag it carries, for a tagged version" - }, - "digest": { - "type": "string", - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" - }, - "fingerprint": { - "type": "string", - "description": "the build's fingerprint" - }, - "commit": { - "type": "string", - "description": "the source commit it was built from" - } - }, - "required": [ - "name", - "kind", - "digest" - ], - "description": "one published thing, as every registry event and answer names it" - }, - "Part": { - "type": "object", - "properties": { - "digest": { - "type": "string", - "description": "the part's digest, sha256:: an image's layer or config, a package's file, a bundle's blob" - }, - "size": { - "type": "integer", - "description": "its size in bytes" - } - }, - "required": [ - "digest", - "size" - ], - "description": "one part of a transfer" - }, - "Outcome": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "the package's, image's, bundle's or archive's name; a package's is its name in its own language (design 32): a Go package's the go.mod module path (git.novox.be/novox/mesh-sdk/go), an npm package's package.json's name (@novox/mesh-sdk)" - }, - "module": { - "type": "string", - "description": "the Go module path a Go package is fetched under, equal to name for a Go package (go-package-registry's published); empty for every other kind of artifact" - }, - "version": { - "type": "string", - "description": "the version the builder gave it (design 51 *The versions*); empty for a bundle or archive published by its digest alone" - }, - "kind": { - "type": "string", - "enum": [ - "package", - "image", - "bundle", - "archive", - "upstream" - ], - "description": "what it is" - }, - "tag": { - "type": "string", - "description": "the tag it carries, for a tagged version" - }, - "digest": { - "type": "string", - "description": "its digest, sha256: (a Go package's h1: hash beside it in the version's record)" - }, - "fingerprint": { - "type": "string", - "description": "the build's fingerprint" - }, - "commit": { - "type": "string", - "description": "the source commit it was built from" - }, - "holder": { - "type": "string", - "description": "the module holding the seat that said it" - }, - "transfer": { - "type": "string", - "description": "the transfer it ends, when it ended one" - }, - "reference": { - "type": "string", - "description": "the reference the seat answers: //@sha256:, or @ for a package (ADR 0605 §9)" - }, - "reason": { - "type": "string", - "description": "why: a refusal's cause, a removal's (retention, tag-gone, by-hand), or why a removal was kept (shared-digest)" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "for kept: the other tags still naming its digest" - } - }, - "required": [ - "name", - "kind", - "digest", - "holder" - ], - "description": "the body of published, refused and removed" - } - } -} diff --git a/seats/index.json b/seats/index.json index 00b0a20..342ca73 100644 --- a/seats/index.json +++ b/seats/index.json @@ -4,19 +4,6 @@ "mesh-store", "mesh-broker", "mesh-config", - "mesh-artifact-store", - "mesh-catalog", - "mesh-delivery", - "mesh-delivery-checker", - "mesh-artifact-sweep", - "npm-package-registry", - "go-package-registry", - "container-image-registry", - "oci-artifact-registry", - "git", - "node-build-go", - "node-build-node", - "node-build-image", "node-service-manager", "node-package-manager", "node-container-runtime", -- 2.54.0 From 82b02ff099b2fe3eade36501e88e387f46a954c0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:14:57 +0100 Subject: [PATCH 70/94] =?UTF-8?q?The=20Connectivity=20folder=20leaves=20me?= =?UTF-8?q?sh-contracts=20for=20mesh-connectivity-contracts,=20which=20hol?= =?UTF-8?q?ds=20the=20same=20seven=20definitions=20(ADR=200636=20=C2=A75,?= =?UTF-8?q?=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- connectivity/module.json | 38 -------- .../seats/mesh-dns-resolver/seat.json | 10 --- connectivity/seats/node-hostname/seat.json | 74 --------------- .../seats/node-intrusion-prevention/seat.json | 90 ------------------- .../seats/node-packet-filter/seat.json | 63 ------------- connectivity/seats/node-resolver/seat.json | 78 ---------------- connectivity/seats/node-uplink/seat.json | 34 ------- .../seats/the-private-network/seat.json | 19 ---- 8 files changed, 406 deletions(-) delete mode 100644 connectivity/module.json delete mode 100644 connectivity/seats/mesh-dns-resolver/seat.json delete mode 100644 connectivity/seats/node-hostname/seat.json delete mode 100644 connectivity/seats/node-intrusion-prevention/seat.json delete mode 100644 connectivity/seats/node-packet-filter/seat.json delete mode 100644 connectivity/seats/node-resolver/seat.json delete mode 100644 connectivity/seats/node-uplink/seat.json delete mode 100644 connectivity/seats/the-private-network/seat.json diff --git a/connectivity/module.json b/connectivity/module.json deleted file mode 100644 index 5581790..0000000 --- a/connectivity/module.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "module": "mesh-connectivity-contracts", - "version": "1", - "build": { - "artifacts": [ - { - "name": "go-theprivatenetwork", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/theprivatenetwork" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "go-theprivatenetworkclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/theprivatenetworkclient" - ], - "series": "0.1" - }, - { - "name": "ts-theprivatenetwork", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/theprivatenetwork" - ], - "series": "0.1", - "generated-from": "." - } - ] - }, - "resources": [] -} diff --git a/connectivity/seats/mesh-dns-resolver/seat.json b/connectivity/seats/mesh-dns-resolver/seat.json deleted file mode 100644 index 0868271..0000000 --- a/connectivity/seats/mesh-dns-resolver/seat.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "format": 1, - "seat": "mesh-dns-resolver", - "scope": "mesh", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0194, ADR 0223", - "delivers": "wildcard-resolution", - "replicated": true -} diff --git a/connectivity/seats/node-hostname/seat.json b/connectivity/seats/node-hostname/seat.json deleted file mode 100644 index 327c4d5..0000000 --- a/connectivity/seats/node-hostname/seat.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "format": 1, - "seat": "node-hostname", - "scope": "node", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0199, ADR 0223", - "verbs": [ - { - "name": "entries", - "description": "Every line of this machine's /etc/hosts, each marked whose it is: the operator's, or the block of the module or tool that writes it.", - "replaces": [ - "cat /etc/hosts", - "getent hosts" - ], - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "add", - "description": "Add one address and its names to the operator's lines of this machine's /etc/hosts — a name for this machine's own programs, not the mesh's.", - "replaces": [ - "edit /etc/hosts", - "HOSTALIASES" - ], - "input": { - "properties": { - "address": { - "description": "the IPv4 or IPv6 address", - "type": "string" - }, - "names": { - "description": "the names for it, separated by spaces", - "type": "string" - } - }, - "required": [ - "address", - "names" - ], - "type": "object" - } - }, - { - "name": "remove", - "description": "Remove one name, or every line of one address, from the operator's lines of this machine's /etc/hosts. A line a module writes is refused, naming the module.", - "replaces": [ - "sed -i /etc/hosts" - ], - "input": { - "properties": { - "name": { - "description": "a host name, or an address to remove every line of", - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - } - } - ], - "paths": [ - { - "path": "/etc/hosts" - }, - { - "path": "/etc/hostname" - } - ] -} diff --git a/connectivity/seats/node-intrusion-prevention/seat.json b/connectivity/seats/node-intrusion-prevention/seat.json deleted file mode 100644 index b2bc355..0000000 --- a/connectivity/seats/node-intrusion-prevention/seat.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "format": 1, - "seat": "node-intrusion-prevention", - "scope": "node", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0121", - "verbs": [ - { - "name": "status", - "description": "Every jail on this machine with how many it is watching and holding now, and the totals since the jail started; one jail's detail when named.", - "replaces": [ - "fail2ban-client status" - ], - "input": { - "properties": { - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "banned", - "description": "Every address banned on this machine right now, with the jail that holds it and when the ban ends.", - "replaces": [ - "fail2ban-client banned" - ], - "input": { - "properties": { - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "ban", - "description": "Ban one address in one jail now, for the jail's ban time — an operator's act on the live ban list, which the mesh never writes itself.", - "replaces": [ - "fail2ban-client set banip" - ], - "input": { - "properties": { - "ip": { - "description": "the address", - "type": "string" - }, - "jail": { - "description": "the jail to hold it", - "type": "string" - } - }, - "required": [ - "ip", - "jail" - ], - "type": "object" - } - }, - { - "name": "unban", - "description": "Let one address go, from one jail or from every jail when none is named.", - "replaces": [ - "fail2ban-client unban", - "fail2ban-client set unbanip" - ], - "input": { - "properties": { - "ip": { - "description": "the address", - "type": "string" - }, - "jail": { - "description": "one jail (optional)", - "type": "string" - } - }, - "required": [ - "ip" - ], - "type": "object" - } - } - ] -} diff --git a/connectivity/seats/node-packet-filter/seat.json b/connectivity/seats/node-packet-filter/seat.json deleted file mode 100644 index 005b50c..0000000 --- a/connectivity/seats/node-packet-filter/seat.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "format": 1, - "seat": "node-packet-filter", - "scope": "node", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0121", - "verbs": [ - { - "name": "rules", - "description": "The packet filter as this machine enforces it now: the nftables ruleset and, where the tool exists, the legacy filter's listings. Narrowed to one table or chain when asked.", - "replaces": [ - "nft list ruleset", - "iptables -L", - "iptables-save" - ], - "input": { - "properties": { - "chain": { - "description": "one chain of that table (optional)", - "type": "string" - }, - "table": { - "description": "one nftables table, as `family name` (optional)", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "reload", - "description": "Load the mesh's own filter again from the file the mesh writes, and answer with the mesh's table as loaded.", - "replaces": [ - "nft -f" - ], - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "remove", - "description": "Remove one rule set the mesh did not write, named exactly as the host reports it (novox/hq ADR 0168) — `chain X (iptables-legacy)` or `table ip6 filter, chain DOCKER-USER`. Refuses the mesh's tables, the runtime's own chains, a built-in chain and an active found firewall's chains. An operator's act, by name, never a flush.", - "replaces": [ - "nft delete", - "iptables -X" - ], - "input": { - "properties": { - "where": { - "description": "the rule set, as `node show` lists it", - "type": "string" - } - }, - "required": [ - "where" - ], - "type": "object" - } - } - ] -} diff --git a/connectivity/seats/node-resolver/seat.json b/connectivity/seats/node-resolver/seat.json deleted file mode 100644 index be8a91f..0000000 --- a/connectivity/seats/node-resolver/seat.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "format": 1, - "seat": "node-resolver", - "scope": "node", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0247", - "verbs": [ - { - "name": "routes", - "description": "What this machine's own resolver sends where: the mesh's resolvers, which answer every name not routed elsewhere, and each link given servers of its own with the domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who wrote it as far as the file says, whether a module took it, and when the resolver's own file was put back.", - "replaces": [ - "resolvectl status", - "resolvectl dns", - "resolvectl domain" - ], - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "route", - "description": "Send these domains, and every name under them, to these servers over this link — and only them: the link is never the machine's default route for names, and the mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away takes its route with it.", - "replaces": [ - "resolvectl dns", - "resolvectl domain", - "resolvectl default-route" - ], - "input": { - "properties": { - "domains": { - "description": "the domains to route there, separated by spaces or commas", - "type": "string" - }, - "link": { - "description": "the network link the servers are reached over, by name (a VPN's tunnel interface)", - "type": "string" - }, - "servers": { - "description": "the servers' addresses, separated by spaces or commas", - "type": "string" - } - }, - "required": [ - "link", - "domains", - "servers" - ], - "type": "object" - } - }, - { - "name": "unroute", - "description": "Take one link's route away: its domains go back to the mesh's resolvers. Nothing changes when the link has none.", - "replaces": [ - "resolvectl revert" - ], - "input": { - "properties": { - "link": { - "description": "the network link, by name", - "type": "string" - } - }, - "required": [ - "link" - ], - "type": "object" - } - } - ], - "paths": [ - { - "path": "/etc/resolv.conf" - } - ] -} diff --git a/connectivity/seats/node-uplink/seat.json b/connectivity/seats/node-uplink/seat.json deleted file mode 100644 index cd59ee5..0000000 --- a/connectivity/seats/node-uplink/seat.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "format": 1, - "seat": "node-uplink", - "scope": "node", - "grain": "seat-list", - "domain": "Connectivity", - "decision": "novox/hq ADR 0117", - "verbs": [ - { - "name": "resolvers", - "description": "The machine's resolver file as it is now: the resolvers, search domains and options it lists, whether it is the file the mesh declares, and when it is not, who wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link in its place, a known writer running.", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - }, - { - "name": "links", - "description": "Every network link on the machine: its state, its addresses, whether the default route leaves through it, and the resolvers and search domains the network manager knows for it — a VPN's tunnel included.", - "optional": true, - "input": { - "properties": {}, - "type": "object" - } - } - ], - "paths": [ - { - "path": "/etc/resolv.conf", - "steps-back-for": "node-resolver" - } - ] -} diff --git a/connectivity/seats/the-private-network/seat.json b/connectivity/seats/the-private-network/seat.json deleted file mode 100644 index bb27936..0000000 --- a/connectivity/seats/the-private-network/seat.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "format": 1, - "seat": "the-private-network", - "scope": "node", - "grain": "whole", - "domain": "Connectivity", - "decision": "novox/hq ADR 0110, ADR 0634, ADR 0635", - "description": "The seat of the private network (ADR 0110): the one network the mesh runs over, which a machine can have only one of; the mesh provides its holder itself and the provision carries no fields.", - "verbs": [], - "provisions": [ - { - "name": "private-network", - "form": { - "type": "object", - "description": "the provision has no fields: it is the machine's reach to the others" - } - } - ] -} -- 2.54.0 From 8c5f85e86bfac99a7c549c3e8c2a15b3e66b1fac Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:16:34 +0100 Subject: [PATCH 71/94] =?UTF-8?q?node-engine=20client:=20the=20node=20key?= =?UTF-8?q?=20of=20the=20have,=20the=20codecs=20of=20the=20health=20report?= =?UTF-8?q?=20and=20of=20the=20data=20items=20(ADR=200634=20=C2=A71,=20?= =?UTF-8?q?=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/have_test.go | 43 +++++++++++++ go/clients/nodeengineclient/health.go | 80 ++++++++++++++++++++++++ go/clients/nodeengineclient/keys.go | 8 +++ 3 files changed, 131 insertions(+) create mode 100644 go/clients/nodeengineclient/health.go diff --git a/go/clients/nodeengineclient/have_test.go b/go/clients/nodeengineclient/have_test.go index e91d135..4b93f33 100644 --- a/go/clients/nodeengineclient/have_test.go +++ b/go/clients/nodeengineclient/have_test.go @@ -105,3 +105,46 @@ func TestHaveNodeCodec(t *testing.T) { t.Error("a record differs by its version") } } + +func TestHealthAndDataCodecs(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + h := seat.HealthByNodeValue{Node: "n", At: at, Fallback: &seat.LoginFallback{Node: "n", From: "old", Why: "w", At: at}} + raw, err := EncodeHealth(h) + if err != nil { + t.Fatal(err) + } + back, err := DecodeHealth(raw) + h.V = keys.Version + if err != nil || !SameHealth(back, h) { + t.Fatalf("round trip: %v %+v", err, back) + } + later := h + later.At = at.Add(time.Hour) + if !SameHealth(h, later) { + t.Error("a report differs by its time of writing") + } + for name, bad := range map[string]seat.HealthByNodeValue{ + "no node": {}, + "another node": {Node: "n", Fallback: &seat.LoginFallback{Node: "m", From: "o", Why: "w", At: at}}, + "no time": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", From: "o", Why: "w"}}, + "no login from": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", Why: "w", At: at}}, + } { + if _, err := EncodeHealth(bad); err == nil { + t.Errorf("%s: encoded", name) + } + } + if _, err := DecodeHealth([]byte(`{"v":2,"node":"n"}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Errorf("version 2: %v", err) + } + d := seat.DataByNodeModuleValue{Node: "n", Module: "m", Digest: digestA} + raw, err = EncodeData(d) + if err != nil || !strings.Contains(string(raw), `"items":[]`) { + t.Fatalf("data: %v %s", err, raw) + } + if _, err := EncodeData(seat.DataByNodeModuleValue{Node: "n", Module: "node"}); err == nil { + t.Error("the module name node keys") + } + if k, err := HaveNodeKey("n"); err != nil || k != "n.node" { + t.Errorf("node key %q %v", k, err) + } +} diff --git a/go/clients/nodeengineclient/health.go b/go/clients/nodeengineclient/health.go new file mode 100644 index 0000000..b893d43 --- /dev/null +++ b/go/clients/nodeengineclient/health.go @@ -0,0 +1,80 @@ +package nodeengineclient + +import ( + "encoding/json" + "fmt" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// A node's own health report (the state health, one key per node, written by that node's engine alone) and the data items +// a module declares as the engine placed them (the state data, `.`): their layouts are the seat's contract +// (seat.HealthByNodeValue, seat.DataByNodeModuleValue); this file is their codec, shared by the engine and its readers. + +// EncodeHealth is h as JSON at the current version; it refuses a report naming no node, and a fall back to the old login +// that names another node, no login fallen back from, no reason or no time. +func EncodeHealth(h seat.HealthByNodeValue) ([]byte, error) { + h.V = keys.Version + if h.Node == "" { + return nil, fmt.Errorf("node-engine: a node's health report names its node") + } + if f := h.Fallback; f != nil && (f.Node != h.Node || f.From == "" || f.Why == "" || f.At.IsZero()) { + return nil, fmt.Errorf("node-engine: a node's fall back to its old login names its node %s, the login fallen back from, why and when", h.Node) + } + return json.Marshal(h) +} + +// DecodeHealth reads a report; it refuses another version (keys.ErrUnknownVersion) and a field the contract does not know. +func DecodeHealth(raw []byte) (seat.HealthByNodeValue, error) { + if err := keys.CheckVersion(raw); err != nil { + return seat.HealthByNodeValue{}, err + } + h, err := seat.DecodeHealthByNodeValue(raw) + if err != nil { + return seat.HealthByNodeValue{}, fmt.Errorf("node-engine: a node's health report is not readable: %w", err) + } + return h, nil +} + +// SameHealth says two reports say the same, whenever each was written. +func SameHealth(a, b seat.HealthByNodeValue) bool { + a.At, b.At = time.Time{}, time.Time{} + return sameJSON(a, b) +} + +// DataKey is a module's key on a node in the state data, `.`, its module part escaped as a have's is. +func DataKey(node, module string) (string, error) { return HaveKey(node, module) } + +// DataNodeKeys is the pattern of every data key of a node, `.*`. +func DataNodeKeys(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return node + ".*", nil +} + +// EncodeData is d as JSON at the current version; it refuses a node or module the key refuses. +func EncodeData(d seat.DataByNodeModuleValue) ([]byte, error) { + d.V = keys.Version + if _, err := DataKey(d.Node, d.Module); err != nil { + return nil, err + } + if d.Items == nil { + d.Items = []seat.PlacedDataItem{} + } + return json.Marshal(d) +} + +// DecodeData reads a module's data items; it refuses another version (keys.ErrUnknownVersion). +func DecodeData(raw []byte) (seat.DataByNodeModuleValue, error) { + if err := keys.CheckVersion(raw); err != nil { + return seat.DataByNodeModuleValue{}, err + } + d, err := seat.DecodeDataByNodeModuleValue(raw) + if err != nil { + return seat.DataByNodeModuleValue{}, fmt.Errorf("node-engine: a module's data items are not readable: %w", err) + } + return d, nil +} diff --git a/go/clients/nodeengineclient/keys.go b/go/clients/nodeengineclient/keys.go index edb9b0a..8876150 100644 --- a/go/clients/nodeengineclient/keys.go +++ b/go/clients/nodeengineclient/keys.go @@ -120,3 +120,11 @@ func ParseComposedKey(key string) (node, module, fingerprintPart string, isNode } return refused(fmt.Errorf("the key has %d parts", len(parts))) } + +// HaveNodeKey is the key, in the state have, of a node's own facts: `.node`. +func HaveNodeKey(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return node + "." + NodeWord, nil +} -- 2.54.0 From 8e51d93c2fad25e4bf14c59a88e86533b0ade2cb Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:19:03 +0100 Subject: [PATCH 72/94] node-engine seat: a node's windows, busy, strays and filters are lists, as the engine writes them (the contract said objects and refused every real value; ADR 0634; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/have_test.go | 13 +++++++++++++ go/seats/nodeengine/contract.go | 8 ++++---- seats/node-engine/seat.json | 20 ++++++++++++++++---- ts/seats/nodeengine/src/index.ts | 8 ++++---- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/go/clients/nodeengineclient/have_test.go b/go/clients/nodeengineclient/have_test.go index 4b93f33..6a408d7 100644 --- a/go/clients/nodeengineclient/have_test.go +++ b/go/clients/nodeengineclient/have_test.go @@ -148,3 +148,16 @@ func TestHealthAndDataCodecs(t *testing.T) { t.Errorf("node key %q %v", k, err) } } + +// The lists a node's facts carry (windows, busy, strays, filters) are arrays on the bus, as the engine has always written +// them; the contract once said objects and refused every real value. +func TestHaveNodeReadsTheListsTheEngineWrites(t *testing.T) { + raw := []byte(`{"v":1,"windows":[{"step":"backup"}],"busy":[{"id":"x"}],"strays":[{"kind":"container"}],"filters":[{"name":"f"}]}`) + n, err := DecodeHaveNode(raw) + if err != nil { + t.Fatal(err) + } + if len(n.Windows) != 1 || len(n.Busy) != 1 || len(n.Strays) != 1 || len(n.Filters) != 1 { + t.Fatalf("lists lost: %+v", n) + } +} diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index b0d565a..26dc404 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -1288,7 +1288,7 @@ const ( // HaveByNodeNodeValue is one value of the state have under the key .node. type HaveByNodeNodeValue struct { - Busy map[string]any `json:"busy,omitempty"` + Busy []map[string]any `json:"busy,omitempty"` // the node's answer to each placement candidate naming it, by proposal id Candidates map[string]CandidateAnswer `json:"candidates,omitempty"` // the digest of the node's own composition it runs @@ -1296,14 +1296,14 @@ type HaveByNodeNodeValue struct { // own, or none (empty) Composes string `json:"composes,omitempty"` ComposesWhy string `json:"composes-why,omitempty"` - Filters map[string]any `json:"filters,omitempty"` + Filters []map[string]any `json:"filters,omitempty"` Profile map[string]any `json:"profile,omitempty"` Record *NodeRecord `json:"record,omitempty"` Sealed []string `json:"sealed,omitempty"` SealedAt map[string]string `json:"sealed-at,omitempty"` - Strays map[string]any `json:"strays,omitempty"` + Strays []map[string]any `json:"strays,omitempty"` V int64 `json:"v"` - Windows map[string]any `json:"windows,omitempty"` + Windows []map[string]any `json:"windows,omitempty"` } // ComposedByNodeModuleFingerprintValue is one value of the state composed under the key ... diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 237a651..49462e9 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -856,16 +856,28 @@ "description": "the digest of the node's own composition it runs" }, "windows": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "busy": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "strays": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "filters": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "profile": { "type": "object" diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index dbc0378..4399ede 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -1043,7 +1043,7 @@ export interface HaveByNodeModuleValue { /** is one value of the state have under the key .node. */ export interface HaveByNodeNodeValue { - "busy"?: Record; + "busy"?: Record[]; /** the node's answer to each placement candidate naming it, by proposal id */ "candidates"?: Record; /** the digest of the node's own composition it runs */ @@ -1051,14 +1051,14 @@ export interface HaveByNodeNodeValue { /** own, or none (empty) */ "composes"?: string; "composes-why"?: string; - "filters"?: Record; + "filters"?: Record[]; "profile"?: Record; "record"?: NodeRecord; "sealed"?: string[]; "sealed-at"?: Record; - "strays"?: Record; + "strays"?: Record[]; "v": number; - "windows"?: Record; + "windows"?: Record[]; } /** is one value of the state composed under the key ... */ -- 2.54.0 From 6d5fe4572d34b1e92dea600668548f36e4cc7004 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:25:57 +0100 Subject: [PATCH 73/94] =?UTF-8?q?Placement=20seat:=20the=20tunnel=20found?= =?UTF-8?q?=20on=20a=20node=20is=20a=20typed=20record,=20not=20a=20free=20?= =?UTF-8?q?object=20(ADR=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../go/seats/meshmoduleassigner/contract.go | 21 ++++++- .../seats/meshmoduleassigner/contract_test.go | 6 ++ .../mesh-module-assigner/fixtures/states.json | 22 ++++++++ .../seats/mesh-module-assigner/seat.json | 56 ++++++++++++++++++- .../ts/seats/meshmoduleassigner/src/index.ts | 21 ++++++- 5 files changed, 123 insertions(+), 3 deletions(-) diff --git a/placement/go/seats/meshmoduleassigner/contract.go b/placement/go/seats/meshmoduleassigner/contract.go index 85f9ec8..10de4c1 100644 --- a/placement/go/seats/meshmoduleassigner/contract.go +++ b/placement/go/seats/meshmoduleassigner/contract.go @@ -343,6 +343,25 @@ func DecodeSeatHoldersBySeatNodeValue(raw []byte) (SeatHoldersBySeatNodeValue, e return v, nil } +// FoundTunnel is a tunnel found on the machine when it was adopted: everything but its private key. +type FoundTunnel struct { + Address string `json:"address,omitempty"` + Config string `json:"config,omitempty"` + Interface string `json:"interface"` + Mtu int64 `json:"mtu,omitempty"` + Peers []FoundTunnelPeer `json:"peers,omitempty"` + Port int64 `json:"port,omitempty"` + PublicKey string `json:"public-key,omitempty"` + Range string `json:"range,omitempty"` + Unit string `json:"unit,omitempty"` +} + +// FoundTunnelPeer is one peer of a found tunnel. +type FoundTunnelPeer struct { + Address string `json:"address,omitempty"` + PublicKey string `json:"public-key"` +} + // Holding is one seat a module placed on the node holds. type Holding struct { Claim string `json:"claim"` @@ -368,7 +387,7 @@ type NodeDecided struct { Site string `json:"site,omitempty"` Taken []string `json:"taken,omitempty"` // the tunnel found on the node when it was adopted - Tunnel map[string]any `json:"tunnel,omitempty"` + Tunnel *FoundTunnel `json:"tunnel,omitempty"` } // PlacedModule is one module placed on a node. diff --git a/placement/go/seats/meshmoduleassigner/contract_test.go b/placement/go/seats/meshmoduleassigner/contract_test.go index 178ad88..a007587 100644 --- a/placement/go/seats/meshmoduleassigner/contract_test.go +++ b/placement/go/seats/meshmoduleassigner/contract_test.go @@ -25,6 +25,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a node that came with a tunnel", subject: "", given: "{\"decided\":{\"tunnel\":{\"interface\":\"wg0\",\"peers\":[{\"address\":\"10.77.0.9/32\",\"public-key\":\"k2\"}],\"port\":51820,\"public-key\":\"k1\"}},\"modules\":[],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"tunnel\":{\"interface\":\"wg0\",\"peers\":[{\"address\":\"10.77.0.9/32\",\"public-key\":\"k2\"}],\"port\":51820,\"public-key\":\"k1\"}},\"modules\":[],\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v PlacementByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "a module's key", subject: "", given: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", wire: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", decode: func(b []byte) (any, error) { var v PlacementByNodeModuleValue diff --git a/placement/seats/mesh-module-assigner/fixtures/states.json b/placement/seats/mesh-module-assigner/fixtures/states.json index 2ccef79..4c176ff 100644 --- a/placement/seats/mesh-module-assigner/fixtures/states.json +++ b/placement/seats/mesh-module-assigner/fixtures/states.json @@ -16,6 +16,28 @@ ] } }, + { + "name": "a node that came with a tunnel", + "of": "state:placement/.node", + "given": { + "v": 1, + "node": "ace", + "decided": { + "tunnel": { + "interface": "wg0", + "port": 51820, + "public-key": "k1", + "peers": [ + { + "public-key": "k2", + "address": "10.77.0.9/32" + } + ] + } + }, + "modules": [] + } + }, { "name": "a module's key", "of": "state:placement/.", diff --git a/placement/seats/mesh-module-assigner/seat.json b/placement/seats/mesh-module-assigner/seat.json index 8e18a70..77108a2 100644 --- a/placement/seats/mesh-module-assigner/seat.json +++ b/placement/seats/mesh-module-assigner/seat.json @@ -514,7 +514,7 @@ } }, "tunnel": { - "type": "object", + "$ref": "#/defs/FoundTunnel", "description": "the tunnel found on the node when it was adopted" }, "peer-names": { @@ -526,6 +526,60 @@ } } }, + "FoundTunnel": { + "type": "object", + "description": "a tunnel found on the machine when it was adopted: everything but its private key", + "properties": { + "interface": { + "type": "string" + }, + "unit": { + "type": "string" + }, + "config": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "mtu": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "range": { + "type": "string" + }, + "public-key": { + "type": "string" + }, + "peers": { + "type": "array", + "items": { + "$ref": "#/defs/FoundTunnelPeer" + } + } + }, + "required": [ + "interface" + ] + }, + "FoundTunnelPeer": { + "type": "object", + "description": "one peer of a found tunnel", + "properties": { + "public-key": { + "type": "string" + }, + "address": { + "type": "string" + } + }, + "required": [ + "public-key" + ] + }, "Placement": { "type": "object", "description": "one node's placement assembled", diff --git a/placement/ts/seats/meshmoduleassigner/src/index.ts b/placement/ts/seats/meshmoduleassigner/src/index.ts index 8f958a4..137274b 100644 --- a/placement/ts/seats/meshmoduleassigner/src/index.ts +++ b/placement/ts/seats/meshmoduleassigner/src/index.ts @@ -178,6 +178,25 @@ export function parseSeatHoldersBySeatNodeKey(key: string): { seat: string; node return { seat, node }; } +/** a tunnel found on the machine when it was adopted: everything but its private key */ +export interface FoundTunnel { + "address"?: string; + "config"?: string; + "interface": string; + "mtu"?: number; + "peers"?: FoundTunnelPeer[]; + "port"?: number; + "public-key"?: string; + "range"?: string; + "unit"?: string; +} + +/** one peer of a found tunnel */ +export interface FoundTunnelPeer { + "address"?: string; + "public-key": string; +} + /** one seat a module placed on the node holds */ export interface Holding { "claim": string; @@ -203,7 +222,7 @@ export interface NodeDecided { "site"?: string; "taken"?: string[]; /** the tunnel found on the node when it was adopted */ - "tunnel"?: Record; + "tunnel"?: FoundTunnel; } /** one module placed on a node */ -- 2.54.0 From 57dd5792340e9e6877954ac4e9e473b3215fa20a Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:27:08 +0100 Subject: [PATCH 74/94] Wants seat: the backup answer of a want is again or skip (ADR 0634; issue 1966) --- placement/go/seats/meshwants/contract.go | 6 ++++++ placement/seats/mesh-wants/seat.json | 6 +++++- placement/ts/seats/meshwants/src/index.ts | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/placement/go/seats/meshwants/contract.go b/placement/go/seats/meshwants/contract.go index fc93351..74860fa 100644 --- a/placement/go/seats/meshwants/contract.go +++ b/placement/go/seats/meshwants/contract.go @@ -286,6 +286,12 @@ type WantsByWantNodeModuleValue struct { Yes string `json:"yes,omitempty"` } +// The values of WantsByWantNodeModuleValue's backup. +const ( + WantsByWantNodeModuleValueBackupAgain = "again" + WantsByWantNodeModuleValueBackupSkip = "skip" +) + // WantsByWantNodeNodeValue is one value of the state wants under the key want..node. type WantsByWantNodeNodeValue struct { Composed string `json:"composed"` diff --git a/placement/seats/mesh-wants/seat.json b/placement/seats/mesh-wants/seat.json index 236ed60..ececc75 100644 --- a/placement/seats/mesh-wants/seat.json +++ b/placement/seats/mesh-wants/seat.json @@ -97,7 +97,11 @@ }, "backup": { "type": "string", - "description": "again or skip" + "description": "again or skip", + "enum": [ + "again", + "skip" + ] }, "words": { "type": "string" diff --git a/placement/ts/seats/meshwants/src/index.ts b/placement/ts/seats/meshwants/src/index.ts index ad3dab8..fe56a7d 100644 --- a/placement/ts/seats/meshwants/src/index.ts +++ b/placement/ts/seats/meshwants/src/index.ts @@ -149,7 +149,7 @@ export type PolicyChangedEvent = Record; /** is one value of the state wants under the key want... */ export interface WantsByWantNodeModuleValue { /** again or skip */ - "backup"?: string; + "backup"?: "again" | "skip"; "composed"?: string; "fp"?: string; "hold"?: boolean; -- 2.54.0 From 54a42db4ec8df8cdd240d858df1183cfb3df10cc Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:28:18 +0100 Subject: [PATCH 75/94] =?UTF-8?q?Module=20assigner=20client:=20the=20place?= =?UTF-8?q?ment=20and=20seat-holders=20states=20by=20name,=20and=20the=20m?= =?UTF-8?q?achines=20a=20placement=20names=20(ADR=200634=20=C2=A74;=20issu?= =?UTF-8?q?e=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../clients/meshmoduleassignerclient/reads.go | 37 +++++++++++++++++++ .../meshmoduleassignerclient/reads_test.go | 22 +++++++++++ 2 files changed, 59 insertions(+) create mode 100644 placement/go/clients/meshmoduleassignerclient/reads.go create mode 100644 placement/go/clients/meshmoduleassignerclient/reads_test.go diff --git a/placement/go/clients/meshmoduleassignerclient/reads.go b/placement/go/clients/meshmoduleassignerclient/reads.go new file mode 100644 index 0000000..99fa9fd --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/reads.go @@ -0,0 +1,37 @@ +package meshmoduleassignerclient + +import ( + "sort" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" +) + +// The holder of the seat and the names a reader of its states declares in its `reads` (`.`) and binds +// to (`_`): named here once, by the seat's state, so no reader builds them from a module's name +// (ADR 0634 §4; the lead's ruling of 2026-11-05). +const ( + // Module is the module that holds the seat mesh-module-assigner. + Module = "mesh-delivery-assigner" + // PlacementRead and SeatHoldersRead are the states `placement` and `seat-holders`, as a manifest's `reads` names them. + PlacementRead = Module + "." + seat.StatePlacement + SeatHoldersRead = Module + "." + seat.StateSeatHolders + // PlacementBucket and SeatHoldersBucket are the states' buckets. + PlacementBucket = Module + "_" + seat.StatePlacement + SeatHoldersBucket = Module + "_" + seat.StateSeatHolders +) + +// MachineNames is every machine the seat places modules on, sorted, from its state `placement` as a reader holds its +// keys: each with a node key `.node` or, in the form before, `placement.`. Module keys and keys of the +// state's other kinds are passed over. +func MachineNames(keys map[string][]byte) []string { + seen := map[string]bool{} + out := []string{} + for k := range keys { + if node, _, kind, err := ParseKey(k); err == nil && kind != ModuleKind && !seen[node] { + seen[node] = true + out = append(out, node) + } + } + sort.Strings(out) + return out +} diff --git a/placement/go/clients/meshmoduleassignerclient/reads_test.go b/placement/go/clients/meshmoduleassignerclient/reads_test.go new file mode 100644 index 0000000..40223bd --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/reads_test.go @@ -0,0 +1,22 @@ +package meshmoduleassignerclient + +import ( + "reflect" + "testing" +) + +func TestTheStatesAreNamedBySeatAndState(t *testing.T) { + for got, want := range map[string]string{PlacementRead: "mesh-delivery-assigner.placement", SeatHoldersRead: "mesh-delivery-assigner.seat-holders", + PlacementBucket: "mesh-delivery-assigner_placement", SeatHoldersBucket: "mesh-delivery-assigner_seat-holders"} { + if got != want { + t.Errorf("got %q, want %q", got, want) + } + } +} + +func TestMachineNamesAreTheNodesAKeyNames(t *testing.T) { + keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "placement.novox": nil, "want.x.y": nil, "bad": nil} + if got, want := MachineNames(keys), []string{"ace", "g14", "novox"}; !reflect.DeepEqual(got, want) { + t.Errorf("MachineNames = %v, want %v", got, want) + } +} -- 2.54.0 From 5f8b8edd20c70441021e32842b50038648d86a02 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:35:17 +0100 Subject: [PATCH 76/94] =?UTF-8?q?node-engine=20client:=20a=20candidate's?= =?UTF-8?q?=20composition=20key=20is=20its=20own,=20over=20mesh-sdk=20keys?= =?UTF-8?q?;=20the=20client=20no=20longer=20imports=20go/delivery=20(ADR?= =?UTF-8?q?=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/client.go | 41 ++++++++++++++++++++-- go/clients/nodeengineclient/client_test.go | 22 ++++++++++-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/go/clients/nodeengineclient/client.go b/go/clients/nodeengineclient/client.go index 0b1939f..b39982e 100644 --- a/go/clients/nodeengineclient/client.go +++ b/go/clients/nodeengineclient/client.go @@ -10,10 +10,11 @@ import ( "errors" "fmt" "regexp" + "strings" "time" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) const ( @@ -56,7 +57,7 @@ var ErrUnknownVersion = errors.New("node-engine: unknown compose-candidate answe var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) func checkAsk(a seat.ComposeCandidateInput) error { - if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + if _, _, err := ParseCompositionKey(a.Candidate); err != nil { return fmt.Errorf("node-engine: a compose-candidate ask names no candidate: %w", err) } return nil @@ -89,7 +90,7 @@ func CheckAnswer(a seat.ComposeCandidateAnswer) error { if a.Node == "" || !nameOK.MatchString(a.Node) { return fmt.Errorf("node-engine: the answer's node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", a.Node) } - if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + if _, _, err := ParseCompositionKey(a.Candidate); err != nil { return err } if a.Took < 0 { @@ -181,3 +182,37 @@ func DecodeAnswer(data []byte) (seat.ComposeCandidateAnswer, error) { } return a, CheckAnswer(a) } + +var heads12 = regexp.MustCompile(`^[0-9a-f]{12}$`) + +// CompositionKey is the key of a candidate the node is asked to compose: the group's check at its heads, `.`, the group escaped as a key part and the heads hash 12 lowercase hex digits (the checker's verdict +// key of the same check). +func CompositionKey(group, headsHash string) (string, error) { + if group == "" { + return "", errors.New("node-engine: the group name is empty") + } + if !heads12.MatchString(headsHash) { + return "", fmt.Errorf("node-engine: %q is not a heads hash, 12 lowercase hex digits", headsHash) + } + return keys.EscapeName(group) + "." + headsHash, nil +} + +// ParseCompositionKey is the inverse of CompositionKey; it refuses a key that is not one, naming it. +func ParseCompositionKey(key string) (group, headsHash string, err error) { + i := strings.LastIndex(key, ".") + if i < 0 { + return "", "", fmt.Errorf("node-engine: %q is not a composition key, .", key) + } + part, headsHash := key[:i], key[i+1:] + if !heads12.MatchString(headsHash) { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash) + } + if part == "" || strings.Contains(part, ".") { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: the group part is empty or holds a dot", key) + } + if group, err = keys.UnescapeName(part); err != nil { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: %w", key, err) + } + return group, headsHash, nil +} diff --git a/go/clients/nodeengineclient/client_test.go b/go/clients/nodeengineclient/client_test.go index 542b207..1586968 100644 --- a/go/clients/nodeengineclient/client_test.go +++ b/go/clients/nodeengineclient/client_test.go @@ -8,13 +8,12 @@ import ( "testing" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could // not take, naming it. func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { - key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab") + key, err := CompositionKey("mesh-sdk#7", "0123456789ab") if err != nil { t.Fatal(err) } @@ -95,3 +94,22 @@ func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { } } } + +// A composition key round-trips its group and heads hash, and refuses what is not one. +func TestCompositionKeyRoundTripsAndRefuses(t *testing.T) { + key, err := CompositionKey("fix/1.2", "0123456789ab") + if err != nil { + t.Fatal(err) + } + if g, h, err := ParseCompositionKey(key); err != nil || g != "fix/1.2" || h != "0123456789ab" { + t.Fatalf("round trip: %q %q %v", g, h, err) + } + for _, bad := range []string{"", "nogroup", "g.0123456789AB", "g.short", ".0123456789ab", "a.b.0123456789ab"} { + if _, _, err := ParseCompositionKey(bad); err == nil { + t.Errorf("%q was taken for a composition key", bad) + } + } + if _, err := CompositionKey("", "0123456789ab"); err == nil { + t.Error("a key with no group was made") + } +} -- 2.54.0 From bb2140c236a59a5585ac6e9f0d1212cf243e5638 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:43:25 +0100 Subject: [PATCH 77/94] =?UTF-8?q?Module=20assigner=20client:=20the=20opera?= =?UTF-8?q?tions=20of=20a=20proposal's=20change=20and=20the=20states=20of?= =?UTF-8?q?=20an=20open=20proposal,=20for=20its=20holder=20and=20for=20the?= =?UTF-8?q?=20enrolment=20that=20builds=20add-node=20(B7f=20finding=204;?= =?UTF-8?q?=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../clients/meshmoduleassignerclient/ops.go | 39 +++++++++++++++++++ .../meshmoduleassignerclient/ops_test.go | 14 +++++++ 2 files changed, 53 insertions(+) create mode 100644 placement/go/clients/meshmoduleassignerclient/ops.go create mode 100644 placement/go/clients/meshmoduleassignerclient/ops_test.go diff --git a/placement/go/clients/meshmoduleassignerclient/ops.go b/placement/go/clients/meshmoduleassignerclient/ops.go new file mode 100644 index 0000000..0c85902 --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/ops.go @@ -0,0 +1,39 @@ +package meshmoduleassignerclient + +// The operations a proposal's change names (novox/hq ADR 0567 §2), the words of a change's `op`. The seat's contract +// carries the field; its callers (the enrolment builds OpAddNode and OpRemoveNode) and its holder share the words here. +const ( + // OpPlace places a module on a node, with its host ports and bindings. + OpPlace = "place" + // OpTakeOff takes a module off a node. + OpTakeOff = "take-off" + // OpPin pins the provider of a provision on a node; OpUnpin removes a pin. + OpPin = "pin" + OpUnpin = "unpin" + // OpHold says a module placed on the node holds a seat; OpUnhold, that it holds it no more. + OpHold = "hold" + OpUnhold = "unhold" + // OpDecide sets a decided field of the node (NodeDecided by its JSON name), its address among them. + OpDecide = "decide" + // OpConfig sets a module's config placement or values on the node. + OpConfig = "config" + // OpSeed is the node's whole placement: the take-over (adopt, §7), never given by a caller. + OpSeed = "seed" + // OpAddNode adds a joining node with its decided fields and the public halves of its keys, on the warrant of the + // enrolment that issued its token (novox/hq ADR 0587 §7): the enroller's alone, the proposal's one change. + OpAddNode = "add-node" + // OpRemoveNode takes a joining node off placement again when its enrolment ends unconfirmed, revoked or expired: the + // enroller's alone, on the same warrant, the proposal's one change, of a node with no module and no holding. + OpRemoveNode = "remove-node" +) + +// The states of an open proposal (the field `state` of the state `proposals`). +const ( + // ProposalAnswering: its candidates are written and the nodes it must hear from have not all answered. + ProposalAnswering = "answering" + // ProposalFailed: its verdict failed; it stays open, for its proposer to read and withdraw, or for a node's later + // answer to judge it again. + ProposalFailed = "failed" + // ProposalAsked: its verdict passed and the operator is asked to approve it. + ProposalAsked = "asked" +) diff --git a/placement/go/clients/meshmoduleassignerclient/ops_test.go b/placement/go/clients/meshmoduleassignerclient/ops_test.go new file mode 100644 index 0000000..d935baa --- /dev/null +++ b/placement/go/clients/meshmoduleassignerclient/ops_test.go @@ -0,0 +1,14 @@ +package meshmoduleassignerclient + +import "testing" + +// The words are the wire's: a holder and a caller that spell one differently refuse each other's proposals. +func TestOpsAndProposalStatesAreTheWire(t *testing.T) { + for got, want := range map[string]string{OpPlace: "place", OpTakeOff: "take-off", OpPin: "pin", OpUnpin: "unpin", OpHold: "hold", + OpUnhold: "unhold", OpDecide: "decide", OpConfig: "config", OpSeed: "seed", OpAddNode: "add-node", OpRemoveNode: "remove-node", + ProposalAnswering: "answering", ProposalFailed: "failed", ProposalAsked: "asked"} { + if got != want { + t.Errorf("%q, want %q", got, want) + } + } +} -- 2.54.0 From d8d319e3ff7a212401704cdf10d7a15cdff11929 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:54:21 +0100 Subject: [PATCH 78/94] =?UTF-8?q?node-engine=20client:=20the=20adoption=20?= =?UTF-8?q?handing's=20two=20ends=20(HandAccounts,=20OpenHanded,=20HandedA?= =?UTF-8?q?ccount,=20KeyFingerprint,=20IsBcrypt)=20over=20the=20seat's=20A?= =?UTF-8?q?ccountsHanded=20(B5f;=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/handed.go | 76 ++++++++++++++++++++++ go/clients/nodeengineclient/handed_test.go | 47 +++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 go/clients/nodeengineclient/handed.go create mode 100644 go/clients/nodeengineclient/handed_test.go diff --git a/go/clients/nodeengineclient/handed.go b/go/clients/nodeengineclient/handed.go new file mode 100644 index 0000000..b8c7490 --- /dev/null +++ b/go/clients/nodeengineclient/handed.go @@ -0,0 +1,76 @@ +package nodeengineclient + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/seal" +) + +// The adoption handing of novox/hq ADR 0547 clause 9 (issue 1945): the node-engine of the node holding mesh-broker +// hands the bcrypt hashes of the bus accounts, in the bus module's have, sealed to the vault's own key (the seat's +// AccountsHanded), so that of every reader of the have state the vault alone can open them. These are the two ends of +// that box, which the node-engine and the vault both hold. + +// HandedAccount is one user of an AccountsHanded box, opened: the bus user and the bcrypt hash the loaded list carries. +type HandedAccount struct { + User string `json:"user"` + Hash string `json:"hash"` +} + +// KeyFingerprint is a public key's fingerprint as a handing names it: `sha256:` of its PEM, without the space +// around it. +func KeyFingerprint(publicPEM string) string { + sum := sha256.Sum256([]byte(strings.TrimSpace(publicPEM))) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// bcryptHash is the form of a bcrypt hash: version, cost, and 53 characters of salt and digest. +var bcryptHash = regexp.MustCompile(`^\$2[abxy]?\$[0-9]{2}\$[./A-Za-z0-9]{53}$`) + +// IsBcrypt says whether s has the form of a bcrypt hash. +func IsBcrypt(s string) bool { return bcryptHash.MatchString(s) } + +// HandAccounts seals the accounts to the vault's public key (SPKI PEM) as the handing of the list whose digest is from. +func HandAccounts(accounts []HandedAccount, vaultPEM, from string, at time.Time) (seat.AccountsHanded, error) { + if strings.TrimSpace(vaultPEM) == "" { + return seat.AccountsHanded{}, errors.New("node-engine: no vault key to seal the handed accounts to") + } + body, err := json.Marshal(accounts) + if err != nil { + return seat.AccountsHanded{}, err + } + box, err := seal.Seal(string(body), vaultPEM) + if err != nil { + return seat.AccountsHanded{}, fmt.Errorf("node-engine: the handed accounts could not be sealed to the vault's key: %w", err) + } + return seat.AccountsHanded{From: from, To: KeyFingerprint(vaultPEM), Users: int64(len(accounts)), + Box: seat.SealedBox{Ct: box.Ct, Eph: box.Eph, Iv: box.IV, Tag: box.Tag, V: int64(box.V)}, At: at.UTC()}, nil +} + +// OpenHanded opens a handing with the vault's private key (PEM), refusing one sealed to another key than publicPEM's +// and a box that does not hold as many users as it says. +func OpenHanded(h seat.AccountsHanded, publicPEM, privatePEM string) ([]HandedAccount, error) { + if h.To != KeyFingerprint(publicPEM) { + return nil, fmt.Errorf("node-engine: the handing is sealed to the key %s, not to this one (%s)", h.To, KeyFingerprint(publicPEM)) + } + plain, err := seal.Open(seal.Box{V: int(h.Box.V), Eph: h.Box.Eph, IV: h.Box.Iv, Tag: h.Box.Tag, Ct: h.Box.Ct}, privatePEM) + if err != nil { + return nil, fmt.Errorf("node-engine: the handing does not open: %w", err) + } + var out []HandedAccount + if err := json.Unmarshal([]byte(plain), &out); err != nil { + return nil, fmt.Errorf("node-engine: the handing holds no accounts: %w", err) + } + if int64(len(out)) != h.Users { + return nil, fmt.Errorf("node-engine: the handing says %d users and holds %d", h.Users, len(out)) + } + return out, nil +} diff --git a/go/clients/nodeengineclient/handed_test.go b/go/clients/nodeengineclient/handed_test.go new file mode 100644 index 0000000..e3cf50b --- /dev/null +++ b/go/clients/nodeengineclient/handed_test.go @@ -0,0 +1,47 @@ +package nodeengineclient + +import ( + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/seal" +) + +// A handing is sealed to the vault's key and opened with it alone; another key, a wrong count and a plain value are refused. +func TestAHandingOpensWithTheVaultsKeyAlone(t *testing.T) { + vault, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + other, err := seal.GenerateKeyPair() + if err != nil { + t.Fatal(err) + } + hash := "$2a$10$" + strings.Repeat("a", 53) + accounts := []HandedAccount{{User: "a", Hash: hash}, {User: "b", Hash: hash}} + h, err := HandAccounts(accounts, vault.PublicKey, "sha256:list", time.Date(2026, 11, 5, 10, 0, 0, 0, time.FixedZone("x", 3600))) + if err != nil { + t.Fatal(err) + } + if h.Users != 2 || h.To != KeyFingerprint(vault.PublicKey) || h.At.Location() != time.UTC { + t.Errorf("handing = %+v", h) + } + got, err := OpenHanded(h, vault.PublicKey, vault.PrivateKey) + if err != nil || len(got) != 2 || got[1].User != "b" { + t.Errorf("opened %v, %v", got, err) + } + if _, err := OpenHanded(h, other.PublicKey, other.PrivateKey); err == nil { + t.Error("a handing sealed to the vault opened for another key") + } + h.Users = 3 + if _, err := OpenHanded(h, vault.PublicKey, vault.PrivateKey); err == nil { + t.Error("a handing that says 3 users and holds 2 was opened") + } + if !IsBcrypt(hash) || IsBcrypt("plain") { + t.Error("IsBcrypt") + } + if _, err := HandAccounts(accounts, " ", "x", time.Now()); err == nil { + t.Error("sealed to no key") + } +} -- 2.54.0 From 9c80664492afbb2b878dfce40dce91f8cf81cf5b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:58:13 +0100 Subject: [PATCH 79/94] =?UTF-8?q?Wants=20client:=20the=20keys,=20check=20a?= =?UTF-8?q?nd=20codec=20of=20a=20want,=20a=20node's=20own=20want=20and=20a?= =?UTF-8?q?=20hold=20over=20the=20seat=20mesh-wants,=20goldens=20from=20go?= =?UTF-8?q?/delivery's=20output=20(B7h;=20ADR=200634=20=C2=A74;=20issue=20?= =?UTF-8?q?1966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- placement/go/clients/meshwantsclient/go.mod | 8 + placement/go/clients/meshwantsclient/wants.go | 326 ++++++++++++++++++ .../go/clients/meshwantsclient/wants_test.go | 162 +++++++++ 3 files changed, 496 insertions(+) create mode 100644 placement/go/clients/meshwantsclient/go.mod create mode 100644 placement/go/clients/meshwantsclient/wants.go create mode 100644 placement/go/clients/meshwantsclient/wants_test.go diff --git a/placement/go/clients/meshwantsclient/go.mod b/placement/go/clients/meshwantsclient/go.mod new file mode 100644 index 0000000..0cc33be --- /dev/null +++ b/placement/go/clients/meshwantsclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 +) diff --git a/placement/go/clients/meshwantsclient/wants.go b/placement/go/clients/meshwantsclient/wants.go new file mode 100644 index 0000000..9f04734 --- /dev/null +++ b/placement/go/clients/meshwantsclient/wants.go @@ -0,0 +1,326 @@ +// Package meshwantsclient is the client of the seat mesh-wants: the keys of its state wants, and the check and the codec +// of a want, a node's own want and a hold, which every module that reads or writes wants shares (the assigner that writes +// them, the node-engine, the provisioner and the artifact store that read them). The layouts are the seat's contract; this +// file writes the version and refuses what the earlier codec refused (novox/hq ADR 0528, ADR 0539, ADR 0546, ADR 0634 §4). +package meshwantsclient + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The three values of the state wants, in the seat's types. +type ( + // Want is a node's want for a module: the fingerprint it should run and the digest of the one composition it allows, + // or Removed, or Self when the node composes it itself; Newest and Rule name what holds the node back. + Want = seat.WantsByWantNodeModuleValue + // NodeWant is a node's own want: the digest of the node's own composition it may apply. + NodeWant = seat.WantsByWantNodeNodeValue + // Held is a fingerprint the assigner holds: the node whose health check failed on it, and the check's words. + Held = seat.WantsByHeldModuleFingerprintValue +) + +// The rules of ADR 0429 §6 and ADR 0539 clause 6, by number, as a want or a difference names the one that holds a node back. +const ( + RuleBuildDone = 1 // the build of F is done + RuleNotHeld = 2 // F is not held + RuleCanary = 3 // the node is the module's canary, or the canary runs F healthy + RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3) + RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary + RuleGrant = 6 // the module's credential for F exists (grants..) + RuleComposed = 7 // the module's composition for that node at F exists (ADR 0539 §6) + // RuleProvisions: every provision the module requires on that node is provided (novox/hq ADR 0570 §4). + 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 of a later release. +const ruleRead = 10 + +// The answers a want carries to a failed backup before an apply (ADR 0539 clause 16). +const ( + // BackupAgain is the answer to back the module up again before the apply. + BackupAgain = seat.WantsByWantNodeModuleValueBackupAgain + // BackupSkip is the answer to go on without a backup. + BackupSkip = seat.WantsByWantNodeModuleValueBackupSkip +) + +// NodeWord is the last part of a node's own key. +const NodeWord = "node" + +const ( + wantWord = "want" + heldWord = "held" + srcWord = "src2:" +) + +var ( + moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`) + digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) +) + +func moduleKeyPart(module string) (string, error) { + if module == "" { + return "", fmt.Errorf("wants: the module name is empty") + } + if !moduleNameOK.MatchString(module) { + return "", fmt.Errorf("wants: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module) + } + return keys.EscapeName(module), nil +} + +func parseModuleKeyPart(part string) (string, error) { + if err := keys.CheckName("module", part); err != nil { + return "", err + } + module, err := keys.UnescapeName(part) + if err != nil { + return "", err + } + if back, err := moduleKeyPart(module); err != nil || back != part { + return "", fmt.Errorf("wants: the module part %q is not a module name as a key writes it", part) + } + return module, nil +} + +func checkFingerprint(fp, of string) error { + if part, ok := strings.CutPrefix(fp, srcWord); !ok || !keys.IsHex64(part) { + return fmt.Errorf("wants: %s: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", of, fp, srcWord) + } + return nil +} + +func checkDigest(digest, of string) error { + if !digestForm.MatchString(digest) { + return fmt.Errorf("wants: %s %q is not sha256:<64 lowercase hex digits>", of, digest) + } + return nil +} + +func checkRule(rule int64, of string) error { + if rule < RuleBuildDone || rule > ruleRead { + return fmt.Errorf("wants: %s names the rule %d, which is not one of 1 to %d", of, rule, ruleRead) + } + return nil +} + +// WantKey is the key, in the state wants, of a node's want for a module: `want..`. A module named `node` is +// refused, since `want..node` is the node's own key. +func WantKey(node, module string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + part, err := moduleKeyPart(module) + if err != nil { + return "", err + } + if module == NodeWord { + return "", fmt.Errorf(`wants: the module name "node" is refused: want.%s.node is the node's own key`, node) + } + return seat.WantsByWantNodeModuleKey(node, part), nil +} + +// WantNodeKey is the key of a node's own want: `want..node`. +func WantNodeKey(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return seat.WantsByWantNodeNodeKey(node), nil +} + +// ParseWantNodeKey is the inverse of WantNodeKey; it refuses a module's want key and any key of another form. +func ParseWantNodeKey(key string) (node string, err error) { + node, ok := seat.ParseWantsByWantNodeNodeKey(key) + if !ok || keys.CheckName("node", node) != nil { + return "", fmt.Errorf("wants: %q is not a node's own want key, want..node", key) + } + return node, nil +} + +// ParseWantKey is the inverse of WantKey; it refuses a held key, the node's own want key and any key of another form. +func ParseWantKey(key string) (node, module string, err error) { + refused := fmt.Errorf("wants: %q is not a want key, want..", key) + node, part, ok := seat.ParseWantsByWantNodeModuleKey(key) + if !ok || part == NodeWord || keys.CheckName("node", node) != nil { + return "", "", refused + } + if module, err = parseModuleKeyPart(part); err != nil { + return "", "", refused + } + return node, module, nil +} + +// HeldKey is the key of a module's fingerprint the assigner holds: `held..`, the hex being the +// fingerprint's key part. +func HeldKey(module, fingerprint string) (string, error) { + if err := keys.CheckName("module", module); err != nil { + return "", err + } + if err := checkFingerprint(fingerprint, "a held key's fingerprint"); err != nil { + return "", err + } + return seat.WantsByHeldModuleFingerprintKey(module, strings.TrimPrefix(fingerprint, srcWord)), nil +} + +// ParseHeldKey is the inverse of HeldKey: the module and the fingerprint (`src2:` and the hex). +func ParseHeldKey(key string) (module, fingerprint string, err error) { + module, part, ok := seat.ParseWantsByHeldModuleFingerprintKey(key) + if !ok || keys.CheckName("module", module) != nil || !keys.IsHex64(part) { + return "", "", fmt.Errorf("wants: %q is not a held key, held..", key) + } + return module, srcWord + part, nil +} + +func checkWant(w Want) error { + if w.Removed { + if w.Fp != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" || w.Release != "" { + return fmt.Errorf("wants: a removed want names no fingerprint, composition, rule, backup answer or put") + } + return nil + } + if w.Hold && !w.Put { + return fmt.Errorf("wants: only a put holds a want") + } + if w.Put && (w.Newest != "" || w.Rule != 0) { + return fmt.Errorf("wants: a put's want is held back by no rule") + } + if err := checkFingerprint(w.Fp, "a want's fingerprint"); err != nil { + return err + } + // A node the catalogue places composes its own modules (ADR 0547 §2): its want is Self and names the build alone. Any + // other want names the one composition it allows (ADR 0539 clause 1). + switch { + case w.Self && w.Composed != "": + return fmt.Errorf("wants: the want of %s says the node composes it, and names a composition", w.Fp) + case !w.Self && w.Composed == "": + return fmt.Errorf("wants: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1), unless the node composes it itself", w.Fp) + case w.Composed != "": + if err := checkDigest(w.Composed, "a want's composition digest"); err != nil { + return err + } + } + if w.Yes != "" { + if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil { + return err + } + } else if w.Release != "" { + return fmt.Errorf("wants: the want of %s names the release %s and no yes it gave", w.Fp, w.Release) + } + switch w.Backup { + case "", BackupAgain, BackupSkip: + default: + return fmt.Errorf("wants: the backup answer %q is not %s or %s", w.Backup, BackupAgain, BackupSkip) + } + if w.Newest == "" && w.Rule == 0 { + return nil + } + if err := checkFingerprint(w.Newest, "a want's newest fingerprint"); err != nil { + return err + } + if w.Newest == w.Fp { + return fmt.Errorf("wants: a want at its newest fingerprint names no rule holding it back") + } + return checkRule(w.Rule, "a want held back") +} + +// CheckWant refuses a want the codec refuses. +func CheckWant(w Want) error { return checkWant(w) } + +// EncodeWant is w as JSON at the kept values' version; it refuses a fingerprint not of the new form, a removed want naming +// one, and a want that names no composition unless the node composes it itself. +func EncodeWant(w Want) ([]byte, error) { + if err := checkWant(w); err != nil { + return nil, err + } + w.V = keys.Version + return json.Marshal(w) +} + +// DecodeWant reads a want; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does not know and +// what EncodeWant refuses. +func DecodeWant(data []byte) (Want, error) { + if err := keys.CheckVersion(data); err != nil { + return Want{}, err + } + w, err := seat.DecodeWantsByWantNodeModuleValue(data) + if err != nil { + return Want{}, fmt.Errorf("wants: value does not fit its shape: %w", err) + } + if err := checkWant(w); err != nil { + return Want{}, err + } + return w, nil +} + +// SameWant says whether two wants are the same value, so the assigner writes only when they differ. +func SameWant(a, b Want) bool { return a == b } + +func checkNodeWant(w NodeWant) error { + switch { + case w.Self && w.Composed != "": + return fmt.Errorf("wants: the node's own want says the node composes it, and names a composition") + case w.Self: + return nil + } + return checkDigest(w.Composed, "the node's own want's composition digest") +} + +// EncodeNodeWant is w as JSON; it refuses a digest not of the sha256 form, and a want that names no composition unless it +// says the node composes its own (Self, ADR 0547 §2). +func EncodeNodeWant(w NodeWant) ([]byte, error) { + if err := checkNodeWant(w); err != nil { + return nil, err + } + w.V = keys.Version + return json.Marshal(w) +} + +// DecodeNodeWant reads a node's own want; it refuses an unknown version, a field a node's own want does not have (a +// fingerprint among them), and what EncodeNodeWant refuses. +func DecodeNodeWant(data []byte) (NodeWant, error) { + if err := keys.CheckVersion(data); err != nil { + return NodeWant{}, err + } + w, err := seat.DecodeWantsByWantNodeNodeValue(data) + if err != nil { + return NodeWant{}, fmt.Errorf("wants: a node's own want does not fit its shape: %w", err) + } + if err := checkNodeWant(w); err != nil { + return NodeWant{}, err + } + return w, nil +} + +func checkHeld(h Held) error { return keys.CheckName("node", h.Node) } + +// EncodeHeld is h as JSON; it refuses a node name a key refuses. +func EncodeHeld(h Held) ([]byte, error) { + if err := checkHeld(h); err != nil { + return nil, err + } + h.V = keys.Version + return json.Marshal(h) +} + +// DecodeHeld reads a hold; it refuses an unknown version and what EncodeHeld refuses. +func DecodeHeld(data []byte) (Held, error) { + if err := keys.CheckVersion(data); err != nil { + return Held{}, err + } + h, err := seat.DecodeWantsByHeldModuleFingerprintValue(data) + if err != nil { + return Held{}, fmt.Errorf("wants: value does not fit its shape: %w", err) + } + if err := checkHeld(h); err != nil { + return Held{}, err + } + return h, nil +} + +// SameHeld says whether two holds are the same value. +func SameHeld(a, b Held) bool { return a == b } diff --git a/placement/go/clients/meshwantsclient/wants_test.go b/placement/go/clients/meshwantsclient/wants_test.go new file mode 100644 index 0000000..5e2ecad --- /dev/null +++ b/placement/go/clients/meshwantsclient/wants_test.go @@ -0,0 +1,162 @@ +package meshwantsclient + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +func rep(c string) string { return strings.Repeat(c, 64) } + +// TestWantsAreWhatTheEarlierCodecWrote holds the values and keys equal to what go/delivery's codec wrote for the same +// values (the golden texts are its output; the field order is the contract's now, so values are compared decoded), and the +// refusals it made. +func TestWantsAreWhatTheEarlierCodecWrote(t *testing.T) { + a, b, c, d := "src2:"+rep("a"), "src2:"+rep("b"), "sha256:"+rep("c"), "sha256:"+rep("d") + sameJSON := func(t *testing.T, got []byte, want string) { + t.Helper() + var g, w any + if err := json.Unmarshal(got, &g); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(want), &w); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(g, w) { + t.Fatalf("wrote %s, the earlier codec wrote %s", got, want) + } + } + t.Run("wants", func(t *testing.T) { + for name, row := range map[string]struct { + w Want + golden string + }{ + "composed": {Want{Fp: a, Composed: c, Words: "w"}, `{"v":1,"fp":"` + a + `","composed":"` + c + `","words":"w"}`}, + "held back": {Want{Fp: a, Self: true, Newest: b, Rule: RuleGrant, Backup: BackupAgain, Words: "held"}, `{"v":1,"fp":"` + a + `","newest":"` + b + `","rule":6,"backup":"again","words":"held","self":true}`}, + "put": {Want{Fp: a, Self: true, Put: true, Hold: true, Yes: d, Release: "r1"}, `{"v":1,"fp":"` + a + `","put":true,"hold":true,"yes":"` + d + `","release":"r1","self":true}`}, + "removed": {Want{Removed: true, Words: "gone"}, `{"v":1,"removed":true,"words":"gone"}`}, + } { + t.Run(name, func(t *testing.T) { + raw, err := EncodeWant(row.w) + if err != nil { + t.Fatal(err) + } + sameJSON(t, raw, row.golden) + back, err := DecodeWant(raw) + if err != nil || !SameWant(back, func() Want { w := row.w; w.V = keys.Version; return w }()) { + t.Fatalf("read back %+v, %v", back, err) + } + }) + } + }) + t.Run("refused", func(t *testing.T) { + for name, w := range map[string]Want{ + "old fingerprint": {Fp: "src1:abc", Composed: c}, + "no composition": {Fp: a}, + "self and composition": {Fp: a, Self: true, Composed: c}, + "hold without put": {Fp: a, Self: true, Hold: true}, + "put held back": {Fp: a, Self: true, Put: true, Newest: b, Rule: RuleStep}, + "removed names a rule": {Removed: true, Rule: 1}, + "release without yes": {Fp: a, Self: true, Release: "r"}, + "backup answer unknown": {Fp: a, Self: true, Backup: "later"}, + "newest equals fp": {Fp: a, Self: true, Newest: a, Rule: RuleStep}, + "rule 11": {Fp: a, Self: true, Newest: b, Rule: 11}, + "newest without a rule": {Fp: a, Self: true, Newest: b}, + "composition not sha256": {Fp: a, Composed: "sha256:xyz"}, + } { + if _, err := EncodeWant(w); err == nil { + t.Errorf("%s: written", name) + } + } + if _, err := EncodeWant(Want{Fp: a, Self: true, Newest: b, Rule: 10}); err != nil { + t.Errorf("rule 10 is read: %v", err) + } + if _, err := DecodeWant([]byte(`{"fp":"` + a + `","self":true}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Errorf("no version: %v", err) + } + if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + a + `","self":true,"extra":1}`)); err == nil { + t.Error("an unknown field is read") + } + }) + t.Run("node want and hold", func(t *testing.T) { + raw, err := EncodeNodeWant(NodeWant{Composed: c, Words: "n"}) + if err != nil { + t.Fatal(err) + } + sameJSON(t, raw, `{"v":1,"composed":"`+c+`","words":"n"}`) + raw, err = EncodeNodeWant(NodeWant{Self: true}) + if err != nil { + t.Fatal(err) + } + sameJSON(t, raw, `{"v":1,"composed":"","self":true}`) + if back, err := DecodeNodeWant(raw); err != nil || !back.Self { + t.Fatalf("%+v %v", back, err) + } + for name, w := range map[string]NodeWant{"none": {}, "both": {Self: true, Composed: c}, "bad": {Composed: "x"}} { + if _, err := EncodeNodeWant(w); err == nil { + t.Errorf("%s: written", name) + } + } + if _, err := DecodeNodeWant([]byte(`{"v":1,"composed":"` + c + `","fp":"` + a + `"}`)); err == nil { + t.Error("a fingerprint in a node's own want is read") + } + raw, err = EncodeHeld(Held{Node: "ace", Words: "unhealthy"}) + if err != nil { + t.Fatal(err) + } + sameJSON(t, raw, `{"v":1,"node":"ace","words":"unhealthy"}`) + if h, err := DecodeHeld(raw); err != nil || !SameHeld(h, Held{V: 1, Node: "ace", Words: "unhealthy"}) { + t.Fatalf("%+v %v", h, err) + } + if _, err := EncodeHeld(Held{Node: "a.b"}); err == nil { + t.Error("a node with a dot is held") + } + }) + t.Run("keys", func(t *testing.T) { + for _, row := range []struct{ module, want string }{ + {"mesh-delivery-assigner", "want.ace.mesh-delivery-assigner"}, + {"novox.be", "want.ace.novox=2ebe"}, + } { + k, err := WantKey("ace", row.module) + if err != nil || k != row.want { + t.Fatalf("WantKey %s = %q, %v", row.module, k, err) + } + if n, m, err := ParseWantKey(k); err != nil || n != "ace" || m != row.module { + t.Fatalf("ParseWantKey %q = %q %q %v", k, n, m, err) + } + } + if k, err := HeldKey("mesh-delivery-assigner", a); err != nil || k != "held.mesh-delivery-assigner."+rep("a") { + t.Fatalf("HeldKey = %q %v", k, err) + } + if m, fp, err := ParseHeldKey("held.mesh-delivery-assigner." + rep("a")); err != nil || m != "mesh-delivery-assigner" || fp != a { + t.Fatalf("ParseHeldKey = %q %q %v", m, fp, err) + } + if k, err := WantNodeKey("ace"); err != nil || k != "want.ace.node" { + t.Fatalf("WantNodeKey = %q %v", k, err) + } + if n, err := ParseWantNodeKey("want.ace.node"); err != nil || n != "ace" { + t.Fatalf("ParseWantNodeKey = %q %v", n, err) + } + if _, err := WantKey("ace", "node"); err == nil { + t.Error("a module named node has a want key") + } + if _, err := HeldKey("novox.be", a); err == nil { + t.Error("a module with a dot has a held key") + } + for _, key := range []string{"want.ace.node", "held.m." + rep("a"), "ace.m", "want.ace", "want..m"} { + if _, _, err := ParseWantKey(key); err == nil { + t.Errorf("%q is a want key", key) + } + } + if _, err := ParseWantNodeKey("want.ace.m"); err == nil { + t.Error("a module's want is a node's own") + } + if _, _, err := ParseHeldKey("want.ace.m"); err == nil { + t.Error("a want key is a held key") + } + }) +} -- 2.54.0 From 2e0563e9838f418daf13949a64b3358159d3eff1 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:02:00 +0100 Subject: [PATCH 80/94] =?UTF-8?q?mesh-config=20seat:=20the=20state=20kept?= =?UTF-8?q?=20typed=20(an=20approved=20held=20value,=20under=20owner.repos?= =?UTF-8?q?itory.number.node.module.key),=20a=20fixture=20from=20the=20hol?= =?UTF-8?q?der's=20test=20values,=20regenerated;=20a=20client=20meshconfig?= =?UTF-8?q?client=20with=20the=20keys=20and=20the=20check=20the=20checker?= =?UTF-8?q?=20and=20the=20assigner=20share=20(B2f;=20ADR=200565=20=C2=A75,?= =?UTF-8?q?=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/meshconfigclient/client.go | 83 +++++++++++++++++++++ go/clients/meshconfigclient/client_test.go | 45 +++++++++++ go/clients/meshconfigclient/go.mod | 5 ++ go/seats/meshconfig/contract.go | 86 ++++++++++++++++++++++ go/seats/meshconfig/contract_test.go | 6 ++ module.json | 9 +++ seats/mesh-config/fixtures/states.json | 20 +++++ seats/mesh-config/seat.json | 74 +++++++++++++++++++ ts/seats/meshconfig/src/index.ts | 62 ++++++++++++++++ 9 files changed, 390 insertions(+) create mode 100644 go/clients/meshconfigclient/client.go create mode 100644 go/clients/meshconfigclient/client_test.go create mode 100644 go/clients/meshconfigclient/go.mod diff --git a/go/clients/meshconfigclient/client.go b/go/clients/meshconfigclient/client.go new file mode 100644 index 0000000..9f99ddb --- /dev/null +++ b/go/clients/meshconfigclient/client.go @@ -0,0 +1,83 @@ +// Package meshconfigclient is the logic every reader of the seat mesh-config's state kept shares (novox/hq ADR 0565 +// clause 5, ADR 0634 §4): the checker and the assigner's composition check lay a pull request's own held values over its +// modules' values. The layout of a value is the seat's contract's; this holds what the readers share over it: a pull +// request's key, a held value's key and the rule that a value is the one its key names. +package meshconfigclient + +import ( + "encoding/json" + "fmt" + "strings" + + seat "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" +) + +// PullKey is a pull request's key, `owner/repository#number` with `/` and `#` as dots, the form the seat keeps a pull +// request's values under. The three parts hold no dot, star, angle bracket or space. +func PullKey(pull string) (string, error) { + owner, repository, number, err := splitPull(pull) + if err != nil { + return "", err + } + return owner + "." + repository + "." + number, nil +} + +func splitPull(pull string) (owner, repository, number string, err error) { + owner, rest, ok := strings.Cut(strings.TrimSpace(pull), "/") + repository, number, ok2 := strings.Cut(rest, "#") + if !ok || !ok2 || owner == "" || repository == "" || number == "" || strings.ContainsAny(owner+repository+number, ".*> ") { + return "", "", "", fmt.Errorf("meshconfig: %q is not a pull request, owner/repository#number", pull) + } + return owner, repository, number, nil +} + +// HeldValueKey is a held value's key in the state kept: the pull request's key, the node, the module and the config key, +// joined by dots. +func HeldValueKey(pull, node, module, key string) (string, error) { + owner, repository, number, err := splitPull(pull) + if err != nil { + return "", err + } + if node == "" || module == "" || key == "" { + return "", fmt.Errorf("meshconfig: a held value names a node, a module and a key") + } + return seat.KeptByOwnerRepositoryNumberNodeModuleKeyKey(owner, repository, number, node, module, key), nil +} + +// Kept is one held value, as the seat types it. +type Kept = seat.KeptByOwnerRepositoryNumberNodeModuleKeyValue + +// CheckKept refuses a held value that names no pull request, head, declaration, node, module or key. +func CheckKept(v Kept) error { + if _, err := PullKey(v.Pull); err != nil { + return err + } + if v.Head == "" || v.Declaration == "" || v.Node == "" || v.Module == "" || v.Key == "" { + return fmt.Errorf("meshconfig: a held value of %s names a head, a declaration, a node, a module and a key", v.Pull) + } + return nil +} + +// DecodeKept reads a value of the state kept under its key: the value the seat types, checked, and the one its key +// names. A value kept under another key than its own fields make is refused, so a reader never lays a value over the +// wrong pull request or module. +func DecodeKept(key string, raw []byte) (Kept, error) { + v, err := seat.DecodeKeptByOwnerRepositoryNumberNodeModuleKeyValue(raw) + if err != nil { + return Kept{}, fmt.Errorf("meshconfig: the held value %s is not one: %w", key, err) + } + if err := CheckKept(v); err != nil { + return Kept{}, err + } + want, err := HeldValueKey(v.Pull, v.Node, v.Module, v.Key) + if err != nil { + return Kept{}, err + } + if want != key { + return Kept{}, fmt.Errorf("meshconfig: the held value kept under %s names %s", key, want) + } + return v, nil +} + +// ValueOf is the value held, as the operator approved it, in its JSON form. +func ValueOf(v Kept) (json.RawMessage, error) { return json.Marshal(v.Value) } diff --git a/go/clients/meshconfigclient/client_test.go b/go/clients/meshconfigclient/client_test.go new file mode 100644 index 0000000..ede862f --- /dev/null +++ b/go/clients/meshconfigclient/client_test.go @@ -0,0 +1,45 @@ +package meshconfigclient + +import ( + "strings" + "testing" +) + +// The keys are the earlier form of go/delivery's HeldValueKey and ConfigCandidateKey (golden), which the holder writes. +func TestKeysAreTheEarlierForm(t *testing.T) { + if k, err := HeldValueKey("novox/mesh-catalog#42", "novox", "mail", "quota"); k != "novox.mesh-catalog.42.novox.mail.quota" || err != nil { + t.Errorf("a held value's key is %q, %v", k, err) + } + for pull, want := range map[string]string{"novox/mesh-catalog#42": "novox.mesh-catalog.42", "x#1": "", "a/b": "", "a/b.c#1": ""} { + if got, err := PullKey(pull); got != want || (want == "") != (err != nil) { + t.Errorf("%s keyed %q, %v", pull, got, err) + } + } + if _, err := HeldValueKey("novox/mesh-catalog#42", "", "mail", "quota"); err == nil { + t.Error("a held value without a node was keyed") + } +} + +const held = `{"pull":"novox/mesh-catalog#434","head":"abc1234def","declaration":"sha256:3a7b","node":"ace","module":"n8n","key":"media-gid","value":2000,"by":"the operator","approved":"operator@telegram","at":"2026-11-05T12:30:00Z"}` + +func TestAHeldValueIsTheOneItsKeyNames(t *testing.T) { + key := "novox.mesh-catalog.434.ace.n8n.media-gid" + v, err := DecodeKept(key, []byte(held)) + if err != nil || v.Node != "ace" || v.Value != float64(2000) { + t.Fatalf("read %+v, %v", v, err) + } + if raw, err := ValueOf(v); err != nil || string(raw) != "2000" { + t.Errorf("the value is %s, %v", raw, err) + } + for name, c := range map[string]struct{ key, raw string }{ + "another pull request's key": {"novox.mesh-catalog.435.ace.n8n.media-gid", held}, + "another module's key": {"novox.mesh-catalog.434.ace.plex.media-gid", held}, + "no head": {key, strings.Replace(held, `"head":"abc1234def",`, "", 1)}, + "a field it does not know": {key, strings.Replace(held, `"by"`, `"extra":1,"by"`, 1)}, + "a pull that is not one": {key, strings.Replace(held, "novox/mesh-catalog#434", "novox", 1)}, + } { + if _, err := DecodeKept(c.key, []byte(c.raw)); err == nil { + t.Errorf("%s was read", name) + } + } +} diff --git a/go/clients/meshconfigclient/go.mod b/go/clients/meshconfigclient/go.mod new file mode 100644 index 0000000..fc30b01 --- /dev/null +++ b/go/clients/meshconfigclient/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient + +go 1.26.0 + +require git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-20261105000000-000000000000 diff --git a/go/seats/meshconfig/contract.go b/go/seats/meshconfig/contract.go index 3c126d1..f7107e2 100644 --- a/go/seats/meshconfig/contract.go +++ b/go/seats/meshconfig/contract.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "strings" + "time" ) // Seat is the seat's name. @@ -47,6 +48,7 @@ func HolderSubjectRetired(holder string) string { return "mesh.stream." + holder const ( StateResolved = "resolved" + StateKept = "kept" ) // ResolvedByNodeModuleKeyKey is the key of one value of the state resolved under the form "..". @@ -92,6 +94,64 @@ func DecodeResolvedByNodeModuleKeyValue(raw []byte) (ResolvedByNodeModuleKeyValu return v, nil } +// KeptByOwnerRepositoryNumberNodeModuleKeyKey is the key of one value of the state kept under the form ".....". +func KeptByOwnerRepositoryNumberNodeModuleKeyKey(owner, repository, number, node, module, key string) string { + return owner + "." + repository + "." + number + "." + node + "." + module + "." + key +} + +// ParseKeptByOwnerRepositoryNumberNodeModuleKeyKey reads the parts of a key of the form ".....", in the order of the form; the last result is false for a key of another form. +func ParseKeptByOwnerRepositoryNumberNodeModuleKeyKey(key string) (string, string, string, string, string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", "", "", "", "", false + } + rest = after0 + v2, after2, found2 := strings.Cut(rest, ".") + if !found2 || v2 == "" { + return "", "", "", "", "", "", false + } + rest = after2 + v4, after4, found4 := strings.Cut(rest, ".") + if !found4 || v4 == "" { + return "", "", "", "", "", "", false + } + rest = after4 + v6, after6, found6 := strings.Cut(rest, ".") + if !found6 || v6 == "" { + return "", "", "", "", "", "", false + } + rest = after6 + v8, after8, found8 := strings.Cut(rest, ".") + if !found8 || v8 == "" { + return "", "", "", "", "", "", false + } + rest = after8 + if rest == "" { + return "", "", "", "", "", "", false + } + v10 := rest + rest = "" + if rest != "" { + return "", "", "", "", "", "", false + } + return v0, v2, v4, v6, v8, v10, true +} + +// DecodeKeptByOwnerRepositoryNumberNodeModuleKeyValue reads one value of the state kept under the form ".....", refusing a field KeptByOwnerRepositoryNumberNodeModuleKeyValue does not know and any text after the value. +func DecodeKeptByOwnerRepositoryNumberNodeModuleKeyValue(raw []byte) (KeptByOwnerRepositoryNumberNodeModuleKeyValue, error) { + var v KeptByOwnerRepositoryNumberNodeModuleKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // ConfigInput is what config takes. type ConfigInput struct { // list (when not given), set, clear or history @@ -197,3 +257,29 @@ type ResolvedByNodeModuleKeyValue struct { Trusted bool `json:"trusted,omitempty"` Value any `json:"value,omitempty"` } + +// KeptByOwnerRepositoryNumberNodeModuleKeyValue is one value of the state kept under the key ...... +type KeptByOwnerRepositoryNumberNodeModuleKeyValue struct { + // who approved it, as the proposal kept it + Approved string `json:"approved,omitempty"` + // when it was kept + At *time.Time `json:"at,omitempty"` + By string `json:"by,omitempty"` + // the digest of what the declaration holds the value to; the value stays only while it is the same + Declaration string `json:"declaration"` + // the head the declaration was read at + Head string `json:"head"` + // the config key + Key string `json:"key"` + Module string `json:"module"` + Node string `json:"node"` + // the proposal's id + Proposal string `json:"proposal,omitempty"` + // the pull request, owner/repository#number + Pull string `json:"pull"` + // the value held, as the operator approved it + Value any `json:"value"` + // where the value was proposed from + Via string `json:"via,omitempty"` + Why string `json:"why,omitempty"` +} diff --git a/go/seats/meshconfig/contract_test.go b/go/seats/meshconfig/contract_test.go index 7a994e6..fed1bff 100644 --- a/go/seats/meshconfig/contract_test.go +++ b/go/seats/meshconfig/contract_test.go @@ -37,6 +37,12 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, + {name: "a held value, as mesh-config writes it (the holder's own test values)", subject: "", given: "{\"approved\":\"operator@telegram\",\"at\":\"2026-11-05T12:30:00Z\",\"by\":\"the operator\",\"declaration\":\"sha256:3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b\",\"head\":\"abc1234def\",\"key\":\"media-gid\",\"module\":\"n8n\",\"node\":\"ace\",\"proposal\":\"p-1\",\"pull\":\"novox/mesh-catalog#434\",\"value\":2000,\"via\":\"telegram\",\"why\":\"the group the media share is owned by\"}", wire: "{\"approved\":\"operator@telegram\",\"at\":\"2026-11-05T12:30:00Z\",\"by\":\"the operator\",\"declaration\":\"sha256:3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b\",\"head\":\"abc1234def\",\"key\":\"media-gid\",\"module\":\"n8n\",\"node\":\"ace\",\"proposal\":\"p-1\",\"pull\":\"novox/mesh-catalog#434\",\"value\":2000,\"via\":\"telegram\",\"why\":\"the group the media share is owned by\"}", + decode: func(b []byte) (any, error) { + var v KeptByOwnerRepositoryNumberNodeModuleKeyValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "renames answers the open windows", subject: "", given: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", wire: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", decode: func(b []byte) (any, error) { var v RenamesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } diff --git a/module.json b/module.json index ca90d2c..1a93c8e 100644 --- a/module.json +++ b/module.json @@ -4,6 +4,15 @@ "slug": "contracts", "build": { "artifacts": [ + { + "name": "go-meshconfigclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshconfigclient" + ], + "series": "0.1" + }, { "name": "go-nodeengineclient", "kind": "package", diff --git a/seats/mesh-config/fixtures/states.json b/seats/mesh-config/fixtures/states.json index ad3d582..edc2be3 100644 --- a/seats/mesh-config/fixtures/states.json +++ b/seats/mesh-config/fixtures/states.json @@ -21,5 +21,25 @@ "source": "module", "value": 500 } + }, + { + "name": "a held value, as mesh-config writes it (the holder's own test values)", + "of": "state:kept/.....", + "why": "no tool reads the state kept; the values are the holder's test (modules/mesh-config pulls_test.go): key novox.mesh-catalog.434.ace.n8n.media-gid", + "given": { + "pull": "novox/mesh-catalog#434", + "head": "abc1234def", + "declaration": "sha256:3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b", + "node": "ace", + "module": "n8n", + "key": "media-gid", + "value": 2000, + "by": "the operator", + "via": "telegram", + "why": "the group the media share is owned by", + "approved": "operator@telegram", + "proposal": "p-1", + "at": "2026-11-05T12:30:00Z" + } } ] diff --git a/seats/mesh-config/seat.json b/seats/mesh-config/seat.json index 83c63f3..9a555b1 100644 --- a/seats/mesh-config/seat.json +++ b/seats/mesh-config/seat.json @@ -267,6 +267,80 @@ } ], "description": "the value each module on each node takes for each config key, built from the sources alone" + }, + { + "name": "kept", + "template": "state", + "key-forms": [ + { + "key": ".....", + "value": { + "type": "object", + "description": "an approved held value, kept apart from the values mesh-config resolves, laid only over the composition of its own pull request at its head (ADR 0565 clause 5)", + "properties": { + "pull": { + "type": "string", + "description": "the pull request, owner/repository#number" + }, + "head": { + "type": "string", + "description": "the head the declaration was read at" + }, + "declaration": { + "type": "string", + "description": "the digest of what the declaration holds the value to; the value stays only while it is the same" + }, + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "key": { + "type": "string", + "description": "the config key" + }, + "value": { + "description": "the value held, as the operator approved it" + }, + "by": { + "type": "string" + }, + "via": { + "type": "string", + "description": "where the value was proposed from" + }, + "why": { + "type": "string" + }, + "approved": { + "type": "string", + "description": "who approved it, as the proposal kept it" + }, + "proposal": { + "type": "string", + "description": "the proposal's id" + }, + "at": { + "type": "string", + "format": "date-time", + "description": "when it was kept" + } + }, + "required": [ + "pull", + "head", + "declaration", + "node", + "module", + "key", + "value" + ] + }, + "description": "one held value: the pull request (owner, repository and number, none holding a dot), the node, the module and the config key, which may hold dots" + } + ], + "description": "the approved held values of open pull requests, read by the checker and the assigner's composition check" } ] } diff --git a/ts/seats/meshconfig/src/index.ts b/ts/seats/meshconfig/src/index.ts index e8fa023..c5e0a15 100644 --- a/ts/seats/meshconfig/src/index.ts +++ b/ts/seats/meshconfig/src/index.ts @@ -34,6 +34,7 @@ export function retiredHolderSubject(holder: string): string { export const states = { resolved: "resolved", + kept: "kept", } as const; /** The key of one value of the state resolved under the form "..". */ @@ -59,6 +60,41 @@ export function parseResolvedByNodeModuleKeyKey(key: string): { node: string; mo return { node, module, key }; } +/** The key of one value of the state kept under the form ".....". */ +export function keptByOwnerRepositoryNumberNodeModuleKeyKey(owner: string, repository: string, number: string, node: string, module: string, key: string): string { + return owner + "." + repository + "." + number + "." + node + "." + module + "." + key; +} + +/** The parts of a key of the form ".....", or undefined for a key of another form. */ +export function parseKeptByOwnerRepositoryNumberNodeModuleKeyKey(key: string): { owner: string; repository: string; number: string; node: string; module: string; key: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const owner = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + const at2 = rest.indexOf("."); + if (at2 <= 0) return undefined; + const repository = rest.slice(0, at2); + rest = rest.slice(at2 + 1); + const at4 = rest.indexOf("."); + if (at4 <= 0) return undefined; + const number = rest.slice(0, at4); + rest = rest.slice(at4 + 1); + const at6 = rest.indexOf("."); + if (at6 <= 0) return undefined; + const node = rest.slice(0, at6); + rest = rest.slice(at6 + 1); + const at8 = rest.indexOf("."); + if (at8 <= 0) return undefined; + const module = rest.slice(0, at8); + rest = rest.slice(at8 + 1); + if (rest === "") return undefined; + const key = rest; + rest = ""; + if (rest !== "") return undefined; + return { owner, repository, number, node, module, key }; +} + /** is what config takes. */ export interface ConfigInput { /** list (when not given), set, clear or history */ @@ -158,3 +194,29 @@ export interface ResolvedByNodeModuleKeyValue { "value"?: unknown; } +/** is one value of the state kept under the key ...... */ +export interface KeptByOwnerRepositoryNumberNodeModuleKeyValue { + /** who approved it, as the proposal kept it */ + "approved"?: string; + /** when it was kept */ + "at"?: string; + "by"?: string; + /** the digest of what the declaration holds the value to; the value stays only while it is the same */ + "declaration": string; + /** the head the declaration was read at */ + "head": string; + /** the config key */ + "key": string; + "module": string; + "node": string; + /** the proposal's id */ + "proposal"?: string; + /** the pull request, owner/repository#number */ + "pull": string; + /** the value held, as the operator approved it */ + "value": unknown; + /** where the value was proposed from */ + "via"?: string; + "why"?: string; +} + -- 2.54.0 From 311654f224b9e5bc6db9f1908de962b1799bd64c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:02:42 +0100 Subject: [PATCH 81/94] =?UTF-8?q?node-engine=20client:=20RootContract,=20t?= =?UTF-8?q?he=20contract=20from=20which=20an=20engine=20judges=20an=20acco?= =?UTF-8?q?unt's=20root=20(B5g;=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/health.go | 4 ++++ go/clients/nodeengineclient/health_test.go | 11 +++++++++++ 2 files changed, 15 insertions(+) create mode 100644 go/clients/nodeengineclient/health_test.go diff --git a/go/clients/nodeengineclient/health.go b/go/clients/nodeengineclient/health.go index b893d43..a5d3b56 100644 --- a/go/clients/nodeengineclient/health.go +++ b/go/clients/nodeengineclient/health.go @@ -13,6 +13,10 @@ import ( // a module declares as the engine placed them (the state data, `.`): their layouts are the seat's contract // (seat.HealthByNodeValue, seat.DataByNodeModuleValue); this file is their codec, shared by the engine and its readers. +// RootContract is the statement contract from which an engine judges an account's root: a report whose contract is lower +// judged nothing about root, and a reader must not read its empty root verdicts as healthy (HealthByNodeValue.Contract). +const RootContract = 3 + // EncodeHealth is h as JSON at the current version; it refuses a report naming no node, and a fall back to the old login // that names another node, no login fallen back from, no reason or no time. func EncodeHealth(h seat.HealthByNodeValue) ([]byte, error) { diff --git a/go/clients/nodeengineclient/health_test.go b/go/clients/nodeengineclient/health_test.go new file mode 100644 index 0000000..7dfdf28 --- /dev/null +++ b/go/clients/nodeengineclient/health_test.go @@ -0,0 +1,11 @@ +package nodeengineclient + +import "testing" + +// A report below RootContract judged nothing about root; the number is the one engines have written since the root +// judgement began, so a change of it is a change of what a reader trusts. +func TestRootContractIsTheOneEnginesJudgeRootFrom(t *testing.T) { + if RootContract != 3 { + t.Fatalf("RootContract = %d, the engines write 3", RootContract) + } +} -- 2.54.0 From 70da0dd17e58460d81180861038d916d6969ebf0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:21:28 +0100 Subject: [PATCH 82/94] =?UTF-8?q?Watchdogs=20client:=20the=20keys,=20check?= =?UTF-8?q?=20and=20codec=20of=20presence,=20pulse=20and=20stuck=20over=20?= =?UTF-8?q?the=20seat=20mesh-watchdogs,=20goldens=20from=20go/delivery's?= =?UTF-8?q?=20output=20(B7n2;=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/clients/meshwatchdogsclient/go.mod | 8 + .../clients/meshwatchdogsclient/watchdogs.go | 205 ++++++++++++++++++ .../meshwatchdogsclient/watchdogs_test.go | 140 ++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 health-and-repair/go/clients/meshwatchdogsclient/go.mod create mode 100644 health-and-repair/go/clients/meshwatchdogsclient/watchdogs.go create mode 100644 health-and-repair/go/clients/meshwatchdogsclient/watchdogs_test.go diff --git a/health-and-repair/go/clients/meshwatchdogsclient/go.mod b/health-and-repair/go/clients/meshwatchdogsclient/go.mod new file mode 100644 index 0000000..5d59e6d --- /dev/null +++ b/health-and-repair/go/clients/meshwatchdogsclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 +) diff --git a/health-and-repair/go/clients/meshwatchdogsclient/watchdogs.go b/health-and-repair/go/clients/meshwatchdogsclient/watchdogs.go new file mode 100644 index 0000000..2855912 --- /dev/null +++ b/health-and-repair/go/clients/meshwatchdogsclient/watchdogs.go @@ -0,0 +1,205 @@ +// Package meshwatchdogsclient is the client of the seat mesh-watchdogs: the keys, the check and the codec of the seat's +// three states presence, pulse and stuck, which every module that reads or writes them shares (the watchman that writes +// them, the assigner and the self-check that read them). The layouts are the seat's contract; this file writes the +// version and refuses what the earlier codec refused (novox/hq ADR 0433, ADR 0634 §4). +package meshwatchdogsclient + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The three values of the seat's states, in the seat's types. +type ( + // NodePresence is whether a node is online (state presence, key ``). + NodePresence = seat.PresenceByNodeValue + // PartPresence is whether a part's instance is online (state presence, key `part..`). + PartPresence = seat.PresenceByPartPartInstanceValue + // Pulse is the watched key mirrored, its revision, and who owes the next move (state pulse). + Pulse = seat.PulseByBucketKeyValue + // Stuck is the key that stopped moving, what it waited for, since when, and whether it moved again (state stuck). + Stuck = seat.StuckByBucketKeyValue +) + +// PartWord is the first part of a part's presence key; a node of that name is refused. +const PartWord = "part" + +// PulseMoveBound is how long a move may take: a pulse key lives for this time minus the move's age. +const PulseMoveBound = 120 * time.Second + +// PresenceHistory is the number of revisions the presence state keeps per key. +const PresenceHistory = 20 + +// PresenceNodeKey is the key of a node's presence: ``. A node named `part` is refused. +func PresenceNodeKey(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + if node == PartWord { + return "", errors.New(`the node name "part" is refused: part.. is a part's key`) + } + return seat.PresenceByNodeKey(node), nil +} + +// PresencePartKey is the key of a part's instance: `part..`. +func PresencePartKey(part, instance string) (string, error) { + if err := keys.CheckName("part", part); err != nil { + return "", err + } + if err := keys.CheckName("instance", instance); err != nil { + return "", err + } + return seat.PresenceByPartPartInstanceKey(part, instance), nil +} + +// ParsePresenceKey is the inverse of PresenceNodeKey and PresencePartKey: for a node's key part and instance are +// empty, for a part's key node is. +func ParsePresenceKey(key string) (node, part, instance string, err error) { + if !strings.Contains(key, ".") { + node, err = PresenceNodeKey(key) + if err != nil { + return "", "", "", fmt.Errorf("%q is not a presence key: %w", key, err) + } + return node, "", "", nil + } + fields := strings.Split(key, ".") + if len(fields) != 3 || fields[0] != PartWord { + return "", "", "", fmt.Errorf("%q is not a presence key, or part..", key) + } + if _, err := PresencePartKey(fields[1], fields[2]); err != nil { + return "", "", "", fmt.Errorf("%q is not a presence key: %w", key, err) + } + return "", fields[1], fields[2], nil +} + +// PulseKey is the key of the move watched at key of bucket: `.`. The bucket is the full bucket name and +// holds no dot; the key may. +func PulseKey(bucket, key string) (string, error) { + if err := keys.CheckName("bucket", bucket); err != nil { + return "", err + } + if key == "" || strings.HasPrefix(key, ".") || strings.HasSuffix(key, ".") || strings.Contains(key, "..") { + return "", fmt.Errorf("the pulse key %q is empty or has an empty part", key) + } + for _, part := range strings.Split(key, ".") { + if err := keys.CheckName("key", part); err != nil { + return "", err + } + } + return seat.PulseByBucketKeyKey(bucket, key), nil +} + +// ParsePulseKey is the inverse of PulseKey, splitting at the first dot. +func ParsePulseKey(pulseKey string) (bucket, key string, err error) { + bucket, key, ok := seat.ParsePulseByBucketKeyKey(pulseKey) + if !ok { + return "", "", fmt.Errorf("%q is not a pulse key, .", pulseKey) + } + if _, err := PulseKey(bucket, key); err != nil { + return "", "", fmt.Errorf("%q is not a pulse key: %w", pulseKey, err) + } + return bucket, key, nil +} + +// StuckKey is the key of the key that stopped moving: `.`, formed as PulseKey forms a pulse's. +func StuckKey(bucket, key string) (string, error) { + k, err := PulseKey(bucket, key) + if err != nil { + return "", fmt.Errorf("no stuck key: %w", err) + } + return k, nil +} + +// ParseStuckKey is the inverse of StuckKey, splitting at the first dot. +func ParseStuckKey(stuckKey string) (bucket, key string, err error) { + bucket, key, err = ParsePulseKey(stuckKey) + if err != nil { + return "", "", fmt.Errorf("%q is not a stuck key: %w", stuckKey, err) + } + return bucket, key, nil +} + +// PulseTTL is the time to live of a pulse key for a move of the given age: PulseMoveBound minus the age. At or past +// the bound the move is stalled, with no time left and nothing to write. +func PulseTTL(moveAge time.Duration) (ttl time.Duration, stalled bool) { + if moveAge >= PulseMoveBound { + return 0, true + } + return PulseMoveBound - moveAge, false +} + +// EncodeNodePresence is p as JSON at the current version. +func EncodeNodePresence(p NodePresence) ([]byte, error) { + p.V = keys.Version + return json.Marshal(p) +} + +// DecodeNodePresence reads a node's presence; it refuses an unknown version (keys.ErrUnknownVersion). +func DecodeNodePresence(data []byte) (NodePresence, error) { + if err := keys.CheckVersion(data); err != nil { + return NodePresence{}, err + } + return decodeStrict[NodePresence](data, "presence") +} + +// EncodePartPresence is p as JSON at the current version. +func EncodePartPresence(p PartPresence) ([]byte, error) { + p.V = keys.Version + return json.Marshal(p) +} + +// DecodePartPresence reads a part's presence; it refuses an unknown version (keys.ErrUnknownVersion). +func DecodePartPresence(data []byte) (PartPresence, error) { + if err := keys.CheckVersion(data); err != nil { + return PartPresence{}, err + } + return decodeStrict[PartPresence](data, "presence") +} + +// EncodePulse is p as JSON at the current version. +func EncodePulse(p Pulse) ([]byte, error) { + p.V = keys.Version + return json.Marshal(p) +} + +// DecodePulse reads a pulse; it refuses an unknown version (keys.ErrUnknownVersion). +func DecodePulse(data []byte) (Pulse, error) { + if err := keys.CheckVersion(data); err != nil { + return Pulse{}, err + } + return decodeStrict[Pulse](data, "pulse") +} + +// EncodeStuck is s as JSON at the current version. +func EncodeStuck(s Stuck) ([]byte, error) { + s.V = keys.Version + return json.Marshal(s) +} + +// DecodeStuck reads a stuck value; it refuses an unknown version (keys.ErrUnknownVersion). +func DecodeStuck(data []byte) (Stuck, error) { + if err := keys.CheckVersion(data); err != nil { + return Stuck{}, err + } + return decodeStrict[Stuck](data, "stuck") +} + +func decodeStrict[T any](data []byte, what string) (T, error) { + var v T + if err := json.Unmarshal(data, &v); err != nil { + return v, fmt.Errorf("the %s value does not fit its shape: %w", what, err) + } + return v, nil +} + +// SameStuck says whether two stuck values are the same, so a writer writes only when they differ. +func SameStuck(a, b Stuck) bool { + return a.V == b.V && a.Bucket == b.Bucket && a.Key == b.Key && a.Waited == b.Waited && a.Since.Equal(b.Since) && + a.Moved == b.Moved +} diff --git a/health-and-repair/go/clients/meshwatchdogsclient/watchdogs_test.go b/health-and-repair/go/clients/meshwatchdogsclient/watchdogs_test.go new file mode 100644 index 0000000..3f5c296 --- /dev/null +++ b/health-and-repair/go/clients/meshwatchdogsclient/watchdogs_test.go @@ -0,0 +1,140 @@ +package meshwatchdogsclient + +import ( + "encoding/json" + "errors" + "reflect" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +func sameJSON(t *testing.T, got []byte, want string) { + t.Helper() + var g, w any + if err := json.Unmarshal(got, &g); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(want), &w); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(g, w) { + t.Fatalf("wrote %s, the earlier codec wrote %s", got, want) + } +} + +// TestTheValuesAreWhatTheEarlierCodecWrote holds the values equal to what go/delivery's codec wrote for the same values +// (the golden texts are its output; the field order is the contract's now, so values are compared decoded), reads them +// back, and refuses an unknown version. +func TestTheValuesAreWhatTheEarlierCodecWrote(t *testing.T) { + since := time.Date(2026, 10, 25, 11, 52, 58, 51397201, time.UTC) + for name, row := range map[string]struct { + encode func() ([]byte, error) + decode func([]byte) (any, error) + golden string + want any + }{ + "node online": { + func() ([]byte, error) { return EncodeNodePresence(NodePresence{Online: true}) }, + func(b []byte) (any, error) { return DecodeNodePresence(b) }, + `{"v":1,"online":true}`, NodePresence{V: 1, Online: true}, + }, + "part offline": { + func() ([]byte, error) { return EncodePartPresence(PartPresence{}) }, + func(b []byte) (any, error) { return DecodePartPresence(b) }, + `{"v":1,"online":false}`, PartPresence{V: 1}, + }, + "pulse": { + func() ([]byte, error) { + return EncodePulse(Pulse{Bucket: "mesh-delivery-assigner_wants", Key: "want.a.m", Revision: 7, Owes: "a"}) + }, + func(b []byte) (any, error) { return DecodePulse(b) }, + `{"v":1,"bucket":"mesh-delivery-assigner_wants","key":"want.a.m","revision":7,"owes":"a"}`, + Pulse{V: 1, Bucket: "mesh-delivery-assigner_wants", Key: "want.a.m", Revision: 7, Owes: "a"}, + }, + "stuck": { + func() ([]byte, error) { + return EncodeStuck(Stuck{Bucket: "mesh-delivery-watchman_presence", Key: "part.b.n", Waited: "b on n to come back", Since: since, Moved: true}) + }, + func(b []byte) (any, error) { return DecodeStuck(b) }, + `{"v":1,"bucket":"mesh-delivery-watchman_presence","key":"part.b.n","waited":"b on n to come back","since":"2026-10-25T11:52:58.051397201Z","moved":true}`, + Stuck{V: 1, Bucket: "mesh-delivery-watchman_presence", Key: "part.b.n", Waited: "b on n to come back", Since: since, Moved: true}, + }, + } { + t.Run(name, func(t *testing.T) { + raw, err := row.encode() + if err != nil { + t.Fatal(err) + } + sameJSON(t, raw, row.golden) + back, err := row.decode([]byte(row.golden)) + if err != nil || !reflect.DeepEqual(back, row.want) { + t.Fatalf("read %+v, %v; want %+v", back, err, row.want) + } + if _, err := row.decode([]byte(`{"v":2}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Fatalf("version 2: %v", err) + } + if _, err := row.decode([]byte(`{}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Fatalf("no version: %v", err) + } + }) + } + if !SameStuck(Stuck{V: 1, Since: since}, Stuck{V: 1, Since: since.In(time.FixedZone("x", 3600))}) || SameStuck(Stuck{Moved: true}, Stuck{}) { + t.Fatal("SameStuck") + } +} + +// TestTheKeysAreWhatTheEarlierCodecFormed holds the key forms and their refusals. +func TestTheKeysAreWhatTheEarlierCodecFormed(t *testing.T) { + if k, err := PresenceNodeKey("ace"); err != nil || k != "ace" { + t.Fatal(k, err) + } + if k, err := PresencePartKey("mesh-delivery-builder", "novox"); err != nil || k != "part.mesh-delivery-builder.novox" { + t.Fatal(k, err) + } + for _, bad := range []string{"part", "", "a.b"} { + if _, err := PresenceNodeKey(bad); err == nil { + t.Fatalf("node %q accepted", bad) + } + } + for key, want := range map[string][3]string{"ace": {"ace", "", ""}, "part.p.i": {"", "p", "i"}} { + n, p, i, err := ParsePresenceKey(key) + if err != nil || [3]string{n, p, i} != want { + t.Fatal(key, n, p, i, err) + } + } + for _, bad := range []string{"part.p", "x.p.i", "part..i", "a.b.c.d", "part"} { + if _, _, _, err := ParsePresenceKey(bad); err == nil { + t.Fatalf("presence key %q accepted", bad) + } + } + k, err := PulseKey("b_w", "want.a.m") + if err != nil || k != "b_w.want.a.m" { + t.Fatal(k, err) + } + if s, err := StuckKey("b_w", "want.a.m"); err != nil || s != k { + t.Fatal(s, err) + } + for _, f := range []func(string) (string, string, error){ParsePulseKey, ParseStuckKey} { + if b, key, err := f(k); err != nil || b != "b_w" || key != "want.a.m" { + t.Fatal(b, key, err) + } + for _, bad := range []string{"nodot", ".k", "b.", "b..k", "b.k."} { + if _, _, err := f(bad); err == nil { + t.Fatalf("pulse key %q accepted", bad) + } + } + } + for _, bad := range [][2]string{{"", "k"}, {"b.c", "k"}, {"b", ""}, {"b", "k..j"}, {"b", "k j"}} { + if _, err := PulseKey(bad[0], bad[1]); err == nil { + t.Fatalf("pulse key %q accepted", bad) + } + } + if ttl, stalled := PulseTTL(30 * time.Second); stalled || ttl != 90*time.Second { + t.Fatal(ttl, stalled) + } + if ttl, stalled := PulseTTL(PulseMoveBound); !stalled || ttl != 0 { + t.Fatal(ttl, stalled) + } +} -- 2.54.0 From 396ccc0e110f28304dda50a91998c0af23c54cab Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:45:22 +0100 Subject: [PATCH 83/94] =?UTF-8?q?Operator-channel=20client:=20the=20bench?= =?UTF-8?q?=20shapes=20tested=20against=20their=20written=20wire,=20with?= =?UTF-8?q?=20no=20mesh-sdk=20channel=20import=20(B6j;=20ADR=200634=20?= =?UTF-8?q?=C2=A75;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../operatorchannelclient/bench_test.go | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go index d720639..4cdc8fc 100644 --- a/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go +++ b/operator-and-conversation/go/clients/operatorchannelclient/bench_test.go @@ -4,38 +4,30 @@ import ( "bytes" "encoding/json" "testing" - "time" seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" - "git.novox.be/novox/mesh-sdk/go/channel" ) -// TestTheBenchShapesAreTheChannelsWire: each shape the benches channel and intake carry, written by mesh-sdk's channel -// package, reads into the contract's type with no field dropped and reads back as the same wire. +// TestTheBenchShapesAreTheChannelsWire: each shape the benches channel and intake carry, as the wire the benches speak +// (written out here, the contract's own), reads into the contract's type with no field dropped and reads back as the same wire. func TestTheBenchShapesAreTheChannelsWire(t *testing.T) { - at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) - who := channel.Sender{Identity: "42", Display: "Op", Authenticated: true} + sender := `{"identity":"42","display":"Op","authenticated":true}` cases := []struct { name string - sdk any + wire string into any }{ - {"message", channel.Message{Handle: "h1", Title: "t", Body: "b", Buttons: []channel.Button{{Label: "Approve", Ticket: "tk"}}, - Note: "n", Urgent: true, Silent: true, Reply: "r", To: "42", Secret: true}, &seat.Message{}}, - {"a send", channel.Message{Handle: "h2", Title: "t", Body: "b"}, &seat.Message{}}, - {"choice", channel.Choice{ID: "c1", Ticket: "tk", Handle: "h1", Sender: who, At: at}, &seat.Choice{}}, - {"link", channel.LinkAsked{ID: "l1", Sender: who, At: at}, &seat.LinkAsked{}}, - {"failed", channel.Failed{Handle: "h1", Why: "blocked", Permanent: true, At: at}, &seat.Failed{}}, - {"standing", channel.Standing{Ready: true, Why: "w", EditsSilently: true, At: at}, &seat.Standing{}}, - {"code", channel.Code{Sender: who, Code: "123456", Purpose: "approve"}, &seat.Code{}}, - {"proof answer", channel.ProofAnswer{Accepted: true, Words: "ok"}, &seat.ProofAnswer{}}, + {"message", `{"handle":"h1","title":"t","body":"b","buttons":[{"label":"Approve","ticket":"tk"}],"note":"n","urgent":true,"silent":true,"reply":"r","to":"42","secret":true}`, &seat.Message{}}, + {"a send", `{"handle":"h2","title":"t","body":"b"}`, &seat.Message{}}, + {"choice", `{"id":"c1","ticket":"tk","handle":"h1","sender":` + sender + `,"at":"2026-11-05T12:00:00Z"}`, &seat.Choice{}}, + {"link", `{"id":"l1","sender":` + sender + `,"at":"2026-11-05T12:00:00Z"}`, &seat.LinkAsked{}}, + {"failed", `{"handle":"h1","why":"blocked","permanent":true,"at":"2026-11-05T12:00:00Z"}`, &seat.Failed{}}, + {"standing", `{"ready":true,"why":"w","edits-silently":true,"at":"2026-11-05T12:00:00Z"}`, &seat.Standing{}}, + {"code", `{"sender":` + sender + `,"code":"123456","purpose":"approve"}`, &seat.Code{}}, + {"proof answer", `{"accepted":true,"words":"ok"}`, &seat.ProofAnswer{}}, } for _, c := range cases { - wire, err := json.Marshal(c.sdk) - if err != nil { - t.Fatal(err) - } - dec := json.NewDecoder(bytes.NewReader(wire)) + dec := json.NewDecoder(bytes.NewReader([]byte(c.wire))) dec.DisallowUnknownFields() if err := dec.Decode(c.into); err != nil { t.Errorf("%s: %v", c.name, err) @@ -44,9 +36,9 @@ func TestTheBenchShapesAreTheChannelsWire(t *testing.T) { back, _ := json.Marshal(c.into) var got, want any _ = json.Unmarshal(back, &got) - _ = json.Unmarshal(wire, &want) + _ = json.Unmarshal([]byte(c.wire), &want) if !jsonEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, wire) + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) } } } @@ -57,17 +49,17 @@ func jsonEqual(a, b any) bool { return bytes.Equal(x, y) } -// TestTheBenchSubjectsAreTheChannelsOwn: the subjects and words are mesh-sdk channel's, character for character. +// TestTheBenchSubjectsAreTheChannelsOwn: the subjects and words, character for character. func TestTheBenchSubjectsAreTheChannelsOwn(t *testing.T) { for _, c := range [][2]string{ - {ChannelSubject(Show, "telegram"), channel.ChannelSubject(channel.Show, "telegram")}, - {ChannelSubject(Edit, "desk"), channel.ChannelSubject(channel.Edit, "desk")}, - {ChannelSubject(Send, "desk"), channel.ChannelSubject(channel.Send, "desk")}, - {IntakeSubject(Chosen, "telegram"), channel.IntakeSubject(channel.Chosen, "telegram")}, - {IntakeSubject(Link, "telegram"), channel.IntakeSubject(channel.Link, "telegram")}, - {IntakeSubject(FailedWhat, "desk"), channel.IntakeSubject(channel.FailedWhat, "desk")}, - {IntakeSubject(StandingWhat, "desk"), channel.IntakeSubject(channel.StandingWhat, "desk")}, - {ProofSubject(CodeProof, "telegram"), channel.ProofSubject(channel.CodeProof, "telegram")}, + {ChannelSubject(Show, "telegram"), "mesh.seat.channel.accept.show.telegram"}, + {ChannelSubject(Edit, "desk"), "mesh.seat.channel.accept.edit.desk"}, + {ChannelSubject(Send, "desk"), "mesh.seat.channel.accept.send.desk"}, + {IntakeSubject(Chosen, "telegram"), "mesh.seat.intake.event.choice.telegram"}, + {IntakeSubject(Link, "telegram"), "mesh.seat.intake.event.link.telegram"}, + {IntakeSubject(FailedWhat, "desk"), "mesh.seat.intake.event.failed.desk"}, + {IntakeSubject(StandingWhat, "desk"), "mesh.seat.intake.event.standing.desk"}, + {ProofSubject(CodeProof, "telegram"), "mesh.seat.intake.proof.code.telegram"}, } { if c[0] != c[1] { t.Errorf("subject %q, want %q", c[0], c[1]) -- 2.54.0 From 6b8c6e2721adefe4440cc706c5b286c9f25eebdf Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:46:25 +0100 Subject: [PATCH 84/94] =?UTF-8?q?Conditions=20client:=20Observation,=20Rai?= =?UTF-8?q?se,=20Clear,=20Says,=20EncodeFinding=20and=20DecodeFinding=20ra?= =?UTF-8?q?ise=20and=20clear=20an=20observer's=20finding=20in=20the=20seat?= =?UTF-8?q?'s=20layout,=20goldens=20from=20go/health's=20output=20(B5l;=20?= =?UTF-8?q?ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshconditionsclient/client_test.go | 84 ++++++++++ .../meshconditionsclient/observation.go | 147 ++++++++++++++++++ .../testdata/finding_full.json | 1 + .../testdata/finding_minimal.json | 1 + 4 files changed, 233 insertions(+) create mode 100644 health-and-repair/go/clients/meshconditionsclient/observation.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json create mode 100644 health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json diff --git a/health-and-repair/go/clients/meshconditionsclient/client_test.go b/health-and-repair/go/clients/meshconditionsclient/client_test.go index c9ba6df..f96ebf8 100644 --- a/health-and-repair/go/clients/meshconditionsclient/client_test.go +++ b/health-and-repair/go/clients/meshconditionsclient/client_test.go @@ -1,6 +1,8 @@ package meshconditionsclient import ( + "os" + "reflect" "strings" "testing" "time" @@ -109,3 +111,85 @@ func when(s string) time.Time { } func ptr(t time.Time) *time.Time { return &t } + +type fakeBucket struct { + kept map[string][]byte + puts, dels int +} + +func (b *fakeBucket) Get(key string) ([]byte, bool, error) { v, ok := b.kept[key]; return v, ok, nil } +func (b *fakeBucket) Put(key string, v []byte) error { + if b.kept == nil { + b.kept = map[string][]byte{} + } + b.kept[key] = v + b.puts++ + return nil +} +func (b *fakeBucket) Delete(key string) error { delete(b.kept, key); b.dels++; return nil } + +// The goldens are what mesh-sdk's go/health EncodeFinding wrote for the same findings: the helpers write the same +// values, read by the seat's strict decoder. +func TestAnObservationIsWrittenAsMeshSDKsHealthWroteItsFinding(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + for _, c := range []struct { + golden string + o Observation + run string + }{ + {"finding_full.json", Observation{Scope: ScopeMachine, ID: "g14", Kind: "silent-kind", Ref: "silent", Machine: "g14", Also: []string{"ace"}, + Severity: Urgent, Summary: "g14 is silent", Said: "no pulse", Headline: "g14 is silent", Explanation: "It said nothing.", + Resolved: "It speaks.", Needs: "look", Source: "S1", Resolver: "operator"}, "r1"}, + {"finding_minimal.json", Observation{Scope: ScopeModule, ID: "nats.g14", Kind: "build-failed", Summary: "failed", + Source: "builder.build-failed"}, ""}, + } { + raw, err := os.ReadFile("testdata/" + c.golden) + if err != nil { + t.Fatal(err) + } + want, err := seat.DecodeFindingsByKeyValue(raw) + if err != nil { + t.Fatalf("%s: %v", c.golden, err) + } + observer := want.Observer + got, err := EncodeFinding(c.o.Finding(observer, c.run, at)) + if err != nil { + t.Fatalf("%s: %v", c.golden, err) + } + have, err := seat.DecodeFindingsByKeyValue(got) + if err != nil || !reflect.DeepEqual(have, want) { + t.Errorf("%s: wrote %s, want %s (%v)", c.golden, got, raw, err) + } + if back, err := DecodeFinding(raw); err != nil || !reflect.DeepEqual(back, want) { + t.Errorf("%s: read back %v", c.golden, err) + } + } +} + +func TestRaiseWritesWhenTheWordsChangeAndClearDeletesWhatIsThere(t *testing.T) { + b := &fakeBucket{} + o := Observation{Scope: ScopeModule, ID: "m", Kind: "k", Summary: "one", Source: "x.k"} + at := when("2026-11-05T12:00:00Z") + if w, err := Raise(b, "obs", "", o, at); err != nil || !w { + t.Fatalf("first: %v %v", w, err) + } + if w, err := Raise(b, "obs", "r2", o, at.Add(time.Hour)); err != nil || w || b.puts != 1 { + t.Fatalf("the same words later: wrote %v, %d puts, %v", w, b.puts, err) + } + o.Summary = "two" + if w, err := Raise(b, "obs", "", o, at); err != nil || !w || b.puts != 2 { + t.Fatalf("changed: %v %d %v", w, b.puts, err) + } + if _, err := Raise(b, "obs", "", Observation{Scope: "moon", ID: "m", Kind: "k"}, at); err == nil || b.puts != 2 { + t.Errorf("a finding the keeper cannot use was written: %v", err) + } + if c, err := Clear(b, ScopeModule, "m", "k"); err != nil || !c || b.dels != 1 { + t.Fatalf("clear: %v %v %d", c, err, b.dels) + } + if c, err := Clear(b, ScopeModule, "m", "k"); err != nil || c || b.dels != 1 { + t.Errorf("clearing what is not there: %v %v %d", c, err, b.dels) + } + if FindingsBucket("obs") != "obs_findings" { + t.Error(FindingsBucket("obs")) + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/observation.go b/health-and-repair/go/clients/meshconditionsclient/observation.go new file mode 100644 index 0000000..e8bc379 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/observation.go @@ -0,0 +1,147 @@ +package meshconditionsclient + +import ( + "encoding/json" + "fmt" + "time" + + seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" +) + +// What an observer says it sees wrong, raised and cleared as a finding in its own state `findings` (novox/hq ADR 0564 +// clause 2, ADR 0634 §4): one write shared by every observer (the self-check, the healers, the watchman, the builder, +// the indexer, the node-engine), in this seat's layout. The condition keeper raises and clears the conditions from them. + +// FindingsState is the name of an observer's state of findings. +const FindingsState = "findings" + +// FindingsBucket is the bucket of an observer's findings, `_findings`. +func FindingsBucket(observer string) string { return observer + "_" + FindingsState } + +// Observation is one thing an observer sees wrong now: what it is about (Scope, ID, Machine, Also), which kind of fault +// (Kind; Ref when the key's last part is not the kind), and its words. +type Observation struct { + Scope, ID, Kind, Ref, Machine string + Also []string + // Severity is Urgent or Warning; Warning when empty. + Severity string + // Summary is one line; Said the one observation kept as evidence (the summary when empty). + Summary, Said string + Headline, Explanation, Resolved, Needs string + // Source is the row, probe or event that saw it. + Source string + // Resolver is who resolves it, self when empty. + Resolver string + Actions []seat.Action + // Since is when the finding is stamped; the zero value leaves it to the caller's moment. + Since time.Time +} + +// Key is the finding's key: `..`, the token Ref or else Kind. +func (o Observation) Key() string { + token := o.Ref + if token == "" { + token = o.Kind + } + return Key(o.Scope, o.ID, token) +} + +// Finding is the observation as the seat's finding written by observer in run, at the moment at. +func (o Observation) Finding(observer, run string, at time.Time) seat.FindingsByKeyValue { + at = at.UTC() + said := o.Said + if said == "" { + said = o.Summary + } + sev := o.Severity + if sev == "" { + sev = Warning + } + f := seat.FindingsByKeyValue{V: FindingVersion, Key: o.Key(), Kind: o.Kind, + Subject: seat.Subject{Scope: o.Scope, ID: o.ID, Machine: o.Machine, Also: o.Also}, + Severity: sev, Summary: o.Summary, Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, + Needs: o.Needs, Actions: o.Actions, Evidence: []seat.Evidence{{At: at, Said: said}}, Source: o.Source, + Resolver: o.Resolver, Observer: observer, Run: run, At: at} + if o.Ref != "" && o.Ref != o.Kind { + f.Ref = o.Ref + } + return f +} + +// EncodeFinding is f as JSON at FindingVersion; it refuses what CheckFinding refuses. +func EncodeFinding(f seat.FindingsByKeyValue) ([]byte, error) { + f.V = FindingVersion + if f.Evidence == nil { + f.Evidence = []seat.Evidence{} + } + if err := CheckFinding(f); err != nil { + return nil, err + } + return json.Marshal(f) +} + +// DecodeFinding reads a finding's value; it refuses another version and what CheckFinding refuses. A finding written +// before the field was named `ref` (novox/hq issue 1952) is read by its old name, `token`. +func DecodeFinding(data []byte) (seat.FindingsByKeyValue, error) { + f, err := seat.DecodeFindingsByKeyValue(data) + if err != nil { + return seat.FindingsByKeyValue{}, fmt.Errorf("health: a finding is not readable: %w", err) + } + if f.V != FindingVersion { + return seat.FindingsByKeyValue{}, fmt.Errorf("health: a finding of version %d, and this reads %d", f.V, FindingVersion) + } + if err := CheckFinding(f); err != nil { + return seat.FindingsByKeyValue{}, err + } + return f, nil +} + +// Says is what a finding says, the moments and the run that wrote it aside: two findings that say the same are one +// write (novox/hq ADR 0289 §1), so the condition moves only when the words do. +func Says(f seat.FindingsByKeyValue) string { + f.At, f.Run, f.V = time.Time{}, "", 0 + ev := make([]seat.Evidence, len(f.Evidence)) + for i, e := range f.Evidence { + ev[i] = seat.Evidence{Said: e.Said} + } + f.Evidence = ev + raw, _ := json.Marshal(f) + return string(raw) +} + +// FindingBucket is the part of an observer's state `findings` that Raise and Clear use. +type FindingBucket interface { + // Get answers the value at key; found is false when there is none. + Get(key string) (value []byte, found bool, err error) + Put(key string, value []byte) error + Delete(key string) error +} + +// Raise writes the observation as observer's finding in run at at, only when what it says differs from the finding kept +// (so the keeper's condition moves only then); wrote says whether it wrote. A finding the keeper could not make a +// condition of is refused, nothing written. +func Raise(b FindingBucket, observer, run string, o Observation, at time.Time) (wrote bool, err error) { + f := o.Finding(observer, run, at) + value, err := EncodeFinding(f) + if err != nil { + return false, err + } + if kept, found, err := b.Get(f.Key); err != nil { + return false, err + } else if found { + if old, err := DecodeFinding(kept); err == nil && Says(old) == Says(f) { + return false, nil + } + } + return true, b.Put(f.Key, value) +} + +// Clear deletes the finding about scope and id of kind (its token); one not there is nothing to clear, and cleared says +// whether one was. +func Clear(b FindingBucket, scope, id, token string) (cleared bool, err error) { + key := Key(scope, id, token) + if _, found, err := b.Get(key); err != nil || !found { + return false, err + } + return true, b.Delete(key) +} diff --git a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json new file mode 100644 index 0000000..54bf2f8 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_full.json @@ -0,0 +1 @@ +{"v":1,"key":"machine.g14.silent","kind":"silent-kind","ref":"silent","subject":{"scope":"machine","id":"g14","machine":"g14","also":["ace"]},"severity":"urgent","summary":"g14 is silent","headline":"g14 is silent","explanation":"It said nothing.","resolved":"It speaks.","needs":"look","evidence":[{"at":"2026-11-05T12:00:00Z","said":"no pulse"}],"source":"S1","resolver":"operator","observer":"mesh-delivery-watchman","run":"r1","at":"2026-11-05T12:00:00Z"} diff --git a/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json new file mode 100644 index 0000000..e0fef12 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/testdata/finding_minimal.json @@ -0,0 +1 @@ +{"v":1,"key":"module.nats.g14.build-failed","kind":"build-failed","subject":{"scope":"module","id":"nats.g14"},"severity":"warning","summary":"failed","headline":"","explanation":"","resolved":"","evidence":[{"at":"2026-11-05T12:00:00Z","said":"failed"}],"source":"builder.build-failed","observer":"mesh-delivery-builder","at":"2026-11-05T12:00:00Z"} -- 2.54.0 From 036f39a5a53134b37d80637120bd7199aa65ae3b Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:56:39 +0000 Subject: [PATCH 85/94] Require every mesh package of the group at the floor version (ADR 0573 sections 2 and 4, issue 1966) A member's go.mod names the oldest version a module accepts; the check builds the group's own heads whatever it names. go.sum lines of mesh packages are left out. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/meshconfigclient/go.mod | 2 +- go/clients/nodeengineclient/go.mod | 5 ++--- go/clients/nodeengineclient/go.sum | 6 ------ health-and-repair/go/clients/meshconditionsclient/go.mod | 4 ++-- health-and-repair/go/clients/meshhandactsclient/go.mod | 4 ++-- health-and-repair/go/clients/meshwatchdogsclient/go.mod | 4 ++-- .../go/clients/meshnodeenrollerclient/go.mod | 4 ++++ identity-and-access/go/clients/meshvaultclient/go.mod | 4 ++-- .../go/clients/operatorchannelclient/go.mod | 4 ++-- .../go/clients/operatorchannelclient/go.sum | 6 ------ placement/go/clients/meshmoduleassignerclient/go.mod | 4 ++-- placement/go/clients/meshwantsclient/go.mod | 4 ++-- provisioning/go/clients/meshprovisionerclient/go.mod | 2 +- 13 files changed, 22 insertions(+), 31 deletions(-) diff --git a/go/clients/meshconfigclient/go.mod b/go/clients/meshconfigclient/go.mod index fc30b01..69546e4 100644 --- a/go/clients/meshconfigclient/go.mod +++ b/go/clients/meshconfigclient/go.mod @@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-contracts/go/clients/meshconfigclient go 1.26.0 -require git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-20261105000000-000000000000 +require git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-00010101000000-000000000000 diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod index cae4c9e..a9b7b4d 100644 --- a/go/clients/nodeengineclient/go.mod +++ b/go/clients/nodeengineclient/go.mod @@ -3,8 +3,7 @@ module git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef + git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) -require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a // indirect diff --git a/go/clients/nodeengineclient/go.sum b/go/clients/nodeengineclient/go.sum index 5c5298e..0daef11 100644 --- a/go/clients/nodeengineclient/go.sum +++ b/go/clients/nodeengineclient/go.sum @@ -1,9 +1,3 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a h1:ZdYc+f3A8l2oDjN+LMngI8grXvT01Ud75/gPIAnuTko= -git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104172218-347a3053580a/go.mod h1:TB20TsQuOd5aer+cXL6PbaYyg2RvgL1R4MzD61Oks3A= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index f2409b8..2126130 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondit go 1.26.0 -require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef +require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod index 67224be..ec41128 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.mod +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -2,6 +2,6 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandac go 1.26.0 -require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-20261105000000-000000000000 +require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef +require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 diff --git a/health-and-repair/go/clients/meshwatchdogsclient/go.mod b/health-and-repair/go/clients/meshwatchdogsclient/go.mod index 5d59e6d..a10a1e7 100644 --- a/health-and-repair/go/clients/meshwatchdogsclient/go.mod +++ b/health-and-repair/go/clients/meshwatchdogsclient/go.mod @@ -3,6 +3,6 @@ module git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchd go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-20261105000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/go.mod b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod index 4b99275..784fe46 100644 --- a/identity-and-access/go/clients/meshnodeenrollerclient/go.mod +++ b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod @@ -1,3 +1,7 @@ module git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000 // indirect +) diff --git a/identity-and-access/go/clients/meshvaultclient/go.mod b/identity-and-access/go/clients/meshvaultclient/go.mod index 39ad4cc..eaf86d5 100644 --- a/identity-and-access/go/clients/meshvaultclient/go.mod +++ b/identity-and-access/go/clients/meshvaultclient/go.mod @@ -3,6 +3,6 @@ module git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaul go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-20261105000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) diff --git a/operator-and-conversation/go/clients/operatorchannelclient/go.mod b/operator-and-conversation/go/clients/operatorchannelclient/go.mod index dedb474..0e7a39e 100644 --- a/operator-and-conversation/go/clients/operatorchannelclient/go.mod +++ b/operator-and-conversation/go/clients/operatorchannelclient/go.mod @@ -3,8 +3,8 @@ module git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/op go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef + git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) require ( diff --git a/operator-and-conversation/go/clients/operatorchannelclient/go.sum b/operator-and-conversation/go/clients/operatorchannelclient/go.sum index e8e153a..ee87eab 100644 --- a/operator-and-conversation/go/clients/operatorchannelclient/go.sum +++ b/operator-and-conversation/go/clients/operatorchannelclient/go.sum @@ -1,9 +1,3 @@ -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a h1:E9ERPqR4uZXypudHGxv2/wNIOYcder9torVqJwJq/DI= -git.novox.be/novox/mesh-contracts/go v0.0.0-20261104172218-347a3053580a/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a h1:UzJfgbyGlZJzsOFevu/c7stv1ccejrS4nAMfYytSd8s= -git.novox.be/novox/mesh-contracts/go/seats/operatorchannel v0.0.0-20261104172218-347a3053580a/go.mod h1:osh0jQyNqYDM27tbxQo6HeUEXgLq9XUewKzNhUXadog= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef h1:/Jic5ho0xz32Z9DH2NSUqVxCF1KyfIOWuRTrX1aLjus= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef/go.mod h1:2wHG7ntLCG8Tpm/SCfgp524a5emUJm1gAC4KrRYsU9s= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/placement/go/clients/meshmoduleassignerclient/go.mod b/placement/go/clients/meshmoduleassignerclient/go.mod index 2ebb4aa..45deaaf 100644 --- a/placement/go/clients/meshmoduleassignerclient/go.mod +++ b/placement/go/clients/meshmoduleassignerclient/go.mod @@ -3,6 +3,6 @@ module git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassigner go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-20261105000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) diff --git a/placement/go/clients/meshwantsclient/go.mod b/placement/go/clients/meshwantsclient/go.mod index 0cc33be..26ce723 100644 --- a/placement/go/clients/meshwantsclient/go.mod +++ b/placement/go/clients/meshwantsclient/go.mod @@ -3,6 +3,6 @@ module git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient go 1.26.0 require ( - git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-20261105000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) diff --git a/provisioning/go/clients/meshprovisionerclient/go.mod b/provisioning/go/clients/meshprovisionerclient/go.mod index b6fdcd2..10f86d6 100644 --- a/provisioning/go/clients/meshprovisionerclient/go.mod +++ b/provisioning/go/clients/meshprovisionerclient/go.mod @@ -2,4 +2,4 @@ module git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisioner go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174118-f94abb95aaef +require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -- 2.54.0 From a2a321dfa2296a60cd0ce91836d7be1ccfafc24f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 18:11:56 +0100 Subject: [PATCH 86/94] Require the external packages a module's build reaches (issue 1966) Versions are the ones the workspace already resolves, with their go.sum lines. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/clients/meshconditionsclient/go.mod | 9 +++++++++ .../go/clients/meshconditionsclient/go.sum | 12 ++++++++++++ .../go/clients/meshhandactsclient/go.mod | 9 +++++++++ .../go/clients/meshhandactsclient/go.sum | 12 ++++++++++++ provisioning/go/clients/meshprovisionerclient/go.mod | 9 +++++++++ provisioning/go/clients/meshprovisionerclient/go.sum | 12 ++++++++++++ 6 files changed, 63 insertions(+) create mode 100644 health-and-repair/go/clients/meshconditionsclient/go.sum create mode 100644 health-and-repair/go/clients/meshhandactsclient/go.sum create mode 100644 provisioning/go/clients/meshprovisionerclient/go.sum diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index 2126130..8843c1e 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -5,3 +5,12 @@ go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/health-and-repair/go/clients/meshconditionsclient/go.sum b/health-and-repair/go/clients/meshconditionsclient/go.sum new file mode 100644 index 0000000..0daef11 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/go.sum @@ -0,0 +1,12 @@ +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/health-and-repair/go/clients/meshhandactsclient/go.mod b/health-and-repair/go/clients/meshhandactsclient/go.mod index ec41128..0b779d9 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.mod +++ b/health-and-repair/go/clients/meshhandactsclient/go.mod @@ -5,3 +5,12 @@ go 1.26.0 require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/health-and-repair/go/clients/meshhandactsclient/go.sum b/health-and-repair/go/clients/meshhandactsclient/go.sum new file mode 100644 index 0000000..0daef11 --- /dev/null +++ b/health-and-repair/go/clients/meshhandactsclient/go.sum @@ -0,0 +1,12 @@ +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/provisioning/go/clients/meshprovisionerclient/go.mod b/provisioning/go/clients/meshprovisionerclient/go.mod index 10f86d6..6ef2f66 100644 --- a/provisioning/go/clients/meshprovisionerclient/go.mod +++ b/provisioning/go/clients/meshprovisionerclient/go.mod @@ -3,3 +3,12 @@ module git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisioner go 1.26.0 require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 + +require ( + github.com/klauspost/compress v1.20.1 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/provisioning/go/clients/meshprovisionerclient/go.sum b/provisioning/go/clients/meshprovisionerclient/go.sum new file mode 100644 index 0000000..0daef11 --- /dev/null +++ b/provisioning/go/clients/meshprovisionerclient/go.sum @@ -0,0 +1,12 @@ +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -- 2.54.0 From 70fe186d7f5743e61f3ce2bbd97a8bfabc5b596e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 18:23:43 +0100 Subject: [PATCH 87/94] =?UTF-8?q?Move=20node-service-manager,=20node-packa?= =?UTF-8?q?ge-manager,=20node-container-runtime=20and=20node-environment?= =?UTF-8?q?=20to=20mesh-placement-contracts:=20their=20domain=20is=20Place?= =?UTF-8?q?ment=20(ADR=200635=20=C2=A72,=20ADR=200636=20=C2=A75;=20issue?= =?UTF-8?q?=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- placement/seats/index.json | 6 +++++- {seats => placement/seats}/node-container-runtime/seat.json | 0 {seats => placement/seats}/node-environment/seat.json | 0 {seats => placement/seats}/node-package-manager/seat.json | 0 {seats => placement/seats}/node-service-manager/seat.json | 0 seats/index.json | 4 ---- 6 files changed, 5 insertions(+), 5 deletions(-) rename {seats => placement/seats}/node-container-runtime/seat.json (100%) rename {seats => placement/seats}/node-environment/seat.json (100%) rename {seats => placement/seats}/node-package-manager/seat.json (100%) rename {seats => placement/seats}/node-service-manager/seat.json (100%) diff --git a/placement/seats/index.json b/placement/seats/index.json index e7d0756..6c07971 100644 --- a/placement/seats/index.json +++ b/placement/seats/index.json @@ -2,6 +2,10 @@ "format": 1, "seats": [ "mesh-module-assigner", - "mesh-wants" + "mesh-wants", + "node-service-manager", + "node-package-manager", + "node-container-runtime", + "node-environment" ] } diff --git a/seats/node-container-runtime/seat.json b/placement/seats/node-container-runtime/seat.json similarity index 100% rename from seats/node-container-runtime/seat.json rename to placement/seats/node-container-runtime/seat.json diff --git a/seats/node-environment/seat.json b/placement/seats/node-environment/seat.json similarity index 100% rename from seats/node-environment/seat.json rename to placement/seats/node-environment/seat.json diff --git a/seats/node-package-manager/seat.json b/placement/seats/node-package-manager/seat.json similarity index 100% rename from seats/node-package-manager/seat.json rename to placement/seats/node-package-manager/seat.json diff --git a/seats/node-service-manager/seat.json b/placement/seats/node-service-manager/seat.json similarity index 100% rename from seats/node-service-manager/seat.json rename to placement/seats/node-service-manager/seat.json diff --git a/seats/index.json b/seats/index.json index 342ca73..6d96506 100644 --- a/seats/index.json +++ b/seats/index.json @@ -4,10 +4,6 @@ "mesh-store", "mesh-broker", "mesh-config", - "node-service-manager", - "node-package-manager", - "node-container-runtime", - "node-environment", "node-login-shell", "node-power", "node-hotkeys", -- 2.54.0 From 828b5b1d38c4dcafa315b368649123d95f678dbe Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 18:37:28 +0100 Subject: [PATCH 88/94] No client names a holder: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../clients/meshmoduleassignerclient/reads.go | 20 +------------------ .../meshmoduleassignerclient/reads_test.go | 9 --------- .../clients/meshprovisionerclient/client.go | 8 +++++--- .../meshprovisionerclient/client_test.go | 9 ++++++--- 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/placement/go/clients/meshmoduleassignerclient/reads.go b/placement/go/clients/meshmoduleassignerclient/reads.go index 99fa9fd..f892d87 100644 --- a/placement/go/clients/meshmoduleassignerclient/reads.go +++ b/placement/go/clients/meshmoduleassignerclient/reads.go @@ -1,24 +1,6 @@ package meshmoduleassignerclient -import ( - "sort" - - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" -) - -// The holder of the seat and the names a reader of its states declares in its `reads` (`.`) and binds -// to (`_`): named here once, by the seat's state, so no reader builds them from a module's name -// (ADR 0634 §4; the lead's ruling of 2026-11-05). -const ( - // Module is the module that holds the seat mesh-module-assigner. - Module = "mesh-delivery-assigner" - // PlacementRead and SeatHoldersRead are the states `placement` and `seat-holders`, as a manifest's `reads` names them. - PlacementRead = Module + "." + seat.StatePlacement - SeatHoldersRead = Module + "." + seat.StateSeatHolders - // PlacementBucket and SeatHoldersBucket are the states' buckets. - PlacementBucket = Module + "_" + seat.StatePlacement - SeatHoldersBucket = Module + "_" + seat.StateSeatHolders -) +import "sort" // MachineNames is every machine the seat places modules on, sorted, from its state `placement` as a reader holds its // keys: each with a node key `.node` or, in the form before, `placement.`. Module keys and keys of the diff --git a/placement/go/clients/meshmoduleassignerclient/reads_test.go b/placement/go/clients/meshmoduleassignerclient/reads_test.go index 40223bd..f531b6d 100644 --- a/placement/go/clients/meshmoduleassignerclient/reads_test.go +++ b/placement/go/clients/meshmoduleassignerclient/reads_test.go @@ -5,15 +5,6 @@ import ( "testing" ) -func TestTheStatesAreNamedBySeatAndState(t *testing.T) { - for got, want := range map[string]string{PlacementRead: "mesh-delivery-assigner.placement", SeatHoldersRead: "mesh-delivery-assigner.seat-holders", - PlacementBucket: "mesh-delivery-assigner_placement", SeatHoldersBucket: "mesh-delivery-assigner_seat-holders"} { - if got != want { - t.Errorf("got %q, want %q", got, want) - } - } -} - func TestMachineNamesAreTheNodesAKeyNames(t *testing.T) { keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "placement.novox": nil, "want.x.y": nil, "bad": nil} if got, want := MachineNames(keys), []string{"ace", "g14", "novox"}; !reflect.DeepEqual(got, want) { diff --git a/provisioning/go/clients/meshprovisionerclient/client.go b/provisioning/go/clients/meshprovisionerclient/client.go index 32a0ed6..2074ef1 100644 --- a/provisioning/go/clients/meshprovisionerclient/client.go +++ b/provisioning/go/clients/meshprovisionerclient/client.go @@ -126,15 +126,17 @@ func VaultName(form string, p Provision) string { return "" } -// VaultModule is the provider of every provision's credential (novox/hq ADR 0570 clauses 1, 6). -const VaultModule = "mesh-vault" +// SecretProvision is the provision the seat mesh-vault delivers: the vault's own offer, and the one provision with no +// credential beside it (novox/hq ADR 0570 clauses 1, 6). The client names the provision, never the module that holds +// the seat (novox/hq ADR 0625 §4). +const SecretProvision = "secret" // CredentialKey is the key of a provision's credential, itself a provision whose provider is the vault (clause 6; the // key form the lead settled on 2026-10-29): `..secret-[.]` for a value of the // consumer's own (pair, issued, given), and `..secret-` for the provider's one // shared value. False for a form with no vault value, and for a provision the vault itself provides. func CredentialKey(form string, consumer, provider End, provision, local string) (string, bool) { - if provider.Module == VaultModule { + if provision == SecretProvision { return "", false // the vault's own offer is the value: it has no credential beside it (the lead, 2026-10-29) } name := catalogue.CredentialProvisionPrefix + provision diff --git a/provisioning/go/clients/meshprovisionerclient/client_test.go b/provisioning/go/clients/meshprovisionerclient/client_test.go index 8461091..bb2bdaa 100644 --- a/provisioning/go/clients/meshprovisionerclient/client_test.go +++ b/provisioning/go/clients/meshprovisionerclient/client_test.go @@ -111,9 +111,12 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { } } // The vault's own offer is the value: no secret-secret beside it. - vaultEntry := catalogue.PlanProvision{Provision: "secret", Provider: "novox", ProviderModule: VaultModule, CredentialForm: FormPair} - if got := DependsOn(keycloak, vaultEntry, nil); len(got) != 0 { - t.Errorf("the vault's secret depends on %v", got) + // It is the seat's provision that has none, whatever module holds the seat (ADR 0625 §4). + for _, holder := range []string{"mesh-vault", "another-holder"} { + vaultEntry := catalogue.PlanProvision{Provision: SecretProvision, Provider: "novox", ProviderModule: holder, CredentialForm: FormPair} + if got := DependsOn(keycloak, vaultEntry, nil); len(got) != 0 { + t.Errorf("the secret provided by %s depends on %v", holder, got) + } } if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) -- 2.54.0 From 276e5e2aefbb6c58755fcbbd69cedece5e2447cd Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 19:22:05 +0100 Subject: [PATCH 89/94] The delivery SDK's enginereads, findings and heard leave for the clients of their seats (ADR 0634 sections 1 and 2, issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../enginereads/enginereads.go | 643 ++++++++++++++++++ .../enginereads/enginereads_test.go | 40 ++ .../meshconditionsclient/findings/findings.go | 88 +++ .../findings/findings_test.go | 71 ++ .../go/clients/meshconditionsclient/go.mod | 2 +- .../meshconditionsclient/heard/heard.go | 115 ++++ .../meshconditionsclient/heard/heard_test.go | 36 + 7 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 go/clients/nodeengineclient/enginereads/enginereads.go create mode 100644 go/clients/nodeengineclient/enginereads/enginereads_test.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/findings/findings.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/findings/findings_test.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/heard/heard.go create mode 100644 health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go diff --git a/go/clients/nodeengineclient/enginereads/enginereads.go b/go/clients/nodeengineclient/enginereads/enginereads.go new file mode 100644 index 0000000..4ae20b2 --- /dev/null +++ b/go/clients/nodeengineclient/enginereads/enginereads.go @@ -0,0 +1,643 @@ +// Package enginereads is how a mesh-delivery part learns what each node's engine reads of a manifest (novox/hq ADR 0618 +// §3, §4): it asks the node seat node-engine's verb manifest-reads, on the part's start, on the seat's event +// manifest-reads-changed for that node and when a person asks again, and keeps each node's last answer in a state of its +// own, one key per node, so a node that is away keeps the statement it last made (ADR 0624 §1). The assigner, the +// builder and the delivery view share it; it is the seat node-engine's client's, not mesh-sdk's (ADR 0634 §1, §2). +package enginereads + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "sort" + "strings" + "sync" + "time" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/busretry" + + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" +) + +// The verb and the event as the seat's contract names them (mesh-contracts seats/nodeengine), as a manifest names them, +// and the subject the event is said on with the engine's machine last. +const ( + Verb = nodeengine.VerbManifestReads + Event = nodeengine.EventManifestReadsChanged + Invoke = "seat:" + nodeengine.Seat + "." + Verb + Consumes = nodeengine.Seat + "." + Event +) + +// SubjectPrefix is the event's subject up to the machine, its last token. +var SubjectPrefix = strings.TrimSuffix(nodeengine.SubjectManifestReadsChanged("*"), "*") + +// The reasons an engine says its reads, in the event's why (ADR 0618 §4). +const ( + // ReadsStarted: the engine started, or what it states changed. + ReadsStarted = "started" + // ReadsReturned: the engine's node returned after being away. + ReadsReturned = "returned" +) + +// ContractVersion is the layout version of the verb's answer and the event's body this reader reads. +const ContractVersion = 1 + +// checkNode refuses a node that is no subject token. +func checkNode(node string) error { + if node == "" || strings.ContainsAny(node, ".*> \t\n") { + return fmt.Errorf("%q is no node", node) + } + return nil +} + +// checkReads refuses an empty reader capability. +func checkReads(node string, reads []string) error { + for _, c := range reads { + if strings.TrimSpace(c) == "" { + return fmt.Errorf("the reads of %s name an empty reader capability", node) + } + } + return nil +} + +// DecodeAnswer reads the verb's answer as the contract lays it out, and judges it: the layout's version, the node a +// name, no empty capability, and its stated-at an RFC 3339 time (the contract carries no checks; its reader does). +func DecodeAnswer(raw []byte) (nodeengine.ManifestReadsAnswer, time.Time, error) { + var a nodeengine.ManifestReadsAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return a, time.Time{}, err + } + if a.V != ContractVersion { + return a, time.Time{}, fmt.Errorf("the answer's layout is version %d, not %d", a.V, ContractVersion) + } + if err := checkNode(a.Node); err != nil { + return a, time.Time{}, err + } + if err := checkReads(a.Node, a.ManifestReads); err != nil { + return a, time.Time{}, err + } + at, err := time.Parse(time.RFC3339Nano, a.StatedAt) + if err != nil { + return a, time.Time{}, fmt.Errorf("stated-at %q is no RFC 3339 time", a.StatedAt) + } + return a, at, nil +} + +// DecodeChanged reads the event's body as the contract lays it out, and judges it as DecodeAnswer does, its why one of +// ReadsStarted and ReadsReturned. +func DecodeChanged(raw []byte) (nodeengine.ManifestReadsChangedEvent, error) { + var e nodeengine.ManifestReadsChangedEvent + if err := json.Unmarshal(raw, &e); err != nil { + return e, err + } + if e.V != ContractVersion { + return e, fmt.Errorf("the event's layout is version %d, not %d", e.V, ContractVersion) + } + if err := checkNode(e.Node); err != nil { + return e, err + } + if e.Why != ReadsStarted && e.Why != ReadsReturned { + return e, fmt.Errorf("the reads of %s changed for %q, not %s or %s", e.Node, e.Why, ReadsStarted, ReadsReturned) + } + if err := checkReads(e.Node, e.ManifestReads); err != nil { + return e, err + } + if _, err := time.Parse(time.RFC3339Nano, e.At); err != nil { + return e, fmt.Errorf("at %q is no RFC 3339 time", e.At) + } + return e, nil +} + +// Where a node's statement came from. +const ( + // FromVerb: the node's engine answered the verb. + FromVerb = "verb" + // FromRecord: the engine did not answer it (an engine built before the verb, or nobody answered) and the node's record + // `.node` was read once in its place (the fallback, until every counted engine serves the verb; issue 774). + FromRecord = "record" + // FromNone: neither said anything, so the node is read as stating the baseline (ADR 0614 §4). + FromNone = "none" +) + +// Version is the version of a statement this package writes and the only one it reads. +const Version = 1 + +// Statement is one node's key: what its engine states it reads, when it stated it, when it was asked, where it came +// from, whether the engine answered that ask (it is present), and, while a person's ask again waits, that ask. +type Statement struct { + V int `json:"v"` + Node string `json:"node"` + ManifestReads []string `json:"manifest-reads,omitempty"` + StatedAt time.Time `json:"stated-at,omitzero"` + AskedAt time.Time `json:"asked-at,omitzero"` + From string `json:"from"` + Answered bool `json:"answered"` + Words string `json:"words,omitempty"` + AskAgain *Again `json:"ask-again,omitempty"` +} + +// Again is a person's ask again of one node, written by the tool and answered by the part that keeps the state. +type Again struct { + By string `json:"by"` + Why string `json:"why"` + At time.Time `json:"at"` +} + +// Encode is s as JSON at Version. +func Encode(s Statement) ([]byte, error) { + if s.Node == "" || strings.ContainsAny(s.Node, ".*> ") { + return nil, fmt.Errorf("enginereads: %q is no node", s.Node) + } + s.V = Version + return json.Marshal(s) +} + +// Decode reads a statement; it refuses another version and a value naming no node. +func Decode(raw []byte) (Statement, error) { + var s Statement + if err := json.Unmarshal(raw, &s); err != nil { + return Statement{}, err + } + if s.V != Version { + return Statement{}, fmt.Errorf("enginereads: version %d, not %d", s.V, Version) + } + if s.Node == "" { + return Statement{}, errors.New("enginereads: a statement names no node") + } + return s, nil +} + +// Caller calls a seat's verb on one node: process.Conn.CallOn. +type Caller func(node, key string, body any) (json.RawMessage, error) + +// Ask asks node's engine the verb once and says what it answered. An engine that answered the call with an error is +// present and states nothing through it; one that did not answer is not present. +func Ask(call Caller, node string, now time.Time) Statement { + s := Statement{Node: node, AskedAt: now.UTC(), From: FromNone} + raw, err := call(node, Invoke, map[string]any{}) + var answered *busretry.Answered + switch { + case err == nil: + a, at, derr := DecodeAnswer(raw) + if derr == nil && a.Node != node { + derr = fmt.Errorf("it names the node %s", a.Node) + } + if derr != nil { + s.Answered, s.Words = true, "the engine answered what is no statement: "+derr.Error() + return s + } + s.ManifestReads, s.StatedAt, s.From, s.Answered = slices.Clone(a.ManifestReads), at.UTC(), FromVerb, true + case errors.As(err, &answered): + s.Answered, s.Words = true, "the engine does not serve "+Verb+": "+answered.Words + default: + s.Words = "nobody answered " + Verb + ": " + err.Error() + } + return s +} + +// NodeOf is the node an event of manifest-reads-changed is about, read from its subject's last token and held equal to +// its body's; ok false for a subject of another event. +func NodeOf(subject string, data []byte) (string, bool, error) { + node, ok := strings.CutPrefix(subject, SubjectPrefix) + if !ok || node == "" || strings.Contains(node, ".") { + return "", false, nil + } + e, err := DecodeChanged(data) + if err != nil { + return node, true, err + } + if e.Node != node { + return node, true, fmt.Errorf("the event on %s names the node %s", subject, e.Node) + } + return node, true, nil +} + +// Keeper keeps every node's statement for one part: in memory, and in the part's state when Put is set, so a statement +// outlives the part's restart. Nothing in it waits on a timer: a node is asked when the part says so. +type Keeper struct { + // Call asks the verb; nil asks nothing (every node then reads as the fallback or the baseline). + Call Caller + // Fallback reads a node's record `.node` once where its engine did not answer the verb; nil reads none. + Fallback func(node string) ([]string, bool) + // Put writes a node's key of the part's state, Delete removes it; nil keeps the statements in memory only. + Put func(node string, value []byte) error + Delete func(node string) error + // Changed is told after a node's statement changed. + Changed func() + Logf func(string, ...any) + Now func() time.Time + // Sync asks in the caller's goroutine (tests); otherwise each ask runs on its own. + Sync bool + + mu sync.Mutex + by map[string]Statement + asking map[string]bool +} + +func (k *Keeper) logf(f string, a ...any) { + if k.Logf != nil { + k.Logf(f, a...) + } +} + +func (k *Keeper) now() time.Time { + if k.Now != nil { + return k.Now() + } + return time.Now() +} + +// AskNode asks node's engine, unless an ask of it runs already; why is said in the log. +func (k *Keeper) AskNode(node, why string) { + k.mu.Lock() + if k.asking == nil { + k.asking = map[string]bool{} + } + if k.asking[node] { + k.mu.Unlock() + return + } + k.asking[node] = true + k.mu.Unlock() + if k.Sync { + k.ask(node, why) + return + } + go k.ask(node, why) +} + +func (k *Keeper) ask(node, why string) { + defer func() { + k.mu.Lock() + delete(k.asking, node) + k.mu.Unlock() + }() + s := Statement{Node: node, AskedAt: k.now().UTC(), From: FromNone, Words: "nothing asks the verb"} + if k.Call != nil { + s = Ask(k.Call, node, k.now()) + } + if s.From != FromVerb && k.Fallback != nil { + if reads, ok := k.Fallback(node); ok { + s.ManifestReads, s.From = slices.Clone(reads), FromRecord + k.logf("%s on %s (%s): %s; its record %s.node is read once in its place (until every counted engine serves the verb)", + Verb, node, why, s.Words, node) + } + } + if s.From == FromNone { + k.logf("%s on %s (%s): %s; the node reads as its last statement, or the baseline", Verb, node, why, s.Words) + } + k.take(s) +} + +// take keeps s, unless the node did not answer and an earlier statement stands: an away node keeps its last one (ADR 0624 +// §1), marked as not answering. +func (k *Keeper) take(s Statement) { + k.mu.Lock() + if k.by == nil { + k.by = map[string]Statement{} + } + if old, ok := k.by[s.Node]; ok && !s.Answered && old.From != FromNone { + old.Answered, old.AskedAt, old.Words, old.AskAgain = false, s.AskedAt, s.Words, nil + s = old + } + k.by[s.Node] = s + k.mu.Unlock() + if k.Put != nil { + raw, err := Encode(s) + if err == nil { + err = k.Put(s.Node, raw) + } + if err != nil { + k.logf("the statement of %s could not be kept: %v", s.Node, err) + } + } + if k.Changed != nil { + k.Changed() + } +} + +// Heard takes a change of the part's own state in: at the start, every node's last statement; later, a person's ask again +// (asked now) or a key a person dropped (forgotten). The part's own writes come back here and change nothing. +func (k *Keeper) Heard(node string, raw []byte, deleted bool) { + if deleted { + k.mu.Lock() + _, had := k.by[node] + delete(k.by, node) + k.mu.Unlock() + if had && k.Changed != nil { + k.Changed() + } + return + } + s, err := Decode(raw) + if err != nil || s.Node != node { + k.logf("the key %s of the engine reads is no statement of it, left: %v", node, err) + return + } + if s.AskAgain != nil { + k.AskNode(node, fmt.Sprintf("asked again by %s: %s", s.AskAgain.By, s.AskAgain.Why)) + return + } + k.mu.Lock() + if k.by == nil { + k.by = map[string]Statement{} + } + _, had := k.by[node] + if !had { + k.by[node] = s + } + k.mu.Unlock() + if !had && k.Changed != nil { + k.Changed() + } +} + +// Forget drops a node the part no longer counts (it left the placement): its key too. +func (k *Keeper) Forget(node string) { + k.mu.Lock() + _, had := k.by[node] + delete(k.by, node) + k.mu.Unlock() + if had && k.Delete != nil { + if err := k.Delete(node); err != nil { + k.logf("the statement of %s could not be removed: %v", node, err) + } + } +} + +// Known says whether the part holds a statement of node, from any source. +func (k *Keeper) Known(node string) bool { + k.mu.Lock() + defer k.mu.Unlock() + _, ok := k.by[node] + return ok +} + +// Statements is every node's statement, by node. +func (k *Keeper) Statements() map[string]Statement { + k.mu.Lock() + defer k.mu.Unlock() + out := make(map[string]Statement, len(k.by)) + for n, s := range k.by { + out[n] = s + } + return out +} + +// Reads is, of the nodes given, what each states it reads: its last statement, nil (the baseline) for a node with none. +func (k *Keeper) Reads(nodes []string) map[string][]string { + k.mu.Lock() + defer k.mu.Unlock() + out := make(map[string][]string, len(nodes)) + for _, n := range nodes { + out[n] = slices.Clone(k.by[n].ManifestReads) + } + return out +} + +// Present is, of the nodes given, those whose engine answered when last asked, with what each states. +func (k *Keeper) Present(nodes []string) map[string][]string { + k.mu.Lock() + defer k.mu.Unlock() + out := map[string][]string{} + for _, n := range nodes { + if s, ok := k.by[n]; ok && s.Answered { + out[n] = slices.Clone(s.ManifestReads) + } + } + return out +} + +// State is what the tool needs of the part's state: the runtime's (stdio.KeptState through KeptState), or a test's. +type State interface { + Keys() ([]string, error) + Get(key string) ([]byte, uint64, bool, error) + Put(key string, value []byte) (uint64, error) + Delete(key string) error + // Await waits at most bound for a revision of key after after whose value done says is the answer. + Await(key string, after uint64, done func([]byte) bool, bound time.Duration) ([]byte, error) +} + +// Read lists every node's statement in st, by node name, or one node's; a key that does not read is named with why. +func Read(st State, node string) (map[string]any, error) { + keys := []string{node} + if node == "" { + var err error + if keys, err = st.Keys(); err != nil { + return nil, err + } + } + sort.Strings(keys) + nodes := map[string]any{} + for _, key := range keys { + raw, _, found, err := st.Get(key) + switch { + case err != nil: + return nil, err + case !found: + nodes[key] = map[string]any{"from": FromNone, "words": "no statement is kept: read as the baseline"} + continue + } + s, derr := Decode(raw) + if derr != nil { + nodes[key] = map[string]any{"unread": derr.Error()} + continue + } + nodes[key] = s + } + return map[string]any{"nodes": nodes}, nil +} + +// AskAgainOf writes node's key with a person's ask again and waits at most bound for the part's answer: the next +// statement it writes there. +func AskAgainOf(st State, node, by, why string, now time.Time, bound time.Duration) (Statement, error) { + if node == "" || strings.ContainsAny(node, ".*> ") { + return Statement{}, fmt.Errorf("%q is no node", node) + } + if strings.TrimSpace(why) == "" { + return Statement{}, errors.New("say why it is asked again") + } + s := Statement{Node: node, From: FromNone} + if raw, _, found, err := st.Get(node); err != nil { + return Statement{}, err + } else if found { + if old, err := Decode(raw); err == nil { + s = old + } + } + s.AskAgain = &Again{By: by, Why: why, At: now.UTC()} + raw, err := Encode(s) + if err != nil { + return Statement{}, err + } + rev, err := st.Put(node, raw) + if err != nil { + return Statement{}, err + } + answer, err := st.Await(node, rev, func(v []byte) bool { + got, err := Decode(v) + return err == nil && got.AskAgain == nil + }, bound) + if err != nil { + return Statement{}, fmt.Errorf("the part did not answer the ask again of %s within %s: %w", node, bound, err) + } + return Decode(answer) +} + +// KeptState is a part's state as the runtime keeps it, for the tool. +func KeptState(s stdio.KeptState) State { return keptState{s} } + +type keptState struct{ s stdio.KeptState } + +func (k keptState) Keys() ([]string, error) { return k.s.Keys() } + +func (k keptState) Get(key string) ([]byte, uint64, bool, error) { + e, err := k.s.Get(key) + if err != nil || e == nil { + return nil, 0, false, err + } + return e.Value, e.Revision, true, nil +} + +func (k keptState) Put(key string, value []byte) (uint64, error) { + return k.s.Put(key, json.RawMessage(value)) +} + +func (k keptState) Delete(key string) error { return k.s.Delete(key) } + +// Await watches key for the answer, and reads it once too in case the watch heard it before it began; a context's bound, +// no timer (ADR 0437 §8). +func (k keptState) Await(key string, after uint64, done func([]byte) bool, bound time.Duration) ([]byte, error) { + heard := make(chan []byte, 1) + if err := k.s.Watch(key, func(c stdio.StateChange) error { + if c.Op == "put" && c.Revision > after && done(c.Value) { + select { + case heard <- c.Value: + default: + } + } + return nil + }); err != nil { + return nil, err + } + if e, err := k.s.Get(key); err == nil && e != nil && e.Revision > after && done(e.Value) { + return e.Value, nil + } + wait, cancel := context.WithTimeout(context.Background(), bound) + defer cancel() + select { + case v := <-heard: + return v, nil + case <-wait.Done(): + return nil, errors.New("it is not running, or cannot read the verb's answer") + } +} + +// AnswerWithin is how long the tool waits for the part to answer an ask again: the verb's own call bound and a margin. +var AnswerWithin = 25 * time.Second + +// Tool is a part's tool over its state of engine reads (tools-first, ADR 0618 §3): read every node's statement or one, +// ask one node's engine again now, and, where drops, drop one node's key (a node the part keeps by mistake). owner names +// the part in the words. +func Tool(name, owner string, st State, drops bool, now func() time.Time) stdio.Tool { + props := map[string]any{ + "node": map[string]any{"type": "string", "description": "one node; every node when left out"}, + "ask_again": map[string]any{"type": "string", "description": "\"true\": ask the node's engine " + Verb + " again now, and answer what it states"}, + "why": map[string]any{"type": "string", "description": "why it is asked again or dropped, in plain words"}, + } + description := "What each node's engine states it reads of a manifest (manifest words, forms and seat names, novox/hq ADR 0618 §3), " + + "as " + owner + " last asked the node-engine seat's verb " + Verb + " and uses it: per node its capabilities, when the engine " + + "stated them and when it was asked, whether it answered (present) and where the statement came from: verb, record (the " + + "engine did not answer and its node record was read once in its place) or none (read as the baseline). A node that is " + + "away keeps its last statement. With node and ask_again, " + owner + " asks that node's engine again and answers its new " + + "statement." + if drops { + props["drop"] = map[string]any{"type": "string", "description": "\"true\": drop the node's key, so it reads as stating the baseline until its engine is asked again"} + description += " With node and drop, the node's key is dropped." + } + return stdio.Tool{Name: name, Description: description, Input: map[string]any{"type": "object", "properties": props}, + Run: func(args map[string]any) (any, error) { + str := func(k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } + node := str("node") + switch { + case str("ask_again") == "true": + return AskAgainOf(st, node, stdio.CallerOf(args), str("why"), now(), AnswerWithin) + case drops && str("drop") == "true": + if node == "" || str("why") == "" { + return nil, errors.New("drop takes node and why") + } + if err := st.Delete(node); err != nil { + return nil, err + } + return map[string]any{"node": node, "dropped": true, + "next": "the node reads as stating the baseline until its engine is asked again (ask_again, its event, or " + owner + "'s start)"}, nil + } + return Read(st, node) + }} +} + +// How a node answered a survey (Survey). +const ( + // Stated: the engine answered the verb. + Stated = "stated" + // NotServed: the engine answered, but does not serve the verb (an engine built before it). + NotServed = "not served" + // Away: nobody answered within the bound: the node is away, or its engine does not serve the verb and nothing else + // answers for it. + Away = "away" +) + +// SurveyRow is one node's row of a survey: how it answered, its statement when it stated one, and its record's reads +// where it did not (the fallback the assigner reads in its place). +type SurveyRow struct { + Node string `json:"node"` + Answer string `json:"answer"` + ManifestReads []string `json:"manifest-reads,omitempty"` + StatedAt time.Time `json:"stated-at,omitzero"` + Record []string `json:"record,omitempty"` + RecordRead bool `json:"record-read,omitempty"` + Words string `json:"words,omitempty"` +} + +// Survey asks every node's engine the verb at once, each ask bounded by bound, so the whole survey takes at most about +// bound however many nodes there are; a node that did not answer is named with its record's reads where record reads +// one. Sorted by node. +func Survey(call Caller, nodes []string, record func(node string) ([]string, bool), bound time.Duration, now time.Time) []SurveyRow { + rows := make([]SurveyRow, len(nodes)) + var wg sync.WaitGroup + for i, node := range nodes { + wg.Add(1) + go func() { + defer wg.Done() + got := make(chan Statement, 1) + go func() { got <- Ask(call, node, now) }() + var s Statement + wait, cancel := context.WithTimeout(context.Background(), bound) + defer cancel() + select { + case s = <-got: + case <-wait.Done(): + s = Statement{Node: node, Words: fmt.Sprintf("no answer within %s", bound)} + } + row := SurveyRow{Node: node, Words: s.Words} + switch { + case s.From == FromVerb: + row.Answer, row.ManifestReads, row.StatedAt = Stated, s.ManifestReads, s.StatedAt + case s.Answered: + row.Answer = NotServed + default: + row.Answer = Away + } + if row.Answer != Stated && record != nil { + row.Record, row.RecordRead = record(node) + } + rows[i] = row + }() + } + wg.Wait() + sort.Slice(rows, func(i, j int) bool { return rows[i].Node < rows[j].Node }) + return rows +} diff --git a/go/clients/nodeengineclient/enginereads/enginereads_test.go b/go/clients/nodeengineclient/enginereads/enginereads_test.go new file mode 100644 index 0000000..b620d1c --- /dev/null +++ b/go/clients/nodeengineclient/enginereads/enginereads_test.go @@ -0,0 +1,40 @@ +package enginereads + +import ( + "strings" + "testing" +) + +// The answer and the event are judged as the contract lays them out and its reader checks: the layout's version, a node +// that is a name, no empty capability, a time, and an event's why one of the two. +func TestTheAnswerAndTheEventAreJudged(t *testing.T) { + const at = "2026-11-05T12:00:00Z" + for name, c := range map[string]struct{ body, bad string }{ + "whole": {`{"v":1,"node":"ace","manifest-reads":["mesh-config.resolved"],"stated-at":"` + at + `"}`, ""}, + "old layout": {`{"v":2,"node":"ace","stated-at":"` + at + `"}`, "version"}, + "bad node": {`{"v":1,"node":"a.b","stated-at":"` + at + `"}`, "no node"}, + "empty read": {`{"v":1,"node":"ace","manifest-reads":[" "],"stated-at":"` + at + `"}`, "empty reader"}, + "no time": {`{"v":1,"node":"ace","stated-at":"yesterday"}`, "RFC 3339"}, + } { + _, _, err := DecodeAnswer([]byte(c.body)) + if (c.bad == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.bad)) { + t.Errorf("answer %s: %v, want %q", name, err, c.bad) + } + } + for name, c := range map[string]struct{ body, bad string }{ + "whole": {`{"v":1,"node":"ace","why":"started","manifest-reads":["a.b"],"at":"` + at + `"}`, ""}, + "returned": {`{"v":1,"node":"ace","why":"returned","at":"` + at + `"}`, ""}, + "old layout": {`{"v":2,"node":"ace","why":"started","at":"` + at + `"}`, "version"}, + "no node": {`{"v":1,"node":"","why":"started","at":"` + at + `"}`, "no node"}, + "another why": {`{"v":1,"node":"ace","why":"bored","at":"` + at + `"}`, "bored"}, + "no time": {`{"v":1,"node":"ace","why":"started","at":"yesterday"}`, "RFC 3339"}, + } { + _, err := DecodeChanged([]byte(c.body)) + if (c.bad == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.bad)) { + t.Errorf("event %s: %v, want %q", name, err, c.bad) + } + } + if Verb != "manifest-reads" || Event != "manifest-reads-changed" || Consumes != "node-engine.manifest-reads-changed" || Invoke != "seat:node-engine.manifest-reads" { + t.Errorf("the names are %s %s %s %s", Verb, Event, Consumes, Invoke) + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/findings/findings.go b/health-and-repair/go/clients/meshconditionsclient/findings/findings.go new file mode 100644 index 0000000..903eb29 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/findings/findings.go @@ -0,0 +1,88 @@ +// Package findings is a part's own bucket `findings` as the state the seat mesh-conditions' client writes into +// (meshconditionsclient.FindingState), so a part gives mesh-sdk's views the FindingWriter of the seat's layout without +// each writing the same adapter over its bucket (novox/hq ADR 0564 §2, ADR 0635 §4). +package findings + +import ( + "context" + "encoding/json" + "errors" + + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// ErrUnbound says the bucket is not on the bus yet, so the finding is not written (the part says it in its log). +var ErrUnbound = errors.New("the findings state is not bound yet") + +// State is the bucket the part's state `findings` is, given when it is bound (process.AwaitedBucket.KV): nil is not bound yet. +type State struct { + Ctx context.Context + KV func() jetstream.KeyValue +} + +var _ meshconditionsclient.FindingState = State{} + +// Put writes one value under its key as JSON. +func (s State) Put(key string, value any) (uint64, error) { + kv := s.KV() + if kv == nil { + return 0, ErrUnbound + } + data, err := json.Marshal(value) + if err != nil { + return 0, err + } + return kv.Put(s.Ctx, key, data) +} + +// Delete clears one key. +func (s State) Delete(key string) error { + kv := s.KV() + if kv == nil { + return ErrUnbound + } + return kv.Delete(s.Ctx, key) +} + +// Writer is the part's views.FindingWriter over its bucket; the function is what views.ProcessReader.Findings and the +// emitter's Findings take. +func Writer(ctx context.Context, kv func() jetstream.KeyValue) views.FindingWriter { + return meshconditionsclient.FindingWriter{State: State{Ctx: ctx, KV: kv}} +} + +// Funcs is a findings state held as the two functions a part keeps over it (the assigner's findings, which also say +// its conditions): a value is put as JSON. +type Funcs struct { + PutBytes func(key string, value []byte) error + DeleteKey func(key string) error +} + +var _ meshconditionsclient.FindingState = Funcs{} + +// Put writes one value under its key as JSON; the state not bound (no function) is ErrUnbound. +func (f Funcs) Put(key string, value any) (uint64, error) { + if f.PutBytes == nil { + return 0, ErrUnbound + } + data, err := json.Marshal(value) + if err != nil { + return 0, err + } + return 0, f.PutBytes(key, data) +} + +// Delete clears one key. +func (f Funcs) Delete(key string) error { + if f.DeleteKey == nil { + return ErrUnbound + } + return f.DeleteKey(key) +} + +// WriterOver is the part's views.FindingWriter over its two functions. +func WriterOver(put func(string, []byte) error, del func(string) error) views.FindingWriter { + return meshconditionsclient.FindingWriter{State: Funcs{PutBytes: put, DeleteKey: del}} +} diff --git a/health-and-repair/go/clients/meshconditionsclient/findings/findings_test.go b/health-and-repair/go/clients/meshconditionsclient/findings/findings_test.go new file mode 100644 index 0000000..01052ac --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/findings/findings_test.go @@ -0,0 +1,71 @@ +package findings + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/nats-io/nats.go/jetstream" + + "git.novox.be/novox/mesh-sdk/go/views" +) + +// fakeKV is the two calls a findings state makes of its bucket. +type fakeKV struct { + jetstream.KeyValue + held map[string][]byte +} + +func (f *fakeKV) Put(_ context.Context, key string, value []byte) (uint64, error) { + f.held[key] = value + return uint64(len(f.held)), nil +} + +func (f *fakeKV) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error { + delete(f.held, key) + return nil +} + +// A part's finding is written in the seat mesh-conditions' layout under the key the condition keeper reads, cleared by the +// same finding, and refused while the bucket is not bound (the part says that in its log). +func TestAFindingIsWrittenInTheSeatsLayoutAndClearedAndRefusedUnbound(t *testing.T) { + kv := &fakeKV{held: map[string][]byte{}} + var bound jetstream.KeyValue + w := Writer(context.Background(), func() jetstream.KeyValue { return bound }) + f := views.Finding{Observer: "mesh-delivery-teller", Kind: views.UnansweredKind, ID: "mesh-delivery-teller.asks", Summary: "s", Headline: "h", + Explanation: "e", At: time.Date(2026, 11, 5, 15, 0, 0, 0, time.UTC)} + if err := w.Put(f); !errors.Is(err, ErrUnbound) { + t.Fatalf("an unbound bucket took the finding: %v", err) + } + bound = kv + if err := w.Put(f); err != nil { + t.Fatal(err) + } + var got struct{ Key, Kind, Observer, Source string } + for key, value := range kv.held { + if err := json.Unmarshal(value, &got); err != nil || got.Key != key || got.Kind != views.UnansweredKind || + got.Observer != f.Observer || got.Source != f.Observer+"."+f.Kind { + t.Fatalf("%s holds %s (%v)", key, value, err) + } + } + if len(kv.held) != 1 { + t.Fatalf("the bucket holds %d findings, want 1", len(kv.held)) + } + if err := w.Delete(f); err != nil || len(kv.held) != 0 { + t.Fatalf("the finding is not cleared: %v, %d left", err, len(kv.held)) + } + // The same finding over two functions (a part whose findings also say its conditions). + held := map[string][]byte{} + o := WriterOver(func(k string, v []byte) error { held[k] = v; return nil }, func(k string) error { delete(held, k); return nil }) + if err := o.Put(f); err != nil || len(held) != 1 { + t.Fatalf("over functions: %v, %d held", err, len(held)) + } + if err := o.Delete(f); err != nil || len(held) != 0 { + t.Fatalf("over functions not cleared: %v, %d left", err, len(held)) + } + if err := WriterOver(nil, nil).Put(f); !errors.Is(err, ErrUnbound) { + t.Fatalf("no functions took the finding: %v", err) + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/go.mod b/health-and-repair/go/clients/meshconditionsclient/go.mod index 8843c1e..9e772c2 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.mod +++ b/health-and-repair/go/clients/meshconditionsclient/go.mod @@ -8,7 +8,7 @@ require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 require ( github.com/klauspost/compress v1.20.1 // indirect - github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect golang.org/x/crypto v0.57.0 // indirect diff --git a/health-and-repair/go/clients/meshconditionsclient/heard/heard.go b/health-and-repair/go/clients/meshconditionsclient/heard/heard.go new file mode 100644 index 0000000..e00d535 --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/heard/heard.go @@ -0,0 +1,115 @@ +// Package heard is the finding a part writes while one of its consumers or watches does not hear the +// family it follows (novox/hq ADR 0615 §5; review round 1 of the registry milestone): the consumer not there yet, a +// hearing that could not start, a watch that cannot be opened. Without it the part's health stays healthy and its +// status says answered while the events it waits on are lost to it. The finding names the observer and the family, +// says what is not heard and why, and is deleted when the consumer binds: at the part's next start, or where the part +// binds again at a change of its membership, then; the condition keeper raises and clears the condition from it (ADR 0564 §2). +package heard + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + "git.novox.be/novox/mesh-sdk/go/views" +) + +// Kind is the finding's kind. +const Kind = "family-not-heard" + +// ID is the finding's subject: `.`, the family by the name the part follows it under (a stream, an +// event, a state's watch). +func ID(observer, family string) string { return observer + "." + family } + +// Key is the finding's key in the observer's findings state. +func Key(observer, family string) string { + return meshconditionsclient.Key(meshconditionsclient.ScopeModule, ID(observer, family), Kind) +} + +// Words are the finding's words: its summary, headline and explanation. consequence says what the observer misses while +// it does not hear family ("the placement is the first view's alone"). +func Words(observer, family, consequence string, why error) (summary, headline, explanation string) { + reason := "it gave no reason" + if why != nil { + reason = strings.TrimSpace(why.Error()) + } + summary = fmt.Sprintf("%s does not hear %s: %s (%s)", observer, family, consequence, reason) + headline = fmt.Sprintf("%s does not hear %s", observer, family) + explanation = fmt.Sprintf("%s follows %s on a consumer of its own, and that consumer is not there or could not be heard, so %s. "+ + "This clears when the consumer binds: at %s's next start, or sooner where %s binds again at a change of its membership. "+ + "Look at whether the grants part made %s's consumers, then start %s again if it does not bind by itself.", observer, family, consequence, observer, observer, observer, observer) + return summary, headline, explanation +} + +// Finding is observer's finding that it does not hear family, in the layout of the seat mesh-conditions' state findings. +func Finding(observer, family, consequence string, why error, at time.Time) (string, []byte, error) { + summary, headline, explanation := Words(observer, family, consequence, why) + v := meshconditionsclient.FindingFor(views.Finding{Observer: observer, Kind: Kind, ID: ID(observer, family), Summary: summary, + Headline: headline, Explanation: explanation, At: at}) + if err := meshconditionsclient.CheckFinding(v); err != nil { + return v.Key, nil, err + } + value, err := json.Marshal(v) + return v.Key, value, err +} + +// Writer writes and deletes the finding in the observer's findings state. Put or Delete nil writes nothing, said in the +// log, so the part still says it. +type Writer struct { + Observer string + Put func(key string, value []byte) error + Delete func(key string) error + Logf func(string, ...any) + Now func() time.Time +} + +// Bind binds the observer's consumer of family (bind makes it and starts hearing it): the finding is deleted when it +// binds, and written, with why, when it does not. It answers bind's error, so a caller that cannot go on without the +// family still stops. +func (w Writer) Bind(family, consequence string, bind func() error) error { + if err := bind(); err != nil { + w.NotHeard(family, consequence, err) + return err + } + w.Heard(family) + return nil +} + +// NotHeard says in the log and in the finding that the observer does not hear family. +func (w Writer) NotHeard(family, consequence string, why error) { + now := time.Now + if w.Now != nil { + now = w.Now + } + summary, _, _ := Words(w.Observer, family, consequence, why) + w.logf("%s", summary) + if w.Put == nil { + w.logf("the finding %s is not written: no findings state", Key(w.Observer, family)) + return + } + key, value, err := Finding(w.Observer, family, consequence, why, now()) + if err == nil { + err = w.Put(key, value) + } + if err != nil { + w.logf("the finding %s is not written: %v", key, err) + } +} + +// Heard deletes the finding: the consumer of family is bound. +func (w Writer) Heard(family string) { + if w.Delete == nil { + return + } + if err := w.Delete(Key(w.Observer, family)); err != nil { + w.logf("the finding %s is not deleted: %v", Key(w.Observer, family), err) + } +} + +func (w Writer) logf(format string, args ...any) { + if w.Logf != nil { + w.Logf(format, args...) + } +} diff --git a/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go b/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go new file mode 100644 index 0000000..cc9b69a --- /dev/null +++ b/health-and-repair/go/clients/meshconditionsclient/heard/heard_test.go @@ -0,0 +1,36 @@ +package heard + +import ( + "errors" + "strings" + "testing" + "time" +) + +// A consumer that does not bind writes the finding, with why, under the observer's findings key; the one that binds +// deletes it; and a state that is not bound is said in the log, not written. +func TestBindWritesTheFindingUntilTheConsumerBinds(t *testing.T) { + puts := map[string][]byte{} + var logs []string + w := Writer{Observer: "mesh-x", Put: func(k string, v []byte) error { puts[k] = v; return nil }, + Delete: func(k string) error { delete(puts, k); return nil }, Logf: func(f string, a ...any) { logs = append(logs, f) }, + Now: func() time.Time { return time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) }} + key := Key("mesh-x", "placement") + if want := "mesh-x.placement"; ID("mesh-x", "placement") != want || !strings.Contains(key, want) || !strings.Contains(key, Kind) { + t.Fatalf("the finding's key %q", key) + } + if err := w.Bind("placement", "the first view is the only picture", func() error { return errors.New("no consumer") }); err == nil { + t.Fatal("a failed bind was not answered") + } + if v := string(puts[key]); !strings.Contains(v, "no consumer") || !strings.Contains(v, "mesh-x does not hear placement") { + t.Errorf("the finding is %q", v) + } + if err := w.Bind("placement", "", func() error { return nil }); err != nil || len(puts) != 0 { + t.Errorf("a bound consumer left %v (%v)", puts, err) + } + w.Put = nil + w.NotHeard("placement", "x", nil) + if len(logs) < 2 || !strings.Contains(logs[len(logs)-1], "not written") { + t.Errorf("an unbound state was not said: %v", logs) + } +} -- 2.54.0 From f0ed2d9b124d4469fbff6720fc211120c74f4f95 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 19:36:21 +0100 Subject: [PATCH 90/94] No client names the holder of the seat mesh-provisioner: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../clients/meshprovisionerclient/client.go | 21 +++++++---- .../meshprovisionerclient/client_test.go | 7 ++-- .../provider/harness/moved.go | 8 +++-- .../provider/harness/moved_test.go | 27 +++++++------- .../provider/harness/tables_test.go | 2 +- .../provider/provider.go | 36 +++++++++++-------- .../provider/provider_test.go | 22 +++++++++--- .../provider/tables_test.go | 32 ++++++++--------- 8 files changed, 95 insertions(+), 60 deletions(-) diff --git a/provisioning/go/clients/meshprovisionerclient/client.go b/provisioning/go/clients/meshprovisionerclient/client.go index 2074ef1..59845c4 100644 --- a/provisioning/go/clients/meshprovisionerclient/client.go +++ b/provisioning/go/clients/meshprovisionerclient/client.go @@ -16,19 +16,26 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// The seat, its holder and its two states, and the names a reader declares in its `reads`. +// The seat and its two states, by their local names. The module that holds the seat, and so the addresses and buckets of the +// states (`.`, `_`), is each reader's own, taken from its manifest's `reads` (novox/hq ADR 0625 §4). const ( Seat = "mesh-provisioner" - Module = "mesh-provisioner" ProvisionsState = "provisions" PlansState = "plans" - ProvisionsRead = Module + "." + ProvisionsState - PlansRead = Module + "." + PlansState - // ProvisionsBucket and PlansBucket are the states' buckets. - ProvisionsBucket = Module + "_" + ProvisionsState - PlansBucket = Module + "_" + PlansState ) +// The states' buckets and `reads` addresses for the module holding the seat, which a reader names from its own manifest. +func ProvisionsBucketOf(holder string) string { return holder + "_" + ProvisionsState } + +// PlansBucketOf is the bucket of the state plans in the holder's name. +func PlansBucketOf(holder string) string { return holder + "_" + PlansState } + +// ProvisionsReadOf is the address a manifest's `reads` names the state provisions by. +func ProvisionsReadOf(holder string) string { return holder + "." + ProvisionsState } + +// PlansReadOf is the address a manifest's `reads` names the state plans by. +func PlansReadOf(holder string) string { return holder + "." + PlansState } + // The states of a provision (clause 1). const ( // Asked is a provision its consumer needs that its provider has not answered yet, or whose dependencies are not diff --git a/provisioning/go/clients/meshprovisionerclient/client_test.go b/provisioning/go/clients/meshprovisionerclient/client_test.go index bb2bdaa..4e48937 100644 --- a/provisioning/go/clients/meshprovisionerclient/client_test.go +++ b/provisioning/go/clients/meshprovisionerclient/client_test.go @@ -118,7 +118,10 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) { t.Errorf("the secret provided by %s depends on %v", holder, got) } } - if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" { - t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead) + if ProvisionsBucketOf(testProvisioner) != "mesh-provisioner_provisions" || PlansReadOf(testProvisioner) != "mesh-provisioner.plans" { + t.Errorf("the states are %s and %s", ProvisionsBucketOf(testProvisioner), PlansReadOf(testProvisioner)) } } + +// testProvisioner is the module holding the seat in these tests: a reader names it from its own manifest's `reads`. +const testProvisioner = "mesh-provisioner" diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go index d0d6e48..1f2e90d 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved.go @@ -32,6 +32,8 @@ import ( type MovedSpec struct { // Module and Provision are the provider module's name and what it provides, as its manifest says. Module, Provision string + // Provisioner is the module that holds the seat mesh-provisioner, as the provider's manifest's `reads` name it. + Provisioner string // Offer is what it serves: its credential form and served values. Offer provider.Offer // Login is the identity a consumer is made under at the provider's end (postgres: the entry's User; keycloak: its @@ -234,7 +236,7 @@ func (m *Moved) retire(ctx context.Context) { // watch keeps the plans as the bus holds them, from the first read on. func (m *Moved) watch(ctx context.Context, bus provider.Bus) error { - err := bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + err := bus.Watch(ctx, provisions.PlansBucketOf(m.spec.Provisioner), func(key string, value []byte) { m.mu.Lock() defer m.mu.Unlock() if value == nil { @@ -297,10 +299,10 @@ func NewMoved(consumersFile string, h *Harness, spec MovedSpec) (*Moved, *provid return nil, nil, nil, errors.New("no consumers' file") } run := newMoved(h, spec) - bus := provider.StateBus(spec.Module) + bus := provider.StateBus(spec.Module, spec.Provisioner) return run, provider.New(provider.Options{ Node: h.Node, - Module: spec.Module, + Module: spec.Module, Provisioner: spec.Provisioner, Offers: map[string]provider.Offer{spec.Provision: spec.Offer}, Bus: bus, Vault: provider.ConsumersFile{Paths: []string{consumersFile}}, diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go index ffa5a0c..7bbc7a0 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/moved_test.go @@ -22,7 +22,7 @@ const ( ) // postgresSpec is what a postgres-like provider says: a login per consumer, and the grant file's logins. -var postgresSpec = MovedSpec{Module: "postgres", Provision: ProvisionName, +var postgresSpec = MovedSpec{Module: "postgres", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"port": servedPort}}, Login: func(e provider.Entry) string { return e.User }, Noun: "login", Made: "login", Backend: "the backend", WithGrants: true} @@ -75,13 +75,13 @@ func (b *bucketBus) putPlan(node, module string, entries ...map[string]any) { if err != nil { panic(err) } - _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = b.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } plan, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) if err != nil { panic(err) } - _ = b.Put(context.Background(), provisions.PlansBucket, node+"."+module, plan) + _ = b.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module, plan) } // retireOn is a consumer's provision retiring with the operator's yes (ADR 0570 clause 5), its plan gone: the one way @@ -92,8 +92,8 @@ func (b *bucketBus) retireOn(node, module, provides string) { Warrant: &provisions.Warrant{Ask: "a-1", Option: "yes", By: "the operator"}} key, _ := pr.Key() value, _ := provisions.Encode(pr) - _ = b.Delete(context.Background(), provisions.PlansBucket, node+"."+module) - _ = b.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = b.Delete(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module) + _ = b.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } // bind is one consumer's assignment, whose plan names this machine's postgres as the provider and carries what the @@ -160,11 +160,11 @@ func (w *movedWorld) pass() []provider.Outcome { w.now = w.now.Add(5 * time.Second) // the plans as the bus holds them now, as the watch would hand them w.run.plans = map[string][]byte{} - for k, v := range w.bus.buckets[provisions.PlansBucket] { + for k, v := range w.bus.buckets[provisions.PlansBucketOf(testProvisioner)] { w.run.plans[k] = v } // the helper reads its buckets from the in-memory bus - w.p = provider.New(provider.Options{Node: "novox", Module: w.spec.Module, Bus: w.bus, + w.p = provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: w.spec.Module, Bus: w.bus, Vault: provider.ConsumersFile{Paths: []string{w.file}}, Offers: map[string]provider.Offer{ProvisionName: w.spec.Offer}, Make: w.run.Make, Gone: w.run.Gone, Logf: func(string, ...any) {}}) @@ -248,7 +248,7 @@ func testAConsumerMissingFromOneReadRetiresNothing(t *testing.T) { w.bus.bind("ace", "app1", nil) w.bus.bind("ace", "app2", nil) w.pass() - _ = w.bus.Delete(ctx, provisions.PlansBucket, "ace.app2") + _ = w.bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.app2") for i := 0; i < 8; i++ { w.pass() } @@ -276,7 +276,7 @@ func testNoPlansReadRetiresNothing(t *testing.T) { w := newMovedWorld(t, bound) w.bus.bind("ace", "app1", nil) w.pass() - w.bus.Delete(ctx, provisions.PlansBucket, "ace.app1") + w.bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.app1") for i := 0; i < 8; i++ { w.pass() } @@ -334,7 +334,7 @@ func testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC) h := &Harness{Resource: ProvisionName, Adapter: a, Node: "novox", Now: func() time.Time { return now }, Receives: filepath.Join(t.TempDir(), "unreadable-or-missing.json"), Log: func(string, ...any) {}} - spec := MovedSpec{Module: "keycloak", Provision: ProvisionName, + spec := MovedSpec{Module: "keycloak", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer}, Login: func(e provider.Entry) string { return e.As }, Noun: "client id", Made: "client", Backend: "Keycloak", Values: func(e provider.Entry) map[string]any { return map[string]any{"label": e.Contributes["label"]} }} @@ -348,7 +348,7 @@ func testASpecWithoutGrantsMakesUnderThePlansAsAndIgnoresTheGrantFile(t *testing if err := run.watch(ctx, bus); err != nil { t.Fatal(err) } - p := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus, + p := provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: "keycloak", Bus: bus, Vault: provider.ConsumersFile{Paths: []string{file}}, Offers: map[string]provider.Offer{ProvisionName: spec.Offer}, Make: run.Make, Gone: run.Gone, Logf: func(string, ...any) {}}) @@ -372,7 +372,7 @@ var ( // keycloakSpec is what a keycloak-like provider says: the client id is the plan's `as`, the values are what the // consumer contributes with the controller's names over them, and no grant file is consulted. -var keycloakSpec = MovedSpec{Module: "keycloak", Provision: ProvisionName, +var keycloakSpec = MovedSpec{Module: "keycloak", Provisioner: testProvisioner, Provision: ProvisionName, Offer: provider.Offer{Credential: provider.PerConsumer, Serves: map[string]any{"issuer": testIssuer, "token-path": "/protocol/openid-connect/token"}}, Login: func(e provider.Entry) string { return e.As }, @@ -462,3 +462,6 @@ func testAConsumerNamedByItsPlanKeepsItsClientId(t *testing.T) { t.Fatalf("retired %v: the plan's client id was counted unasked", w.a.removed) } } + +// testProvisioner is the module holding the seat mesh-provisioner in these tests. +const testProvisioner = "mesh-provisioner" diff --git a/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go index 988396f..78af4fd 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/harness/tables_test.go @@ -25,7 +25,7 @@ func TestHarnessRuns(t *testing.T) { w.run.FirstRetry = time.Millisecond w.run.loaded = false bus := &earlyBus{bucketBus: w.bus, refuse: 3} - p := provider.New(provider.Options{Node: "novox", Module: "postgres", Bus: w.bus, + p := provider.New(provider.Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: w.bus, Vault: provider.ConsumersFile{Paths: []string{w.file}}, Make: w.run.Make, Gone: w.run.Gone, Every: time.Hour, Logf: func(string, ...any) {}}) ctx, cancel := context.WithCancel(context.Background()) diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider.go b/provisioning/go/clients/meshprovisionerclient/provider/provider.go index d3c0e82..0e393c1 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/provider.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider.go @@ -162,6 +162,9 @@ type Consumer struct { type Options struct { // Node and Module are this provider: the machine it runs on and the module serving. Node, Module string + // Provisioner is the module that holds the seat mesh-provisioner, whose states plans and provisions the provider reads: its + // own manifest's `reads` name it (`.plans`); the client names no holder (ADR 0625 §4). + Provisioner string // Offers are the provisions it serves, by name. Offers map[string]Offer Bus Bus @@ -238,12 +241,15 @@ type Outcome struct { // Run watches the binds and the provider's own entries, and passes once at start, on every change and every // Every, until ctx ends. func (p *Provider) Run(ctx context.Context) error { + if p.opts.Provisioner == "" { + return errors.New("the provider names no module holding the seat mesh-provisioner (Options.Provisioner), so it reads no plans") + } if err := p.watch(ctx, BucketOf(p.opts.Module), p.entries); err != nil { return fmt.Errorf("watching %s: %w", BucketOf(p.opts.Module), err) } - for _, bucket := range []string{provisions.PlansBucket, provisions.ProvisionsBucket} { + for _, bucket := range []string{provisions.PlansBucketOf(p.opts.Provisioner), provisions.ProvisionsBucketOf(p.opts.Provisioner)} { into := p.plans - if bucket == provisions.ProvisionsBucket { + if bucket == provisions.ProvisionsBucketOf(p.opts.Provisioner) { into = p.provisions } if err := p.watch(ctx, bucket, into); err != nil { @@ -317,13 +323,13 @@ func (p *Provider) Pass(ctx context.Context) ([]Outcome, error) { if err != nil { return nil, fmt.Errorf("reading %s: %w", BucketOf(p.opts.Module), err) } - plans, err := read(provisions.PlansBucket) + plans, err := read(provisions.PlansBucketOf(p.opts.Provisioner)) if err != nil { - return nil, fmt.Errorf("reading %s: %w", provisions.PlansBucket, err) + return nil, fmt.Errorf("reading %s: %w", provisions.PlansBucketOf(p.opts.Provisioner), err) } - provs, err := read(provisions.ProvisionsBucket) + provs, err := read(provisions.ProvisionsBucketOf(p.opts.Provisioner)) if err != nil { - return nil, fmt.Errorf("reading %s: %w", provisions.ProvisionsBucket, err) + return nil, fmt.Errorf("reading %s: %w", provisions.ProvisionsBucketOf(p.opts.Provisioner), err) } return p.passIn(ctx, Inputs{Entries: entries, Plans: plans, Provisions: provs}) } @@ -705,26 +711,28 @@ func sortedKeys[V any](m map[string]V) []string { } // StateBus is the Bus of a provider that runs as a module bundle over the runtime's stdio: the provider's own bucket, -// BucketOf(module), is the module's state `bound`, and provisions.PlansBucket and provisions.ProvisionsBucket are the -// states it reads, `mesh-provisioner.plans` and `mesh-provisioner.provisions` (each declared in the module's manifest). +// BucketOf(module), is the module's state `bound`, and the provisioner holder's plans and provisions are the +// states it reads, `.plans` and `.provisions` (each declared in the module's manifest). // Any other bucket is refused. // // A stdio state watch lasts as long as the process and cannot be ended (stdio.KeptState.Watch), so Watch honours the // Bus contract by silencing instead: once ctx has ended the handler is never called again, though the runtime keeps // sending changes that are dropped. A Pass, which watches twice per call, is therefore correct, only the dropped // changes of its two spent watches remain. -func StateBus(module string) Bus { return stateBus{module: module} } +func StateBus(module, provisioner string) Bus { + return stateBus{module: module, provisioner: provisioner} +} -type stateBus struct{ module string } +type stateBus struct{ module, provisioner string } func (b stateBus) state(bucket string) (stdio.KeptState, error) { switch bucket { case BucketOf(b.module): return stdio.State("bound"), nil - case provisions.PlansBucket: - return stdio.State(provisions.PlansRead), nil - case provisions.ProvisionsBucket: - return stdio.State(provisions.ProvisionsRead), nil + case provisions.PlansBucketOf(b.provisioner): + return stdio.State(provisions.PlansReadOf(b.provisioner)), nil + case provisions.ProvisionsBucketOf(b.provisioner): + return stdio.State(provisions.ProvisionsReadOf(b.provisioner)), nil } return stdio.KeptState{}, fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket) } diff --git a/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go index 594052a..5a6365e 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/provider_test.go @@ -194,13 +194,13 @@ func planned(t *testing.T, bus *standIn, node, module string, ps ...catalogue.Pl if err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, value) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, value) } raw, err := provisions.EncodePlan(provisions.Plan{Node: node, Module: module, Plan: catalogue.Plan{Provisions: ps}}) if err != nil { t.Fatal(err) } - if err := bus.Put(context.Background(), provisions.PlansBucket, node+"."+module, raw); err != nil { + if err := bus.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module, raw); err != nil { t.Fatal(err) } } @@ -217,8 +217,8 @@ func retire(t *testing.T, bus *standIn, node, module, provision, provider, provi if err != nil { t.Fatal(err) } - _ = bus.Delete(context.Background(), provisions.PlansBucket, node+"."+module) - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, key, raw) + _ = bus.Delete(context.Background(), provisions.PlansBucketOf(testProvisioner), node+"."+module) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), key, raw) } func bind(provision, local, provider, providerModule string) map[string]string { @@ -245,7 +245,7 @@ func entryOf(t *testing.T, bus *standIn, key string) Entry { // postgres is a provider of postgres-database on novox with a login per consumer (ADR 0048, ADR 0158 §11), its // served values derived from each consumer's identity (ADR 0202 §1). func postgres(bus *standIn, v *vault, m *maker) *Provider { - return New(Options{ + return New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Gone: m.Gone, Offers: map[string]Offer{ "postgres-database": {Credential: PerConsumer, Serves: map[string]any{ @@ -292,3 +292,15 @@ func keysOf(v any, into *[]string) { } } } + +// testProvisioner is the module holding the seat mesh-provisioner in these tests (a reader names it from its manifest). +const testProvisioner = "mesh-provisioner" + +// A provider that does not say which module holds the seat mesh-provisioner reads no plans, and says so at its start: the +// client names no holder, so a reader that forgot to give it is loud (ADR 0625 §4). +func TestAProviderWithoutTheHoldersNameDoesNotRun(t *testing.T) { + p := New(Options{Node: "novox", Module: "postgres", Bus: &standIn{}, Logf: func(string, ...any) {}}) + if err := p.Run(context.Background()); err == nil || !strings.Contains(err.Error(), "mesh-provisioner") { + t.Errorf("Run = %v", err) + } +} diff --git a/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go index 2dbf2d6..98f94f5 100644 --- a/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go +++ b/provisioning/go/clients/meshprovisionerclient/provider/tables_test.go @@ -73,7 +73,7 @@ func TestEntries(t *testing.T) { v := &vault{values: map[string]string{"provision/novox/sonarr-api": "one-value"}} assign(t, bus, "ace", "overseerr", bind("sonarr-api", "", "novox", "sonarr")) assign(t, bus, "g14", "homepage", bind("sonarr-api", "", "novox", "sonarr")) - p := New(Options{Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "sonarr", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"sonarr-api": {Credential: Shared, User: "sonarr", Serves: map[string]any{"url": "http://sonarr.novox.mesh:8989"}}}}) if _, err := p.Pass(context.Background()); err != nil { @@ -129,7 +129,7 @@ func TestEntries(t *testing.T) { bus, m := newStandIn(), &maker{} v := &vault{values: map[string]string{"pair/novox/redis-cache/ace/letta/queue": "q-secret"}} assign(t, bus, "ace", "letta", bind("redis-cache", "queue", "novox", "redis")) - p := New(Options{Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "redis", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"redis-cache": {Credential: PerConsumer, Serves: map[string]any{"port": 6379}}}}) if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) @@ -255,7 +255,7 @@ func TestEntries(t *testing.T) { b[k] = val } assign(t, bus, "ace", "letta", b) - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"postgres-database": {Credential: offer, User: "shared-user"}}}) out, err := p.Pass(context.Background()) if err != nil || len(out) != 1 || out[0].Status != Answered { @@ -288,7 +288,7 @@ func TestEntries(t *testing.T) { b := bind("route", "", "novox", "route-proxy") b["credential-form"] = "pair" assign(t, bus, "ace", "letta", b) - p := New(Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "route-proxy", Bus: bus, Logf: func(string, ...any) {}, Offers: map[string]Offer{"route": {Credential: NoCredential}}}) out, err := p.Pass(context.Background()) if err != nil || len(out) != 1 || out[0].Status != Answered { @@ -318,12 +318,12 @@ func TestEntries(t *testing.T) { if err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "ace.letta.postgres-database", raw) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), "ace.letta.postgres-database", raw) // And umami's key carries none: asked, but no plan names it. bare := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "umami", Node: "g14"}, Provider: provisions.End{Module: "postgres", Node: "novox"}, State: provisions.Asked} raw, _ = provisions.Encode(bare) - _ = bus.Put(context.Background(), provisions.ProvisionsBucket, "g14.umami.postgres-database", raw) + _ = bus.Put(context.Background(), provisions.ProvisionsBucketOf(testProvisioner), "g14.umami.postgres-database", raw) out, err := postgres(bus, v, m).Pass(context.Background()) if err != nil { @@ -416,7 +416,7 @@ func TestBinds(t *testing.T) { if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) } - _ = bus.Put(context.Background(), provisions.PlansBucket, "ace.letta", []byte(bad)) + _ = bus.Put(context.Background(), provisions.PlansBucketOf(testProvisioner), "ace.letta", []byte(bad)) assign(t, bus, "g14", "umami", bind("postgres-database", "", "novox", "postgres")) var logged []string p.opts.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } @@ -628,7 +628,7 @@ func TestCredentialsAndKeys(t *testing.T) { v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1", "provision/novox/postgres-database": "s2"}} assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, Offers: map[string]Offer{"postgres-database": {Credential: form, User: "shared-user"}}}) if _, err := p.Pass(context.Background()); err != nil { t.Fatal(err) @@ -664,7 +664,7 @@ func TestCredentialsAndKeys(t *testing.T) { assign(t, bus, "ace", "letta", bind("postgres-database", "", "shanks", "postgres")) plan, _ := provisions.EncodePlan(provisions.Plan{Node: "ace", Module: "letta", Plan: catalogue.Plan{ Provisions: []catalogue.PlanProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}}}) - _ = bus.Put(ctx, provisions.PlansBucket, "ace.letta", plan) + _ = bus.Put(ctx, provisions.PlansBucketOf(testProvisioner), "ace.letta", plan) key := "ace.letta.postgres-database" put := func(k string, state string, depends []string, warrant *provisions.Warrant) { pr := provisions.Provision{Provision: "postgres-database", Consumer: provisions.End{Module: "letta", Node: "ace"}, @@ -680,7 +680,7 @@ func TestCredentialsAndKeys(t *testing.T) { if err != nil { t.Fatal(err) } - _ = bus.Put(ctx, provisions.ProvisionsBucket, k, raw) + _ = bus.Put(ctx, provisions.ProvisionsBucketOf(testProvisioner), k, raw) } p := postgres(bus, v, m) steps := []struct { @@ -697,7 +697,7 @@ func TestCredentialsAndKeys(t *testing.T) { }, status: Waiting, says: "depends on ace.letta.secret, which is asked"}, {name: "the dependency provided", before: func() { put("ace.letta.secret", provisions.Provided, nil, nil) }, status: Answered, entry: true}, - {name: "its plan gone: nothing let go by itself", before: func() { _ = bus.Delete(ctx, provisions.PlansBucket, "ace.letta") }, + {name: "its plan gone: nothing let go by itself", before: func() { _ = bus.Delete(ctx, provisions.PlansBucketOf(testProvisioner), "ace.letta") }, entry: true}, {name: "retiring, not yet answered", before: func() { put(key, provisions.Retiring, nil, nil) }, entry: true}, {name: "retiring, the operator's yes", before: func() { @@ -769,7 +769,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { bus := newStandIn() assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) var made []string - p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, + p := New(Options{Provisioner: testProvisioner, Node: "novox", Module: "postgres", Bus: bus, Vault: v, Offers: map[string]Offer{"postgres-database": {Serves: map[string]any{"port": 5432}}}, Make: func(_ context.Context, c Consumer) error { made = append(made, c.Entry.User+"="+string(c.Credential)) @@ -814,7 +814,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { // `mesh-provisioner.plans` (novox/hq ADR 0570 clause 2), writes only the own bucket's state, and refuses any other // bucket, the controller's assignments among them. t.Run("StateBusMapsTheProvidersBucketsToTheRuntimesStates", func(t *testing.T) { - bus := StateBus("postgres") + bus := StateBus("postgres", testProvisioner) rt := serve(t, []stdio.Tool{ {Name: "watch", Run: func(a map[string]any) (any, error) { return nil, bus.Watch(context.Background(), a["bucket"].(string), func(string, []byte) {}) @@ -828,7 +828,7 @@ func TestConsumersFileAndStateBus(t *testing.T) { }) for i, c := range []struct{ bucket, state string }{ {BucketOf("postgres"), "bound"}, - {provisions.PlansBucket, "mesh-provisioner.plans"}, + {provisions.PlansBucketOf(testProvisioner), "mesh-provisioner.plans"}, } { rt.call(i+1, "watch", map[string]any{"bucket": c.bucket}) w := rt.next() @@ -869,12 +869,12 @@ func TestConsumersFileAndStateBus(t *testing.T) { // A watch whose ctx has ended calls its handler no more, though the runtime keeps sending changes: the Bus contract // says a watch lasts until ctx ends, and a stdio state watch lasts as long as the process. t.Run("StateBusWatchCallsItsHandlerNoMoreOnceItsContextEnded", func(t *testing.T) { - bus := StateBus("postgres") + bus := StateBus("postgres", testProvisioner) ctx, cancel := context.WithCancel(context.Background()) handled := make(chan string, 8) rt := serve(t, []stdio.Tool{ {Name: "start", Run: func(map[string]any) (any, error) { - return "started", bus.Watch(ctx, provisions.PlansBucket, func(key string, value []byte) { + return "started", bus.Watch(ctx, provisions.PlansBucketOf(testProvisioner), func(key string, value []byte) { handled <- key + "=" + string(value) }) }}, -- 2.54.0 From 07ec4b1f31c5eff98cded349ca7b0ceb99ba6aa8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 21:24:07 +0100 Subject: [PATCH 91/94] Regenerate the contracts' TypeScript: the key parsers name their parameter apart from the parts (issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../ts/seats/meshconditions/src/index.ts | 24 +-- .../ts/seats/meshhandacts/src/index.ts | 32 ++-- .../ts/seats/meshhealers/src/index.ts | 24 +-- .../ts/seats/meshwatchdogs/src/index.ts | 76 +++++----- .../ts/seats/meshnodeenroller/src/index.ts | 32 ++-- .../ts/seats/meshvault/src/index.ts | 32 ++-- .../ts/seats/operatorchannel/src/index.ts | 20 +-- .../ts/seats/meshmoduleassigner/src/index.ts | 138 +++++++++--------- placement/ts/seats/meshwants/src/index.ts | 106 +++++++------- .../ts/seats/meshprovisioner/src/index.ts | 84 +++++------ ts/seats/meshconfig/src/index.ts | 80 +++++----- ts/seats/nodeengine/src/index.ts | 132 ++++++++--------- 12 files changed, 390 insertions(+), 390 deletions(-) diff --git a/health-and-repair/ts/seats/meshconditions/src/index.ts b/health-and-repair/ts/seats/meshconditions/src/index.ts index 2371334..a18cdf0 100644 --- a/health-and-repair/ts/seats/meshconditions/src/index.ts +++ b/health-and-repair/ts/seats/meshconditions/src/index.ts @@ -35,12 +35,12 @@ export function conditionsByKeyKey(key: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseConditionsByKeyKey(key: string): { key: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseConditionsByKeyKey(_key: string): { key: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { key }; } @@ -50,12 +50,12 @@ export function findingsByKeyKey(key: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseFindingsByKeyKey(key: string): { key: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseFindingsByKeyKey(_key: string): { key: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { key }; } diff --git a/health-and-repair/ts/seats/meshhandacts/src/index.ts b/health-and-repair/ts/seats/meshhandacts/src/index.ts index 8568bb0..0b4ea7b 100644 --- a/health-and-repair/ts/seats/meshhandacts/src/index.ts +++ b/health-and-repair/ts/seats/meshhandacts/src/index.ts @@ -29,12 +29,12 @@ export function actsByIDKey(id: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseActsByIDKey(key: string): { id: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const id = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseActsByIDKey(_key: string): { id: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { id }; } @@ -44,16 +44,16 @@ export function warrantsByAskerAskKey(asker: string, ask: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseWarrantsByAskerAskKey(key: string): { asker: string; ask: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const asker = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const ask = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseWarrantsByAskerAskKey(_key: string): { asker: string; ask: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const asker = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const ask = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { asker, ask }; } diff --git a/health-and-repair/ts/seats/meshhealers/src/index.ts b/health-and-repair/ts/seats/meshhealers/src/index.ts index 3e0c695..3840537 100644 --- a/health-and-repair/ts/seats/meshhealers/src/index.ts +++ b/health-and-repair/ts/seats/meshhealers/src/index.ts @@ -29,12 +29,12 @@ export function attemptsByAttemptKey(attempt: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseAttemptsByAttemptKey(key: string): { attempt: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const attempt = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseAttemptsByAttemptKey(_key: string): { attempt: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const attempt = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { attempt }; } @@ -44,12 +44,12 @@ export function wouldByWouldKey(would: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseWouldByWouldKey(key: string): { would: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const would = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseWouldByWouldKey(_key: string): { would: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const would = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { would }; } diff --git a/health-and-repair/ts/seats/meshwatchdogs/src/index.ts b/health-and-repair/ts/seats/meshwatchdogs/src/index.ts index ed74cd9..3a5240a 100644 --- a/health-and-repair/ts/seats/meshwatchdogs/src/index.ts +++ b/health-and-repair/ts/seats/meshwatchdogs/src/index.ts @@ -24,12 +24,12 @@ export function presenceByNodeKey(node: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parsePresenceByNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePresenceByNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node }; } @@ -39,18 +39,18 @@ export function presenceByPartPartInstanceKey(part: string, instance: string): s } /** The parts of a key of the form "part..", or undefined for a key of another form. */ -export function parsePresenceByPartPartInstanceKey(key: string): { part: string; instance: string } | undefined { - let rest = key; - if (!rest.startsWith("part.")) return undefined; - rest = rest.slice(5); - const at1 = rest.indexOf("."); - if (at1 <= 0) return undefined; - const part = rest.slice(0, at1); - rest = rest.slice(at1 + 1); - if (rest === "") return undefined; - const instance = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePresenceByPartPartInstanceKey(_key: string): { part: string; instance: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("part.")) return undefined; + _rest = _rest.slice(5); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const part = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + if (_rest === "") return undefined; + const instance = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { part, instance }; } @@ -60,16 +60,16 @@ export function pulseByBucketKeyKey(bucket: string, key: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parsePulseByBucketKeyKey(key: string): { bucket: string; key: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const bucket = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePulseByBucketKeyKey(_key: string): { bucket: string; key: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const bucket = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { bucket, key }; } @@ -79,16 +79,16 @@ export function stuckByBucketKeyKey(bucket: string, key: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseStuckByBucketKeyKey(key: string): { bucket: string; key: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const bucket = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseStuckByBucketKeyKey(_key: string): { bucket: string; key: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const bucket = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { bucket, key }; } diff --git a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts index 3ca423d..1312a13 100644 --- a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts +++ b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts @@ -43,14 +43,14 @@ export function enrolmentsByNodeNodeKey(node: string): string { } /** The parts of a key of the form "node.", or undefined for a key of another form. */ -export function parseEnrolmentsByNodeNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (!rest.startsWith("node.")) return undefined; - rest = rest.slice(5); - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseEnrolmentsByNodeNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("node.")) return undefined; + _rest = _rest.slice(5); + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node }; } @@ -60,14 +60,14 @@ export function enrolmentsByAddressAddressKey(address: string): string { } /** The parts of a key of the form "address.
", or undefined for a key of another form. */ -export function parseEnrolmentsByAddressAddressKey(key: string): { address: string } | undefined { - let rest = key; - if (!rest.startsWith("address.")) return undefined; - rest = rest.slice(8); - if (rest === "") return undefined; - const address = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseEnrolmentsByAddressAddressKey(_key: string): { address: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("address.")) return undefined; + _rest = _rest.slice(8); + if (_rest === "") return undefined; + const address = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { address }; } diff --git a/identity-and-access/ts/seats/meshvault/src/index.ts b/identity-and-access/ts/seats/meshvault/src/index.ts index 112701a..e3f2c11 100644 --- a/identity-and-access/ts/seats/meshvault/src/index.ts +++ b/identity-and-access/ts/seats/meshvault/src/index.ts @@ -91,11 +91,11 @@ export function signersByKeyKey(): string { } /** The parts of a key of the form "key", or undefined for a key of another form. */ -export function parseSignersByKeyKey(key: string): { } | undefined { - let rest = key; - if (!rest.startsWith("key")) return undefined; - rest = rest.slice(3); - if (rest !== "") return undefined; +export function parseSignersByKeyKey(_key: string): { } | undefined { + let _rest = _key; + if (!_rest.startsWith("key")) return undefined; + _rest = _rest.slice(3); + if (_rest !== "") return undefined; return { }; } @@ -105,11 +105,11 @@ export function signersByAuthorityKey(): string { } /** The parts of a key of the form "authority", or undefined for a key of another form. */ -export function parseSignersByAuthorityKey(key: string): { } | undefined { - let rest = key; - if (!rest.startsWith("authority")) return undefined; - rest = rest.slice(9); - if (rest !== "") return undefined; +export function parseSignersByAuthorityKey(_key: string): { } | undefined { + let _rest = _key; + if (!_rest.startsWith("authority")) return undefined; + _rest = _rest.slice(9); + if (_rest !== "") return undefined; return { }; } @@ -119,12 +119,12 @@ export function nodeLoginsByNodeKey(node: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseNodeLoginsByNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseNodeLoginsByNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node }; } diff --git a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts index 4b2837b..1331987 100644 --- a/operator-and-conversation/ts/seats/operatorchannel/src/index.ts +++ b/operator-and-conversation/ts/seats/operatorchannel/src/index.ts @@ -68,16 +68,16 @@ export function asksByAskerIDKey(asker: string, id: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseAsksByAskerIDKey(key: string): { asker: string; id: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const asker = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const id = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseAsksByAskerIDKey(_key: string): { asker: string; id: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const asker = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const id = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { asker, id }; } diff --git a/placement/ts/seats/meshmoduleassigner/src/index.ts b/placement/ts/seats/meshmoduleassigner/src/index.ts index 137274b..116f6b5 100644 --- a/placement/ts/seats/meshmoduleassigner/src/index.ts +++ b/placement/ts/seats/meshmoduleassigner/src/index.ts @@ -43,13 +43,13 @@ export function placementByNodeNodeKey(node: string): string { } /** The parts of a key of the form ".node", or undefined for a key of another form. */ -export function parsePlacementByNodeNodeKey(key: string): { node: string } | undefined { - let rest = key; - const at0 = rest.indexOf(".node"); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 5); - if (rest !== "") return undefined; +export function parsePlacementByNodeNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf(".node"); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 5); + if (_rest !== "") return undefined; return { node }; } @@ -59,16 +59,16 @@ export function placementByNodeModuleKey(node: string, module: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parsePlacementByNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePlacementByNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -78,12 +78,12 @@ export function proposalsByProposalKey(proposal: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseProposalsByProposalKey(key: string): { proposal: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const proposal = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseProposalsByProposalKey(_key: string): { proposal: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const proposal = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { proposal }; } @@ -93,16 +93,16 @@ export function candidatesByProposalNodeKey(proposal: string, node: string): str } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseCandidatesByProposalNodeKey(key: string): { proposal: string; node: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const proposal = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseCandidatesByProposalNodeKey(_key: string): { proposal: string; node: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const proposal = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { proposal, node }; } @@ -112,18 +112,18 @@ export function pulsesByAnswerProposalNodeKey(proposal: string, node: string): s } /** The parts of a key of the form "answer..", or undefined for a key of another form. */ -export function parsePulsesByAnswerProposalNodeKey(key: string): { proposal: string; node: string } | undefined { - let rest = key; - if (!rest.startsWith("answer.")) return undefined; - rest = rest.slice(7); - const at1 = rest.indexOf("."); - if (at1 <= 0) return undefined; - const proposal = rest.slice(0, at1); - rest = rest.slice(at1 + 1); - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePulsesByAnswerProposalNodeKey(_key: string): { proposal: string; node: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("answer.")) return undefined; + _rest = _rest.slice(7); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const proposal = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { proposal, node }; } @@ -133,14 +133,14 @@ export function pulsesByAskProposalKey(proposal: string): string { } /** The parts of a key of the form "ask.", or undefined for a key of another form. */ -export function parsePulsesByAskProposalKey(key: string): { proposal: string } | undefined { - let rest = key; - if (!rest.startsWith("ask.")) return undefined; - rest = rest.slice(4); - if (rest === "") return undefined; - const proposal = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePulsesByAskProposalKey(_key: string): { proposal: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("ask.")) return undefined; + _rest = _rest.slice(4); + if (_rest === "") return undefined; + const proposal = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { proposal }; } @@ -150,12 +150,12 @@ export function seatHoldersBySeatKey(seat: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseSeatHoldersBySeatKey(key: string): { seat: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const seat = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseSeatHoldersBySeatKey(_key: string): { seat: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const seat = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { seat }; } @@ -165,16 +165,16 @@ export function seatHoldersBySeatNodeKey(seat: string, node: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseSeatHoldersBySeatNodeKey(key: string): { seat: string; node: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const seat = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseSeatHoldersBySeatNodeKey(_key: string): { seat: string; node: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const seat = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { seat, node }; } diff --git a/placement/ts/seats/meshwants/src/index.ts b/placement/ts/seats/meshwants/src/index.ts index fe56a7d..38dab79 100644 --- a/placement/ts/seats/meshwants/src/index.ts +++ b/placement/ts/seats/meshwants/src/index.ts @@ -29,18 +29,18 @@ export function wantsByWantNodeModuleKey(node: string, module: string): string { } /** The parts of a key of the form "want..", or undefined for a key of another form. */ -export function parseWantsByWantNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - if (!rest.startsWith("want.")) return undefined; - rest = rest.slice(5); - const at1 = rest.indexOf("."); - if (at1 <= 0) return undefined; - const node = rest.slice(0, at1); - rest = rest.slice(at1 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseWantsByWantNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("want.")) return undefined; + _rest = _rest.slice(5); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -50,15 +50,15 @@ export function wantsByWantNodeNodeKey(node: string): string { } /** The parts of a key of the form "want..node", or undefined for a key of another form. */ -export function parseWantsByWantNodeNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (!rest.startsWith("want.")) return undefined; - rest = rest.slice(5); - const at1 = rest.indexOf(".node"); - if (at1 <= 0) return undefined; - const node = rest.slice(0, at1); - rest = rest.slice(at1 + 5); - if (rest !== "") return undefined; +export function parseWantsByWantNodeNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("want.")) return undefined; + _rest = _rest.slice(5); + const _at1 = _rest.indexOf(".node"); + if (_at1 <= 0) return undefined; + const node = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 5); + if (_rest !== "") return undefined; return { node }; } @@ -68,18 +68,18 @@ export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: str } /** The parts of a key of the form "held..", or undefined for a key of another form. */ -export function parseWantsByHeldModuleFingerprintKey(key: string): { module: string; fingerprint: string } | undefined { - let rest = key; - if (!rest.startsWith("held.")) return undefined; - rest = rest.slice(5); - const at1 = rest.indexOf("."); - if (at1 <= 0) return undefined; - const module = rest.slice(0, at1); - rest = rest.slice(at1 + 1); - if (rest === "") return undefined; - const fingerprint = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseWantsByHeldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined { + let _rest = _key; + if (!_rest.startsWith("held.")) return undefined; + _rest = _rest.slice(5); + const _at1 = _rest.indexOf("."); + if (_at1 <= 0) return undefined; + const module = _rest.slice(0, _at1); + _rest = _rest.slice(_at1 + 1); + if (_rest === "") return undefined; + const fingerprint = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { module, fingerprint }; } @@ -89,16 +89,16 @@ export function differencesByNodeModuleKey(node: string, module: string): string } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseDifferencesByNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseDifferencesByNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -108,16 +108,16 @@ export function compositionsByGroupHeadsKey(group: string, heads: string): strin } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseCompositionsByGroupHeadsKey(key: string): { group: string; heads: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const group = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const heads = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseCompositionsByGroupHeadsKey(_key: string): { group: string; heads: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const group = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const heads = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { group, heads }; } diff --git a/provisioning/ts/seats/meshprovisioner/src/index.ts b/provisioning/ts/seats/meshprovisioner/src/index.ts index a438b91..23c0e47 100644 --- a/provisioning/ts/seats/meshprovisioner/src/index.ts +++ b/provisioning/ts/seats/meshprovisioner/src/index.ts @@ -19,16 +19,16 @@ export function plansByNodeModuleKey(node: string, module: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parsePlansByNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePlansByNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -38,20 +38,20 @@ export function provisionsByNodeConsumerProvisionKey(node: string, consumer: str } /** The parts of a key of the form "..", or undefined for a key of another form. */ -export function parseProvisionsByNodeConsumerProvisionKey(key: string): { node: string; consumer: string; provision: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - const at2 = rest.indexOf("."); - if (at2 <= 0) return undefined; - const consumer = rest.slice(0, at2); - rest = rest.slice(at2 + 1); - if (rest === "") return undefined; - const provision = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseProvisionsByNodeConsumerProvisionKey(_key: string): { node: string; consumer: string; provision: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const consumer = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const provision = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, consumer, provision }; } @@ -61,24 +61,24 @@ export function provisionsByNodeConsumerProvisionLocalKey(node: string, consumer } /** The parts of a key of the form "...", or undefined for a key of another form. */ -export function parseProvisionsByNodeConsumerProvisionLocalKey(key: string): { node: string; consumer: string; provision: string; local: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - const at2 = rest.indexOf("."); - if (at2 <= 0) return undefined; - const consumer = rest.slice(0, at2); - rest = rest.slice(at2 + 1); - const at4 = rest.indexOf("."); - if (at4 <= 0) return undefined; - const provision = rest.slice(0, at4); - rest = rest.slice(at4 + 1); - if (rest === "") return undefined; - const local = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseProvisionsByNodeConsumerProvisionLocalKey(_key: string): { node: string; consumer: string; provision: string; local: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const consumer = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + const _at4 = _rest.indexOf("."); + if (_at4 <= 0) return undefined; + const provision = _rest.slice(0, _at4); + _rest = _rest.slice(_at4 + 1); + if (_rest === "") return undefined; + const local = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, consumer, provision, local }; } diff --git a/ts/seats/meshconfig/src/index.ts b/ts/seats/meshconfig/src/index.ts index c5e0a15..7a6aa32 100644 --- a/ts/seats/meshconfig/src/index.ts +++ b/ts/seats/meshconfig/src/index.ts @@ -43,20 +43,20 @@ export function resolvedByNodeModuleKeyKey(node: string, module: string, key: st } /** The parts of a key of the form "..", or undefined for a key of another form. */ -export function parseResolvedByNodeModuleKeyKey(key: string): { node: string; module: string; key: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - const at2 = rest.indexOf("."); - if (at2 <= 0) return undefined; - const module = rest.slice(0, at2); - rest = rest.slice(at2 + 1); - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseResolvedByNodeModuleKeyKey(_key: string): { node: string; module: string; key: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const module = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module, key }; } @@ -66,32 +66,32 @@ export function keptByOwnerRepositoryNumberNodeModuleKeyKey(owner: string, repos } /** The parts of a key of the form ".....", or undefined for a key of another form. */ -export function parseKeptByOwnerRepositoryNumberNodeModuleKeyKey(key: string): { owner: string; repository: string; number: string; node: string; module: string; key: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const owner = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - const at2 = rest.indexOf("."); - if (at2 <= 0) return undefined; - const repository = rest.slice(0, at2); - rest = rest.slice(at2 + 1); - const at4 = rest.indexOf("."); - if (at4 <= 0) return undefined; - const number = rest.slice(0, at4); - rest = rest.slice(at4 + 1); - const at6 = rest.indexOf("."); - if (at6 <= 0) return undefined; - const node = rest.slice(0, at6); - rest = rest.slice(at6 + 1); - const at8 = rest.indexOf("."); - if (at8 <= 0) return undefined; - const module = rest.slice(0, at8); - rest = rest.slice(at8 + 1); - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseKeptByOwnerRepositoryNumberNodeModuleKeyKey(_key: string): { owner: string; repository: string; number: string; node: string; module: string; key: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const owner = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const repository = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + const _at4 = _rest.indexOf("."); + if (_at4 <= 0) return undefined; + const number = _rest.slice(0, _at4); + _rest = _rest.slice(_at4 + 1); + const _at6 = _rest.indexOf("."); + if (_at6 <= 0) return undefined; + const node = _rest.slice(0, _at6); + _rest = _rest.slice(_at6 + 1); + const _at8 = _rest.indexOf("."); + if (_at8 <= 0) return undefined; + const module = _rest.slice(0, _at8); + _rest = _rest.slice(_at8 + 1); + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { owner, repository, number, node, module, key }; } diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 4399ede..d68248e 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -60,16 +60,16 @@ export function haveByNodeModuleKey(node: string, module: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseHaveByNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseHaveByNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -79,13 +79,13 @@ export function haveByNodeNodeKey(node: string): string { } /** The parts of a key of the form ".node", or undefined for a key of another form. */ -export function parseHaveByNodeNodeKey(key: string): { node: string } | undefined { - let rest = key; - const at0 = rest.indexOf(".node"); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 5); - if (rest !== "") return undefined; +export function parseHaveByNodeNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf(".node"); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 5); + if (_rest !== "") return undefined; return { node }; } @@ -95,20 +95,20 @@ export function composedByNodeModuleFingerprintKey(node: string, module: string, } /** The parts of a key of the form "..", or undefined for a key of another form. */ -export function parseComposedByNodeModuleFingerprintKey(key: string): { node: string; module: string; fingerprint: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - const at2 = rest.indexOf("."); - if (at2 <= 0) return undefined; - const module = rest.slice(0, at2); - rest = rest.slice(at2 + 1); - if (rest === "") return undefined; - const fingerprint = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseComposedByNodeModuleFingerprintKey(_key: string): { node: string; module: string; fingerprint: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + const _at2 = _rest.indexOf("."); + if (_at2 <= 0) return undefined; + const module = _rest.slice(0, _at2); + _rest = _rest.slice(_at2 + 1); + if (_rest === "") return undefined; + const fingerprint = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module, fingerprint }; } @@ -118,13 +118,13 @@ export function composedByNodeNodeKey(node: string): string { } /** The parts of a key of the form ".node", or undefined for a key of another form. */ -export function parseComposedByNodeNodeKey(key: string): { node: string } | undefined { - let rest = key; - const at0 = rest.indexOf(".node"); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 5); - if (rest !== "") return undefined; +export function parseComposedByNodeNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf(".node"); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 5); + if (_rest !== "") return undefined; return { node }; } @@ -134,16 +134,16 @@ export function dataByNodeModuleKey(node: string, module: string): string { } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseDataByNodeModuleKey(key: string): { node: string; module: string } | undefined { - let rest = key; - const at0 = rest.indexOf("."); - if (at0 <= 0) return undefined; - const node = rest.slice(0, at0); - rest = rest.slice(at0 + 1); - if (rest === "") return undefined; - const module = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseDataByNodeModuleKey(_key: string): { node: string; module: string } | undefined { + let _rest = _key; + const _at0 = _rest.indexOf("."); + if (_at0 <= 0) return undefined; + const node = _rest.slice(0, _at0); + _rest = _rest.slice(_at0 + 1); + if (_rest === "") return undefined; + const module = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node, module }; } @@ -153,12 +153,12 @@ export function packagesByNodeKey(node: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parsePackagesByNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parsePackagesByNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node }; } @@ -168,12 +168,12 @@ export function healthByNodeKey(node: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseHealthByNodeKey(key: string): { node: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const node = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseHealthByNodeKey(_key: string): { node: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const node = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { node }; } @@ -183,12 +183,12 @@ export function findingsByKeyKey(key: string): string { } /** The parts of a key of the form "", or undefined for a key of another form. */ -export function parseFindingsByKeyKey(key: string): { key: string } | undefined { - let rest = key; - if (rest === "") return undefined; - const key = rest; - rest = ""; - if (rest !== "") return undefined; +export function parseFindingsByKeyKey(_key: string): { key: string } | undefined { + let _rest = _key; + if (_rest === "") return undefined; + const key = _rest; + _rest = ""; + if (_rest !== "") return undefined; return { key }; } -- 2.54.0 From fdcec5d384015fd2c216baf86d1ad25892649e81 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 21:39:56 +0100 Subject: [PATCH 92/94] The Placement domain goes: the module assigner and the four node seats move to the Core place, the assigner's state and verb placement become assignments, mesh-wants goes to mesh-delivery (issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- format/seat-definition.schema.json | 1 - .../meshmoduleassignerclient/client_test.go | 6 +- go/clients/meshmoduleassignerclient/go.mod | 8 + .../meshmoduleassignerclient/holders.go | 2 +- .../meshmoduleassignerclient/holders_test.go | 10 +- .../clients/meshmoduleassignerclient/keys.go | 42 +-- .../clients/meshmoduleassignerclient/ops.go | 0 .../meshmoduleassignerclient/ops_test.go | 0 .../meshmoduleassignerclient/placed.go | 4 +- .../meshmoduleassignerclient/placement.go | 2 +- .../meshmoduleassignerclient/proposal.go | 0 .../clients/meshmoduleassignerclient/reads.go | 0 .../meshmoduleassignerclient/reads_test.go | 0 .../seats/meshmoduleassigner/contract.go | 82 ++--- .../seats/meshmoduleassigner/contract_test.go | 6 +- go/seats/meshmoduleassigner/go.mod | 3 + module.json | 29 ++ .../clients/meshmoduleassignerclient/go.mod | 8 - placement/go/clients/meshwantsclient/go.mod | 8 - placement/go/clients/meshwantsclient/wants.go | 326 ----------------- .../go/clients/meshwantsclient/wants_test.go | 162 --------- placement/go/seats/meshmoduleassigner/go.mod | 3 - placement/go/seats/meshwants/contract.go | 337 ------------------ placement/go/seats/meshwants/contract_test.go | 76 ---- placement/go/seats/meshwants/go.mod | 3 - placement/module.json | 58 --- placement/seats/index.json | 11 - .../seats/mesh-wants/fixtures/states.json | 47 --- placement/seats/mesh-wants/seat.json | 284 --------------- .../meshmoduleassigner/package-lock.json | 12 - placement/ts/seats/meshwants/package.json | 13 - placement/ts/seats/meshwants/src/index.ts | 211 ----------- placement/ts/seats/meshwants/tsconfig.json | 12 - seats/index.json | 7 +- .../mesh-module-assigner/fixtures/events.json | 0 .../mesh-module-assigner/fixtures/states.json | 6 +- .../mesh-module-assigner/seat.json | 10 +- .../node-container-runtime/seat.json | 2 +- .../node-environment/seat.json | 2 +- .../node-package-manager/seat.json | 2 +- .../node-service-manager/seat.json | 2 +- .../meshmoduleassigner}/package-lock.json | 4 +- .../seats/meshmoduleassigner/package.json | 2 +- .../seats/meshmoduleassigner/src/index.ts | 36 +- .../seats/meshmoduleassigner/tsconfig.json | 0 45 files changed, 156 insertions(+), 1683 deletions(-) rename {placement/go => go}/clients/meshmoduleassignerclient/client_test.go (97%) create mode 100644 go/clients/meshmoduleassignerclient/go.mod rename {placement/go => go}/clients/meshmoduleassignerclient/holders.go (94%) rename {placement/go => go}/clients/meshmoduleassignerclient/holders_test.go (81%) rename {placement/go => go}/clients/meshmoduleassignerclient/keys.go (91%) rename {placement/go => go}/clients/meshmoduleassignerclient/ops.go (100%) rename {placement/go => go}/clients/meshmoduleassignerclient/ops_test.go (100%) rename {placement/go => go}/clients/meshmoduleassignerclient/placed.go (97%) rename {placement/go => go}/clients/meshmoduleassignerclient/placement.go (99%) rename {placement/go => go}/clients/meshmoduleassignerclient/proposal.go (100%) rename {placement/go => go}/clients/meshmoduleassignerclient/reads.go (100%) rename {placement/go => go}/clients/meshmoduleassignerclient/reads_test.go (100%) rename {placement/go => go}/seats/meshmoduleassigner/contract.go (88%) rename {placement/go => go}/seats/meshmoduleassigner/contract_test.go (98%) create mode 100644 go/seats/meshmoduleassigner/go.mod delete mode 100644 placement/go/clients/meshmoduleassignerclient/go.mod delete mode 100644 placement/go/clients/meshwantsclient/go.mod delete mode 100644 placement/go/clients/meshwantsclient/wants.go delete mode 100644 placement/go/clients/meshwantsclient/wants_test.go delete mode 100644 placement/go/seats/meshmoduleassigner/go.mod delete mode 100644 placement/go/seats/meshwants/contract.go delete mode 100644 placement/go/seats/meshwants/contract_test.go delete mode 100644 placement/go/seats/meshwants/go.mod delete mode 100644 placement/module.json delete mode 100644 placement/seats/index.json delete mode 100644 placement/seats/mesh-wants/fixtures/states.json delete mode 100644 placement/seats/mesh-wants/seat.json delete mode 100644 placement/ts/seats/meshmoduleassigner/package-lock.json delete mode 100644 placement/ts/seats/meshwants/package.json delete mode 100644 placement/ts/seats/meshwants/src/index.ts delete mode 100644 placement/ts/seats/meshwants/tsconfig.json rename {placement/seats => seats}/mesh-module-assigner/fixtures/events.json (100%) rename {placement/seats => seats}/mesh-module-assigner/fixtures/states.json (94%) rename {placement/seats => seats}/mesh-module-assigner/seat.json (97%) rename {placement/seats => seats}/node-container-runtime/seat.json (85%) rename {placement/seats => seats}/node-environment/seat.json (83%) rename {placement/seats => seats}/node-package-manager/seat.json (83%) rename {placement/seats => seats}/node-service-manager/seat.json (99%) rename {placement/ts/seats/meshwants => ts/seats/meshmoduleassigner}/package-lock.json (53%) rename {placement/ts => ts}/seats/meshmoduleassigner/package.json (78%) rename {placement/ts => ts}/seats/meshmoduleassigner/src/index.ts (93%) rename {placement/ts => ts}/seats/meshmoduleassigner/tsconfig.json (100%) diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index 9dbc5ce..46a1557 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -39,7 +39,6 @@ "Module", "Config", "Core", - "Placement", "Provisioning", "Identity and access", "Delivery", diff --git a/placement/go/clients/meshmoduleassignerclient/client_test.go b/go/clients/meshmoduleassignerclient/client_test.go similarity index 97% rename from placement/go/clients/meshmoduleassignerclient/client_test.go rename to go/clients/meshmoduleassignerclient/client_test.go index 06453e0..8248650 100644 --- a/placement/go/clients/meshmoduleassignerclient/client_test.go +++ b/go/clients/meshmoduleassignerclient/client_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -66,7 +66,7 @@ func TestPlacementClient(t *testing.T) { Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}}, } novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}}, - Holds: []seat.Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}} + Holds: []seat.Holding{{Claim: "mesh-example", Scope: "mesh", Module: "postgres"}}} t.Run("value", func(t *testing.T) { raw, err := EncodePlacement(g14) @@ -230,7 +230,7 @@ func TestPlacementClient(t *testing.T) { t.Errorf("an unchanged placement writes %+v", again) } // Keys that do not agree keep what the node had, and say so. - stray, _ := EncodeModule(seat.PlacementByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}}) + stray, _ := EncodeModule(seat.AssignmentsByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}}) if _, err := keys.Put("g14.app", stray, 99, false); err == nil { t.Error("a module key the node key does not name the value of was taken silently") } diff --git a/go/clients/meshmoduleassignerclient/go.mod b/go/clients/meshmoduleassignerclient/go.mod new file mode 100644 index 0000000..5ba273a --- /dev/null +++ b/go/clients/meshmoduleassignerclient/go.mod @@ -0,0 +1,8 @@ +module git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient + +go 1.26.0 + +require ( + git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 +) diff --git a/placement/go/clients/meshmoduleassignerclient/holders.go b/go/clients/meshmoduleassignerclient/holders.go similarity index 94% rename from placement/go/clients/meshmoduleassignerclient/holders.go rename to go/clients/meshmoduleassignerclient/holders.go index 763d2be..8e0f845 100644 --- a/placement/go/clients/meshmoduleassignerclient/holders.go +++ b/go/clients/meshmoduleassignerclient/holders.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) // SeatHoldersKey is a seat's key in the state `seat-holders`: `` for a seat held once for the mesh (node empty), diff --git a/placement/go/clients/meshmoduleassignerclient/holders_test.go b/go/clients/meshmoduleassignerclient/holders_test.go similarity index 81% rename from placement/go/clients/meshmoduleassignerclient/holders_test.go rename to go/clients/meshmoduleassignerclient/holders_test.go index 88d520c..6d51a54 100644 --- a/placement/go/clients/meshmoduleassignerclient/holders_test.go +++ b/go/clients/meshmoduleassignerclient/holders_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) // The digest a node key names is kept as the first writer made it, so a node key written before this client reads. @@ -18,14 +18,14 @@ func TestDigestIsTheFirstWritersAndHoldersAndStandingRead(t *testing.T) { keys := map[string][]byte{ "mesh-module-assigner": []byte(`{"v":1,"seat":"mesh-module-assigner","scope":"mesh","holders":[{"module":"mesh-delivery-assigner","node":"novox"}]}`), - "node-files.ace": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"ace","holders":[{"module":"node-files","node":"ace"}]}`), + "node-example.ace": []byte(`{"v":1,"seat":"node-example","scope":"node","node":"ace","holders":[{"module":"node-example","node":"ace"}]}`), "mesh-vault": []byte(`{"v":2,"seat":"mesh-vault","scope":"mesh","holders":[]}`), } for _, c := range []struct { seat, node string want int bad bool - }{{"mesh-module-assigner", "", 1, false}, {"node-files", "ace", 1, false}, {"node-files", "g14", 0, false}, {"mesh-vault", "", 0, true}} { + }{{"mesh-module-assigner", "", 1, false}, {"node-example", "ace", 1, false}, {"node-example", "g14", 0, false}, {"mesh-vault", "", 0, true}} { got, err := HoldersOf(keys, c.seat, c.node) if (err != nil) != c.bad || len(got) != c.want { t.Errorf("HoldersOf(%s, %q) = %v, %v", c.seat, c.node, got, err) @@ -37,9 +37,9 @@ func TestDigestIsTheFirstWritersAndHoldersAndStandingRead(t *testing.T) { p := NewPlaced() t0 := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) node := func(modules ...string) []byte { - n := seat.PlacementByNodeNodeValue{Node: "ace"} + n := seat.AssignmentsByNodeNodeValue{Node: "ace"} for _, name := range modules { - n.Modules = append(n.Modules, seat.PlacementByNodeNodeValueModulesItem{Module: name, Digest: "d"}) + n.Modules = append(n.Modules, seat.AssignmentsByNodeNodeValueModulesItem{Module: name, Digest: "d"}) } raw, err := EncodeNode(n) if err != nil { diff --git a/placement/go/clients/meshmoduleassignerclient/keys.go b/go/clients/meshmoduleassignerclient/keys.go similarity index 91% rename from placement/go/clients/meshmoduleassignerclient/keys.go rename to go/clients/meshmoduleassignerclient/keys.go index 27822a8..ac2dfe1 100644 --- a/placement/go/clients/meshmoduleassignerclient/keys.go +++ b/go/clients/meshmoduleassignerclient/keys.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) // **One key per module and node** (research 072 row 37; issue 774). The seat's state `placement` keeps a node's placement @@ -104,7 +104,7 @@ func checkPlacementModule(module string) error { } // EncodeNode is n as JSON at Version. -func EncodeNode(n seat.PlacementByNodeNodeValue) ([]byte, error) { +func EncodeNode(n seat.AssignmentsByNodeNodeValue) ([]byte, error) { if err := checkPlacementNode(n.Node); err != nil { return nil, err } @@ -114,24 +114,24 @@ func EncodeNode(n seat.PlacementByNodeNodeValue) ([]byte, error) { // DecodeNode reads a node key's value; it refuses an unknown version, a node or module that is not a name, and a module // named twice or without a digest. -func DecodeNode(data []byte) (seat.PlacementByNodeNodeValue, error) { +func DecodeNode(data []byte) (seat.AssignmentsByNodeNodeValue, error) { if err := checkVersion(data); err != nil { - return seat.PlacementByNodeNodeValue{}, err + return seat.AssignmentsByNodeNodeValue{}, err } - n, err := seat.DecodePlacementByNodeNodeValue(data) + n, err := seat.DecodeAssignmentsByNodeNodeValue(data) if err != nil { return n, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) } if err := checkPlacementNode(n.Node); err != nil { - return seat.PlacementByNodeNodeValue{}, err + return seat.AssignmentsByNodeNodeValue{}, err } seen := map[string]bool{} for _, r := range n.Modules { if err := checkPlacementModule(r.Module); err != nil { - return seat.PlacementByNodeNodeValue{}, err + return seat.AssignmentsByNodeNodeValue{}, err } if seen[r.Module] || r.Digest == "" { - return seat.PlacementByNodeNodeValue{}, fmt.Errorf("meshmoduleassignerclient: the node key of %s names %s twice or without a digest", n.Node, r.Module) + return seat.AssignmentsByNodeNodeValue{}, fmt.Errorf("meshmoduleassignerclient: the node key of %s names %s twice or without a digest", n.Node, r.Module) } seen[r.Module] = true } @@ -139,7 +139,7 @@ func DecodeNode(data []byte) (seat.PlacementByNodeNodeValue, error) { } // EncodeModule is m as JSON at Version. -func EncodeModule(m seat.PlacementByNodeModuleValue) ([]byte, error) { +func EncodeModule(m seat.AssignmentsByNodeModuleValue) ([]byte, error) { if err := checkPlacementNode(m.Node); err != nil { return nil, err } @@ -154,19 +154,19 @@ func EncodeModule(m seat.PlacementByNodeModuleValue) ([]byte, error) { } // DecodeModule reads a module key's value. -func DecodeModule(data []byte) (seat.PlacementByNodeModuleValue, error) { +func DecodeModule(data []byte) (seat.AssignmentsByNodeModuleValue, error) { if err := checkVersion(data); err != nil { - return seat.PlacementByNodeModuleValue{}, err + return seat.AssignmentsByNodeModuleValue{}, err } - m, err := seat.DecodePlacementByNodeModuleValue(data) + m, err := seat.DecodeAssignmentsByNodeModuleValue(data) if err != nil { return m, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) } if err := checkPlacementNode(m.Node); err != nil { - return seat.PlacementByNodeModuleValue{}, err + return seat.AssignmentsByNodeModuleValue{}, err } if err := checkPlacementModule(m.Placed.Module); err != nil { - return seat.PlacementByNodeModuleValue{}, err + return seat.AssignmentsByNodeModuleValue{}, err } return m, nil } @@ -185,9 +185,9 @@ type Keys struct { // keysOf is what is held of one node's keys. type keysOf struct { - node *seat.PlacementByNodeNodeValue + node *seat.AssignmentsByNodeNodeValue nodeRev uint64 - modules map[string]seat.PlacementByNodeModuleValue + modules map[string]seat.AssignmentsByNodeModuleValue moduleRevs map[string]uint64 legacy *seat.Placement legacyRev uint64 @@ -245,7 +245,7 @@ func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (nod defer k.mu.Unlock() of := k.nodes[node] if of == nil { - of = &keysOf{modules: map[string]seat.PlacementByNodeModuleValue{}, moduleRevs: map[string]uint64{}} + of = &keysOf{modules: map[string]seat.AssignmentsByNodeModuleValue{}, moduleRevs: map[string]uint64{}} k.nodes[node] = of } switch kind { @@ -405,7 +405,7 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { defer k.mu.Unlock() of := k.nodes[node] if of == nil { - of = &keysOf{modules: map[string]seat.PlacementByNodeModuleValue{}, moduleRevs: map[string]uint64{}} + of = &keysOf{modules: map[string]seat.AssignmentsByNodeModuleValue{}, moduleRevs: map[string]uint64{}} } var out []KeyWrite deleteModules := func(keep map[string]bool) { @@ -440,21 +440,21 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { committed[m.Module] = m } } - n := seat.PlacementByNodeNodeValue{Node: node, Decided: after.Decided, Pins: after.Pins, Holds: after.Holds, Modules: make([]seat.PlacementByNodeNodeValueModulesItem, 0, len(after.Modules))} + n := seat.AssignmentsByNodeNodeValue{Node: node, Decided: after.Decided, Pins: after.Pins, Holds: after.Holds, Modules: make([]seat.AssignmentsByNodeNodeValueModulesItem, 0, len(after.Modules))} keep := map[string]bool{} for _, m := range after.Modules { if err := checkPlacementModule(m.Module); err != nil { return nil, err } digest := Digest(m) - n.Modules = append(n.Modules, seat.PlacementByNodeNodeValueModulesItem{Module: m.Module, Digest: digest}) + n.Modules = append(n.Modules, seat.AssignmentsByNodeNodeValueModulesItem{Module: m.Module, Digest: digest}) keep[m.Module] = true was, wasPlaced := committed[m.Module] if held, ok := of.modules[m.Module]; ok && Digest(held.Placed) == digest && (held.Previous == nil || wasPlaced && Digest(was) == digest) { continue // the key holds it, and no previous value a reader could take in its place } - v := seat.PlacementByNodeModuleValue{Node: node, Placed: m} + v := seat.AssignmentsByNodeModuleValue{Node: node, Placed: m} if wasPlaced && Digest(was) != digest { v.Previous = &was } diff --git a/placement/go/clients/meshmoduleassignerclient/ops.go b/go/clients/meshmoduleassignerclient/ops.go similarity index 100% rename from placement/go/clients/meshmoduleassignerclient/ops.go rename to go/clients/meshmoduleassignerclient/ops.go diff --git a/placement/go/clients/meshmoduleassignerclient/ops_test.go b/go/clients/meshmoduleassignerclient/ops_test.go similarity index 100% rename from placement/go/clients/meshmoduleassignerclient/ops_test.go rename to go/clients/meshmoduleassignerclient/ops_test.go diff --git a/placement/go/clients/meshmoduleassignerclient/placed.go b/go/clients/meshmoduleassignerclient/placed.go similarity index 97% rename from placement/go/clients/meshmoduleassignerclient/placed.go rename to go/clients/meshmoduleassignerclient/placed.go index bf9fe53..1bae32a 100644 --- a/placement/go/clients/meshmoduleassignerclient/placed.go +++ b/go/clients/meshmoduleassignerclient/placed.go @@ -7,7 +7,7 @@ import ( "sync" "time" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) // Placed is where the placement (the seat mesh-placement's state `placement`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when: @@ -65,7 +65,7 @@ func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node var derr error var named string if kind == NodeKind { - var n seat.PlacementByNodeNodeValue + var n seat.AssignmentsByNodeNodeValue n, derr = DecodeNode(value) named = n.Node for _, r := range n.Modules { diff --git a/placement/go/clients/meshmoduleassignerclient/placement.go b/go/clients/meshmoduleassignerclient/placement.go similarity index 99% rename from placement/go/clients/meshmoduleassignerclient/placement.go rename to go/clients/meshmoduleassignerclient/placement.go index 823287e..efe9612 100644 --- a/placement/go/clients/meshmoduleassignerclient/placement.go +++ b/go/clients/meshmoduleassignerclient/placement.go @@ -15,7 +15,7 @@ import ( "strconv" "strings" - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/keys" ) diff --git a/placement/go/clients/meshmoduleassignerclient/proposal.go b/go/clients/meshmoduleassignerclient/proposal.go similarity index 100% rename from placement/go/clients/meshmoduleassignerclient/proposal.go rename to go/clients/meshmoduleassignerclient/proposal.go diff --git a/placement/go/clients/meshmoduleassignerclient/reads.go b/go/clients/meshmoduleassignerclient/reads.go similarity index 100% rename from placement/go/clients/meshmoduleassignerclient/reads.go rename to go/clients/meshmoduleassignerclient/reads.go diff --git a/placement/go/clients/meshmoduleassignerclient/reads_test.go b/go/clients/meshmoduleassignerclient/reads_test.go similarity index 100% rename from placement/go/clients/meshmoduleassignerclient/reads_test.go rename to go/clients/meshmoduleassignerclient/reads_test.go diff --git a/placement/go/seats/meshmoduleassigner/contract.go b/go/seats/meshmoduleassigner/contract.go similarity index 88% rename from placement/go/seats/meshmoduleassigner/contract.go rename to go/seats/meshmoduleassigner/contract.go index 10de4c1..fd50676 100644 --- a/placement/go/seats/meshmoduleassigner/contract.go +++ b/go/seats/meshmoduleassigner/contract.go @@ -15,11 +15,11 @@ import ( const Seat = "mesh-module-assigner" const ( - VerbPlacement = "placement" - VerbProposals = "proposals" - VerbPropose = "propose" - VerbWithdraw = "withdraw" - VerbAgain = "again" + VerbAssignments = "assignments" + VerbProposals = "proposals" + VerbPropose = "propose" + VerbWithdraw = "withdraw" + VerbAgain = "again" ) const ( @@ -31,7 +31,7 @@ const ( // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbPlacement, VerbProposals, VerbPropose, VerbWithdraw, VerbAgain} + return []string{VerbAssignments, VerbProposals, VerbPropose, VerbWithdraw, VerbAgain} } // Events is every event published on the seat's subject: a new copy each call. @@ -58,18 +58,18 @@ const SubjectPlacementJudged = "mesh.seat.mesh-module-assigner.event.placement-j const SubjectPlacementChanged = "mesh.seat.mesh-module-assigner.event.placement-changed" const ( - StatePlacement = "placement" + StateAssignments = "assignments" StateProposals = "proposals" StateCandidates = "candidates" StatePulses = "pulses" StateSeatHolders = "seat-holders" ) -// PlacementByNodeNodeKey is the key of one value of the state placement under the form ".node". -func PlacementByNodeNodeKey(node string) string { return node + ".node" } +// AssignmentsByNodeNodeKey is the key of one value of the state assignments under the form ".node". +func AssignmentsByNodeNodeKey(node string) string { return node + ".node" } -// ParsePlacementByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. -func ParsePlacementByNodeNodeKey(key string) (string, bool) { +// ParseAssignmentsByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. +func ParseAssignmentsByNodeNodeKey(key string) (string, bool) { rest := key v0, after0, found0 := strings.Cut(rest, ".node") if !found0 || v0 == "" { @@ -82,9 +82,9 @@ func ParsePlacementByNodeNodeKey(key string) (string, bool) { return v0, true } -// DecodePlacementByNodeNodeValue reads one value of the state placement under the form ".node", refusing a field PlacementByNodeNodeValue does not know and any text after the value. -func DecodePlacementByNodeNodeValue(raw []byte) (PlacementByNodeNodeValue, error) { - var v PlacementByNodeNodeValue +// DecodeAssignmentsByNodeNodeValue reads one value of the state assignments under the form ".node", refusing a field AssignmentsByNodeNodeValue does not know and any text after the value. +func DecodeAssignmentsByNodeNodeValue(raw []byte) (AssignmentsByNodeNodeValue, error) { + var v AssignmentsByNodeNodeValue dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(&v); err != nil { @@ -96,11 +96,11 @@ func DecodePlacementByNodeNodeValue(raw []byte) (PlacementByNodeNodeValue, error return v, nil } -// PlacementByNodeModuleKey is the key of one value of the state placement under the form ".". -func PlacementByNodeModuleKey(node, module string) string { return node + "." + module } +// AssignmentsByNodeModuleKey is the key of one value of the state assignments under the form ".". +func AssignmentsByNodeModuleKey(node, module string) string { return node + "." + module } -// ParsePlacementByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. -func ParsePlacementByNodeModuleKey(key string) (string, string, bool) { +// ParseAssignmentsByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseAssignmentsByNodeModuleKey(key string) (string, string, bool) { rest := key v0, after0, found0 := strings.Cut(rest, ".") if !found0 || v0 == "" { @@ -118,9 +118,9 @@ func ParsePlacementByNodeModuleKey(key string) (string, string, bool) { return v0, v2, true } -// DecodePlacementByNodeModuleValue reads one value of the state placement under the form ".", refusing a field PlacementByNodeModuleValue does not know and any text after the value. -func DecodePlacementByNodeModuleValue(raw []byte) (PlacementByNodeModuleValue, error) { - var v PlacementByNodeModuleValue +// DecodeAssignmentsByNodeModuleValue reads one value of the state assignments under the form ".", refusing a field AssignmentsByNodeModuleValue does not know and any text after the value. +func DecodeAssignmentsByNodeModuleValue(raw []byte) (AssignmentsByNodeModuleValue, error) { + var v AssignmentsByNodeModuleValue dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(&v); err != nil { @@ -547,8 +547,8 @@ type Warrant struct { Warrant string `json:"warrant"` } -// PlacementInput is what placement takes. -type PlacementInput struct { +// AssignmentsInput is what assignments takes. +type AssignmentsInput struct { // "true": the changes in order, from the stream and the state's revisions History string `json:"history,omitempty"` // one module: the nodes it is placed on @@ -559,20 +559,20 @@ type PlacementInput struct { Seats string `json:"seats,omitempty"` } -// The values of PlacementInput's history. +// The values of AssignmentsInput's history. const ( - PlacementInputHistoryTrue = "true" - PlacementInputHistoryFalse = "false" + AssignmentsInputHistoryTrue = "true" + AssignmentsInputHistoryFalse = "false" ) -// The values of PlacementInput's seats. +// The values of AssignmentsInput's seats. const ( - PlacementInputSeatsTrue = "true" - PlacementInputSeatsFalse = "false" + AssignmentsInputSeatsTrue = "true" + AssignmentsInputSeatsFalse = "false" ) -// PlacementAnswer is what placement answers. -type PlacementAnswer = map[string]any +// AssignmentsAnswer is what assignments answers. +type AssignmentsAnswer = map[string]any // ProposalsInput is what proposals takes. type ProposalsInput struct { @@ -628,26 +628,26 @@ type PlacementJudgedEvent = PlacementEvent // PlacementChangedEvent is the body of the event placement-changed. type PlacementChangedEvent = PlacementEvent -// PlacementByNodeNodeValueModulesItem is a type of the contract. -type PlacementByNodeNodeValueModulesItem struct { +// AssignmentsByNodeNodeValueModulesItem is a type of the contract. +type AssignmentsByNodeNodeValueModulesItem struct { // the digest of the module key's value it stands for Digest string `json:"digest"` Module string `json:"module"` } -// PlacementByNodeNodeValue is one value of the state placement under the key .node. -type PlacementByNodeNodeValue struct { +// AssignmentsByNodeNodeValue is one value of the state assignments under the key .node. +type AssignmentsByNodeNodeValue struct { Decided NodeDecided `json:"decided"` Holds []Holding `json:"holds,omitempty"` // the modules placed, in the order the composition reads them - Modules []PlacementByNodeNodeValueModulesItem `json:"modules"` - Node string `json:"node"` - Pins map[string]Provider `json:"pins,omitempty"` - V int64 `json:"v,omitempty"` + Modules []AssignmentsByNodeNodeValueModulesItem `json:"modules"` + Node string `json:"node"` + Pins map[string]Provider `json:"pins,omitempty"` + V int64 `json:"v,omitempty"` } -// PlacementByNodeModuleValue is one value of the state placement under the key .. -type PlacementByNodeModuleValue struct { +// AssignmentsByNodeModuleValue is one value of the state assignments under the key .. +type AssignmentsByNodeModuleValue struct { Node string `json:"node"` Placed PlacedModule `json:"placed"` // the value it replaces while a write is under way diff --git a/placement/go/seats/meshmoduleassigner/contract_test.go b/go/seats/meshmoduleassigner/contract_test.go similarity index 98% rename from placement/go/seats/meshmoduleassigner/contract_test.go rename to go/seats/meshmoduleassigner/contract_test.go index a007587..2fad606 100644 --- a/placement/go/seats/meshmoduleassigner/contract_test.go +++ b/go/seats/meshmoduleassigner/contract_test.go @@ -21,19 +21,19 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v PlacementProposedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.placement-proposed$")}, {name: "a node's key", subject: "", given: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", decode: func(b []byte) (any, error) { - var v PlacementByNodeNodeValue + var v AssignmentsByNodeNodeValue err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, {name: "a node that came with a tunnel", subject: "", given: "{\"decided\":{\"tunnel\":{\"interface\":\"wg0\",\"peers\":[{\"address\":\"10.77.0.9/32\",\"public-key\":\"k2\"}],\"port\":51820,\"public-key\":\"k1\"}},\"modules\":[],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"tunnel\":{\"interface\":\"wg0\",\"peers\":[{\"address\":\"10.77.0.9/32\",\"public-key\":\"k2\"}],\"port\":51820,\"public-key\":\"k1\"}},\"modules\":[],\"node\":\"ace\",\"v\":1}", decode: func(b []byte) (any, error) { - var v PlacementByNodeNodeValue + var v AssignmentsByNodeNodeValue err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, {name: "a module's key", subject: "", given: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", wire: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", decode: func(b []byte) (any, error) { - var v PlacementByNodeModuleValue + var v AssignmentsByNodeModuleValue err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, diff --git a/go/seats/meshmoduleassigner/go.mod b/go/seats/meshmoduleassigner/go.mod new file mode 100644 index 0000000..2d1f766 --- /dev/null +++ b/go/seats/meshmoduleassigner/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner + +go 1.26.0 diff --git a/module.json b/module.json index 1a93c8e..1d7d5c0 100644 --- a/module.json +++ b/module.json @@ -81,6 +81,35 @@ ], "series": "0.1", "generated-from": "." + }, + { + "name": "go-meshmoduleassigner", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshmoduleassignerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshmoduleassignerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshmoduleassigner", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." } ] }, diff --git a/placement/go/clients/meshmoduleassignerclient/go.mod b/placement/go/clients/meshmoduleassignerclient/go.mod deleted file mode 100644 index 45deaaf..0000000 --- a/placement/go/clients/meshmoduleassignerclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/placement/go/clients/meshwantsclient/go.mod b/placement/go/clients/meshwantsclient/go.mod deleted file mode 100644 index 26ce723..0000000 --- a/placement/go/clients/meshwantsclient/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient - -go 1.26.0 - -require ( - git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000 - git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 -) diff --git a/placement/go/clients/meshwantsclient/wants.go b/placement/go/clients/meshwantsclient/wants.go deleted file mode 100644 index 9f04734..0000000 --- a/placement/go/clients/meshwantsclient/wants.go +++ /dev/null @@ -1,326 +0,0 @@ -// Package meshwantsclient is the client of the seat mesh-wants: the keys of its state wants, and the check and the codec -// of a want, a node's own want and a hold, which every module that reads or writes wants shares (the assigner that writes -// them, the node-engine, the provisioner and the artifact store that read them). The layouts are the seat's contract; this -// file writes the version and refuses what the earlier codec refused (novox/hq ADR 0528, ADR 0539, ADR 0546, ADR 0634 §4). -package meshwantsclient - -import ( - "encoding/json" - "fmt" - "regexp" - "strings" - - seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" - "git.novox.be/novox/mesh-sdk/go/keys" -) - -// The three values of the state wants, in the seat's types. -type ( - // Want is a node's want for a module: the fingerprint it should run and the digest of the one composition it allows, - // or Removed, or Self when the node composes it itself; Newest and Rule name what holds the node back. - Want = seat.WantsByWantNodeModuleValue - // NodeWant is a node's own want: the digest of the node's own composition it may apply. - NodeWant = seat.WantsByWantNodeNodeValue - // Held is a fingerprint the assigner holds: the node whose health check failed on it, and the check's words. - Held = seat.WantsByHeldModuleFingerprintValue -) - -// The rules of ADR 0429 §6 and ADR 0539 clause 6, by number, as a want or a difference names the one that holds a node back. -const ( - RuleBuildDone = 1 // the build of F is done - RuleNotHeld = 2 // F is not held - RuleCanary = 3 // the node is the module's canary, or the canary runs F healthy - RuleStep = 4 // at most step other nodes are ahead (step 0: no limit, ADR 0528 §3) - RuleProviders = 5 // every provider the module requires runs healthy on that node, or at its canary - RuleGrant = 6 // the module's credential for F exists (grants..) - RuleComposed = 7 // the module's composition for that node at F exists (ADR 0539 §6) - // RuleProvisions: every provision the module requires on that node is provided (novox/hq ADR 0570 §4). - 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 of a later release. -const ruleRead = 10 - -// The answers a want carries to a failed backup before an apply (ADR 0539 clause 16). -const ( - // BackupAgain is the answer to back the module up again before the apply. - BackupAgain = seat.WantsByWantNodeModuleValueBackupAgain - // BackupSkip is the answer to go on without a backup. - BackupSkip = seat.WantsByWantNodeModuleValueBackupSkip -) - -// NodeWord is the last part of a node's own key. -const NodeWord = "node" - -const ( - wantWord = "want" - heldWord = "held" - srcWord = "src2:" -) - -var ( - moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`) - digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) -) - -func moduleKeyPart(module string) (string, error) { - if module == "" { - return "", fmt.Errorf("wants: the module name is empty") - } - if !moduleNameOK.MatchString(module) { - return "", fmt.Errorf("wants: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module) - } - return keys.EscapeName(module), nil -} - -func parseModuleKeyPart(part string) (string, error) { - if err := keys.CheckName("module", part); err != nil { - return "", err - } - module, err := keys.UnescapeName(part) - if err != nil { - return "", err - } - if back, err := moduleKeyPart(module); err != nil || back != part { - return "", fmt.Errorf("wants: the module part %q is not a module name as a key writes it", part) - } - return module, nil -} - -func checkFingerprint(fp, of string) error { - if part, ok := strings.CutPrefix(fp, srcWord); !ok || !keys.IsHex64(part) { - return fmt.Errorf("wants: %s: %q is not a fingerprint of the new form, %s and 64 lowercase hex digits", of, fp, srcWord) - } - return nil -} - -func checkDigest(digest, of string) error { - if !digestForm.MatchString(digest) { - return fmt.Errorf("wants: %s %q is not sha256:<64 lowercase hex digits>", of, digest) - } - return nil -} - -func checkRule(rule int64, of string) error { - if rule < RuleBuildDone || rule > ruleRead { - return fmt.Errorf("wants: %s names the rule %d, which is not one of 1 to %d", of, rule, ruleRead) - } - return nil -} - -// WantKey is the key, in the state wants, of a node's want for a module: `want..`. A module named `node` is -// refused, since `want..node` is the node's own key. -func WantKey(node, module string) (string, error) { - if err := keys.CheckName("node", node); err != nil { - return "", err - } - part, err := moduleKeyPart(module) - if err != nil { - return "", err - } - if module == NodeWord { - return "", fmt.Errorf(`wants: the module name "node" is refused: want.%s.node is the node's own key`, node) - } - return seat.WantsByWantNodeModuleKey(node, part), nil -} - -// WantNodeKey is the key of a node's own want: `want..node`. -func WantNodeKey(node string) (string, error) { - if err := keys.CheckName("node", node); err != nil { - return "", err - } - return seat.WantsByWantNodeNodeKey(node), nil -} - -// ParseWantNodeKey is the inverse of WantNodeKey; it refuses a module's want key and any key of another form. -func ParseWantNodeKey(key string) (node string, err error) { - node, ok := seat.ParseWantsByWantNodeNodeKey(key) - if !ok || keys.CheckName("node", node) != nil { - return "", fmt.Errorf("wants: %q is not a node's own want key, want..node", key) - } - return node, nil -} - -// ParseWantKey is the inverse of WantKey; it refuses a held key, the node's own want key and any key of another form. -func ParseWantKey(key string) (node, module string, err error) { - refused := fmt.Errorf("wants: %q is not a want key, want..", key) - node, part, ok := seat.ParseWantsByWantNodeModuleKey(key) - if !ok || part == NodeWord || keys.CheckName("node", node) != nil { - return "", "", refused - } - if module, err = parseModuleKeyPart(part); err != nil { - return "", "", refused - } - return node, module, nil -} - -// HeldKey is the key of a module's fingerprint the assigner holds: `held..`, the hex being the -// fingerprint's key part. -func HeldKey(module, fingerprint string) (string, error) { - if err := keys.CheckName("module", module); err != nil { - return "", err - } - if err := checkFingerprint(fingerprint, "a held key's fingerprint"); err != nil { - return "", err - } - return seat.WantsByHeldModuleFingerprintKey(module, strings.TrimPrefix(fingerprint, srcWord)), nil -} - -// ParseHeldKey is the inverse of HeldKey: the module and the fingerprint (`src2:` and the hex). -func ParseHeldKey(key string) (module, fingerprint string, err error) { - module, part, ok := seat.ParseWantsByHeldModuleFingerprintKey(key) - if !ok || keys.CheckName("module", module) != nil || !keys.IsHex64(part) { - return "", "", fmt.Errorf("wants: %q is not a held key, held..", key) - } - return module, srcWord + part, nil -} - -func checkWant(w Want) error { - if w.Removed { - if w.Fp != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold || w.Self || w.Yes != "" || w.Release != "" { - return fmt.Errorf("wants: a removed want names no fingerprint, composition, rule, backup answer or put") - } - return nil - } - if w.Hold && !w.Put { - return fmt.Errorf("wants: only a put holds a want") - } - if w.Put && (w.Newest != "" || w.Rule != 0) { - return fmt.Errorf("wants: a put's want is held back by no rule") - } - if err := checkFingerprint(w.Fp, "a want's fingerprint"); err != nil { - return err - } - // A node the catalogue places composes its own modules (ADR 0547 §2): its want is Self and names the build alone. Any - // other want names the one composition it allows (ADR 0539 clause 1). - switch { - case w.Self && w.Composed != "": - return fmt.Errorf("wants: the want of %s says the node composes it, and names a composition", w.Fp) - case !w.Self && w.Composed == "": - return fmt.Errorf("wants: the want of %s names no composition: a want names the digest of the one composition it allows (ADR 0539 clause 1), unless the node composes it itself", w.Fp) - case w.Composed != "": - if err := checkDigest(w.Composed, "a want's composition digest"); err != nil { - return err - } - } - if w.Yes != "" { - if err := checkDigest(w.Yes, "the digest a want's yes allows"); err != nil { - return err - } - } else if w.Release != "" { - return fmt.Errorf("wants: the want of %s names the release %s and no yes it gave", w.Fp, w.Release) - } - switch w.Backup { - case "", BackupAgain, BackupSkip: - default: - return fmt.Errorf("wants: the backup answer %q is not %s or %s", w.Backup, BackupAgain, BackupSkip) - } - if w.Newest == "" && w.Rule == 0 { - return nil - } - if err := checkFingerprint(w.Newest, "a want's newest fingerprint"); err != nil { - return err - } - if w.Newest == w.Fp { - return fmt.Errorf("wants: a want at its newest fingerprint names no rule holding it back") - } - return checkRule(w.Rule, "a want held back") -} - -// CheckWant refuses a want the codec refuses. -func CheckWant(w Want) error { return checkWant(w) } - -// EncodeWant is w as JSON at the kept values' version; it refuses a fingerprint not of the new form, a removed want naming -// one, and a want that names no composition unless the node composes it itself. -func EncodeWant(w Want) ([]byte, error) { - if err := checkWant(w); err != nil { - return nil, err - } - w.V = keys.Version - return json.Marshal(w) -} - -// DecodeWant reads a want; it refuses an unknown version (keys.ErrUnknownVersion), a field the contract does not know and -// what EncodeWant refuses. -func DecodeWant(data []byte) (Want, error) { - if err := keys.CheckVersion(data); err != nil { - return Want{}, err - } - w, err := seat.DecodeWantsByWantNodeModuleValue(data) - if err != nil { - return Want{}, fmt.Errorf("wants: value does not fit its shape: %w", err) - } - if err := checkWant(w); err != nil { - return Want{}, err - } - return w, nil -} - -// SameWant says whether two wants are the same value, so the assigner writes only when they differ. -func SameWant(a, b Want) bool { return a == b } - -func checkNodeWant(w NodeWant) error { - switch { - case w.Self && w.Composed != "": - return fmt.Errorf("wants: the node's own want says the node composes it, and names a composition") - case w.Self: - return nil - } - return checkDigest(w.Composed, "the node's own want's composition digest") -} - -// EncodeNodeWant is w as JSON; it refuses a digest not of the sha256 form, and a want that names no composition unless it -// says the node composes its own (Self, ADR 0547 §2). -func EncodeNodeWant(w NodeWant) ([]byte, error) { - if err := checkNodeWant(w); err != nil { - return nil, err - } - w.V = keys.Version - return json.Marshal(w) -} - -// DecodeNodeWant reads a node's own want; it refuses an unknown version, a field a node's own want does not have (a -// fingerprint among them), and what EncodeNodeWant refuses. -func DecodeNodeWant(data []byte) (NodeWant, error) { - if err := keys.CheckVersion(data); err != nil { - return NodeWant{}, err - } - w, err := seat.DecodeWantsByWantNodeNodeValue(data) - if err != nil { - return NodeWant{}, fmt.Errorf("wants: a node's own want does not fit its shape: %w", err) - } - if err := checkNodeWant(w); err != nil { - return NodeWant{}, err - } - return w, nil -} - -func checkHeld(h Held) error { return keys.CheckName("node", h.Node) } - -// EncodeHeld is h as JSON; it refuses a node name a key refuses. -func EncodeHeld(h Held) ([]byte, error) { - if err := checkHeld(h); err != nil { - return nil, err - } - h.V = keys.Version - return json.Marshal(h) -} - -// DecodeHeld reads a hold; it refuses an unknown version and what EncodeHeld refuses. -func DecodeHeld(data []byte) (Held, error) { - if err := keys.CheckVersion(data); err != nil { - return Held{}, err - } - h, err := seat.DecodeWantsByHeldModuleFingerprintValue(data) - if err != nil { - return Held{}, fmt.Errorf("wants: value does not fit its shape: %w", err) - } - if err := checkHeld(h); err != nil { - return Held{}, err - } - return h, nil -} - -// SameHeld says whether two holds are the same value. -func SameHeld(a, b Held) bool { return a == b } diff --git a/placement/go/clients/meshwantsclient/wants_test.go b/placement/go/clients/meshwantsclient/wants_test.go deleted file mode 100644 index 5e2ecad..0000000 --- a/placement/go/clients/meshwantsclient/wants_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package meshwantsclient - -import ( - "encoding/json" - "errors" - "reflect" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/keys" -) - -func rep(c string) string { return strings.Repeat(c, 64) } - -// TestWantsAreWhatTheEarlierCodecWrote holds the values and keys equal to what go/delivery's codec wrote for the same -// values (the golden texts are its output; the field order is the contract's now, so values are compared decoded), and the -// refusals it made. -func TestWantsAreWhatTheEarlierCodecWrote(t *testing.T) { - a, b, c, d := "src2:"+rep("a"), "src2:"+rep("b"), "sha256:"+rep("c"), "sha256:"+rep("d") - sameJSON := func(t *testing.T, got []byte, want string) { - t.Helper() - var g, w any - if err := json.Unmarshal(got, &g); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(want), &w); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(g, w) { - t.Fatalf("wrote %s, the earlier codec wrote %s", got, want) - } - } - t.Run("wants", func(t *testing.T) { - for name, row := range map[string]struct { - w Want - golden string - }{ - "composed": {Want{Fp: a, Composed: c, Words: "w"}, `{"v":1,"fp":"` + a + `","composed":"` + c + `","words":"w"}`}, - "held back": {Want{Fp: a, Self: true, Newest: b, Rule: RuleGrant, Backup: BackupAgain, Words: "held"}, `{"v":1,"fp":"` + a + `","newest":"` + b + `","rule":6,"backup":"again","words":"held","self":true}`}, - "put": {Want{Fp: a, Self: true, Put: true, Hold: true, Yes: d, Release: "r1"}, `{"v":1,"fp":"` + a + `","put":true,"hold":true,"yes":"` + d + `","release":"r1","self":true}`}, - "removed": {Want{Removed: true, Words: "gone"}, `{"v":1,"removed":true,"words":"gone"}`}, - } { - t.Run(name, func(t *testing.T) { - raw, err := EncodeWant(row.w) - if err != nil { - t.Fatal(err) - } - sameJSON(t, raw, row.golden) - back, err := DecodeWant(raw) - if err != nil || !SameWant(back, func() Want { w := row.w; w.V = keys.Version; return w }()) { - t.Fatalf("read back %+v, %v", back, err) - } - }) - } - }) - t.Run("refused", func(t *testing.T) { - for name, w := range map[string]Want{ - "old fingerprint": {Fp: "src1:abc", Composed: c}, - "no composition": {Fp: a}, - "self and composition": {Fp: a, Self: true, Composed: c}, - "hold without put": {Fp: a, Self: true, Hold: true}, - "put held back": {Fp: a, Self: true, Put: true, Newest: b, Rule: RuleStep}, - "removed names a rule": {Removed: true, Rule: 1}, - "release without yes": {Fp: a, Self: true, Release: "r"}, - "backup answer unknown": {Fp: a, Self: true, Backup: "later"}, - "newest equals fp": {Fp: a, Self: true, Newest: a, Rule: RuleStep}, - "rule 11": {Fp: a, Self: true, Newest: b, Rule: 11}, - "newest without a rule": {Fp: a, Self: true, Newest: b}, - "composition not sha256": {Fp: a, Composed: "sha256:xyz"}, - } { - if _, err := EncodeWant(w); err == nil { - t.Errorf("%s: written", name) - } - } - if _, err := EncodeWant(Want{Fp: a, Self: true, Newest: b, Rule: 10}); err != nil { - t.Errorf("rule 10 is read: %v", err) - } - if _, err := DecodeWant([]byte(`{"fp":"` + a + `","self":true}`)); !errors.Is(err, keys.ErrUnknownVersion) { - t.Errorf("no version: %v", err) - } - if _, err := DecodeWant([]byte(`{"v":1,"fp":"` + a + `","self":true,"extra":1}`)); err == nil { - t.Error("an unknown field is read") - } - }) - t.Run("node want and hold", func(t *testing.T) { - raw, err := EncodeNodeWant(NodeWant{Composed: c, Words: "n"}) - if err != nil { - t.Fatal(err) - } - sameJSON(t, raw, `{"v":1,"composed":"`+c+`","words":"n"}`) - raw, err = EncodeNodeWant(NodeWant{Self: true}) - if err != nil { - t.Fatal(err) - } - sameJSON(t, raw, `{"v":1,"composed":"","self":true}`) - if back, err := DecodeNodeWant(raw); err != nil || !back.Self { - t.Fatalf("%+v %v", back, err) - } - for name, w := range map[string]NodeWant{"none": {}, "both": {Self: true, Composed: c}, "bad": {Composed: "x"}} { - if _, err := EncodeNodeWant(w); err == nil { - t.Errorf("%s: written", name) - } - } - if _, err := DecodeNodeWant([]byte(`{"v":1,"composed":"` + c + `","fp":"` + a + `"}`)); err == nil { - t.Error("a fingerprint in a node's own want is read") - } - raw, err = EncodeHeld(Held{Node: "ace", Words: "unhealthy"}) - if err != nil { - t.Fatal(err) - } - sameJSON(t, raw, `{"v":1,"node":"ace","words":"unhealthy"}`) - if h, err := DecodeHeld(raw); err != nil || !SameHeld(h, Held{V: 1, Node: "ace", Words: "unhealthy"}) { - t.Fatalf("%+v %v", h, err) - } - if _, err := EncodeHeld(Held{Node: "a.b"}); err == nil { - t.Error("a node with a dot is held") - } - }) - t.Run("keys", func(t *testing.T) { - for _, row := range []struct{ module, want string }{ - {"mesh-delivery-assigner", "want.ace.mesh-delivery-assigner"}, - {"novox.be", "want.ace.novox=2ebe"}, - } { - k, err := WantKey("ace", row.module) - if err != nil || k != row.want { - t.Fatalf("WantKey %s = %q, %v", row.module, k, err) - } - if n, m, err := ParseWantKey(k); err != nil || n != "ace" || m != row.module { - t.Fatalf("ParseWantKey %q = %q %q %v", k, n, m, err) - } - } - if k, err := HeldKey("mesh-delivery-assigner", a); err != nil || k != "held.mesh-delivery-assigner."+rep("a") { - t.Fatalf("HeldKey = %q %v", k, err) - } - if m, fp, err := ParseHeldKey("held.mesh-delivery-assigner." + rep("a")); err != nil || m != "mesh-delivery-assigner" || fp != a { - t.Fatalf("ParseHeldKey = %q %q %v", m, fp, err) - } - if k, err := WantNodeKey("ace"); err != nil || k != "want.ace.node" { - t.Fatalf("WantNodeKey = %q %v", k, err) - } - if n, err := ParseWantNodeKey("want.ace.node"); err != nil || n != "ace" { - t.Fatalf("ParseWantNodeKey = %q %v", n, err) - } - if _, err := WantKey("ace", "node"); err == nil { - t.Error("a module named node has a want key") - } - if _, err := HeldKey("novox.be", a); err == nil { - t.Error("a module with a dot has a held key") - } - for _, key := range []string{"want.ace.node", "held.m." + rep("a"), "ace.m", "want.ace", "want..m"} { - if _, _, err := ParseWantKey(key); err == nil { - t.Errorf("%q is a want key", key) - } - } - if _, err := ParseWantNodeKey("want.ace.m"); err == nil { - t.Error("a module's want is a node's own") - } - if _, _, err := ParseHeldKey("want.ace.m"); err == nil { - t.Error("a want key is a held key") - } - }) -} diff --git a/placement/go/seats/meshmoduleassigner/go.mod b/placement/go/seats/meshmoduleassigner/go.mod deleted file mode 100644 index cda234d..0000000 --- a/placement/go/seats/meshmoduleassigner/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner - -go 1.26.0 diff --git a/placement/go/seats/meshwants/contract.go b/placement/go/seats/meshwants/contract.go deleted file mode 100644 index 74860fa..0000000 --- a/placement/go/seats/meshwants/contract.go +++ /dev/null @@ -1,337 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// Package meshwants is the contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). -package meshwants - -import ( - "bytes" - "encoding/json" - "errors" - "strings" -) - -// Seat is the seat's name. -const Seat = "mesh-wants" - -const ( - VerbUpgrade = "upgrade" - VerbBus = "bus" -) - -const ( - EventPolicyChanged = "policy-changed" -) - -// Verbs is every verb the seat serves, in its definition's order: a new copy each call. -func Verbs() []string { - return []string{VerbUpgrade, VerbBus} -} - -// Events is every event published on the seat's subject: a new copy each call. -func Events() []string { - return []string{EventPolicyChanged} -} - -// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. -const ( - Family = "mesh-wants.events" - FamilyMaxAgeDays = 7 -) - -// SubjectPolicyChanged is where policy-changed is published. -const SubjectPolicyChanged = "mesh.seat.mesh-wants.event.policy-changed" - -const ( - StateWants = "wants" - StateDifferences = "differences" - StateCompositions = "compositions" -) - -// WantsByWantNodeModuleKey is the key of one value of the state wants under the form "want..". -func WantsByWantNodeModuleKey(node, module string) string { return "want." + node + "." + module } - -// ParseWantsByWantNodeModuleKey reads the parts of a key of the form "want..", in the order of the form; the last result is false for a key of another form. -func ParseWantsByWantNodeModuleKey(key string) (string, string, bool) { - rest := key - if !strings.HasPrefix(rest, "want.") { - return "", "", false - } - rest = rest[5:] - v1, after1, found1 := strings.Cut(rest, ".") - if !found1 || v1 == "" { - return "", "", false - } - rest = after1 - if rest == "" { - return "", "", false - } - v3 := rest - rest = "" - if rest != "" { - return "", "", false - } - return v1, v3, true -} - -// DecodeWantsByWantNodeModuleValue reads one value of the state wants under the form "want..", refusing a field WantsByWantNodeModuleValue does not know and any text after the value. -func DecodeWantsByWantNodeModuleValue(raw []byte) (WantsByWantNodeModuleValue, error) { - var v WantsByWantNodeModuleValue - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&v); err != nil { - return v, err - } - if dec.More() { - return v, errors.New("text after the value") - } - return v, nil -} - -// WantsByWantNodeNodeKey is the key of one value of the state wants under the form "want..node". -func WantsByWantNodeNodeKey(node string) string { return "want." + node + ".node" } - -// ParseWantsByWantNodeNodeKey reads the parts of a key of the form "want..node", in the order of the form; the last result is false for a key of another form. -func ParseWantsByWantNodeNodeKey(key string) (string, bool) { - rest := key - if !strings.HasPrefix(rest, "want.") { - return "", false - } - rest = rest[5:] - v1, after1, found1 := strings.Cut(rest, ".node") - if !found1 || v1 == "" { - return "", false - } - rest = after1 - if rest != "" { - return "", false - } - return v1, true -} - -// DecodeWantsByWantNodeNodeValue reads one value of the state wants under the form "want..node", refusing a field WantsByWantNodeNodeValue does not know and any text after the value. -func DecodeWantsByWantNodeNodeValue(raw []byte) (WantsByWantNodeNodeValue, error) { - var v WantsByWantNodeNodeValue - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&v); err != nil { - return v, err - } - if dec.More() { - return v, errors.New("text after the value") - } - return v, nil -} - -// WantsByHeldModuleFingerprintKey is the key of one value of the state wants under the form "held..". -func WantsByHeldModuleFingerprintKey(module, fingerprint string) string { - return "held." + module + "." + fingerprint -} - -// ParseWantsByHeldModuleFingerprintKey reads the parts of a key of the form "held..", in the order of the form; the last result is false for a key of another form. -func ParseWantsByHeldModuleFingerprintKey(key string) (string, string, bool) { - rest := key - if !strings.HasPrefix(rest, "held.") { - return "", "", false - } - rest = rest[5:] - v1, after1, found1 := strings.Cut(rest, ".") - if !found1 || v1 == "" { - return "", "", false - } - rest = after1 - if rest == "" { - return "", "", false - } - v3 := rest - rest = "" - if rest != "" { - return "", "", false - } - return v1, v3, true -} - -// DecodeWantsByHeldModuleFingerprintValue reads one value of the state wants under the form "held..", refusing a field WantsByHeldModuleFingerprintValue does not know and any text after the value. -func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFingerprintValue, error) { - var v WantsByHeldModuleFingerprintValue - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&v); err != nil { - return v, err - } - if dec.More() { - return v, errors.New("text after the value") - } - return v, nil -} - -// DifferencesByNodeModuleKey is the key of one value of the state differences under the form ".". -func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module } - -// ParseDifferencesByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. -func ParseDifferencesByNodeModuleKey(key string) (string, string, bool) { - rest := key - v0, after0, found0 := strings.Cut(rest, ".") - if !found0 || v0 == "" { - return "", "", false - } - rest = after0 - if rest == "" { - return "", "", false - } - v2 := rest - rest = "" - if rest != "" { - return "", "", false - } - return v0, v2, true -} - -// DecodeDifferencesByNodeModuleValue reads one value of the state differences under the form ".", refusing a field DifferencesByNodeModuleValue does not know and any text after the value. -func DecodeDifferencesByNodeModuleValue(raw []byte) (DifferencesByNodeModuleValue, error) { - var v DifferencesByNodeModuleValue - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&v); err != nil { - return v, err - } - if dec.More() { - return v, errors.New("text after the value") - } - return v, nil -} - -// CompositionsByGroupHeadsKey is the key of one value of the state compositions under the form ".". -func CompositionsByGroupHeadsKey(group, heads string) string { return group + "." + heads } - -// ParseCompositionsByGroupHeadsKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. -func ParseCompositionsByGroupHeadsKey(key string) (string, string, bool) { - rest := key - v0, after0, found0 := strings.Cut(rest, ".") - if !found0 || v0 == "" { - return "", "", false - } - rest = after0 - if rest == "" { - return "", "", false - } - v2 := rest - rest = "" - if rest != "" { - return "", "", false - } - return v0, v2, true -} - -// DecodeCompositionsByGroupHeadsValue reads one value of the state compositions under the form ".", refusing a field CompositionsByGroupHeadsValue does not know and any text after the value. -func DecodeCompositionsByGroupHeadsValue(raw []byte) (CompositionsByGroupHeadsValue, error) { - var v CompositionsByGroupHeadsValue - dec := json.NewDecoder(bytes.NewReader(raw)) - dec.DisallowUnknownFields() - if err := dec.Decode(&v); err != nil { - return v, err - } - if dec.More() { - return v, errors.New("text after the value") - } - return v, nil -} - -// UpgradeInput is what upgrade takes. -type UpgradeInput struct { - // "true": each node's own composition, by its digest, and the assigner's findings - Compare string `json:"compare,omitempty"` - // one module - Module string `json:"module,omitempty"` - // with module: roll, together, record, or default to take a person's choice back - Policy string `json:"policy,omitempty"` - // with policy: why, required and kept - Why string `json:"why,omitempty"` -} - -// The values of UpgradeInput's compare. -const ( - UpgradeInputCompareTrue = "true" - UpgradeInputCompareFalse = "false" -) - -// UpgradeAnswer is what upgrade answers. -type UpgradeAnswer = map[string]any - -// BusInput is what bus takes. -type BusInput struct { -} - -// BusAnswer is what bus answers. -type BusAnswer = map[string]any - -// PolicyChangedEvent is the body of the event policy-changed. -type PolicyChangedEvent = map[string]any - -// WantsByWantNodeModuleValue is one value of the state wants under the key want... -type WantsByWantNodeModuleValue struct { - // again or skip - Backup string `json:"backup,omitempty"` - Composed string `json:"composed,omitempty"` - Fp string `json:"fp,omitempty"` - Hold bool `json:"hold,omitempty"` - Newest string `json:"newest,omitempty"` - Put bool `json:"put,omitempty"` - Release string `json:"release,omitempty"` - Removed bool `json:"removed,omitempty"` - // the rule that holds the node back, 1 to 10 - Rule int64 `json:"rule,omitempty"` - Self bool `json:"self,omitempty"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` - Yes string `json:"yes,omitempty"` -} - -// The values of WantsByWantNodeModuleValue's backup. -const ( - WantsByWantNodeModuleValueBackupAgain = "again" - WantsByWantNodeModuleValueBackupSkip = "skip" -) - -// WantsByWantNodeNodeValue is one value of the state wants under the key want..node. -type WantsByWantNodeNodeValue struct { - Composed string `json:"composed"` - Self bool `json:"self,omitempty"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` -} - -// WantsByHeldModuleFingerprintValue is one value of the state wants under the key held... -type WantsByHeldModuleFingerprintValue struct { - Node string `json:"node"` - V int64 `json:"v,omitempty"` - Words string `json:"words,omitempty"` -} - -// DifferencesByNodeModuleValue is one value of the state differences under the key .. -type DifferencesByNodeModuleValue struct { - Rule int64 `json:"rule,omitempty"` - Sent string `json:"sent,omitempty"` - SentBuild string `json:"sent-build,omitempty"` - Unexplained bool `json:"unexplained,omitempty"` - V int64 `json:"v,omitempty"` - Want string `json:"want,omitempty"` - WantRemoved bool `json:"want-removed,omitempty"` - Words string `json:"words,omitempty"` -} - -// CompositionsByGroupHeadsValue is one value of the state compositions under the key .. -type CompositionsByGroupHeadsValue struct { - Group string `json:"group"` - // 12 lowercase hex digits - Heads string `json:"heads"` - Machine string `json:"machine,omitempty"` - Machines []string `json:"machines,omitempty"` - Sdk string `json:"sdk,omitempty"` - // pass, fail or waits - State string `json:"state"` - // milliseconds - Took int64 `json:"took,omitempty"` - V int64 `json:"v,omitempty"` - Warnings []string `json:"warnings,omitempty"` - Words string `json:"words,omitempty"` -} diff --git a/placement/go/seats/meshwants/contract_test.go b/placement/go/seats/meshwants/contract_test.go deleted file mode 100644 index 1355a9b..0000000 --- a/placement/go/seats/meshwants/contract_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -package meshwants - -import ( - "encoding/json" - "reflect" - "regexp" - "testing" -) - -// TestFixtures reads every fixture of seats/mesh-wants/fixtures: each decodes into its type, a field the type does not -// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. -func TestFixtures(t *testing.T) { - cases := []struct { - name, subject, given, wire string - decode func([]byte) (any, error) - subjectIs *regexp.Regexp - }{ - {name: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByWantNodeModuleValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByWantNodeNodeValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a held fingerprint", subject: "", given: "{\"node\":\"ace\",\"v\":1}", wire: "{\"node\":\"ace\",\"v\":1}", - decode: func(b []byte) (any, error) { - var v WantsByHeldModuleFingerprintValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a difference", subject: "", given: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", wire: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", - decode: func(b []byte) (any, error) { - var v DifferencesByNodeModuleValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - {name: "a composition", subject: "", given: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", wire: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", - decode: func(b []byte) (any, error) { - var v CompositionsByGroupHeadsValue - err := json.Unmarshal(b, &v) - return v, err - }, subjectIs: nil}, - } - for _, c := range cases { - v, err := c.decode([]byte(c.given)) - if err != nil { - t.Errorf("%s: does not decode: %v", c.name, err) - continue - } - back, err := json.Marshal(v) - if err != nil { - t.Errorf("%s: does not encode: %v", c.name, err) - continue - } - var got, want any - if err := json.Unmarshal(back, &got); err != nil { - t.Fatal(err) - } - if err := json.Unmarshal([]byte(c.wire), &want); err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) - } - if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { - t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) - } - } -} diff --git a/placement/go/seats/meshwants/go.mod b/placement/go/seats/meshwants/go.mod deleted file mode 100644 index a075868..0000000 --- a/placement/go/seats/meshwants/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants - -go 1.26.0 diff --git a/placement/module.json b/placement/module.json deleted file mode 100644 index 9be9cf6..0000000 --- a/placement/module.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "module": "mesh-placement-contracts", - "version": "1", - "build": { - "artifacts": [ - { - "name": "go-meshmoduleassigner", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/meshmoduleassigner" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "go-meshwants", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/meshwants" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "go-meshmoduleassignerclient", - "kind": "package", - "language": "go", - "paths": [ - "go/clients/meshmoduleassignerclient" - ], - "series": "0.1" - }, - { - "name": "ts-meshmoduleassigner", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/meshmoduleassigner" - ], - "series": "0.1", - "generated-from": "." - }, - { - "name": "ts-meshwants", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/meshwants" - ], - "series": "0.1", - "generated-from": "." - } - ] - }, - "resources": [] -} diff --git a/placement/seats/index.json b/placement/seats/index.json deleted file mode 100644 index 6c07971..0000000 --- a/placement/seats/index.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "format": 1, - "seats": [ - "mesh-module-assigner", - "mesh-wants", - "node-service-manager", - "node-package-manager", - "node-container-runtime", - "node-environment" - ] -} diff --git a/placement/seats/mesh-wants/fixtures/states.json b/placement/seats/mesh-wants/fixtures/states.json deleted file mode 100644 index cb38a39..0000000 --- a/placement/seats/mesh-wants/fixtures/states.json +++ /dev/null @@ -1,47 +0,0 @@ -[ - { - "name": "a want", - "of": "state:wants/want..", - "given": { - "v": 1, - "fp": "ab12", - "rule": 3 - } - }, - { - "name": "a node's own want", - "of": "state:wants/want..node", - "given": { - "v": 1, - "composed": "sha256:aa", - "self": true - } - }, - { - "name": "a held fingerprint", - "of": "state:wants/held..", - "given": { - "v": 1, - "node": "ace" - } - }, - { - "name": "a difference", - "of": "state:differences/.", - "given": { - "v": 1, - "want": "ab12", - "unexplained": true - } - }, - { - "name": "a composition", - "of": "state:compositions/.", - "given": { - "v": 1, - "group": "g1", - "heads": "0123456789ab", - "state": "pass" - } - } -] diff --git a/placement/seats/mesh-wants/seat.json b/placement/seats/mesh-wants/seat.json deleted file mode 100644 index ececc75..0000000 --- a/placement/seats/mesh-wants/seat.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "format": 1, - "seat": "mesh-wants", - "scope": "mesh", - "grain": "whole", - "domain": "Placement", - "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", - "description": "The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).", - "verbs": [ - { - "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, each node's own composition and the assigner's findings.", - "input": { - "properties": { - "compare": { - "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module", - "type": "string" - }, - "policy": { - "description": "with module: roll, together, record, or default to take a person's choice back", - "type": "string" - }, - "why": { - "description": "with policy: why, required and kept", - "type": "string" - } - }, - "type": "object" - }, - "answer": { - "type": "object", - "description": "each module's policy, or the one chosen, or the comparison" - } - }, - { - "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": { - "properties": {}, - "type": "object" - }, - "answer": { - "type": "object", - "description": "what a bus upgrade would do, read only" - } - } - ], - "events": [ - { - "name": "policy-changed", - "body": { - "type": "object" - } - } - ], - "family": { - "name": "mesh-wants.events", - "max-age-days": 7 - }, - "states": [ - { - "name": "wants", - "template": "state", - "key-forms": [ - { - "key": "want..", - "value": { - "type": "object", - "description": "a node's want for a module", - "properties": { - "v": { - "type": "integer" - }, - "fp": { - "type": "string" - }, - "composed": { - "type": "string" - }, - "removed": { - "type": "boolean" - }, - "newest": { - "type": "string" - }, - "rule": { - "type": "integer", - "description": "the rule that holds the node back, 1 to 10" - }, - "backup": { - "type": "string", - "description": "again or skip", - "enum": [ - "again", - "skip" - ] - }, - "words": { - "type": "string" - }, - "put": { - "type": "boolean" - }, - "hold": { - "type": "boolean" - }, - "yes": { - "type": "string" - }, - "release": { - "type": "string" - }, - "self": { - "type": "boolean" - } - } - }, - "description": "a node's want for a module" - }, - { - "key": "want..node", - "value": { - "type": "object", - "description": "a node's own want", - "properties": { - "v": { - "type": "integer" - }, - "composed": { - "type": "string" - }, - "words": { - "type": "string" - }, - "self": { - "type": "boolean" - } - }, - "required": [ - "composed" - ] - }, - "description": "a node's own want, naming the digest of its own composition" - }, - { - "key": "held..", - "value": { - "type": "object", - "description": "a fingerprint the assigner holds", - "properties": { - "v": { - "type": "integer" - }, - "node": { - "type": "string" - }, - "words": { - "type": "string" - } - }, - "required": [ - "node" - ] - }, - "description": "a module's fingerprint the assigner holds" - } - ], - "history": 20, - "description": "what each node should run, and the fingerprints held back" - }, - { - "name": "differences", - "template": "state", - "key-forms": [ - { - "key": ".", - "value": { - "type": "object", - "description": "the difference between a want and what is sent", - "properties": { - "v": { - "type": "integer" - }, - "want": { - "type": "string" - }, - "want-removed": { - "type": "boolean" - }, - "sent": { - "type": "string" - }, - "sent-build": { - "type": "string" - }, - "rule": { - "type": "integer" - }, - "unexplained": { - "type": "boolean" - }, - "words": { - "type": "string" - } - } - }, - "description": "a module on a node" - } - ], - "history": 64, - "description": "differences between a want and what the node-engine is sent" - }, - { - "name": "compositions", - "template": "state", - "key-forms": [ - { - "key": ".", - "value": { - "type": "object", - "description": "the composition verdict of a group's check at its heads", - "properties": { - "v": { - "type": "integer" - }, - "group": { - "type": "string" - }, - "heads": { - "type": "string", - "description": "12 lowercase hex digits" - }, - "state": { - "type": "string", - "description": "pass, fail or waits" - }, - "words": { - "type": "string" - }, - "sdk": { - "type": "string" - }, - "machines": { - "type": "array", - "items": { - "type": "string" - } - }, - "machine": { - "type": "string" - }, - "warnings": { - "type": "array", - "items": { - "type": "string" - } - }, - "took": { - "type": "integer", - "description": "milliseconds" - } - }, - "required": [ - "group", - "heads", - "state" - ] - }, - "description": "a group's check at its heads" - } - ], - "history": 5, - "description": "composition verdicts of a group's check; the checker reads them" - } - ] -} diff --git a/placement/ts/seats/meshmoduleassigner/package-lock.json b/placement/ts/seats/meshmoduleassigner/package-lock.json deleted file mode 100644 index b346586..0000000 --- a/placement/ts/seats/meshmoduleassigner/package-lock.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "lockfileVersion": 3, - "name": "@novox/mesh-placement-contracts-mesh-module-assigner", - "packages": { - "": { - "name": "@novox/mesh-placement-contracts-mesh-module-assigner", - "version": "0.1.0" - } - }, - "requires": true, - "version": "0.1.0" -} diff --git a/placement/ts/seats/meshwants/package.json b/placement/ts/seats/meshwants/package.json deleted file mode 100644 index d4a29ee..0000000 --- a/placement/ts/seats/meshwants/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "description": "The contract of the seat mesh-wants, generated from its definition.", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src" - ], - "name": "@novox/mesh-placement-contracts-mesh-wants", - "type": "module", - "types": "./src/index.ts", - "version": "0.1.0" -} diff --git a/placement/ts/seats/meshwants/src/index.ts b/placement/ts/seats/meshwants/src/index.ts deleted file mode 100644 index 38dab79..0000000 --- a/placement/ts/seats/meshwants/src/index.ts +++ /dev/null @@ -1,211 +0,0 @@ -// Code generated by seatgen from the seat definitions; DO NOT EDIT. - -// The contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). - -export const seat = "mesh-wants"; - -export const verbs = { - upgrade: "upgrade", - bus: "bus", -} as const; - -export const events = { - policyChanged: "policy-changed", -} as const; - -export const family = { name: "mesh-wants.events", maxAgeDays: 7 } as const; - -export const policyChangedSubject = "mesh.seat.mesh-wants.event.policy-changed"; - -export const states = { - wants: "wants", - differences: "differences", - compositions: "compositions", -} as const; - -/** The key of one value of the state wants under the form "want..". */ -export function wantsByWantNodeModuleKey(node: string, module: string): string { - return "want." + node + "." + module; -} - -/** The parts of a key of the form "want..", or undefined for a key of another form. */ -export function parseWantsByWantNodeModuleKey(_key: string): { node: string; module: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("want.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf("."); - if (_at1 <= 0) return undefined; - const node = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 1); - if (_rest === "") return undefined; - const module = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { node, module }; -} - -/** The key of one value of the state wants under the form "want..node". */ -export function wantsByWantNodeNodeKey(node: string): string { - return "want." + node + ".node"; -} - -/** The parts of a key of the form "want..node", or undefined for a key of another form. */ -export function parseWantsByWantNodeNodeKey(_key: string): { node: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("want.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf(".node"); - if (_at1 <= 0) return undefined; - const node = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 5); - if (_rest !== "") return undefined; - return { node }; -} - -/** The key of one value of the state wants under the form "held..". */ -export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: string): string { - return "held." + module + "." + fingerprint; -} - -/** The parts of a key of the form "held..", or undefined for a key of another form. */ -export function parseWantsByHeldModuleFingerprintKey(_key: string): { module: string; fingerprint: string } | undefined { - let _rest = _key; - if (!_rest.startsWith("held.")) return undefined; - _rest = _rest.slice(5); - const _at1 = _rest.indexOf("."); - if (_at1 <= 0) return undefined; - const module = _rest.slice(0, _at1); - _rest = _rest.slice(_at1 + 1); - if (_rest === "") return undefined; - const fingerprint = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { module, fingerprint }; -} - -/** The key of one value of the state differences under the form ".". */ -export function differencesByNodeModuleKey(node: string, module: string): string { - return node + "." + module; -} - -/** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseDifferencesByNodeModuleKey(_key: string): { node: string; module: string } | undefined { - let _rest = _key; - const _at0 = _rest.indexOf("."); - if (_at0 <= 0) return undefined; - const node = _rest.slice(0, _at0); - _rest = _rest.slice(_at0 + 1); - if (_rest === "") return undefined; - const module = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { node, module }; -} - -/** The key of one value of the state compositions under the form ".". */ -export function compositionsByGroupHeadsKey(group: string, heads: string): string { - return group + "." + heads; -} - -/** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parseCompositionsByGroupHeadsKey(_key: string): { group: string; heads: string } | undefined { - let _rest = _key; - const _at0 = _rest.indexOf("."); - if (_at0 <= 0) return undefined; - const group = _rest.slice(0, _at0); - _rest = _rest.slice(_at0 + 1); - if (_rest === "") return undefined; - const heads = _rest; - _rest = ""; - if (_rest !== "") return undefined; - return { group, heads }; -} - -/** is what upgrade takes. */ -export interface UpgradeInput { - /** "true": each node's own composition, by its digest, and the assigner's findings */ - "compare"?: "true" | "false"; - /** one module */ - "module"?: string; - /** with module: roll, together, record, or default to take a person's choice back */ - "policy"?: string; - /** with policy: why, required and kept */ - "why"?: string; -} - -/** is what upgrade answers. */ -export type UpgradeAnswer = Record; - -/** is what bus takes. */ -export interface BusInput { -} - -/** is what bus answers. */ -export type BusAnswer = Record; - -/** is the body of the event policy-changed. */ -export type PolicyChangedEvent = Record; - -/** is one value of the state wants under the key want... */ -export interface WantsByWantNodeModuleValue { - /** again or skip */ - "backup"?: "again" | "skip"; - "composed"?: string; - "fp"?: string; - "hold"?: boolean; - "newest"?: string; - "put"?: boolean; - "release"?: string; - "removed"?: boolean; - /** the rule that holds the node back, 1 to 10 */ - "rule"?: number; - "self"?: boolean; - "v"?: number; - "words"?: string; - "yes"?: string; -} - -/** is one value of the state wants under the key want..node. */ -export interface WantsByWantNodeNodeValue { - "composed": string; - "self"?: boolean; - "v"?: number; - "words"?: string; -} - -/** is one value of the state wants under the key held... */ -export interface WantsByHeldModuleFingerprintValue { - "node": string; - "v"?: number; - "words"?: string; -} - -/** is one value of the state differences under the key .. */ -export interface DifferencesByNodeModuleValue { - "rule"?: number; - "sent"?: string; - "sent-build"?: string; - "unexplained"?: boolean; - "v"?: number; - "want"?: string; - "want-removed"?: boolean; - "words"?: string; -} - -/** is one value of the state compositions under the key .. */ -export interface CompositionsByGroupHeadsValue { - "group": string; - /** 12 lowercase hex digits */ - "heads": string; - "machine"?: string; - "machines"?: string[]; - "sdk"?: string; - /** pass, fail or waits */ - "state": string; - /** milliseconds */ - "took"?: number; - "v"?: number; - "warnings"?: string[]; - "words"?: string; -} - diff --git a/placement/ts/seats/meshwants/tsconfig.json b/placement/ts/seats/meshwants/tsconfig.json deleted file mode 100644 index 60274c6..0000000 --- a/placement/ts/seats/meshwants/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*.ts"] -} diff --git a/seats/index.json b/seats/index.json index 6d96506..1b83c4b 100644 --- a/seats/index.json +++ b/seats/index.json @@ -21,6 +21,11 @@ "node-lock-screen", "node-clipboard", "node-bar", - "node-compositor" + "node-compositor", + "mesh-module-assigner", + "node-service-manager", + "node-package-manager", + "node-container-runtime", + "node-environment" ] } diff --git a/placement/seats/mesh-module-assigner/fixtures/events.json b/seats/mesh-module-assigner/fixtures/events.json similarity index 100% rename from placement/seats/mesh-module-assigner/fixtures/events.json rename to seats/mesh-module-assigner/fixtures/events.json diff --git a/placement/seats/mesh-module-assigner/fixtures/states.json b/seats/mesh-module-assigner/fixtures/states.json similarity index 94% rename from placement/seats/mesh-module-assigner/fixtures/states.json rename to seats/mesh-module-assigner/fixtures/states.json index 4c176ff..04e940b 100644 --- a/placement/seats/mesh-module-assigner/fixtures/states.json +++ b/seats/mesh-module-assigner/fixtures/states.json @@ -1,7 +1,7 @@ [ { "name": "a node's key", - "of": "state:placement/.node", + "of": "state:assignments/.node", "given": { "v": 1, "node": "ace", @@ -18,7 +18,7 @@ }, { "name": "a node that came with a tunnel", - "of": "state:placement/.node", + "of": "state:assignments/.node", "given": { "v": 1, "node": "ace", @@ -40,7 +40,7 @@ }, { "name": "a module's key", - "of": "state:placement/.", + "of": "state:assignments/.", "given": { "v": 1, "node": "ace", diff --git a/placement/seats/mesh-module-assigner/seat.json b/seats/mesh-module-assigner/seat.json similarity index 97% rename from placement/seats/mesh-module-assigner/seat.json rename to seats/mesh-module-assigner/seat.json index 77108a2..4e49210 100644 --- a/placement/seats/mesh-module-assigner/seat.json +++ b/seats/mesh-module-assigner/seat.json @@ -3,13 +3,13 @@ "seat": "mesh-module-assigner", "scope": "mesh", "grain": "whole", - "domain": "Placement", + "domain": "Core", "decision": "novox/hq ADR 0567, ADR 0634, ADR 0635", "description": "The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).", "verbs": [ { - "name": "placement", - "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", + "name": "assignments", + "description": "Read the assignments: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", "optional": true, "input": { "properties": { @@ -188,7 +188,7 @@ }, "states": [ { - "name": "placement", + "name": "assignments", "template": "state", "key-forms": [ { @@ -276,7 +276,7 @@ } ], "history": 64, - "description": "placements, one key per node and one per module placed on it, written only on the operator's Approve of a proposal by compare-and-set at the revision it was laid over" + "description": "assignments, one key per node and one per module placed on it, written only on the operator's Approve of a proposal by compare-and-set at the revision it was laid over" }, { "name": "proposals", diff --git a/placement/seats/node-container-runtime/seat.json b/seats/node-container-runtime/seat.json similarity index 85% rename from placement/seats/node-container-runtime/seat.json rename to seats/node-container-runtime/seat.json index 1b1bf02..8346a78 100644 --- a/placement/seats/node-container-runtime/seat.json +++ b/seats/node-container-runtime/seat.json @@ -3,6 +3,6 @@ "seat": "node-container-runtime", "scope": "node", "grain": "seat-list", - "domain": "Placement", + "domain": "Core", "decision": "novox/hq ADR 0166, ADR 0207" } diff --git a/placement/seats/node-environment/seat.json b/seats/node-environment/seat.json similarity index 83% rename from placement/seats/node-environment/seat.json rename to seats/node-environment/seat.json index 75bb5d3..6a73957 100644 --- a/placement/seats/node-environment/seat.json +++ b/seats/node-environment/seat.json @@ -3,6 +3,6 @@ "seat": "node-environment", "scope": "node", "grain": "seat-list", - "domain": "Placement", + "domain": "Core", "decision": "novox/hq ADR 0203" } diff --git a/placement/seats/node-package-manager/seat.json b/seats/node-package-manager/seat.json similarity index 83% rename from placement/seats/node-package-manager/seat.json rename to seats/node-package-manager/seat.json index 034e878..b5fec3a 100644 --- a/placement/seats/node-package-manager/seat.json +++ b/seats/node-package-manager/seat.json @@ -3,6 +3,6 @@ "seat": "node-package-manager", "scope": "node", "grain": "seat-list", - "domain": "Placement", + "domain": "Core", "decision": "novox/hq ADR 0207" } diff --git a/placement/seats/node-service-manager/seat.json b/seats/node-service-manager/seat.json similarity index 99% rename from placement/seats/node-service-manager/seat.json rename to seats/node-service-manager/seat.json index 695bc00..c46eeff 100644 --- a/placement/seats/node-service-manager/seat.json +++ b/seats/node-service-manager/seat.json @@ -3,7 +3,7 @@ "seat": "node-service-manager", "scope": "node", "grain": "seat-list", - "domain": "Placement", + "domain": "Core", "decision": "novox/hq ADR 0177", "verbs": [ { diff --git a/placement/ts/seats/meshwants/package-lock.json b/ts/seats/meshmoduleassigner/package-lock.json similarity index 53% rename from placement/ts/seats/meshwants/package-lock.json rename to ts/seats/meshmoduleassigner/package-lock.json index e274634..c05cbfb 100644 --- a/placement/ts/seats/meshwants/package-lock.json +++ b/ts/seats/meshmoduleassigner/package-lock.json @@ -1,9 +1,9 @@ { "lockfileVersion": 3, - "name": "@novox/mesh-placement-contracts-mesh-wants", + "name": "@novox/mesh-contracts-mesh-module-assigner", "packages": { "": { - "name": "@novox/mesh-placement-contracts-mesh-wants", + "name": "@novox/mesh-contracts-mesh-module-assigner", "version": "0.1.0" } }, diff --git a/placement/ts/seats/meshmoduleassigner/package.json b/ts/seats/meshmoduleassigner/package.json similarity index 78% rename from placement/ts/seats/meshmoduleassigner/package.json rename to ts/seats/meshmoduleassigner/package.json index 5c25c3f..e7e83e9 100644 --- a/placement/ts/seats/meshmoduleassigner/package.json +++ b/ts/seats/meshmoduleassigner/package.json @@ -6,7 +6,7 @@ "files": [ "src" ], - "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "name": "@novox/mesh-contracts-mesh-module-assigner", "type": "module", "types": "./src/index.ts", "version": "0.1.0" diff --git a/placement/ts/seats/meshmoduleassigner/src/index.ts b/ts/seats/meshmoduleassigner/src/index.ts similarity index 93% rename from placement/ts/seats/meshmoduleassigner/src/index.ts rename to ts/seats/meshmoduleassigner/src/index.ts index 116f6b5..062c37d 100644 --- a/placement/ts/seats/meshmoduleassigner/src/index.ts +++ b/ts/seats/meshmoduleassigner/src/index.ts @@ -5,7 +5,7 @@ export const seat = "mesh-module-assigner"; export const verbs = { - placement: "placement", + assignments: "assignments", proposals: "proposals", propose: "propose", withdraw: "withdraw", @@ -30,20 +30,20 @@ export const placementJudgedSubject = "mesh.seat.mesh-module-assigner.event.plac export const placementChangedSubject = "mesh.seat.mesh-module-assigner.event.placement-changed"; export const states = { - placement: "placement", + assignments: "assignments", proposals: "proposals", candidates: "candidates", pulses: "pulses", seatHolders: "seat-holders", } as const; -/** The key of one value of the state placement under the form ".node". */ -export function placementByNodeNodeKey(node: string): string { +/** The key of one value of the state assignments under the form ".node". */ +export function assignmentsByNodeNodeKey(node: string): string { return node + ".node"; } /** The parts of a key of the form ".node", or undefined for a key of another form. */ -export function parsePlacementByNodeNodeKey(_key: string): { node: string } | undefined { +export function parseAssignmentsByNodeNodeKey(_key: string): { node: string } | undefined { let _rest = _key; const _at0 = _rest.indexOf(".node"); if (_at0 <= 0) return undefined; @@ -53,13 +53,13 @@ export function parsePlacementByNodeNodeKey(_key: string): { node: string } | un return { node }; } -/** The key of one value of the state placement under the form ".". */ -export function placementByNodeModuleKey(node: string, module: string): string { +/** The key of one value of the state assignments under the form ".". */ +export function assignmentsByNodeModuleKey(node: string, module: string): string { return node + "." + module; } /** The parts of a key of the form ".", or undefined for a key of another form. */ -export function parsePlacementByNodeModuleKey(_key: string): { node: string; module: string } | undefined { +export function parseAssignmentsByNodeModuleKey(_key: string): { node: string; module: string } | undefined { let _rest = _key; const _at0 = _rest.indexOf("."); if (_at0 <= 0) return undefined; @@ -381,8 +381,8 @@ export interface Warrant { "warrant": string; } -/** is what placement takes. */ -export interface PlacementInput { +/** is what assignments takes. */ +export interface AssignmentsInput { /** "true": the changes in order, from the stream and the state's revisions */ "history"?: "true" | "false"; /** one module: the nodes it is placed on */ @@ -393,8 +393,8 @@ export interface PlacementInput { "seats"?: "true" | "false"; } -/** is what placement answers. */ -export type PlacementAnswer = Record; +/** is what assignments answers. */ +export type AssignmentsAnswer = Record; /** is what proposals takes. */ export interface ProposalsInput { @@ -450,25 +450,25 @@ export type PlacementJudgedEvent = PlacementEvent; /** is the body of the event placement-changed. */ export type PlacementChangedEvent = PlacementEvent; -export interface PlacementByNodeNodeValueModulesItem { +export interface AssignmentsByNodeNodeValueModulesItem { /** the digest of the module key's value it stands for */ "digest": string; "module": string; } -/** is one value of the state placement under the key .node. */ -export interface PlacementByNodeNodeValue { +/** is one value of the state assignments under the key .node. */ +export interface AssignmentsByNodeNodeValue { "decided": NodeDecided; "holds"?: Holding[]; /** the modules placed, in the order the composition reads them */ - "modules": PlacementByNodeNodeValueModulesItem[]; + "modules": AssignmentsByNodeNodeValueModulesItem[]; "node": string; "pins"?: Record; "v"?: number; } -/** is one value of the state placement under the key .. */ -export interface PlacementByNodeModuleValue { +/** is one value of the state assignments under the key .. */ +export interface AssignmentsByNodeModuleValue { "node": string; "placed": PlacedModule; /** the value it replaces while a write is under way */ diff --git a/placement/ts/seats/meshmoduleassigner/tsconfig.json b/ts/seats/meshmoduleassigner/tsconfig.json similarity index 100% rename from placement/ts/seats/meshmoduleassigner/tsconfig.json rename to ts/seats/meshmoduleassigner/tsconfig.json -- 2.54.0 From 76de6889fc981a258c3b6f3d98ab0607b517d209 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 21:57:16 +0100 Subject: [PATCH 93/94] The assigner's seat says assignments throughout: the type, the events and the defs; the legacy key form placement. leaves the client (issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../{placement.go => assignments.go} | 63 +++--- .../meshmoduleassignerclient/client_test.go | 112 ++++------ .../meshmoduleassignerclient/holders_test.go | 2 +- go/clients/meshmoduleassignerclient/keys.go | 202 +++++------------- go/clients/meshmoduleassignerclient/ops.go | 6 +- go/clients/meshmoduleassignerclient/placed.go | 47 ++-- .../meshmoduleassignerclient/proposal.go | 2 +- go/clients/meshmoduleassignerclient/reads.go | 4 +- .../meshmoduleassignerclient/reads_test.go | 4 +- go/seats/meshmoduleassigner/contract.go | 118 +++++----- go/seats/meshmoduleassigner/contract_test.go | 8 +- .../mesh-module-assigner/fixtures/events.json | 6 +- seats/mesh-module-assigner/seat.json | 56 ++--- ts/seats/meshmoduleassigner/src/index.ts | 106 ++++----- 14 files changed, 297 insertions(+), 439 deletions(-) rename go/clients/meshmoduleassignerclient/{placement.go => assignments.go} (79%) diff --git a/go/clients/meshmoduleassignerclient/placement.go b/go/clients/meshmoduleassignerclient/assignments.go similarity index 79% rename from go/clients/meshmoduleassignerclient/placement.go rename to go/clients/meshmoduleassignerclient/assignments.go index efe9612..354c918 100644 --- a/go/clients/meshmoduleassignerclient/placement.go +++ b/go/clients/meshmoduleassignerclient/assignments.go @@ -1,6 +1,6 @@ // Package meshmoduleassignerclient is the logic every module that reads where modules are placed shares over the seat -// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `placement` assembled into a -// node's placement, who stands on which node since when, who holds a seat, and what a placement says to the composition. +// mesh-module-assigner (novox/hq ADR 0567, ADR 0634 §4, ADR 0625 §4): the keys of its state `assignments` assembled into a +// node's assignments, who stands on which node since when, who holds a seat, and what a assignments says to the composition. // The layouts are the seat's contract's (package meshmoduleassigner); the check of a proposal is the assigner's own. package meshmoduleassignerclient @@ -26,10 +26,7 @@ const Version = keys.Version // NodeWord is the last word of a node key, `.node`; no module is named it. const NodeWord = "node" -const ( - legacyWord = "placement" - digestLen = 16 -) +const digestLen = 16 // ErrUnknownVersion is what a decode returns for a value whose version is missing or not Version. var ErrUnknownVersion = errors.New("meshmoduleassignerclient: unknown value version") @@ -62,9 +59,9 @@ func checkVersion(data []byte) error { return nil } -// CheckPlacement refuses a placement that names a node or module that is not a name, a module twice, a port that is not +// CheckAssignments refuses a assignments that names a node or module that is not a name, a module twice, a port that is not // a number, a binding or pin naming no provider, and a holding by a module not placed on the node. -func CheckPlacement(p seat.Placement) error { +func CheckAssignments(p seat.Assignments) error { if err := checkName("node", p.Node); err != nil { return err } @@ -74,7 +71,7 @@ func CheckPlacement(p seat.Placement) error { return err } if seen[m.Module] { - return fmt.Errorf("meshmoduleassignerclient: the placement of %s names %s twice", p.Node, m.Module) + return fmt.Errorf("meshmoduleassignerclient: the assignments of %s names %s twice", p.Node, m.Module) } seen[m.Module] = true for port, at := range m.Ports { @@ -93,51 +90,51 @@ func CheckPlacement(p seat.Placement) error { } for provision, to := range p.Pins { if provision == "" || to.Node == "" || to.Module == "" { - return fmt.Errorf("meshmoduleassignerclient: the placement of %s has a pin with no provision, node or module", p.Node) + return fmt.Errorf("meshmoduleassignerclient: the assignments of %s has a pin with no provision, node or module", p.Node) } } for _, h := range p.Holds { if h.Claim == "" || !seen[h.Module] { - return fmt.Errorf("meshmoduleassignerclient: the placement of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim) + return fmt.Errorf("meshmoduleassignerclient: the assignments of %s has %s hold %q, which is not a module placed there or names no seat", p.Node, h.Module, h.Claim) } } return nil } -// EncodePlacement is p as JSON at Version; it refuses what DecodePlacement refuses. -func EncodePlacement(p seat.Placement) ([]byte, error) { - if err := CheckPlacement(p); err != nil { +// EncodeAssignments is p as JSON at Version; it refuses what DecodeAssignments refuses. +func EncodeAssignments(p seat.Assignments) ([]byte, error) { + if err := CheckAssignments(p); err != nil { return nil, err } p.V = Version return json.Marshal(p) } -// DecodePlacement reads a placement file or key value (the form before the node and module keys). -func DecodePlacement(data []byte) (seat.Placement, error) { +// DecodeAssignments reads a assignments file or key value (the form before the node and module keys). +func DecodeAssignments(data []byte) (seat.Assignments, error) { if err := checkVersion(data); err != nil { - return seat.Placement{}, err + return seat.Assignments{}, err } - var p seat.Placement + var p seat.Assignments if err := json.Unmarshal(data, &p); err != nil { - return seat.Placement{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) + return seat.Assignments{}, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) } - if err := CheckPlacement(p); err != nil { - return seat.Placement{}, err + if err := CheckAssignments(p); err != nil { + return seat.Assignments{}, err } return p, nil } -// SamePlacement says whether two placements are the same value, so the holder writes only when they differ. -func SamePlacement(a, b seat.Placement) bool { +// SameAssignments says whether two assignments are the same value, so the holder writes only when they differ. +func SameAssignments(a, b seat.Assignments) bool { a.Revision, b.Revision = "", "" - ja, errA := EncodePlacement(a) - jb, errB := EncodePlacement(b) + ja, errA := EncodeAssignments(a) + jb, errB := EncodeAssignments(b) return errA == nil && errB == nil && string(ja) == string(jb) } // Assigned is the modules placed on the node, in the file's order. -func Assigned(p seat.Placement) []string { +func Assigned(p seat.Assignments) []string { out := make([]string, 0, len(p.Modules)) for _, m := range p.Modules { out = append(out, m.Module) @@ -151,7 +148,7 @@ func Chosen(p seat.Provider) catalogue.Chosen { } // ChosenPins is the node's pins as the composition reads them; nil when it has none. -func ChosenPins(p seat.Placement) map[string]catalogue.Chosen { +func ChosenPins(p seat.Assignments) map[string]catalogue.Chosen { if len(p.Pins) == 0 { return nil } @@ -163,7 +160,7 @@ func ChosenPins(p seat.Placement) map[string]catalogue.Chosen { } // Bound is, by consumer module and provision, where each binding sends it; nil when none. -func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen { +func Bound(p seat.Assignments) map[string]map[string]catalogue.Chosen { var out map[string]map[string]catalogue.Chosen for _, m := range p.Modules { if len(m.Bindings) == 0 { @@ -182,7 +179,7 @@ func Bound(p seat.Placement) map[string]map[string]catalogue.Chosen { // HostPorts is, by module and the port it declares, the host port it is published on; nil when none. The check has // refused a port that is not a number, so none is dropped here. -func HostPorts(p seat.Placement) map[string]map[int]int { +func HostPorts(p seat.Assignments) map[string]map[int]int { var out map[string]map[int]int for _, m := range p.Modules { if len(m.Ports) == 0 { @@ -202,7 +199,7 @@ func HostPorts(p seat.Placement) map[string]map[int]int { // Layers is one module's settings layers on the node, as the composition reads them: the whole mesh's layer when it has // one, then the node's, which carries the module's config values as mesh-config resolved them for this node (values, -// nil when none) beside its placement; a module with no layer and no value has none. A module in the settings form +// nil when none) beside its assignments; a module with no layer and no value has none. A module in the settings form // carries its values in the file (MeshWideValues, Values), which then stand for mesh-config's. func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue.Layer { var out []catalogue.Layer @@ -229,7 +226,7 @@ func Layers(m seat.PlacedModule, node string, values map[string]any) []catalogue } // Held is the node's holdings as the composition reads them. -func Held(p seat.Placement) []catalogue.Held { +func Held(p seat.Assignments) []catalogue.Held { out := make([]catalogue.Held, 0, len(p.Holds)) for _, h := range p.Holds { out = append(out, catalogue.Held{Claim: h.Claim, Scope: h.Scope, Node: p.Node, Module: h.Module, Site: h.Site, Kind: h.Kind}) @@ -238,10 +235,10 @@ func Held(p seat.Placement) []catalogue.Held { } // InsideAddresses is every node's address on the private network, sorted and without repeats. -func InsideAddresses(placements map[string]seat.Placement) []string { +func InsideAddresses(assignments map[string]seat.Assignments) []string { seen := map[string]bool{} out := []string{} - for _, p := range placements { + for _, p := range assignments { if a := strings.TrimSpace(p.Decided.Address); a != "" && !seen[a] { seen[a] = true out = append(out, a) diff --git a/go/clients/meshmoduleassignerclient/client_test.go b/go/clients/meshmoduleassignerclient/client_test.go index 8248650..cbc750a 100644 --- a/go/clients/meshmoduleassignerclient/client_test.go +++ b/go/clients/meshmoduleassignerclient/client_test.go @@ -11,7 +11,7 @@ import ( "git.novox.be/novox/mesh-sdk/go/catalogue" ) -func TestPlacementClient(t *testing.T) { +func TestAssignmentsClient(t *testing.T) { t.Run("proposal id", func(t *testing.T) { for id, ok := range map[string]bool{"p-20261028t183000-1a2b3c": true, "abc": true, "ab": false, "A-bc": false, "-abc": false, "a.bc": false, "": false} { if (CheckProposalID(id) == nil) != ok { @@ -20,17 +20,16 @@ func TestPlacementClient(t *testing.T) { } }) t.Run("key and file", func(t *testing.T) { - // Research 072 row 37: one node key and one key per module, a legacy key read beside them, any other refused. + // Research 072 row 37: one node key and one key per module, any other refused. for key, want := range map[string]struct { node, module string kind KeyKind - }{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind}, - "placement.g14": {"g14", "", LegacyKind}} { + }{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind}} { if n, m, k, err := ParseKey(key); err != nil || n != want.node || m != want.module || k != want.kind { t.Errorf("ParseKey(%q) = %q, %q, %d, %v", key, n, m, k, err) } } - for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b", "g14", "g14.a.b", ".node"} { + for _, bad := range []string{"want.g14.x", "g14", "g14.a.b", ".node"} { if _, _, _, err := ParseKey(bad); err == nil { t.Errorf("ParseKey(%q) took it", bad) } @@ -44,19 +43,15 @@ func TestPlacementClient(t *testing.T) { if k, err := NodeKeys("g14"); err != nil || k != "g14.>" { t.Errorf("NodeKeys = %q, %v", k, err) } - if k, err := LegacyKey("g14"); err != nil || k != "placement.g14" { - t.Errorf("LegacyKey = %q, %v", k, err) - } for name, err := range map[string]error{"a dotted node": func() error { _, err := NodeKey("a.b"); return err }(), - "a node named placement": func() error { _, err := NodeKey("placement"); return err }(), - "a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} { + "a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} { if err == nil { t.Errorf("%s was taken", name) } } }) - g14 := seat.Placement{Node: "g14", Revision: "abc", + g14 := seat.Assignments{Node: "g14", Revision: "abc", Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}}, Modules: []seat.PlacedModule{ {Module: "zsh"}, @@ -65,24 +60,24 @@ func TestPlacementClient(t *testing.T) { Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}}, Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}}, } - novox := seat.Placement{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}}, + novox := seat.Assignments{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}}, Holds: []seat.Holding{{Claim: "mesh-example", Scope: "mesh", Module: "postgres"}}} t.Run("value", func(t *testing.T) { - raw, err := EncodePlacement(g14) + raw, err := EncodeAssignments(g14) if err != nil { t.Fatal(err) } - back, err := DecodePlacement(raw) - if err != nil || back.V != Version || !SamePlacement(back, g14) { + back, err := DecodeAssignments(raw) + if err != nil || back.V != Version || !SameAssignments(back, g14) { t.Fatalf("round trip: %+v, %v", back, err) } moved := g14 moved.Revision = "def" - if !SamePlacement(g14, moved) { + if !SameAssignments(g14, moved) { t.Error("a new revision alone counts as a change") } - if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) { + if _, err := DecodeAssignments([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) { t.Errorf("version 9: %v", err) } for name, bad := range map[string]string{ @@ -95,7 +90,7 @@ func TestPlacementClient(t *testing.T) { "node not a name": `{"v":1,"node":"g.14","modules":[]}`, "value not shaped": `{"v":1,"node":"g14","modules":{}}`, } { - if _, err := DecodePlacement([]byte(bad)); err == nil { + if _, err := DecodeAssignments([]byte(bad)); err == nil { t.Errorf("%s: taken", name) } } @@ -134,37 +129,26 @@ func TestPlacementClient(t *testing.T) { if got := Layers(seat.PlacedModule{Module: "a"}, "g14", nil); got != nil { t.Errorf("no layer and no value is no layer: %#v", got) } - empty := seat.Placement{Node: "x"} + empty := seat.Assignments{Node: "x"} if ChosenPins(empty) != nil || Bound(empty) != nil || HostPorts(empty) != nil || len(Held(empty)) != 0 { - t.Error("an empty placement reads as something") + t.Error("an empty assignments reads as something") } }) - // Research 072 row 37: a node's placement as its node key and module keys, written so a reader never takes a part of - // a write, read beside the form before, and converted from it once. + // Research 072 row 37: a node's assignments as its node key and module keys, written so a reader never takes a part of + // a write, t.Run("one key per module and node", func(t *testing.T) { - values, err := PlacementValues(g14) - if err != nil || len(values) != 4 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil || - values["placement.g14"] == nil { - t.Fatalf("PlacementValues = %v, %v", values, err) + values, err := AssignmentsValues(g14) + if err != nil || len(values) != 3 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil { + t.Fatalf("AssignmentsValues = %v, %v", values, err) } - got, err := PlacementsOf(values) - if err != nil || !SamePlacement(got["g14"], g14) || len(got) != 1 { - t.Fatalf("PlacementsOf = %+v, %v", got, err) + got, err := AssignmentsOf(values) + if err != nil || !SameAssignments(got["g14"], g14) || len(got) != 1 { + t.Fatalf("AssignmentsOf = %+v, %v", got, err) } - if got := InsideAddresses(map[string]seat.Placement{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" { + if got := InsideAddresses(map[string]seat.Assignments{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" { t.Errorf("InsideAddresses = %v", got) } - legacy, _ := EncodePlacement(novox) - stale, _ := EncodePlacement(seat.Placement{Node: "g14"}) - both := map[string][]byte{"placement.novox": legacy, "placement.g14": stale} - for k, v := range values { - both[k] = v - } - if got, err := PlacementsOf(both); err != nil || !SamePlacement(got["g14"], g14) || !SamePlacement(got["novox"], novox) { - t.Errorf("a node key wins over a legacy key, and a legacy key alone places its node: %+v, %v", got, err) - } - keys := NewKeys() rev := uint64(0) apply := func(writes []KeyWrite, upTo int) { @@ -179,27 +163,19 @@ func TestPlacementClient(t *testing.T) { } } } - // The form before converts once: its node and module keys, the legacy key left for the readers not yet delivered. + // A node written whole onto no keys: its module keys, then its node key. rev++ - legacyG14, _ := EncodePlacement(g14) - if _, err := keys.Put("placement.g14", legacyG14, rev, false); err != nil || !keys.Legacy("g14") { - t.Fatalf("the legacy key: %v", err) + first, err := keys.Writes("g14", &g14) + if err != nil || len(first) != 3 || !first[2].Commit { + t.Fatalf("Writes = %+v, %v", first, err) } - migrations, err := keys.Migrations() - if err != nil || len(migrations["g14"]) != 3 || !migrations["g14"][2].Commit { - // The legacy key holds the placement already: no mirror write. - t.Fatalf("Migrations = %+v, %v", migrations, err) - } - apply(migrations["g14"], -1) - if m, _ := keys.Migrations(); len(m) != 0 || keys.Legacy("g14") { - t.Fatalf("a converted node converts again: %+v", m) - } - if p, r, ok := keys.Of("g14"); !ok || !SamePlacement(p, g14) || r != rev { - t.Fatalf("Of after the conversion = %+v, %d, %v", p, r, ok) + apply(first, -1) + if p, r, ok := keys.Of("g14"); !ok || !SameAssignments(p, g14) || r != rev { + t.Fatalf("Of after the write = %+v, %d, %v", p, r, ok) } // A write under way is never read half: app's new port is not read before the node key names it. - after := clonePlacement(g14) + after := cloneAssignments(g14) after.Modules = []seat.PlacedModule{{Module: "app", Ports: map[string]int64{"8080": 18081}}, {Module: "new"}} after.Holds, after.Pins = nil, nil writes, err := keys.Writes("g14", &after) @@ -210,32 +186,28 @@ func TestPlacementClient(t *testing.T) { for _, w := range writes { kinds = append(kinds, fmt.Sprintf("%s:%v:%d", w.Key, w.Value != nil, w.Base)) } - if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 placement.g14:true:1 g14.zsh:false:2" || - !writes[3].Mirror { + if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 g14.zsh:false:2" { t.Fatalf("Writes = %v", kinds) } apply(writes, 2) - if p, _, _ := keys.Of("g14"); !SamePlacement(p, g14) { + if p, _, _ := keys.Of("g14"); !SameAssignments(p, g14) { t.Errorf("the module keys written before the node key are read: %+v", p) } apply(writes[2:], -1) - if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) { + if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) { t.Errorf("the write done is not read whole: %+v", p) } - if legacy, _ := PlacementsOf(map[string][]byte{"placement.g14": writes[3].Value}); !SamePlacement(legacy["g14"], after) { - t.Errorf("the mirror is not the placement written: %+v", legacy) - } - // A module key that stays is not written again, nor a mirror that holds it. + // A module key that stays is not written again. if again, _ := keys.Writes("g14", &after); len(again) != 1 || !again[0].Commit { - t.Errorf("an unchanged placement writes %+v", again) + t.Errorf("an unchanged assignments writes %+v", again) } // Keys that do not agree keep what the node had, and say so. stray, _ := EncodeModule(seat.AssignmentsByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}}) if _, err := keys.Put("g14.app", stray, 99, false); err == nil { t.Error("a module key the node key does not name the value of was taken silently") } - if p, _, _ := keys.Of("g14"); !SamePlacement(p, after) { - t.Errorf("a stray module key moved the placement: %+v", p) + if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) { + t.Errorf("a stray module key moved the assignments: %+v", p) } // The holder's next write puts the module key right again. repair, _ := keys.Writes("g14", &after) @@ -243,16 +215,16 @@ func TestPlacementClient(t *testing.T) { t.Fatalf("the repair writes %+v", repair) } apply(repair, -1) - // No placement: the legacy key first, then the node key, then the module keys; none is read after. + // No assignments: the node key first, then the module keys; none is read after. gone, _ := keys.Writes("g14", nil) kinds = nil for _, w := range gone { kinds = append(kinds, w.Key) } - if strings.Join(kinds, " ") != "placement.g14 g14.node g14.app g14.new" { + if strings.Join(kinds, " ") != "g14.node g14.app g14.new" { t.Errorf("Writes(nil) = %v", kinds) } - apply(gone, 2) + apply(gone, 1) if _, _, ok := keys.Of("g14"); ok { t.Error("a node key deleted still places its node") } diff --git a/go/clients/meshmoduleassignerclient/holders_test.go b/go/clients/meshmoduleassignerclient/holders_test.go index 6d51a54..17d904d 100644 --- a/go/clients/meshmoduleassignerclient/holders_test.go +++ b/go/clients/meshmoduleassignerclient/holders_test.go @@ -18,7 +18,7 @@ func TestDigestIsTheFirstWritersAndHoldersAndStandingRead(t *testing.T) { keys := map[string][]byte{ "mesh-module-assigner": []byte(`{"v":1,"seat":"mesh-module-assigner","scope":"mesh","holders":[{"module":"mesh-delivery-assigner","node":"novox"}]}`), - "node-example.ace": []byte(`{"v":1,"seat":"node-example","scope":"node","node":"ace","holders":[{"module":"node-example","node":"ace"}]}`), + "node-example.ace": []byte(`{"v":1,"seat":"node-example","scope":"node","node":"ace","holders":[{"module":"node-example","node":"ace"}]}`), "mesh-vault": []byte(`{"v":2,"seat":"mesh-vault","scope":"mesh","holders":[]}`), } for _, c := range []struct { diff --git a/go/clients/meshmoduleassignerclient/keys.go b/go/clients/meshmoduleassignerclient/keys.go index ac2dfe1..72a01a5 100644 --- a/go/clients/meshmoduleassignerclient/keys.go +++ b/go/clients/meshmoduleassignerclient/keys.go @@ -11,29 +11,26 @@ import ( seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) -// **One key per module and node** (research 072 row 37; issue 774). The seat's state `placement` keeps a node's placement +// **One key per module and node** (research 072 row 37; issue 774). The seat's state `assignments` keeps a node's assignments // under the node's own keys, so a node watches `.>` alone and a change of one module writes that module's key: // `.node`, the node key (what a person decided of the node, its pins and holdings, and the modules placed on it, // each with the digest of its module key's value), and `.`, a module key. The node key is the commit of a // write; a reader takes for each module the node key names the value of its module key whose digest the node key names, // the current one once the node key is written, the previous one while the write is under way, so a reader never -// composes a part of a write. The form before, one value `placement.` per node, is still read and mirrored until -// a later change deletes it. +// composes a part of a write. -// KeyKind is the kind of a key of the state `placement`. +// KeyKind is the kind of a key of the state `assignments`. type KeyKind int -// The kinds of key: a node key, a module key, and the form before (one value per node). +// The kinds of key: a node key and a module key. const ( NodeKind KeyKind = iota + 1 ModuleKind - LegacyKind ) -// NodeKey is a node's node key, `.node`. A node named `placement` is refused: its keys would read as the -// legacy form's. +// NodeKey is a node's node key, `.node`. func NodeKey(node string) (string, error) { - if err := checkPlacementNode(node); err != nil { + if err := checkAssignmentsNode(node); err != nil { return "", err } return node + "." + NodeWord, nil @@ -41,41 +38,30 @@ func NodeKey(node string) (string, error) { // ModuleKey is a module's key on a node, `.`; a module named `node` is refused. func ModuleKey(node, module string) (string, error) { - if err := checkPlacementNode(node); err != nil { + if err := checkAssignmentsNode(node); err != nil { return "", err } - if err := checkPlacementModule(module); err != nil { + if err := checkAssignmentsModule(module); err != nil { return "", err } return node + "." + module, nil } -// NodeKeys is the pattern of every key of one node, `.>`: what a node watches of its own placement. +// NodeKeys is the pattern of every key of one node, `.>`: what a node watches of its own assignments. func NodeKeys(node string) (string, error) { - if err := checkPlacementNode(node); err != nil { + if err := checkAssignmentsNode(node); err != nil { return "", err } return node + ".>", nil } -// LegacyKey is a node's key in the form before, `placement.`, read while this change is delivered. -func LegacyKey(node string) (string, error) { - if err := checkName("node", node); err != nil { - return "", err - } - return legacyWord + "." + node, nil -} - -// ParseKey reads a key of the state `placement` in either form: its node, its module (empty for a node key and -// a legacy key) and its kind. Any other key is refused, naming it. +// ParseKey reads a key of the state `assignments` its node, its module (empty for a node key) and its kind. Any other key is refused, naming it. func ParseKey(key string) (node, module string, kind KeyKind, err error) { first, rest, ok := strings.Cut(key, ".") if !ok || strings.Contains(rest, ".") || checkName("node", first) != nil || checkName("module", rest) != nil { - return "", "", 0, fmt.Errorf("meshmoduleassignerclient: %q is not a placement key, .node or .", key) + return "", "", 0, fmt.Errorf("meshmoduleassignerclient: %q is not a assignments key, .node or .", key) } switch { - case first == legacyWord: - return rest, "", LegacyKind, nil case rest == NodeWord: return first, "", NodeKind, nil default: @@ -83,17 +69,14 @@ func ParseKey(key string) (node, module string, kind KeyKind, err error) { } } -func checkPlacementNode(node string) error { +func checkAssignmentsNode(node string) error { if err := checkName("node", node); err != nil { return err } - if node == legacyWord { - return fmt.Errorf("meshmoduleassignerclient: no node is named %q: its placement keys would read as the form before", legacyWord) - } return nil } -func checkPlacementModule(module string) error { +func checkAssignmentsModule(module string) error { if err := checkName("module", module); err != nil { return err } @@ -105,7 +88,7 @@ func checkPlacementModule(module string) error { // EncodeNode is n as JSON at Version. func EncodeNode(n seat.AssignmentsByNodeNodeValue) ([]byte, error) { - if err := checkPlacementNode(n.Node); err != nil { + if err := checkAssignmentsNode(n.Node); err != nil { return nil, err } n.V = Version @@ -122,12 +105,12 @@ func DecodeNode(data []byte) (seat.AssignmentsByNodeNodeValue, error) { if err != nil { return n, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) } - if err := checkPlacementNode(n.Node); err != nil { + if err := checkAssignmentsNode(n.Node); err != nil { return seat.AssignmentsByNodeNodeValue{}, err } seen := map[string]bool{} for _, r := range n.Modules { - if err := checkPlacementModule(r.Module); err != nil { + if err := checkAssignmentsModule(r.Module); err != nil { return seat.AssignmentsByNodeNodeValue{}, err } if seen[r.Module] || r.Digest == "" { @@ -140,10 +123,10 @@ func DecodeNode(data []byte) (seat.AssignmentsByNodeNodeValue, error) { // EncodeModule is m as JSON at Version. func EncodeModule(m seat.AssignmentsByNodeModuleValue) ([]byte, error) { - if err := checkPlacementNode(m.Node); err != nil { + if err := checkAssignmentsNode(m.Node); err != nil { return nil, err } - if err := checkPlacementModule(m.Placed.Module); err != nil { + if err := checkAssignmentsModule(m.Placed.Module); err != nil { return nil, err } if m.Previous != nil && m.Previous.Module != m.Placed.Module { @@ -162,20 +145,20 @@ func DecodeModule(data []byte) (seat.AssignmentsByNodeModuleValue, error) { if err != nil { return m, fmt.Errorf("meshmoduleassignerclient: value does not fit its shape: %w", err) } - if err := checkPlacementNode(m.Node); err != nil { + if err := checkAssignmentsNode(m.Node); err != nil { return seat.AssignmentsByNodeModuleValue{}, err } - if err := checkPlacementModule(m.Placed.Module); err != nil { + if err := checkAssignmentsModule(m.Placed.Module); err != nil { return seat.AssignmentsByNodeModuleValue{}, err } return m, nil } -// Keys is the state `placement` as a reader holds it, key by key in either form, and every node's placement +// Keys is the state `assignments` as a reader holds it, key by key in either form, and every node's assignments // assembled from it: the one reader of the state's keys for every part (issue 774), so no part parses them itself. A // write of the holder's under way is never assembled half: a module key's value is taken only as far as the node key // names its digest (see the package's comment above NodeWord), and a node whose keys do not agree keeps the -// placement last assembled for it, Put saying why. +// assignments last assembled for it, Put saying why. // // Safe for use from a watch and from a reader at once. type Keys struct { @@ -189,18 +172,16 @@ type keysOf struct { nodeRev uint64 modules map[string]seat.AssignmentsByNodeModuleValue moduleRevs map[string]uint64 - legacy *seat.Placement - legacyRev uint64 - last *seat.Placement // the placement last assembled whole, nil when none + last *seat.Assignments // the assignments last assembled whole, nil when none } // NewKeys holds no key yet. func NewKeys() *Keys { return &Keys{nodes: map[string]*keysOf{}} } -// PlacementsOf is every node's placement assembled from the state's keys as a reader holds them (key to value; a +// AssignmentsOf is every node's assignments assembled from the state's keys as a reader holds them (key to value; a // deleted key absent), and an error naming every key that did not read and every node whose keys do not agree (that // node is then assembled from its module keys' values as they are). -func PlacementsOf(keys map[string][]byte) (map[string]seat.Placement, error) { +func AssignmentsOf(keys map[string][]byte) (map[string]seat.Assignments, error) { k := NewKeys() var errs []error names := make([]string, 0, len(keys)) @@ -278,19 +259,6 @@ func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (nod // Its revision is kept even when its value does not read, so the holder can write over it. of.moduleRevs[module] = revision } - case LegacyKind: - of.legacy, of.legacyRev = nil, 0 - if !deleted { - p, derr := DecodePlacement(value) - switch { - case derr != nil: - readErr = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr) - case p.Node != node: - readErr = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, p.Node) - default: - of.legacy, of.legacyRev = &p, revision - } - } } p, whole, err := of.assemble(node) switch { @@ -302,18 +270,14 @@ func (k *Keys) put(key string, value []byte, revision uint64, deleted bool) (nod return node, readErr, err } -// assemble is the node's placement from its keys: nil when none places it, whole false (and an error) when the node key +// assemble is the node's assignments from its keys: nil when none places it, whole false (and an error) when the node key // names a module digest no module key's value has. -func (of *keysOf) assemble(node string) (*seat.Placement, bool, error) { +func (of *keysOf) assemble(node string) (*seat.Assignments, bool, error) { if of.node == nil { - if of.legacy == nil { - return nil, true, nil - } - p := clonePlacement(*of.legacy) - return &p, true, nil + return nil, true, nil } n := of.node - p := seat.Placement{V: Version, Node: node, Decided: n.Decided, Pins: n.Pins, Holds: n.Holds, Modules: make([]seat.PlacedModule, 0, len(n.Modules))} + p := seat.Assignments{V: Version, Node: node, Decided: n.Decided, Pins: n.Pins, Holds: n.Holds, Modules: make([]seat.PlacedModule, 0, len(n.Modules))} var astray []string for _, r := range n.Modules { m, ok := of.modules[r.Module] @@ -330,43 +294,39 @@ func (of *keysOf) assemble(node string) (*seat.Placement, bool, error) { } } if len(astray) > 0 { - return &p, false, fmt.Errorf("the node key of %s names %s at a value no module key holds: %s keeps the placement it had", + return &p, false, fmt.Errorf("the node key of %s names %s at a value no module key holds: %s keeps the assignments it had", node, strings.Join(astray, ", "), node) } return &p, true, nil } -// Of is node's placement and the revision of the key it was read from (its node key, or its legacy key), and whether +// Of is node's assignments and the revision of the key it was read from (its node key), and whether // any key places it. -func (k *Keys) Of(node string) (seat.Placement, uint64, bool) { +func (k *Keys) Of(node string) (seat.Assignments, uint64, bool) { k.mu.Lock() defer k.mu.Unlock() of := k.nodes[node] if of == nil || of.last == nil { - return seat.Placement{}, 0, false + return seat.Assignments{}, 0, false } - rev := of.nodeRev - if of.node == nil { - rev = of.legacyRev - } - return clonePlacement(*of.last), rev, true + return cloneAssignments(*of.last), of.nodeRev, true } -// All is every node's placement, by node, and the revision of each one's key (Of). -func (k *Keys) All() map[string]seat.Placement { +// All is every node's assignments, by node, and the revision of each one's key (Of). +func (k *Keys) All() map[string]seat.Assignments { out, _ := k.AllRevisions() return out } // AllRevisions is All with the revision of each node's key. -func (k *Keys) AllRevisions() (map[string]seat.Placement, map[string]uint64) { +func (k *Keys) AllRevisions() (map[string]seat.Assignments, map[string]uint64) { k.mu.Lock() nodes := make([]string, 0, len(k.nodes)) for n := range k.nodes { nodes = append(nodes, n) } k.mu.Unlock() - out, revs := map[string]seat.Placement{}, map[string]uint64{} + out, revs := map[string]seat.Assignments{}, map[string]uint64{} for _, n := range nodes { if p, rev, ok := k.Of(n); ok { out[n], revs[n] = p, rev @@ -375,28 +335,22 @@ func (k *Keys) AllRevisions() (map[string]seat.Placement, map[string]uint64) { return out, revs } -// KeyWrite is one write of a key of the state `placement`, by compare-and-set: Value nil deletes the key at +// KeyWrite is one write of a key of the state `assignments`, by compare-and-set: Value nil deletes the key at // Base, Base 0 creates it, any other Base updates it at that revision. type KeyWrite struct { Node string Key string Value []byte Base uint64 - // Commit says this write is the node key's: once it is written, the node's placement is the new one. + // Commit says this write is the node key's: once it is written, the node's assignments is the new one. Commit bool - // Mirror says this write is the legacy key's, `placement.`, kept in step after the commit for the readers not - // yet delivered: a reader of both forms never reads it once the node key is there, so a mirror write that fails - // harms no such reader, and is made again at the node's next write. - Mirror bool } -// Writes are the writes that take node from the placement its keys hold to after (nil: no placement), in the order +// Writes are the writes that take node from the assignments its keys hold to after (nil: no assignments), in the order // they are made: the module keys that change, each carrying the value it replaces; then the node key, the commit; then -// the legacy key `placement.` with the whole placement, a mirror for the readers not yet delivered, unless it -// holds it already; then the deletes of the module keys after no longer names. For no placement the legacy key goes -// first, then the node key, then the module keys, so a crash between them never leaves the legacy value to be read -// again. A module key whose value stays is not written. -func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { +// the deletes of the module keys after no longer names. For no assignments the node key goes first, then the module +// keys. A module key whose value stays is not written. +func (k *Keys) Writes(node string, after *seat.Assignments) ([]KeyWrite, error) { nodeKey, err := NodeKey(node) if err != nil { return nil, err @@ -418,10 +372,6 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { } } if after == nil { - if of.legacy != nil { - key, _ := LegacyKey(node) - out = append(out, KeyWrite{Node: node, Key: key, Base: of.legacyRev}) - } if of.node != nil { out = append(out, KeyWrite{Node: node, Key: nodeKey, Base: of.nodeRev, Commit: true}) } @@ -429,9 +379,9 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { return out, nil } if after.Node != node { - return nil, fmt.Errorf("meshmoduleassignerclient: the placement written under %s names the node %q", node, after.Node) + return nil, fmt.Errorf("meshmoduleassignerclient: the assignments written under %s names the node %q", node, after.Node) } - if err := CheckPlacement(*after); err != nil { + if err := CheckAssignments(*after); err != nil { return nil, err } committed := map[string]seat.PlacedModule{} @@ -443,7 +393,7 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { n := seat.AssignmentsByNodeNodeValue{Node: node, Decided: after.Decided, Pins: after.Pins, Holds: after.Holds, Modules: make([]seat.AssignmentsByNodeNodeValueModulesItem, 0, len(after.Modules))} keep := map[string]bool{} for _, m := range after.Modules { - if err := checkPlacementModule(m.Module); err != nil { + if err := checkAssignmentsModule(m.Module); err != nil { return nil, err } digest := Digest(m) @@ -470,58 +420,12 @@ func (k *Keys) Writes(node string, after *seat.Placement) ([]KeyWrite, error) { return nil, err } out = append(out, KeyWrite{Node: node, Key: nodeKey, Value: raw, Base: of.nodeRev, Commit: true}) - // The mirror of the form before, while readers of it alone may run (the delivery of this change); a later change - // stops writing it and deletes it. - if of.legacy == nil || !SamePlacement(*of.legacy, *after) { - legacy, err := EncodePlacement(*after) - if err != nil { - return nil, err - } - key, _ := LegacyKey(node) - out = append(out, KeyWrite{Node: node, Key: key, Value: legacy, Base: of.legacyRev, Mirror: true}) - } deleteModules(keep) return out, nil } -// Migrations are, by node, the writes that convert every node held only in the form before (its legacy key, no node -// key) into its node and module keys, its legacy key kept as the mirror for the readers not yet delivered (Writes); empty -// once every node is converted, so the holder's conversion at each start is idempotent. -func (k *Keys) Migrations() (map[string][]KeyWrite, error) { - k.mu.Lock() - var legacy []string - for node, of := range k.nodes { - if of.node == nil && of.legacy != nil && checkPlacementNode(node) == nil { - legacy = append(legacy, node) - } - } - k.mu.Unlock() - sort.Strings(legacy) - out := map[string][]KeyWrite{} - var errs []error - for _, node := range legacy { - p, _, _ := k.Of(node) - w, err := k.Writes(node, &p) - if err != nil { - errs = append(errs, fmt.Errorf("%s: %w", node, err)) - continue - } - out[node] = w - } - return out, errors.Join(errs...) -} - -// Legacy says node is held only in the form before. -func (k *Keys) Legacy(node string) bool { - k.mu.Lock() - defer k.mu.Unlock() - of := k.nodes[node] - return of != nil && of.node == nil && of.legacy != nil -} - -// PlacementValues is every key and value of p written whole onto no keys before: its module keys, its node key and the -// legacy mirror, as a writer that holds no key of the node (a test's bus, a fresh mesh) writes them. -func PlacementValues(p seat.Placement) (map[string][]byte, error) { +// AssignmentsValues is every key and value of p written whole onto no keys before: its module keys and its node key, as a writer that holds no key of the node (a test's bus, a fresh mesh) writes them. +func AssignmentsValues(p seat.Assignments) (map[string][]byte, error) { writes, err := NewKeys().Writes(p.Node, &p) if err != nil { return nil, err @@ -533,10 +437,10 @@ func PlacementValues(p seat.Placement) (map[string][]byte, error) { return out, nil } -// clonePlacement is p with its own maps and slices, so a reader's copy is not a held one. -func clonePlacement(p seat.Placement) seat.Placement { +// cloneAssignments is p with its own maps and slices, so a reader's copy is not a held one. +func cloneAssignments(p seat.Assignments) seat.Assignments { raw, _ := json.Marshal(p) - var out seat.Placement + var out seat.Assignments _ = json.Unmarshal(raw, &out) if out.Modules == nil { out.Modules = []seat.PlacedModule{} diff --git a/go/clients/meshmoduleassignerclient/ops.go b/go/clients/meshmoduleassignerclient/ops.go index 0c85902..2343936 100644 --- a/go/clients/meshmoduleassignerclient/ops.go +++ b/go/clients/meshmoduleassignerclient/ops.go @@ -15,14 +15,14 @@ const ( OpUnhold = "unhold" // OpDecide sets a decided field of the node (NodeDecided by its JSON name), its address among them. OpDecide = "decide" - // OpConfig sets a module's config placement or values on the node. + // OpConfig sets a module's config assignments or values on the node. OpConfig = "config" - // OpSeed is the node's whole placement: the take-over (adopt, §7), never given by a caller. + // OpSeed is the node's whole assignments: the take-over (adopt, §7), never given by a caller. OpSeed = "seed" // OpAddNode adds a joining node with its decided fields and the public halves of its keys, on the warrant of the // enrolment that issued its token (novox/hq ADR 0587 §7): the enroller's alone, the proposal's one change. OpAddNode = "add-node" - // OpRemoveNode takes a joining node off placement again when its enrolment ends unconfirmed, revoked or expired: the + // OpRemoveNode takes a joining node off assignments again when its enrolment ends unconfirmed, revoked or expired: the // enroller's alone, on the same warrant, the proposal's one change, of a node with no module and no holding. OpRemoveNode = "remove-node" ) diff --git a/go/clients/meshmoduleassignerclient/placed.go b/go/clients/meshmoduleassignerclient/placed.go index 1bae32a..2f6a115 100644 --- a/go/clients/meshmoduleassignerclient/placed.go +++ b/go/clients/meshmoduleassignerclient/placed.go @@ -10,15 +10,14 @@ import ( seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) -// Placed is where the placement (the seat mesh-placement's state `placement`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when: +// Placed is where the assignments (the seat mesh-assignments's state `assignments`, held by the assigner; ADR 0567, ADR 0549 §3) places each module, and since when: // the one reader of that state for every part that asks "does module stand on node" (issue 774, design 47's third // milestone), in place of each part reading the controller's assignments (`mesh-controller_assignments`, which goes // with the controller's lease, ADR 0549 §6). // // Its input is the state's revisions in the order the bus wrote them, as a watch with the history (jetstream's -// IncludeHistory) hands them: Put takes each, in either form of the state's keys (keys.go): a node's node key -// names its modules (a module key says nothing of which stand), and its legacy key `placement.` does until a node -// key of it is heard. A module stands since the revision of its node's key that first named it +// IncludeHistory) hands them: Put takes each, a node's node key +// names its modules (a module key says nothing of which stand). A module stands since the revision of its node's key that first named it // in an unbroken run. A module in the first revision heard of a key stands since the zero time: that revision is the // key's first (the seed of ADR 0549 §4 placed what already ran there) or the oldest the bus still keeps // (the state's history), and either way the module may have run there before it. A key's value that does not read places @@ -29,17 +28,14 @@ type Placed struct { mu sync.Mutex since map[string]map[string]time.Time // node -> module -> since when it stands heard map[string]bool // the nodes whose key was heard at all - // newForm are the nodes whose node key `.node` was heard: their legacy key `placement.` is no longer - // read (keys.go). - newForm map[string]bool } -// NewPlaced is a Placed that holds no placement yet. +// NewPlaced is a Placed that holds no assignments yet. func NewPlaced() *Placed { - return &Placed{since: map[string]map[string]time.Time{}, heard: map[string]bool{}, newForm: map[string]bool{}} + return &Placed{since: map[string]map[string]time.Time{}, heard: map[string]bool{}} } -// Put takes one revision of a key of the state `placement` (its value, whether it is a delete, and when the bus wrote +// Put takes one revision of a key of the state `assignments` (its value, whether it is a delete, and when the bus wrote // it). It answers the node the key is of and the modules whose standing on it changed (placed or removed), in name // order, and an error for a key or a value that does not read (the node then places nothing). func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node string, changed []string, err error) { @@ -50,37 +46,22 @@ func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node if kind == ModuleKind { return node, nil, nil // which modules stand is the node key's to say } - p.mu.Lock() - if kind == LegacyKind && p.newForm[node] { - p.mu.Unlock() - return node, nil, nil // the node is read from its node key once one was heard - } - if kind == NodeKind { - p.newForm[node] = true - } - p.mu.Unlock() now := map[string]bool{} if !deleted { var modules []string var derr error var named string - if kind == NodeKind { - var n seat.AssignmentsByNodeNodeValue - n, derr = DecodeNode(value) - named = n.Node - for _, r := range n.Modules { - modules = append(modules, r.Module) - } - } else { - var pl seat.Placement - pl, derr = DecodePlacement(value) - named, modules = pl.Node, Assigned(pl) + var n seat.AssignmentsByNodeNodeValue + n, derr = DecodeNode(value) + named = n.Node + for _, r := range n.Modules { + modules = append(modules, r.Module) } switch { case derr != nil: - err = fmt.Errorf("the placement of %s does not read, so it places nothing there: %w", node, derr) + err = fmt.Errorf("the assignments of %s does not read, so it places nothing there: %w", node, derr) case named != node: - err = fmt.Errorf("the placement under %s names the node %q, so it places nothing there", key, named) + err = fmt.Errorf("the assignments under %s names the node %q, so it places nothing there", key, named) default: for _, m := range modules { now[m] = true @@ -117,7 +98,7 @@ func (p *Placed) Put(key string, value []byte, deleted bool, at time.Time) (node return node, changed, err } -// Seeded says some node's placement is held: before the assigner's adopt writes the first placement nothing is placed, +// Seeded says some node's assignments is held: before the assigner's adopt writes the first assignments nothing is placed, // and a reader keeps what it did before (ADR 0549 §5). func (p *Placed) Seeded() bool { p.mu.Lock() diff --git a/go/clients/meshmoduleassignerclient/proposal.go b/go/clients/meshmoduleassignerclient/proposal.go index 284a358..e715447 100644 --- a/go/clients/meshmoduleassignerclient/proposal.go +++ b/go/clients/meshmoduleassignerclient/proposal.go @@ -8,7 +8,7 @@ import ( var proposalIDOK = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,62}$`) // CheckProposalID refuses an id that is no proposal's: lower-case letters, digits and dashes, 3 to 63 of them, so it is one -// key token. The holder writes proposals under it, and the keys of the asks about a proposal (`placement.`) carry it. +// key token. The holder writes proposals under it, and the keys of the asks about a proposal (`assignments.`) carry it. func CheckProposalID(id string) error { if !proposalIDOK.MatchString(id) { return fmt.Errorf("mesh-module-assigner: %q is no proposal id (lower-case letters, digits and dashes, one key token)", id) diff --git a/go/clients/meshmoduleassignerclient/reads.go b/go/clients/meshmoduleassignerclient/reads.go index f892d87..8189ddd 100644 --- a/go/clients/meshmoduleassignerclient/reads.go +++ b/go/clients/meshmoduleassignerclient/reads.go @@ -2,8 +2,8 @@ package meshmoduleassignerclient import "sort" -// MachineNames is every machine the seat places modules on, sorted, from its state `placement` as a reader holds its -// keys: each with a node key `.node` or, in the form before, `placement.`. Module keys and keys of the +// MachineNames is every machine the seat places modules on, sorted, from its state `assignments` as a reader holds its +// keys: each with a node key `.node`. Module keys and keys of the // state's other kinds are passed over. func MachineNames(keys map[string][]byte) []string { seen := map[string]bool{} diff --git a/go/clients/meshmoduleassignerclient/reads_test.go b/go/clients/meshmoduleassignerclient/reads_test.go index f531b6d..4b2d078 100644 --- a/go/clients/meshmoduleassignerclient/reads_test.go +++ b/go/clients/meshmoduleassignerclient/reads_test.go @@ -6,8 +6,8 @@ import ( ) func TestMachineNamesAreTheNodesAKeyNames(t *testing.T) { - keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "placement.novox": nil, "want.x.y": nil, "bad": nil} - if got, want := MachineNames(keys), []string{"ace", "g14", "novox"}; !reflect.DeepEqual(got, want) { + keys := map[string][]byte{"g14.node": nil, "g14.app": nil, "ace.node": nil, "want.x.y": nil, "bad": nil} + if got, want := MachineNames(keys), []string{"ace", "g14"}; !reflect.DeepEqual(got, want) { t.Errorf("MachineNames = %v, want %v", got, want) } } diff --git a/go/seats/meshmoduleassigner/contract.go b/go/seats/meshmoduleassigner/contract.go index fd50676..e4ca190 100644 --- a/go/seats/meshmoduleassigner/contract.go +++ b/go/seats/meshmoduleassigner/contract.go @@ -1,6 +1,6 @@ // Code generated by seatgen from the seat definitions; DO NOT EDIT. -// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). +// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it. package meshmoduleassigner import ( @@ -23,10 +23,10 @@ const ( ) const ( - EventPlacementProposed = "placement-proposed" - EventPlacementWithdrawn = "placement-withdrawn" - EventPlacementJudged = "placement-judged" - EventPlacementChanged = "placement-changed" + EventAssignmentsProposed = "assignments-proposed" + EventAssignmentsWithdrawn = "assignments-withdrawn" + EventAssignmentsJudged = "assignments-judged" + EventAssignmentsChanged = "assignments-changed" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. @@ -36,7 +36,7 @@ func Verbs() []string { // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventPlacementProposed, EventPlacementWithdrawn, EventPlacementJudged, EventPlacementChanged} + return []string{EventAssignmentsProposed, EventAssignmentsWithdrawn, EventAssignmentsJudged, EventAssignmentsChanged} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -45,17 +45,17 @@ const ( FamilyMaxAgeDays = 7 ) -// SubjectPlacementProposed is where placement-proposed is published. -const SubjectPlacementProposed = "mesh.seat.mesh-module-assigner.event.placement-proposed" +// SubjectAssignmentsProposed is where assignments-proposed is published. +const SubjectAssignmentsProposed = "mesh.seat.mesh-module-assigner.event.assignments-proposed" -// SubjectPlacementWithdrawn is where placement-withdrawn is published. -const SubjectPlacementWithdrawn = "mesh.seat.mesh-module-assigner.event.placement-withdrawn" +// SubjectAssignmentsWithdrawn is where assignments-withdrawn is published. +const SubjectAssignmentsWithdrawn = "mesh.seat.mesh-module-assigner.event.assignments-withdrawn" -// SubjectPlacementJudged is where placement-judged is published. -const SubjectPlacementJudged = "mesh.seat.mesh-module-assigner.event.placement-judged" +// SubjectAssignmentsJudged is where assignments-judged is published. +const SubjectAssignmentsJudged = "mesh.seat.mesh-module-assigner.event.assignments-judged" -// SubjectPlacementChanged is where placement-changed is published. -const SubjectPlacementChanged = "mesh.seat.mesh-module-assigner.event.placement-changed" +// SubjectAssignmentsChanged is where assignments-changed is published. +const SubjectAssignmentsChanged = "mesh.seat.mesh-module-assigner.event.assignments-changed" const ( StateAssignments = "assignments" @@ -343,6 +343,36 @@ func DecodeSeatHoldersBySeatNodeValue(raw []byte) (SeatHoldersBySeatNodeValue, e return v, nil } +// Assignments is one node's assignments assembled. +type Assignments struct { + Decided NodeDecided `json:"decided"` + Holds []Holding `json:"holds,omitempty"` + Modules []PlacedModule `json:"modules"` + Node string `json:"node"` + // by provision, the provider chosen for the node + Pins map[string]Provider `json:"pins,omitempty"` + Revision string `json:"revision,omitempty"` + V int64 `json:"v,omitempty"` +} + +// AssignmentsEventWarrant is who approved it, through which channel, when. +type AssignmentsEventWarrant struct { + At time.Time `json:"at"` + Warrant string `json:"warrant"` + Words string `json:"words"` +} + +// AssignmentsEvent is what each assignments event carries. +type AssignmentsEvent struct { + // for assignments-withdrawn, why it ended + Ended string `json:"ended,omitempty"` + // for assignments-changed, by node the revisions before and after and the modules added, removed or changed + Nodes []map[string]any `json:"nodes,omitempty"` + Proposal Proposal `json:"proposal"` + // who approved it, through which channel, when + Warrant *AssignmentsEventWarrant `json:"warrant,omitempty"` +} + // FoundTunnel is a tunnel found on the machine when it was adopted: everything but its private key. type FoundTunnel struct { Address string `json:"address,omitempty"` @@ -396,9 +426,9 @@ type PlacedModule struct { Bindings map[string]Provider `json:"bindings,omitempty"` HasLayer bool `json:"has-layer,omitempty"` HasMeshWide bool `json:"has-mesh-wide,omitempty"` - // this node's settings layer of the module's placement + // this node's settings layer of the module's assignments Layer map[string]any `json:"layer,omitempty"` - // the whole mesh's settings layer of the module's placement + // the whole mesh's settings layer of the module's assignments MeshWide map[string]any `json:"mesh-wide,omitempty"` // config values a person set in the whole mesh's layer, for a module in the settings form MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"` @@ -409,36 +439,6 @@ type PlacedModule struct { Values map[string]any `json:"values,omitempty"` } -// Placement is one node's placement assembled. -type Placement struct { - Decided NodeDecided `json:"decided"` - Holds []Holding `json:"holds,omitempty"` - Modules []PlacedModule `json:"modules"` - Node string `json:"node"` - // by provision, the provider chosen for the node - Pins map[string]Provider `json:"pins,omitempty"` - Revision string `json:"revision,omitempty"` - V int64 `json:"v,omitempty"` -} - -// PlacementEventWarrant is who approved it, through which channel, when. -type PlacementEventWarrant struct { - At time.Time `json:"at"` - Warrant string `json:"warrant"` - Words string `json:"words"` -} - -// PlacementEvent is what each placement event carries. -type PlacementEvent struct { - // for placement-withdrawn, why it ended - Ended string `json:"ended,omitempty"` - // for placement-changed, by node the revisions before and after and the modules added, removed or changed - Nodes []map[string]any `json:"nodes,omitempty"` - Proposal Proposal `json:"proposal"` - // who approved it, through which channel, when - Warrant *PlacementEventWarrant `json:"warrant,omitempty"` -} - // Proposal is an open proposal. type Proposal struct { Again *time.Time `json:"again,omitempty"` @@ -496,8 +496,8 @@ type ProposalChange struct { // a decided field's value Value any `json:"value,omitempty"` Values map[string]any `json:"values,omitempty"` - // the node's whole placement, for a seed - Whole *Placement `json:"whole,omitempty"` + // the node's whole assignments, for a seed + Whole *Assignments `json:"whole,omitempty"` } // Provider is the module on a node that answers a requirement or holds a seat. @@ -616,17 +616,17 @@ type AgainInput struct { // AgainAnswer is what again answers. type AgainAnswer = map[string]any -// PlacementProposedEvent is the body of the event placement-proposed. -type PlacementProposedEvent = PlacementEvent +// AssignmentsProposedEvent is the body of the event assignments-proposed. +type AssignmentsProposedEvent = AssignmentsEvent -// PlacementWithdrawnEvent is the body of the event placement-withdrawn. -type PlacementWithdrawnEvent = PlacementEvent +// AssignmentsWithdrawnEvent is the body of the event assignments-withdrawn. +type AssignmentsWithdrawnEvent = AssignmentsEvent -// PlacementJudgedEvent is the body of the event placement-judged. -type PlacementJudgedEvent = PlacementEvent +// AssignmentsJudgedEvent is the body of the event assignments-judged. +type AssignmentsJudgedEvent = AssignmentsEvent -// PlacementChangedEvent is the body of the event placement-changed. -type PlacementChangedEvent = PlacementEvent +// AssignmentsChangedEvent is the body of the event assignments-changed. +type AssignmentsChangedEvent = AssignmentsEvent // AssignmentsByNodeNodeValueModulesItem is a type of the contract. type AssignmentsByNodeNodeValueModulesItem struct { @@ -660,9 +660,9 @@ type ProposalsByProposalValue = Proposal // CandidatesByProposalNodeValue is one value of the state candidates under the key .. type CandidatesByProposalNodeValue struct { - // the node's placement with the proposal laid over it; absent when the proposal takes its key away - Placement *Placement `json:"placement,omitempty"` - Proposal string `json:"proposal"` + // the node's assignments with the proposal laid over it; absent when the proposal takes its key away + Assignments *Assignments `json:"assignments,omitempty"` + Proposal string `json:"proposal"` // the proposal changes this node's composition through another's key Reached bool `json:"reached,omitempty"` V int64 `json:"v,omitempty"` diff --git a/go/seats/meshmoduleassigner/contract_test.go b/go/seats/meshmoduleassigner/contract_test.go index 2fad606..29ddac3 100644 --- a/go/seats/meshmoduleassigner/contract_test.go +++ b/go/seats/meshmoduleassigner/contract_test.go @@ -17,8 +17,12 @@ func TestFixtures(t *testing.T) { decode func([]byte) (any, error) subjectIs *regexp.Regexp }{ - {name: "a proposal says placement-proposed", subject: "mesh.seat.mesh-module-assigner.event.placement-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", - decode: func(b []byte) (any, error) { var v PlacementProposedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.placement-proposed$")}, + {name: "a proposal says assignments-proposed", subject: "mesh.seat.mesh-module-assigner.event.assignments-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", + decode: func(b []byte) (any, error) { + var v AssignmentsProposedEvent + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.assignments-proposed$")}, {name: "a node's key", subject: "", given: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", decode: func(b []byte) (any, error) { var v AssignmentsByNodeNodeValue diff --git a/seats/mesh-module-assigner/fixtures/events.json b/seats/mesh-module-assigner/fixtures/events.json index 1886c81..7525d5a 100644 --- a/seats/mesh-module-assigner/fixtures/events.json +++ b/seats/mesh-module-assigner/fixtures/events.json @@ -1,8 +1,8 @@ [ { - "name": "a proposal says placement-proposed", - "of": "event:placement-proposed", - "subject": "mesh.seat.mesh-module-assigner.event.placement-proposed", + "name": "a proposal says assignments-proposed", + "of": "event:assignments-proposed", + "subject": "mesh.seat.mesh-module-assigner.event.assignments-proposed", "given": { "proposal": { "id": "p-20261105t120000-aabbccdd", diff --git a/seats/mesh-module-assigner/seat.json b/seats/mesh-module-assigner/seat.json index 4e49210..1eb0669 100644 --- a/seats/mesh-module-assigner/seat.json +++ b/seats/mesh-module-assigner/seat.json @@ -5,11 +5,11 @@ "grain": "whole", "domain": "Core", "decision": "novox/hq ADR 0567, ADR 0634, ADR 0635", - "description": "The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).", + "description": "The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it.", "verbs": [ { "name": "assignments", - "description": "Read the assignments: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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.", + "description": "Read the assignments: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With 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 assignments-changed event.", "optional": true, "input": { "properties": { @@ -42,12 +42,12 @@ }, "answer": { "type": "object", - "description": "placement as read: nodes, keys with their revisions, history, seat holders, the state's fill" + "description": "assignments as read: nodes, keys with their revisions, history, seat holders, the state's fill" } }, { "name": "proposals", - "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.", + "description": "Every open proposal of assignments: 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.", "optional": true, "input": { "properties": { @@ -65,7 +65,7 @@ }, { "name": "propose", - "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", + "description": "Propose a change of assignments, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", "optional": true, "input": { "additionalProperties": false, @@ -110,7 +110,7 @@ }, { "name": "withdraw", - "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", + "description": "Withdraw an open proposal: its key and its candidates are deleted and assignments-withdrawn carries it whole. With why.", "optional": true, "input": { "properties": { @@ -158,27 +158,27 @@ ], "events": [ { - "name": "placement-proposed", + "name": "assignments-proposed", "body": { - "$ref": "#/defs/PlacementEvent" + "$ref": "#/defs/AssignmentsEvent" } }, { - "name": "placement-withdrawn", + "name": "assignments-withdrawn", "body": { - "$ref": "#/defs/PlacementEvent" + "$ref": "#/defs/AssignmentsEvent" } }, { - "name": "placement-judged", + "name": "assignments-judged", "body": { - "$ref": "#/defs/PlacementEvent" + "$ref": "#/defs/AssignmentsEvent" } }, { - "name": "placement-changed", + "name": "assignments-changed", "body": { - "$ref": "#/defs/PlacementEvent" + "$ref": "#/defs/AssignmentsEvent" } } ], @@ -313,9 +313,9 @@ "type": "boolean", "description": "the proposal changes this node's composition through another's key" }, - "placement": { - "$ref": "#/defs/Placement", - "description": "the node's placement with the proposal laid over it; absent when the proposal takes its key away" + "assignments": { + "$ref": "#/defs/Assignments", + "description": "the node's assignments with the proposal laid over it; absent when the proposal takes its key away" } }, "required": [ @@ -380,7 +380,7 @@ "description": "a node seat on one node" } ], - "description": "who holds which seat on which node, composed from every node's placement" + "description": "who holds which seat on which node, composed from every node's assignments" } ], "defs": { @@ -423,14 +423,14 @@ }, "mesh-wide": { "type": "object", - "description": "the whole mesh's settings layer of the module's placement" + "description": "the whole mesh's settings layer of the module's assignments" }, "has-mesh-wide": { "type": "boolean" }, "layer": { "type": "object", - "description": "this node's settings layer of the module's placement" + "description": "this node's settings layer of the module's assignments" }, "has-layer": { "type": "boolean" @@ -580,9 +580,9 @@ "public-key" ] }, - "Placement": { + "Assignments": { "type": "object", - "description": "one node's placement assembled", + "description": "one node's assignments assembled", "properties": { "v": { "type": "integer" @@ -685,8 +685,8 @@ "type": "object" }, "whole": { - "$ref": "#/defs/Placement", - "description": "the node's whole placement, for a seed" + "$ref": "#/defs/Assignments", + "description": "the node's whole assignments, for a seed" }, "decided": { "$ref": "#/defs/NodeDecided" @@ -903,9 +903,9 @@ "state" ] }, - "PlacementEvent": { + "AssignmentsEvent": { "type": "object", - "description": "what each placement event carries", + "description": "what each assignments event carries", "properties": { "proposal": { "$ref": "#/defs/Proposal" @@ -915,7 +915,7 @@ "items": { "type": "object" }, - "description": "for placement-changed, by node the revisions before and after and the modules added, removed or changed" + "description": "for assignments-changed, by node the revisions before and after and the modules added, removed or changed" }, "warrant": { "type": "object", @@ -940,7 +940,7 @@ }, "ended": { "type": "string", - "description": "for placement-withdrawn, why it ended" + "description": "for assignments-withdrawn, why it ended" } }, "required": [ diff --git a/ts/seats/meshmoduleassigner/src/index.ts b/ts/seats/meshmoduleassigner/src/index.ts index 062c37d..5ce1e00 100644 --- a/ts/seats/meshmoduleassigner/src/index.ts +++ b/ts/seats/meshmoduleassigner/src/index.ts @@ -1,6 +1,6 @@ // Code generated by seatgen from the seat definitions; DO NOT EDIT. -// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). +// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of assignments and the proposals that change it. export const seat = "mesh-module-assigner"; @@ -13,21 +13,21 @@ export const verbs = { } as const; export const events = { - placementProposed: "placement-proposed", - placementWithdrawn: "placement-withdrawn", - placementJudged: "placement-judged", - placementChanged: "placement-changed", + assignmentsProposed: "assignments-proposed", + assignmentsWithdrawn: "assignments-withdrawn", + assignmentsJudged: "assignments-judged", + assignmentsChanged: "assignments-changed", } as const; export const family = { name: "mesh-module-assigner.events", maxAgeDays: 7 } as const; -export const placementProposedSubject = "mesh.seat.mesh-module-assigner.event.placement-proposed"; +export const assignmentsProposedSubject = "mesh.seat.mesh-module-assigner.event.assignments-proposed"; -export const placementWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.placement-withdrawn"; +export const assignmentsWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.assignments-withdrawn"; -export const placementJudgedSubject = "mesh.seat.mesh-module-assigner.event.placement-judged"; +export const assignmentsJudgedSubject = "mesh.seat.mesh-module-assigner.event.assignments-judged"; -export const placementChangedSubject = "mesh.seat.mesh-module-assigner.event.placement-changed"; +export const assignmentsChangedSubject = "mesh.seat.mesh-module-assigner.event.assignments-changed"; export const states = { assignments: "assignments", @@ -178,6 +178,36 @@ export function parseSeatHoldersBySeatNodeKey(_key: string): { seat: string; nod return { seat, node }; } +/** one node's assignments assembled */ +export interface Assignments { + "decided": NodeDecided; + "holds"?: Holding[]; + "modules": PlacedModule[]; + "node": string; + /** by provision, the provider chosen for the node */ + "pins"?: Record; + "revision"?: string; + "v"?: number; +} + +/** who approved it, through which channel, when */ +export interface AssignmentsEventWarrant { + "at": string; + "warrant": string; + "words": string; +} + +/** what each assignments event carries */ +export interface AssignmentsEvent { + /** for assignments-withdrawn, why it ended */ + "ended"?: string; + /** for assignments-changed, by node the revisions before and after and the modules added, removed or changed */ + "nodes"?: Record[]; + "proposal": Proposal; + /** who approved it, through which channel, when */ + "warrant"?: AssignmentsEventWarrant; +} + /** a tunnel found on the machine when it was adopted: everything but its private key */ export interface FoundTunnel { "address"?: string; @@ -231,9 +261,9 @@ export interface PlacedModule { "bindings"?: Record; "has-layer"?: boolean; "has-mesh-wide"?: boolean; - /** this node's settings layer of the module's placement */ + /** this node's settings layer of the module's assignments */ "layer"?: Record; - /** the whole mesh's settings layer of the module's placement */ + /** the whole mesh's settings layer of the module's assignments */ "mesh-wide"?: Record; /** config values a person set in the whole mesh's layer, for a module in the settings form */ "mesh-wide-values"?: Record; @@ -244,36 +274,6 @@ export interface PlacedModule { "values"?: Record; } -/** one node's placement assembled */ -export interface Placement { - "decided": NodeDecided; - "holds"?: Holding[]; - "modules": PlacedModule[]; - "node": string; - /** by provision, the provider chosen for the node */ - "pins"?: Record; - "revision"?: string; - "v"?: number; -} - -/** who approved it, through which channel, when */ -export interface PlacementEventWarrant { - "at": string; - "warrant": string; - "words": string; -} - -/** what each placement event carries */ -export interface PlacementEvent { - /** for placement-withdrawn, why it ended */ - "ended"?: string; - /** for placement-changed, by node the revisions before and after and the modules added, removed or changed */ - "nodes"?: Record[]; - "proposal": Proposal; - /** who approved it, through which channel, when */ - "warrant"?: PlacementEventWarrant; -} - /** an open proposal */ export interface Proposal { "again"?: string; @@ -331,8 +331,8 @@ export interface ProposalChange { /** a decided field's value */ "value"?: unknown; "values"?: Record; - /** the node's whole placement, for a seed */ - "whole"?: Placement; + /** the node's whole assignments, for a seed */ + "whole"?: Assignments; } /** the module on a node that answers a requirement or holds a seat */ @@ -438,17 +438,17 @@ export interface AgainInput { /** is what again answers. */ export type AgainAnswer = Record; -/** is the body of the event placement-proposed. */ -export type PlacementProposedEvent = PlacementEvent; +/** is the body of the event assignments-proposed. */ +export type AssignmentsProposedEvent = AssignmentsEvent; -/** is the body of the event placement-withdrawn. */ -export type PlacementWithdrawnEvent = PlacementEvent; +/** is the body of the event assignments-withdrawn. */ +export type AssignmentsWithdrawnEvent = AssignmentsEvent; -/** is the body of the event placement-judged. */ -export type PlacementJudgedEvent = PlacementEvent; +/** is the body of the event assignments-judged. */ +export type AssignmentsJudgedEvent = AssignmentsEvent; -/** is the body of the event placement-changed. */ -export type PlacementChangedEvent = PlacementEvent; +/** is the body of the event assignments-changed. */ +export type AssignmentsChangedEvent = AssignmentsEvent; export interface AssignmentsByNodeNodeValueModulesItem { /** the digest of the module key's value it stands for */ @@ -481,8 +481,8 @@ export type ProposalsByProposalValue = Proposal; /** is one value of the state candidates under the key .. */ export interface CandidatesByProposalNodeValue { - /** the node's placement with the proposal laid over it; absent when the proposal takes its key away */ - "placement"?: Placement; + /** the node's assignments with the proposal laid over it; absent when the proposal takes its key away */ + "assignments"?: Assignments; "proposal": string; /** the proposal changes this node's composition through another's key */ "reached"?: boolean; -- 2.54.0 From b77560786a6a229e1b538d880773e468557105d7 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 00:51:22 +0100 Subject: [PATCH 94/94] Complete go.mod and go.sum for the externals each module builds with A check's tests run without network, so each module must carry every external module it builds with. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/go.mod | 8 ++++++++ go/clients/nodeengineclient/go.sum | 4 ++-- .../go/clients/meshconditionsclient/go.sum | 12 ++++++++++++ .../go/clients/meshhandactsclient/go.sum | 12 ++++++++++++ .../go/clients/meshnodeenrollerclient/go.mod | 4 +--- .../go/clients/operatorchannelclient/go.sum | 12 ++++++------ 6 files changed, 41 insertions(+), 11 deletions(-) diff --git a/go/clients/nodeengineclient/go.mod b/go/clients/nodeengineclient/go.mod index a9b7b4d..b759ccf 100644 --- a/go/clients/nodeengineclient/go.mod +++ b/go/clients/nodeengineclient/go.mod @@ -7,3 +7,11 @@ require ( git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 ) +require ( + github.com/klauspost/compress v1.20.0 // indirect + github.com/nats-io/nats.go v1.54.0 // indirect + github.com/nats-io/nkeys v0.4.16 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/sys v0.48.0 // indirect +) diff --git a/go/clients/nodeengineclient/go.sum b/go/clients/nodeengineclient/go.sum index 0daef11..65100b8 100644 --- a/go/clients/nodeengineclient/go.sum +++ b/go/clients/nodeengineclient/go.sum @@ -1,5 +1,5 @@ -github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= -github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= diff --git a/health-and-repair/go/clients/meshconditionsclient/go.sum b/health-and-repair/go/clients/meshconditionsclient/go.sum index 0daef11..999068e 100644 --- a/health-and-repair/go/clients/meshconditionsclient/go.sum +++ b/health-and-repair/go/clients/meshconditionsclient/go.sum @@ -1,5 +1,15 @@ +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= @@ -10,3 +20,5 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/health-and-repair/go/clients/meshhandactsclient/go.sum b/health-and-repair/go/clients/meshhandactsclient/go.sum index 0daef11..999068e 100644 --- a/health-and-repair/go/clients/meshhandactsclient/go.sum +++ b/health-and-repair/go/clients/meshhandactsclient/go.sum @@ -1,5 +1,15 @@ +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= @@ -10,3 +20,5 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/identity-and-access/go/clients/meshnodeenrollerclient/go.mod b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod index 784fe46..9997570 100644 --- a/identity-and-access/go/clients/meshnodeenrollerclient/go.mod +++ b/identity-and-access/go/clients/meshnodeenrollerclient/go.mod @@ -2,6 +2,4 @@ module git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnode go 1.26.0 -require ( - git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000 // indirect -) +require git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000 diff --git a/operator-and-conversation/go/clients/operatorchannelclient/go.sum b/operator-and-conversation/go/clients/operatorchannelclient/go.sum index ee87eab..999068e 100644 --- a/operator-and-conversation/go/clients/operatorchannelclient/go.sum +++ b/operator-and-conversation/go/clients/operatorchannelclient/go.sum @@ -1,13 +1,13 @@ -github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= -github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= -github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= @@ -20,5 +20,5 @@ golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= -golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -- 2.54.0