diff --git a/go/catalogue/toolchain_manifests_test.go b/go/catalogue/toolchain_manifests_test.go new file mode 100644 index 0000000..a56fd2b --- /dev/null +++ b/go/catalogue/toolchain_manifests_test.go @@ -0,0 +1,88 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// Defends novox/hq ADR 0477 §4 (issue 1573): the go and nodejs modules own the toolchain packages and provide +// them with the machine's reach, so a node without one refuses the module that needs it at resolution, naming +// the provision, and does not find out at its first build. The manifests are the catalogue's, as they are. + +// builderNode is a machine that can run a build module: the package manager and user namespaces it asks of +// the machine, and the forge account (mesh-scope, provided by the forge) stood in for. +func builderNode() Node { + return Node{Name: "builder", Site: "house", + Capabilities: map[string]bool{"package-manager": true, "user-namespaces": true}} +} + +func toolchainShelf(t *testing.T) map[string]Manifest { + t.Helper() + m := map[string]Manifest{ + "forge": {Module: "forge", Version: "1", Provides: []Offer{{Name: "forge-account"}, {Name: "npm-package-registry"}}}, + } + for _, name := range []string{"go", "nodejs", "mesh-build-go", "mesh-build-node", "dev-tools"} { + m[name] = catalogueManifest(t, name) + } + return m +} + +func TestAModuleNeedingAToolchainIsRefusedOnANodeWithoutIt(t *testing.T) { + for _, c := range []struct{ module, provision string }{ + {"mesh-build-go", "go-toolchain"}, + {"mesh-build-node", "node-toolchain"}, + {"dev-tools", "go-toolchain"}, + } { + _, err := Resolve(toolchainShelf(t), []string{"forge", c.module}, builderNode(), World{}) + if err == nil { + t.Fatalf("%s composed on a node with no toolchain module", c.module) + } + for _, want := range []string{`"` + c.provision + `" is wanted by ` + c.module, + "usable only on the machine that provides it"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("%s: the refusal does not say %q:\n%v", c.module, want, err) + } + } + } +} + +func TestAModuleNeedingAToolchainComposesOnANodeWithIt(t *testing.T) { + for _, c := range []struct{ module, owner string }{ + {"mesh-build-go", "go"}, + {"mesh-build-node", "nodejs"}, + {"dev-tools", "go"}, + } { + assigned := []string{"forge", c.owner, c.module} + if c.module == "dev-tools" { + assigned = append(assigned, "nodejs") + } + got, err := Resolve(toolchainShelf(t), assigned, builderNode(), World{}) + if err != nil { + t.Fatalf("%s with %s did not compose: %v", c.module, c.owner, err) + } + in := map[string]bool{} + for _, n := range names(got) { + in[n] = true + } + if !in[c.module] || !in[c.owner] { + t.Errorf("%s with %s composed %v", c.module, c.owner, names(got)) + } + } +} + +// Only the owners declare the packages: a build module that declared go again would compose without the +// owner's provision being met by anything, which is the gap the provision closes. +func TestOnlyTheToolchainModulesDeclareTheToolchainPackages(t *testing.T) { + owner := map[string]string{"go": "go", "nodejs": "nodejs", "npm": "nodejs"} + for name, m := range toolchainShelf(t) { + for _, r := range m.Resources { + if r["type"] != "package" { + continue + } + pkg, _ := r["package"].(string) + if o, ok := owner[pkg]; ok && o != name { + t.Errorf("%s declares the package %s, which only %s may", name, pkg, o) + } + } + } +} diff --git a/go/delivery/buildrecord.go b/go/delivery/buildrecord.go index d3f9fb9..1bccc22 100644 --- a/go/delivery/buildrecord.go +++ b/go/delivery/buildrecord.go @@ -25,7 +25,7 @@ type BuildRecord struct { } // Artifact is one thing a build made: mesh-controller internal/chain/records.go Artifact, json keys name, kind and -// reference, nothing else (novox/hq ADR 0478 clause 7). +// reference, nothing else (novox/hq ADR 0478 clause 8). type Artifact struct { Name string `json:"name"` Kind string `json:"kind"` diff --git a/go/delivery/source.go b/go/delivery/source.go index e82ce1d..8d85b64 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -390,7 +390,7 @@ type Module struct { Repository string `json:"repository"` Commit string `json:"commit"` // Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the - // repository's root (novox/hq ADR 0478 clause 8, issue 1615): the build tools need it. Empty means not known: a value + // repository's root (novox/hq ADR 0478 clause 9, issue 1615): the build tools need it. Empty means not known: a value // written before the indexer learned paths has none. Path string `json:"path,omitempty"` // Toolchains are the toolchains FP2 was computed with, by language (go, typescript): the version installed and the diff --git a/go/testdata/beside/CAPTURED b/go/testdata/beside/CAPTURED index dc56ca5..9f1bb1c 100644 --- a/go/testdata/beside/CAPTURED +++ b/go/testdata/beside/CAPTURED @@ -7,10 +7,10 @@ The copy carries the catalogue's private details: domains, first names in module private addresses. That is acceptable while mesh-sdk is private; if it ever goes public, this copy is a second place to clean besides the catalogue itself. -Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 2026-10-20 from the mains of both +Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 2026-10-21 from the mains of both repositories; before that it was mesh-controller's copy, which still held two modules that had left the catalogue. -mesh-catalog bb0fe93bbdddd869ad808210352461efc506c3d3 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json +mesh-catalog b5dbfbb784507e211372a7c9640d91f7d6dadd84 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json mesh-host cdcba9ca73755854d21e4e657d7e46c32b9b5265 internal/apply/placement_guard.go To move them, from this repository's go/ directory, with the two repositories checked out beside mesh-sdk: diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-backlight old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-display old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-kbd-notify old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-media old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-notify old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-session old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad b/go/testdata/beside/mesh-catalog/modules/asus-zephyrus-g14/files/bin/zephyrus-touchpad old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird b/go/testdata/beside/mesh-catalog/modules/betterbird/files/betterbird old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/brother/files/settings.json b/go/testdata/beside/mesh-catalog/modules/brother/files/settings.json new file mode 100644 index 0000000..80aed32 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/brother/files/settings.json @@ -0,0 +1,9 @@ +{ + "scanner": "", + "scan-folder": "", + "printer": "", + "printer-certificate": "", + "resolution": 300, + "color-mode": "color", + "crop": true +} diff --git a/go/testdata/beside/mesh-catalog/modules/brother/files/store.json b/go/testdata/beside/mesh-catalog/modules/brother/files/store.json new file mode 100644 index 0000000..8c2075d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/brother/files/store.json @@ -0,0 +1,9 @@ +{ + "scheme": "${bound:s3-bucket:scheme}", + "at": "${bound:s3-bucket:at}", + "port": "${bound:s3-bucket:port}", + "bucket": "${bound:s3-bucket:bucket}", + "region": "${bound:s3-bucket:region}", + "key": "${bound:s3-bucket:as}", + "secret": "${secret:s3-bucket}" +} diff --git a/go/testdata/beside/mesh-catalog/modules/brother/module.json.captured b/go/testdata/beside/mesh-catalog/modules/brother/module.json.captured new file mode 100644 index 0000000..57559d5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/brother/module.json.captured @@ -0,0 +1,280 @@ +{ + "module": "brother", + "version": "1", + "requires": [ + "s3-bucket" + ], + "binds": { + "s3-bucket": "${dir:state}/store-binding.json" + }, + "secrets": { + "s3-bucket": "${dir:state}/store.secret" + }, + "config": { + "scanner": { + "type": "string", + "description": "The network scanner's eSCL address, such as http://192.0.2.10/eSCL: the scanner's own address and the path its eSCL service answers on; the operator's, with no default.", + "pattern": "^https?://.+", + "trusted": true, + "required": true + }, + "scan-folder": { + "type": "string", + "description": "The absolute path of the folder the scans are filed in as PDF files; the module makes it when it files the first scan, and never touches a file in it that it did not write. The operator's, with no default.", + "pattern": "^/.+$", + "trusted": true, + "required": true + }, + "printer": { + "type": "string", + "description": "The printer's host, such as 192.0.2.10: where its admin page (HTTPS) and its IPP service answer. The device is the scanner's own; the operator's, with no default.", + "pattern": "^[A-Za-z0-9.:-]+$", + "trusted": true, + "required": true + }, + "printer-certificate": { + "type": "string", + "description": "The SHA-256 fingerprint of the certificate the printer's admin page serves, as 32 upper-case hex pairs joined by colons; several, separated by commas, when the printer serves more than one (it serves an ECDSA and an RSA certificate for one name). The module trusts those certificates alone and refuses any other; the operator's, with no default.", + "pattern": "^([0-9A-F]{2}:){31}[0-9A-F]{2}(, *([0-9A-F]{2}:){31}[0-9A-F]{2})*$", + "trusted": true, + "required": true + }, + "resolution": { + "type": "number", + "description": "The resolution asked of the scanner, in dpi: 100, 200, 300 or 600. A scanner may give another; the scan is filed at the resolution it gave.", + "enum": [ + 100, + 200, + 300, + 600 + ], + "trusted": false, + "default": 300 + }, + "color-mode": { + "type": "string", + "description": "Whether the scan is in colour or in grays: color or gray.", + "enum": [ + "color", + "gray" + ], + "trusted": false, + "default": "color" + }, + "crop": { + "type": "boolean", + "description": "Whether the document is cut out of the platen's white lid; false files the whole platen. A feeder scan is never cropped.", + "trusted": false, + "default": true + } + }, + "own-secrets": { + "admin-password": { + "path": "${dir:state}/admin-password.secret", + "issued-by": "outside", + "prompt": "the admin password of the Brother printer", + "owner": "brother-device" + }, + "broker-device": { + "path": "${dir:state}/broker-device.secret", + "owner": "brother-device" + } + }, + "streams": [ + { + "name": "device-asks", + "events": [ + "device.asked" + ], + "max-age-days": 1, + "max-mib": 8, + "retention": "work-queue", + "max-deliveries": 3, + "duplicates": 24 + } + ], + "state": [ + { + "name": "device-answers", + "ttl-seconds": 600 + } + ], + "seats": [ + { + "name": "scanner", + "scope": "mesh", + "serves": [ + "scan", + "status", + "list", + "document" + ] + }, + { + "name": "printer", + "scope": "mesh", + "serves": [ + "status", + "settings", + "print", + "upload" + ] + } + ], + "claims": [ + { + "name": "scanner", + "scope": "mesh", + "serves": [ + "scan", + "status", + "list", + "document" + ] + }, + { + "name": "printer", + "scope": "mesh", + "serves": [ + "status", + "settings", + "print", + "upload" + ] + } + ], + "emits": [ + "scan.added" + ], + "tools": [ + "scanner.scan", + "scanner.status", + "scanner.list", + "scanner.document", + "printer.status", + "printer.settings", + "printer.print", + "printer.upload", + "brother_health" + ], + "resources": [ + { + "id": "account", + "type": "user", + "name": "brother-device", + "shell": "/usr/bin/nologin" + }, + { + "id": "state", + "type": "directory", + "mode": "0711", + "owner": "${machine:account}", + "place": ".", + "unknown": "program" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "owner": "${machine:account}", + "mode": "0644", + "takes-config": [ + "scanner", + "scan-folder", + "printer", + "printer-certificate", + "resolution", + "color-mode", + "crop" + ], + "merge": "json", + "template": "files/settings.json", + "trusted": true + }, + { + "id": "store", + "type": "file", + "path": "${dir:state}/store.json", + "owner": "brother-device", + "mode": "0600", + "template": "files/store.json" + }, + { + "id": "device", + "type": "process", + "name": "brother-device", + "artifact": "tools", + "run": [ + "./brother", + "device" + ], + "user": "brother-device", + "env": { + "BROTHER_SETTINGS": "${dir:state}/settings.json", + "BROTHER_ADMIN_PASSWORD": "${dir:state}/admin-password.secret", + "BROTHER_BROKER": "${dir:state}/broker-device.secret", + "BROTHER_STORE": "${dir:state}/store.json" + }, + "restart-on": [ + "settings", + "store" + ], + "bus": { + "queues": [ + "device-asks" + ], + "state": [ + "device-answers" + ], + "emits": [ + "scan.added" + ] + }, + "health": { + "kind": "unit" + } + }, + { + "id": "watch", + "type": "process", + "name": "brother-watch", + "artifact": "tools", + "run": [ + "./brother", + "watch" + ], + "user": "brother-device", + "health": { + "kind": "tool", + "tool": "brother_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + } + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/brother", + "binary": "brother", + "loads": [ + "brother" + ], + "env": { + "BROTHER_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor b/go/testdata/beside/mesh-catalog/modules/ca-trust/files/anchor old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent b/go/testdata/beside/mesh-catalog/modules/claude-code/files/claude-agent old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel b/go/testdata/beside/mesh-catalog/modules/clipmenu/files/bin/xsel old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured index 8ee6fd1..094f833 100644 --- a/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured @@ -4,12 +4,11 @@ "capabilities": [ "package-manager" ], + "requires": [ + "go-toolchain", + "node-toolchain" + ], "resources": [ - { - "id": "go", - "type": "package", - "package": "go" - }, { "id": "c-compiler", "type": "package", @@ -20,16 +19,6 @@ "type": "package", "package": "make" }, - { - "id": "nodejs", - "type": "package", - "package": "nodejs" - }, - { - "id": "npm", - "type": "package", - "package": "npm" - }, { "id": "python", "type": "package", diff --git a/go/testdata/beside/mesh-catalog/modules/feh/files/fehbg b/go/testdata/beside/mesh-catalog/modules/feh/files/fehbg old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured b/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured index 2de9730..118224c 100644 --- a/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/gitea/module.json.captured @@ -540,6 +540,8 @@ ], "reads": [ "mesh-controller.assignments", - "ssh-client.keys" + "ssh-client.keys", + "mesh-build-go.keys", + "mesh-build-node.keys" ] } diff --git a/go/testdata/beside/mesh-catalog/modules/go/module.json.captured b/go/testdata/beside/mesh-catalog/modules/go/module.json.captured new file mode 100644 index 0000000..48a6bf5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/go/module.json.captured @@ -0,0 +1,20 @@ +{ + "module": "go", + "version": "1", + "capabilities": [ + "package-manager" + ], + "provides": [ + { + "name": "go-toolchain", + "reach": "machine" + } + ], + "resources": [ + { + "id": "go", + "type": "package", + "package": "go" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 b/go/testdata/beside/mesh-catalog/modules/i3/files/wms/i3 old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3bar-watchdog old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-updates old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value b/go/testdata/beside/mesh-catalog/modules/i3status-rust/files/bin/i3status-value old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured index e23963d..ef82b87 100644 --- a/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured @@ -6,7 +6,9 @@ "virtualisation" ], "requires": [ - "npm-package-registry" + "npm-package-registry", + "go-toolchain", + "node-toolchain" ], "contributes": { "npm-package-registry": { @@ -84,16 +86,6 @@ "type": "package", "package": "iproute2" }, - { - "id": "npm", - "type": "package", - "package": "npm" - }, - { - "id": "go", - "type": "package", - "package": "go" - }, { "id": "incus", "type": "package", diff --git a/go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver b/go/testdata/beside/mesh-catalog/modules/lemurs/files/xserver old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge new file mode 100644 index 0000000..6bbf4e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig new file mode 100644 index 0000000..0525230 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts new file mode 100644 index 0000000..e0df6d8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured new file mode 100644 index 0000000..9f0fd7f --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured @@ -0,0 +1,148 @@ +{ + "module": "mesh-build-go", + "version": "1", + "moved": true, + "runs-as": "mesh-build-go", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "state": [ + "toolchains", + "keys", + "steps" + ], + "tools": [ + "build_go_fetch", + "build_go_test", + "build_go_digest", + "build_go_build", + "build_go_step", + "build_go_steps", + "build_go_toolchains", + "build_go_status", + "build_go_health" + ], + "streams": [ + { + "name": "toolchains-go", + "events": ["toolchain.**"] + } + ], + "requires": [ + "forge-account", + "go-toolchain" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-build-go", + "runtime-health": { + "kind": "tool", + "tool": "build_go_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-build-go", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-build-go" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-build-go", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-build-go-work", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "path": "/var/lib/mesh-build-go-cache", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-dir", + "type": "directory", + "path": "${dir:state}/.ssh", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-config", + "type": "file", + "path": "${dir:ssh-dir}/config", + "owner": "mesh-build-go", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${dir:ssh-dir}/known_hosts", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/known_hosts" + }, + { + "id": "git-config", + "type": "file", + "path": "${dir:state}/.config/git/config", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/gitconfig" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "openssh", + "type": "package", + "package": "openssh" + }, + { + "id": "postgresql", + "type": "package", + "package": "postgresql" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-build-go", + "binary": "mesh-build-go", + "loads": [ + "mesh-build-go" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge new file mode 100644 index 0000000..6bbf4e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig new file mode 100644 index 0000000..0525230 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts new file mode 100644 index 0000000..e0df6d8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured new file mode 100644 index 0000000..b92212a --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured @@ -0,0 +1,169 @@ +{ + "module": "mesh-build-node", + "version": "1", + "moved": true, + "runs-as": "mesh-build-node", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "state": [ + "toolchains", + "keys", + "steps" + ], + "tools": [ + "build_node_fetch", + "build_node_test", + "build_node_build", + "build_node_step", + "build_node_steps", + "build_node_toolchains", + "build_node_status", + "build_node_health" + ], + "streams": [ + { + "name": "toolchains-node", + "events": [ + "toolchain.**" + ] + } + ], + "requires": [ + "forge-account", + "npm-package-registry", + "node-toolchain" + ], + "contributes": { + "npm-package-registry": { + "access": "read" + } + }, + "binds": { + "npm-package-registry": "${dir:state}/package-registry.json" + }, + "secrets": { + "npm-package-registry": "${dir:state}/package-registry.secret" + }, + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-build-node", + "runtime-health": { + "kind": "tool", + "tool": "build_node_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-build-node", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-build-node" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-build-node", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-build-node-work", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "path": "/var/lib/mesh-build-node-cache", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "ssh-dir", + "type": "directory", + "path": "${dir:state}/.ssh", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "ssh-config", + "type": "file", + "path": "${dir:ssh-dir}/config", + "owner": "mesh-build-node", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${dir:ssh-dir}/known_hosts", + "owner": "mesh-build-node", + "mode": "0644", + "template": "files/known_hosts" + }, + { + "id": "git-config", + "type": "file", + "path": "${dir:state}/.config/git/config", + "owner": "mesh-build-node", + "mode": "0644", + "template": "files/gitconfig" + }, + { + "id": "toolchain-dir", + "type": "archive", + "path": "${dir:state}/toolchain", + "owner": "mesh-build-node", + "artifact": "toolchain", + "unknown": "program" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "openssh", + "type": "package", + "package": "openssh" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-build-node", + "binary": "mesh-build-node", + "loads": [ + "mesh-build-node" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + }, + { + "name": "toolchain", + "kind": "archive", + "from": "toolchain" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured deleted file mode 100644 index 1a40896..0000000 --- a/go/testdata/beside/mesh-catalog/modules/mesh-delivery/module.json.captured +++ /dev/null @@ -1,95 +0,0 @@ -{ - "module": "mesh-delivery", - "version": "1", - "slug": "deliver", - "claims": [ - { - "name": "mesh-delivery", - "scope": "mesh", - "serves": [ - "deliveries", - "times", - "show", - "groups", - "what-if", - "checks", - "table", - "stalled", - "recheck", - "release", - "stop", - "close", - "retire-history" - ] - } - ], - "consumes": [ - "gitea.pull.updated", - "gitea.pull.merged", - "gitea.pull.closed", - "gitea.forge.status", - "gitea.forge.pull-request.edited", - "gitea.forge.pull-request.closed", - "mesh-controller.checked", - "mesh-controller.plan-moved" - ], - "emits": [ - "transition", - "group", - "merged" - ], - "invokes": [ - "seat:mesh-controller.delivery-plan", - "seat:mesh-controller.delivery-check", - "seat:mesh-controller.deliver", - "seat:mesh-controller.delivery-backups-again", - "seat:mesh-controller.delivery-stop", - "seat:mesh-controller.delivery-walks", - "seat:mesh-controller.forget-checks", - "gitea.gitea_note_append", - "gitea.gitea_delivery_view", - "gitea.gitea_commit_status", - "gitea.gitea_commit_statuses", - "gitea.gitea_contains", - "gitea.gitea_open_pulls", - "gitea.gitea_merge_pull_request", - "gitea.gitea_add_comment", - "seat:operator-channel.notify" - ], - "uses": [ - "operator-channel" - ], - "state": [ - "deliveries", - "groups", - "asked", - "pulls" - ], - "tools": [ - "delivery_status" - ], - "resources": [ - { - "id": "state", - "type": "directory", - "mode": "0700", - "place": ".", - "unknown": "program" - } - ], - "build": { - "artifacts": [ - { - "name": "tools", - "kind": "bundle", - "language": "go", - "system": "arch", - "from": "cmd/mesh-delivery", - "binary": "mesh-delivery", - "loads": [ - "mesh-delivery" - ] - } - ] - } -} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured index 592e81c..dcfc71c 100644 --- a/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/mesh-issues/module.json.captured @@ -57,7 +57,6 @@ "consumes": [ "mesh-issues.opened", "mesh-issues.moved", - "mesh-delivery.transition", "mesh-delivery-gate.transition" ], "invokes": [ diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/callers.json b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/callers.json new file mode 100644 index 0000000..84dd800 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/callers.json @@ -0,0 +1,3 @@ +{ + "callers": ["mesh-mail-web"] +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/database-url b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/database-url new file mode 100644 index 0000000..dc59da9 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/files/database-url @@ -0,0 +1 @@ +postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/module.json.captured new file mode 100644 index 0000000..b83e8bb --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-reader/module.json.captured @@ -0,0 +1,141 @@ +{ + "module": "mesh-mail-reader", + "version": "1", + "runs-as": "mesh-mail-reader", + "claims": [ + { + "name": "mail-reading", + "scope": "mesh", + "serves": ["entries", "groups", "correct", "lessons", "undo", "status"] + } + ], + "consumes": [ + "mesh-mail.mail.*.message.added" + ], + "invokes": [ + "seat:mail-store.read", + "seat:mail-store.accounts", + "claude-code.claude_code_judge", + "claude-code.claude_code_judged", + "messenger.messenger_check", + "seat:operator-channel.tell" + ], + "requires": ["postgres-database"], + "contributes": { + "postgres-database": { + "name": "mail-reader" + } + }, + "binds": { + "postgres-database": "${dir:state}/database.json" + }, + "secrets": { + "postgres-database": "${dir:state}/database.secret" + }, + "config": { + "callers": { + "type": "list", + "description": "Who may call one of mail-reading's six verbs (entries, groups, correct, lessons, undo, status): a module's name (on every machine), ., or person. (novox/hq ADR 0459 §2). mesh-mail-web alone by default.", + "trusted": true, + "default": ["mesh-mail-web"] + } + }, + "tools": [ + "mail-reading.entries", + "mail-reading.groups", + "mail-reading.correct", + "mail-reading.lessons", + "mail-reading.undo", + "mail-reading.status", + "mail_reader_health" + ], + "runtime-health": { + "kind": "tool", + "tool": "mail_reader_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-mail-reader", + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-mail-reader", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-mail-reader" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-mail-reader", + "place": ".", + "unknown": "program" + }, + { + "id": "callers", + "trusted": true, + "type": "file", + "path": "${dir:state}/callers.json", + "mode": "0600", + "owner": "mesh-mail-reader", + "merge": "json", + "template": "files/callers.json", + "takes-config": ["callers"] + }, + { + "id": "database-url", + "type": "file", + "path": "${dir:state}/database.url", + "mode": "0600", + "owner": "mesh-mail-reader", + "template": "files/database-url" + }, + { + "id": "prepare", + "type": "process", + "name": "mesh-mail-reader-prepare", + "artifact": "tools", + "run": [ + "./mesh-mail-reader", + "prepare" + ], + "run-once": true, + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url" + }, + "restart-on": [ + "database-url" + ] + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-mail-reader", + "binary": "mesh-mail-reader", + "loads": [ + "mesh-mail-reader" + ], + "env": { + "DATABASE_URL_FILE": "${dir:state}/database.url", + "MESH_MAIL_READER_CALLERS": "${dir:state}/callers.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured index fd92839..c6c0d3d 100644 --- a/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail-web/module.json.captured @@ -15,7 +15,13 @@ "seat:mail-store.search", "seat:mail-store.read", "seat:mail-store.send", - "seat:mail-store.send-status" + "seat:mail-store.send-status", + "seat:mail-reading.entries", + "seat:mail-reading.groups", + "seat:mail-reading.status", + "seat:mail-reading.correct", + "seat:mail-reading.lessons", + "seat:mail-reading.undo" ], "reads": ["mesh-mail.accounts"], "requires": ["route", "oidc-client"], diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured index 1eba0cf..735fbc5 100644 --- a/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/mesh-mail/module.json.captured @@ -18,6 +18,11 @@ "scope": "node", "needs": ["node-notifier"], "serves": ["status"] + }, + { + "name": "mail-reading", + "scope": "mesh", + "serves": ["entries", "groups", "correct", "lessons", "undo", "status"] } ], "claims": [ diff --git a/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json b/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json index ce76334..6d22854 100644 --- a/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json +++ b/go/testdata/beside/mesh-catalog/modules/messenger/files/settings.json @@ -1,4 +1,5 @@ { "time-zone": "", - "only-what-needs-me": [] + "only-what-needs-me": [], + "messages-per-hour": 100 } diff --git a/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured b/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured index 8501bd4..6045195 100644 --- a/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/messenger/module.json.captured @@ -132,6 +132,14 @@ "description": "The kinds of channel set to say only what needs the operator: a walk that delivered with nothing held and nothing open is not told there (ADR 0308 §4); empty is none.", "trusted": true, "default": [] + }, + "messages-per-hour": { + "type": "number", + "description": "messages per channel per hour before the rest are held as one; 0 turns the pause off. The tell verb has its own cap of 10 and is not counted here.", + "minimum": 0, + "maximum": 1000, + "trusted": false, + "default": 100 } }, "resources": [ @@ -155,7 +163,7 @@ "type": "file", "path": "${dir:state}/settings.json", "mode": "0600", - "takes-config": ["time-zone", "desktop-machines", "only-what-needs-me"], + "takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour"], "merge": "json", "template": "files/settings.json", "owner": "messenger" diff --git a/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured new file mode 100644 index 0000000..c357344 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured @@ -0,0 +1,25 @@ +{ + "module": "nodejs", + "version": "1", + "capabilities": [ + "package-manager" + ], + "provides": [ + { + "name": "node-toolchain", + "reach": "machine" + } + ], + "resources": [ + { + "id": "nodejs", + "type": "package", + "package": "nodejs" + }, + { + "id": "npm", + "type": "package", + "package": "npm" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.service b/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.service new file mode 100644 index 0000000..69741dd --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.service @@ -0,0 +1,12 @@ +# Generated by the mesh. Do not edit — module pacman writes this file and replaces it at every push. +[Unit] +Description=Weekly full upgrade (the mesh's pacman module, ADR 0477) +Wants=network-online.target +After=network-online.target + +[Service] +# The upgrade pacman_upgrade runs (pacman -Syu --noconfirm), as a oneshot whose result is the module's health +# (pacman_upgrade_health). The mesh restarts no machine for an upgrade: a new kernel waits for the next restart. +Type=oneshot +Nice=10 +ExecStart=/usr/bin/pacman -Syu --noconfirm diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.timer b/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.timer new file mode 100644 index 0000000..a8a9b4d --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/pacman/files/mesh-pacman-weekly-upgrade.timer @@ -0,0 +1,11 @@ +# Generated by the mesh. Do not edit — module pacman writes this file and replaces it at every push. +[Unit] +Description=Weekly full upgrade, Saturday 04:00 local time on every node (the mesh's pacman module, ADR 0477) + +[Timer] +# The same moment on every node: no random delay. A node that was off or asleep then upgrades at its next start. +OnCalendar=Sat *-*-* 04:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured b/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured index 746c8c5..ca1a81a 100644 --- a/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/pacman/module.json.captured @@ -19,6 +19,7 @@ "pacman_files", "pacman_updates", "pacman_upgrade", + "pacman_upgrade_health", "pacman_orphans", "pacman_remove_orphans", "pacman_cache", @@ -63,14 +64,53 @@ "type": "service", "unit": "reflector.timer", "state": "running", - "boot": "enabled" + "boot": "enabled", + "health": { + "kind": "unit" + } }, { "id": "cache-cleaning", "type": "service", "unit": "paccache.timer", "state": "running", - "boot": "enabled" + "boot": "enabled", + "health": { + "kind": "unit" + } + }, + { + "id": "upgrade-service", + "type": "file", + "path": "/etc/systemd/system/mesh-pacman-weekly-upgrade.service", + "mode": "0644", + "template": "files/mesh-pacman-weekly-upgrade.service" + }, + { + "id": "upgrade-timer", + "type": "file", + "path": "/etc/systemd/system/mesh-pacman-weekly-upgrade.timer", + "mode": "0644", + "template": "files/mesh-pacman-weekly-upgrade.timer" + }, + { + "id": "weekly-upgrade", + "type": "service", + "unit": "mesh-pacman-weekly-upgrade.timer", + "state": "running", + "boot": "enabled", + "restart-on": [ + "upgrade-service", + "upgrade-timer" + ], + "health": { + "kind": "tool", + "tool": "pacman_upgrade_health", + "interval": "60s", + "timeout": "20s", + "looks": 2, + "grace": "60s" + } } ], "build": { diff --git a/go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment b/go/testdata/beside/mesh-catalog/modules/power/files/bin/power-moment old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/dmenu old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-launch old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu b/go/testdata/beside/mesh-catalog/modules/rofi/files/bin/rofi-powermenu old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock b/go/testdata/beside/mesh-catalog/modules/screen-lock/files/bin/screen-lock old mode 100644 new mode 100755 diff --git a/go/testdata/beside/mesh-catalog/modules/xorg/files/xinitrc b/go/testdata/beside/mesh-catalog/modules/xorg/files/xinitrc old mode 100644 new mode 100755