From 6bedcd3f21d5818ad1ba2c9992a970e64e426bdf Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 14:30:56 +0200 Subject: [PATCH] Rename seat claims to the mesh-*/node-* convention; retire verdaccio (ADR 0121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claims renamed to match the controller's seat set: node-dns-resolver (dnsmasq), node-intrusion-prevention (fail2ban), node-packet-filter (nftables), node-resolver-config (resolv-conf, resolved-split-dns), node-uplink (networkmanager, systemd-networkd, dhcpcd), mesh-build-machine (builder, +mesh scope), mesh-catalog (mesh-catalog). showcase now declares its own seat and claims it. verdaccio removed — the mesh keeps distribution as its registry and gitea already serves npm, so a second npm registry is redundant. --- modules/builder/module.json | 4 +- modules/dhcpcd/module.json | 2 +- modules/dnsmasq/module.json | 6 +- modules/fail2ban/module.json | 2 +- modules/mesh-catalog/module.json | 2 +- modules/networkmanager/module.json | 2 +- modules/nftables/module.json | 4 +- modules/resolv-conf/module.json | 22 ++- modules/resolved-split-dns/module.json | 44 +++-- modules/showcase/module.json | 241 +++++++++++++++++++------ modules/systemd-networkd/module.json | 2 +- modules/verdaccio/Dockerfile | 30 --- modules/verdaccio/client.ts | 91 ---------- modules/verdaccio/index.ts | 45 ----- modules/verdaccio/module.json | 130 ------------- modules/verdaccio/package.json | 14 -- modules/verdaccio/tools/index.ts | 35 ---- modules/verdaccio/tsconfig.json | 12 -- 18 files changed, 242 insertions(+), 446 deletions(-) delete mode 100644 modules/verdaccio/Dockerfile delete mode 100644 modules/verdaccio/client.ts delete mode 100644 modules/verdaccio/index.ts delete mode 100644 modules/verdaccio/module.json delete mode 100644 modules/verdaccio/package.json delete mode 100644 modules/verdaccio/tools/index.ts delete mode 100644 modules/verdaccio/tsconfig.json diff --git a/modules/builder/module.json b/modules/builder/module.json index 9d38701..165c84b 100644 --- a/modules/builder/module.json +++ b/modules/builder/module.json @@ -6,8 +6,8 @@ ], "claims": [ { - "name": "the-build-machine", - "scope": "node" + "name": "mesh-build-machine", + "scope": "mesh" } ], "requires": [ diff --git a/modules/dhcpcd/module.json b/modules/dhcpcd/module.json index 1e7aa67..89ffe07 100644 --- a/modules/dhcpcd/module.json +++ b/modules/dhcpcd/module.json @@ -7,7 +7,7 @@ ], "claims": [ { - "name": "the-uplink", + "name": "node-uplink", "scope": "node" } ], diff --git a/modules/dnsmasq/module.json b/modules/dnsmasq/module.json index 58f3050..d972161 100644 --- a/modules/dnsmasq/module.json +++ b/modules/dnsmasq/module.json @@ -16,7 +16,7 @@ }, "claims": [ { - "name": "the-dns-port", + "name": "node-dns-resolver", "scope": "node" } ], @@ -46,7 +46,7 @@ "type": "file", "path": "/etc/dnsmasq.conf", "mode": "0644", - "content": "# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than\n# patched, because this module owns the file and a patch would leave whatever\n# was there before to be discovered later.\n\n# What the mesh computed: one wildcard per machine \u2014 its name and everything\n# under it \u2014 and the mesh's own suffix as a local domain, so a name under it is\n# answered here or not at all and is never asked upstream. Rewritten whenever a\n# machine joins or leaves, which is why the service below restarts on it: a\n# reload makes dnsmasq re-read hosts files, not its configuration, and a\n# wildcard is configuration.\nconf-file=/etc/mesh-resolver/nodes.conf\n\n# Where it answers. Both are names the mesh chose, so this file needs to know\n# nothing about this particular machine:\n#\n# mesh0 the private network, so anything on it can ask \u2014 including\n# this machine's containers. This module writes the runtime's\n# `dns` key into its own configuration file, beside whatever the\n# machine had there (novox/hq ADR 0102), naming this address: a\n# container cannot reach the machine's loopback, and a runtime\n# whose host resolves at loopback falls back to a public resolver\n# and never sees a mesh name. The runtime reads that key when it\n# starts and not on a reload, and a restart stops every container\n# on the machine, so this module orders neither: the key holds for\n# every container created after the runtime next starts. On the\n# machine this replaces the predecessor wrote the same value, so\n# nothing there is waiting on it.\n# 127.0.0.1 this machine's own use. The predecessor's resolver answered\n# here, and the resolv.conf it wrote on every machine says so;\n# that file stays in force on an adopted machine until the mesh's\n# module for it is taken, so the resolver has to answer where the\n# machine already asks or the machine loses DNS the moment this\n# module is taken. Not .53 or .54: systemd-resolved holds BOTH \u2014\n# .53 is its stub and .54 its proxy stub \u2014 and neither is .1, so\n# the two coexist on a machine that runs it. This module used to\n# answer on 127.0.0.55 instead: a convention of its own, beside\n# the one every machine already followed. One address, this one,\n# and the modules that point a machine at the mesh name the same.\n#\n# Whatever address it listens on, it takes the machine's DNS port.\n# That is why this module claims `the-dns-port`.\n#\n# bind-dynamic rather than bind-interfaces: mesh0 does not exist until the\n# machine is on the private network, and binding an interface that is not there\n# yet fails to start rather than waiting for it.\nbind-dynamic\ninterface=mesh0\nlisten-address=127.0.0.1\n\n# **It must never read resolv.conf to find out where to forward.** Whatever\n# points this machine at the mesh writes this resolver's own address there \u2014 so\n# a resolver that read it for upstreams would find itself, and every query it\n# could not answer locally would loop until its receive queue filled. That is\n# not theoretical: it filled with 15KB of queries and every lookup on the\n# machine hung. no-resolv is what makes that loop impossible: the upstreams are\n# the two lines below, and nothing on the machine can redirect them.\n#\n# It forwards, because it is now asked for everything. The module that points\n# this machine at the mesh names this resolver alone \u2014 as the predecessor's\n# did \u2014 so the host and every container resolve the world through it. The\n# upstreams are the ones the predecessor's module shipped as its defaults. The\n# mesh's own names never reach them: the local= line in the file above stops\n# them here, answered or refused.\nno-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n\n# A name without a dot is never forwarded \u2014 a bare hostname is answered from\n# /etc/hosts or not at all \u2014 and reverse lookups of private ranges are answered\n# here rather than asking the world who 10.x is.\ndomain-needed\nbogus-priv\n\n# The operator's own names have a home the mesh never rewrites (novox/hq issue\n# 122: a workstation's job includes names \u2014 Mediahuis's 13, say \u2014 that are\n# neither a mesh machine nor a routed name). Two homes, because both shapes\n# exist in the wild and neither is the mesh's to own:\n#\n# /etc/dnsmasq.d/*.conf drop-in dnsmasq directives \u2014 an address=, a second\n# upstream for one domain, a cname. HAL's dnsmasq-app\n# carried exactly this line, so it is a proven shape\n# and the files a migrating workstation already has\n# land here untouched.\n# /etc/hosts.local plain ` ` lines, the /etc/hosts a person\n# kept \u2014 read as additional hosts, so the generated\n# /etc/hosts (which the mesh owns and rewrites) never\n# has to carry an operator entry to keep it resolving.\n#\n# Both are the operator's: the mesh creates neither and rewrites neither, and a\n# machine with no such file loses nothing. This is what lets mesh-wireguard take\n# /etc/hosts without taking the names a workstation needs down with it \u2014 they\n# were moved here first.\nconf-dir=/etc/dnsmasq.d/,*.conf\naddn-hosts=/etc/hosts.local\n" + "content": "# Managed by the mesh. dnsmasq's own defaults are replaced whole rather than\n# patched, because this module owns the file and a patch would leave whatever\n# was there before to be discovered later.\n\n# What the mesh computed: one wildcard per machine \u2014 its name and everything\n# under it \u2014 and the mesh's own suffix as a local domain, so a name under it is\n# answered here or not at all and is never asked upstream. Rewritten whenever a\n# machine joins or leaves, which is why the service below restarts on it: a\n# reload makes dnsmasq re-read hosts files, not its configuration, and a\n# wildcard is configuration.\nconf-file=/etc/mesh-resolver/nodes.conf\n\n# Where it answers. Both are names the mesh chose, so this file needs to know\n# nothing about this particular machine:\n#\n# mesh0 the private network, so anything on it can ask \u2014 including\n# this machine's containers. This module writes the runtime's\n# `dns` key into its own configuration file, beside whatever the\n# machine had there (novox/hq ADR 0102), naming this address: a\n# container cannot reach the machine's loopback, and a runtime\n# whose host resolves at loopback falls back to a public resolver\n# and never sees a mesh name. The runtime reads that key when it\n# starts and not on a reload, and a restart stops every container\n# on the machine, so this module orders neither: the key holds for\n# every container created after the runtime next starts. On the\n# machine this replaces the predecessor wrote the same value, so\n# nothing there is waiting on it.\n# 127.0.0.1 this machine's own use. The predecessor's resolver answered\n# here, and the resolv.conf it wrote on every machine says so;\n# that file stays in force on an adopted machine until the mesh's\n# module for it is taken, so the resolver has to answer where the\n# machine already asks or the machine loses DNS the moment this\n# module is taken. Not .53 or .54: systemd-resolved holds BOTH \u2014\n# .53 is its stub and .54 its proxy stub \u2014 and neither is .1, so\n# the two coexist on a machine that runs it. This module used to\n# answer on 127.0.0.55 instead: a convention of its own, beside\n# the one every machine already followed. One address, this one,\n# and the modules that point a machine at the mesh name the same.\n#\n# Whatever address it listens on, it takes the machine's DNS port.\n# That is why this module claims `node-dns-resolver`.\n#\n# bind-dynamic rather than bind-interfaces: mesh0 does not exist until the\n# machine is on the private network, and binding an interface that is not there\n# yet fails to start rather than waiting for it.\nbind-dynamic\ninterface=mesh0\nlisten-address=127.0.0.1\n\n# **It must never read resolv.conf to find out where to forward.** Whatever\n# points this machine at the mesh writes this resolver's own address there \u2014 so\n# a resolver that read it for upstreams would find itself, and every query it\n# could not answer locally would loop until its receive queue filled. That is\n# not theoretical: it filled with 15KB of queries and every lookup on the\n# machine hung. no-resolv is what makes that loop impossible: the upstreams are\n# the two lines below, and nothing on the machine can redirect them.\n#\n# It forwards, because it is now asked for everything. The module that points\n# this machine at the mesh names this resolver alone \u2014 as the predecessor's\n# did \u2014 so the host and every container resolve the world through it. The\n# upstreams are the ones the predecessor's module shipped as its defaults. The\n# mesh's own names never reach them: the local= line in the file above stops\n# them here, answered or refused.\nno-resolv\nserver=1.1.1.1\nserver=8.8.8.8\n\n# A name without a dot is never forwarded \u2014 a bare hostname is answered from\n# /etc/hosts or not at all \u2014 and reverse lookups of private ranges are answered\n# here rather than asking the world who 10.x is.\ndomain-needed\nbogus-priv\n\n# The operator's own names have a home the mesh never rewrites (novox/hq issue\n# 122: a workstation's job includes names \u2014 Mediahuis's 13, say \u2014 that are\n# neither a mesh machine nor a routed name). Two homes, because both shapes\n# exist in the wild and neither is the mesh's to own:\n#\n# /etc/dnsmasq.d/*.conf drop-in dnsmasq directives \u2014 an address=, a second\n# upstream for one domain, a cname. HAL's dnsmasq-app\n# carried exactly this line, so it is a proven shape\n# and the files a migrating workstation already has\n# land here untouched.\n# /etc/hosts.local plain ` ` lines, the /etc/hosts a person\n# kept \u2014 read as additional hosts, so the generated\n# /etc/hosts (which the mesh owns and rewrites) never\n# has to carry an operator entry to keep it resolving.\n#\n# Both are the operator's: the mesh creates neither and rewrites neither, and a\n# machine with no such file loses nothing. This is what lets mesh-wireguard take\n# /etc/hosts without taking the names a workstation needs down with it \u2014 they\n# were moved here first.\nconf-dir=/etc/dnsmasq.d/,*.conf\naddn-hosts=/etc/hosts.local\n" }, { "id": "runtime-dns", @@ -72,7 +72,7 @@ "facts": { "node-zones": { "path": "/etc/mesh-resolver/nodes.conf", - "template": "# Generated by the mesh. Do not edit — this file is replaced whenever a machine\n# joins or leaves, and an edit would survive until then and vanish.\n\nlocal=/{{.Suffix}}/\n{{range .Machines}}address=/{{.FQDN}}/{{.Address}}\n{{end}}" + "template": "# Generated by the mesh. Do not edit \u2014 this file is replaced whenever a machine\n# joins or leaves, and an edit would survive until then and vanish.\n\nlocal=/{{.Suffix}}/\n{{range .Machines}}address=/{{.FQDN}}/{{.Address}}\n{{end}}" } } } diff --git a/modules/fail2ban/module.json b/modules/fail2ban/module.json index 8b94092..9876432 100644 --- a/modules/fail2ban/module.json +++ b/modules/fail2ban/module.json @@ -6,7 +6,7 @@ ], "claims": [ { - "name": "the-intrusion-prevention", + "name": "node-intrusion-prevention", "scope": "node" } ], diff --git a/modules/mesh-catalog/module.json b/modules/mesh-catalog/module.json index 097368a..4c52d90 100644 --- a/modules/mesh-catalog/module.json +++ b/modules/mesh-catalog/module.json @@ -7,7 +7,7 @@ ], "claims": [ { - "name": "the-catalogue", + "name": "mesh-catalog", "scope": "mesh" } ], diff --git a/modules/networkmanager/module.json b/modules/networkmanager/module.json index 6bf00f1..5aaa8f6 100644 --- a/modules/networkmanager/module.json +++ b/modules/networkmanager/module.json @@ -7,7 +7,7 @@ ], "claims": [ { - "name": "the-uplink", + "name": "node-uplink", "scope": "node" } ], diff --git a/modules/nftables/module.json b/modules/nftables/module.json index 11a6be6..dea15e0 100644 --- a/modules/nftables/module.json +++ b/modules/nftables/module.json @@ -6,7 +6,7 @@ ], "claims": [ { - "name": "the-packet-filter", + "name": "node-packet-filter", "scope": "node" } ], @@ -30,7 +30,7 @@ "id": "stock-unit-stop", "type": "file", "path": "/etc/systemd/system/nftables.service.d/mesh.conf", - "content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) — a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n", + "content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) \u2014 a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n", "mode": "0644" }, { diff --git a/modules/resolv-conf/module.json b/modules/resolv-conf/module.json index 520e864..dd71fbc 100644 --- a/modules/resolv-conf/module.json +++ b/modules/resolv-conf/module.json @@ -2,12 +2,22 @@ "module": "resolv-conf", "version": "1", "slug": "resolv", - - "requires": ["wildcard-resolution"], - "claims": [{"name": "the-resolver-configuration", "scope": "node"}], - + "requires": [ + "wildcard-resolution" + ], + "claims": [ + { + "name": "node-resolver-config", + "scope": "node" + } + ], "resources": [ - {"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644", - "content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead — this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver, and only it — the one line the predecessor wrote on every\n# machine it set up. It answers the mesh's names itself and forwards everything\n# else to upstreams named in its own configuration, never read from this file.\n# This file used to carry a second nameserver as a placeholder for \"whatever\n# this machine used before\"; that was never a fallback for names the mesh does\n# not know — a resolver's second line is asked only when the first does not\n# answer at all — and now that the first answers everything it would be a line\n# nothing ever reached.\nnameserver 127.0.0.1\noptions edns0\n"} + { + "id": "resolv", + "type": "file", + "path": "/etc/resolv.conf", + "mode": "0644", + "content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead \u2014 this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver, and only it \u2014 the one line the predecessor wrote on every\n# machine it set up. It answers the mesh's names itself and forwards everything\n# else to upstreams named in its own configuration, never read from this file.\n# This file used to carry a second nameserver as a placeholder for \"whatever\n# this machine used before\"; that was never a fallback for names the mesh does\n# not know \u2014 a resolver's second line is asked only when the first does not\n# answer at all \u2014 and now that the first answers everything it would be a line\n# nothing ever reached.\nnameserver 127.0.0.1\noptions edns0\n" + } ] } diff --git a/modules/resolved-split-dns/module.json b/modules/resolved-split-dns/module.json index 246ba83..c79d759 100644 --- a/modules/resolved-split-dns/module.json +++ b/modules/resolved-split-dns/module.json @@ -2,18 +2,38 @@ "module": "resolved-split-dns", "version": "1", "slug": "splitdns", - - "requires": ["wildcard-resolution"], - "claims": [{"name": "the-resolver-configuration", "scope": "node"}], - + "requires": [ + "wildcard-resolution" + ], + "claims": [ + { + "name": "node-resolver-config", + "scope": "node" + } + ], "resources": [ - {"id": "drop-in", "type": "directory", "path": "/etc/systemd/resolved.conf.d", "mode": "0755"}, - - {"id": "route", "type": "file", - "path": "/etc/systemd/resolved.conf.d/mesh.conf", "mode": "0644", - "content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims. The mesh's resolver can forward the rest too;\n# this module is for a machine that wants systemd-resolved to stay in charge of\n# that, and only lends it the mesh's suffix.\n#\n# 127.0.0.1 is where the mesh's resolver answers on every machine — a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54 itself, which is why the resolver is on\n# neither, and why the two coexist here.\n[Resolve]\nDNS=127.0.0.1\nDomains=~internal\n"}, - - {"id": "resolved", "type": "service", "unit": "systemd-resolved.service", - "state": "running", "boot": "enabled", "restart-on": ["route"]} + { + "id": "drop-in", + "type": "directory", + "path": "/etc/systemd/resolved.conf.d", + "mode": "0755" + }, + { + "id": "route", + "type": "file", + "path": "/etc/systemd/resolved.conf.d/mesh.conf", + "mode": "0644", + "content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims. The mesh's resolver can forward the rest too;\n# this module is for a machine that wants systemd-resolved to stay in charge of\n# that, and only lends it the mesh's suffix.\n#\n# 127.0.0.1 is where the mesh's resolver answers on every machine \u2014 a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54 itself, which is why the resolver is on\n# neither, and why the two coexist here.\n[Resolve]\nDNS=127.0.0.1\nDomains=~internal\n" + }, + { + "id": "resolved", + "type": "service", + "unit": "systemd-resolved.service", + "state": "running", + "boot": "enabled", + "restart-on": [ + "route" + ] + } ] } diff --git a/modules/showcase/module.json b/modules/showcase/module.json index 289cf76..f17d0c5 100644 --- a/modules/showcase/module.json +++ b/modules/showcase/module.json @@ -2,72 +2,195 @@ "module": "showcase", "version": "1", "slug": "show", - - "capabilities": ["container-runtime"], - - "provides": [{ "name": "greeting", "scope": "mesh" }], - "serves": { "greeting": { "path": "/greeting" } }, - "requires": ["postgres-database"], - "binds": { "postgres-database": "/var/lib/showcase/database.json" }, - "secrets": { "postgres-database": "/var/lib/showcase/database.secret" }, - "own-secrets": { "broker": "/var/lib/mesh/showcase/broker" }, - - "claims": [{ "name": "the-showcase", "scope": "node" }], - - "emits": ["module.showcase.acknowledged"], - "consumes": ["module.showcase.greeted"], - + "capabilities": [ + "container-runtime" + ], + "provides": [ + { + "name": "greeting", + "scope": "mesh" + } + ], + "serves": { + "greeting": { + "path": "/greeting" + } + }, + "requires": [ + "postgres-database" + ], + "binds": { + "postgres-database": "/var/lib/showcase/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/showcase/database.secret" + }, + "own-secrets": { + "broker": "/var/lib/mesh/showcase/broker" + }, + "claims": [ + { + "name": "the-showcase", + "scope": "node" + } + ], + "emits": [ + "module.showcase.acknowledged" + ], + "consumes": [ + "module.showcase.greeted" + ], "listens": [ - { "port": 8080, "protocol": "tcp", "from": "mesh", - "why": "the port the daemon itself listens on. The mesh assigns the machine-side number and tells consumers that one (ADR 0038)" } + { + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "the port the daemon itself listens on. The mesh assigns the machine-side number and tells consumers that one (ADR 0038)" + } ], - "build": { "artifacts": [ - { "name": "code", "kind": "bundle", "language": "typescript", - "entrypoints": ["index.js", "tools/index.js", "provisioner/index.js", - "daemon/index.js", "step/index.js", "report/index.js"] }, - { "name": "files", "kind": "archive", "from": "files" }, - { "name": "helper", "kind": "upstream", - "from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" } + { + "name": "code", + "kind": "bundle", + "language": "typescript", + "entrypoints": [ + "index.js", + "tools/index.js", + "provisioner/index.js", + "daemon/index.js", + "step/index.js", + "report/index.js" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + }, + { + "name": "helper", + "kind": "upstream", + "from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" + } ] }, - "resources": [ - { "id": "account", "type": "user", "name": "showcase", "shell": "/usr/bin/nologin", - "home": "/var/lib/showcase" }, - - { "id": "logs", "type": "access", "path": "/var/log", "mode": "0755" }, - - { "id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/showcase", "mode": "0700" }, - { "id": "state", "type": "directory", "path": "/var/lib/showcase", "mode": "0755" }, - - { "id": "settings", "type": "file", "path": "/var/lib/showcase/showcase.env", "mode": "0600", - "content": "SHOWCASE_GREETING=hello\nSHOWCASE_EVERY_SECONDS=30\nSHOWCASE_STATE=/var/lib/showcase\nSHOWCASE_DATABASE=${bound:postgres-database:at}\nSHOWCASE_LISTEN=${port:8080}\n" }, - - { "id": "packed", "type": "archive", "path": "/opt/showcase", "artifact": "files" }, - - { "id": "net", "type": "network", "name": "showcase" }, - - { "id": "tooling", "type": "package", "package": "jq" }, - - { "id": "migrate", "type": "process", "name": "showcase-migrate", "artifact": "code", - "run": ["node", "step/index.js"], "run-once": true, - "env-file": ["/var/lib/showcase/showcase.env"] }, - - { "id": "server", "type": "process", "name": "showcase", "artifact": "code", - "run": ["node", "daemon/index.js"], "user": "showcase", - "env-file": ["/var/lib/showcase/showcase.env"], - "restart-on": ["settings"] }, - - { "id": "reporting", "type": "process", "name": "showcase-report", "artifact": "code", - "run": ["node", "report/index.js"], "schedule": "0 3 * * *", - "env-file": ["/var/lib/showcase/showcase.env"] }, - - { "id": "tools", "type": "container", "name": "mesh-showcase", "artifact": "helper", + { + "id": "account", + "type": "user", + "name": "showcase", + "shell": "/usr/bin/nologin", + "home": "/var/lib/showcase" + }, + { + "id": "logs", + "type": "access", + "path": "/var/log", + "mode": "0755" + }, + { + "id": "mesh-state", + "type": "directory", + "path": "/var/lib/mesh/showcase", + "mode": "0700" + }, + { + "id": "state", + "type": "directory", + "path": "/var/lib/showcase", + "mode": "0755" + }, + { + "id": "settings", + "type": "file", + "path": "/var/lib/showcase/showcase.env", + "mode": "0600", + "content": "SHOWCASE_GREETING=hello\nSHOWCASE_EVERY_SECONDS=30\nSHOWCASE_STATE=/var/lib/showcase\nSHOWCASE_DATABASE=${bound:postgres-database:at}\nSHOWCASE_LISTEN=${port:8080}\n" + }, + { + "id": "packed", + "type": "archive", + "path": "/opt/showcase", + "artifact": "files" + }, + { + "id": "net", + "type": "network", + "name": "showcase" + }, + { + "id": "tooling", + "type": "package", + "package": "jq" + }, + { + "id": "migrate", + "type": "process", + "name": "showcase-migrate", + "artifact": "code", + "run": [ + "node", + "step/index.js" + ], + "run-once": true, + "env-file": [ + "/var/lib/showcase/showcase.env" + ] + }, + { + "id": "server", + "type": "process", + "name": "showcase", + "artifact": "code", + "run": [ + "node", + "daemon/index.js" + ], + "user": "showcase", + "env-file": [ + "/var/lib/showcase/showcase.env" + ], + "restart-on": [ + "settings" + ] + }, + { + "id": "reporting", + "type": "process", + "name": "showcase-report", + "artifact": "code", + "run": [ + "node", + "report/index.js" + ], + "schedule": "0 3 * * *", + "env-file": [ + "/var/lib/showcase/showcase.env" + ] + }, + { + "id": "tools", + "type": "container", + "name": "mesh-showcase", + "artifact": "helper", "network": "showcase", - "volumes": ["/var/lib/mesh/showcase/broker:/run/secrets/broker:ro"], - "env": { "MESH_BROKER_FILE": "/run/secrets/broker" }, - "args": ["sleep", "infinity"] } + "volumes": [ + "/var/lib/mesh/showcase/broker:/run/secrets/broker:ro" + ], + "env": { + "MESH_BROKER_FILE": "/run/secrets/broker" + }, + "args": [ + "sleep", + "infinity" + ] + } + ], + "seats": [ + { + "name": "the-showcase", + "scope": "node" + } ] } diff --git a/modules/systemd-networkd/module.json b/modules/systemd-networkd/module.json index c4f6b51..040b67e 100644 --- a/modules/systemd-networkd/module.json +++ b/modules/systemd-networkd/module.json @@ -7,7 +7,7 @@ ], "claims": [ { - "name": "the-uplink", + "name": "node-uplink", "scope": "node" } ], diff --git a/modules/verdaccio/Dockerfile b/modules/verdaccio/Dockerfile deleted file mode 100644 index ee4fec8..0000000 --- a/modules/verdaccio/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# verdaccio's runtime: the tool runtime, carrying this module's compiled code. -# -# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in -# the base images, published like any other artifact — which is what makes this buildable by the -# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that -# happens to have the siblings. -# -# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the -# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`. -ARG BUILD_BASE -ARG RUNTIME_BASE - -FROM ${BUILD_BASE} AS build -# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own -# node_modules — the module is compiled against exactly the sdk it will run against. The compiler -# is invoked by its real path: node_modules/.bin entries are launcher symlinks the base image -# resolved away. -WORKDIR /app/modules/verdaccio -COPY . . -RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts \ - --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist - -FROM ${RUNTIME_BASE} -COPY --from=build /app/modules/verdaccio/dist /app/modules/verdaccio/dist -# Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a -# provider's provisioner runs its reconcile loop in the same process, with the broker connected — -# the convention novox/hq issues 060/061 settled. A container that instead ran only its -# provisioner (`run`) served no tools and emitted no events; a container that named no command -# ran no provisioner at all. -ENV MESH_TOOL_MODULES=/app/modules/verdaccio/dist/index.js,/app/modules/verdaccio/dist/tools/index.js diff --git a/modules/verdaccio/client.ts b/modules/verdaccio/client.ts deleted file mode 100644 index 9c5feb1..0000000 --- a/modules/verdaccio/client.ts +++ /dev/null @@ -1,91 +0,0 @@ -// The Verdaccio (npm registry) client — verdaccio's own code, living in the module (novox/hq -// ADR 0039). Both this module's tools and its events entrypoint import it, and nothing outside -// verdaccio does. - -import { readFileSync } from "node:fs"; - -export interface VerdaccioPackage { - name: string; - version?: string; - description?: string; - time?: string; -} - -export interface PackageInfo { - name: string; - latest?: string; - versions: string[]; - description?: string; - modified?: string; -} - -/** The settings-merged config the mesh delivers (novox/hq ADR 0046): { url, apiKey, token, password, user, ... }. */ -function meshConfig(file?: string): Record { - if (!file) return {}; - try { return JSON.parse(readFileSync(file, "utf8")) as Record; } - catch { return {}; } -} - -export class VerdaccioClient { - readonly baseUrl: string; - - // A bearer token is optional: package listing and reading are public on most registries, so the - // token is sent only when configured, for a registry that gates reads behind auth. - constructor( - url: string, - private readonly token?: string, - ) { - this.baseUrl = url.replace(/\/+$/, ""); - } - - /** - * Build from the module's resolved environment. The URL is MESH_VERDACCIO_URL (or the local - * port); an optional MESH_VERDACCIO_TOKEN authenticates. Throws when no URL is configured. - */ - static fromEnv(env: NodeJS.ProcessEnv = process.env): VerdaccioClient { - const cfg = meshConfig(env.MESH_VERDACCIO_CONFIG_FILE); - const url = cfg.url ?? (env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`); - if (!url) throw new Error("no verdaccio URL — set MESH_VERDACCIO_URL"); - return new VerdaccioClient(url, cfg.token ?? env.MESH_VERDACCIO_TOKEN); - } - - private async getJson(path: string): Promise { - const res = await fetch(`${this.baseUrl}${path}`, { - headers: { - Accept: "application/json", - ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), - }, - }); - if (!res.ok) throw new Error(`Verdaccio ${path}: ${res.status} ${await res.text()}`); - return res.json() as Promise; - } - - /** - * Every package the registry hosts, from Verdaccio's own web API — the same list its UI shows. - * Each entry carries the latest version and the time it was last published. - */ - async listPackages(): Promise { - const raw = await this.getJson("/-/verdaccio/data/packages"); - return (raw ?? []).map((p) => ({ - name: p.name, - version: p.version ?? p["dist-tags"]?.latest, - description: p.description, - time: p.time?.modified ?? p.time, - })); - } - - /** - * The full detail of one package — its dist-tags, every published version, and timestamps — - * from the standard npm packument endpoint (`GET /`). - */ - async getPackageInfo(name: string): Promise { - const doc = await this.getJson(`/${encodeURIComponent(name).replace(/%2F/g, "/")}`); - return { - name: doc.name ?? name, - latest: doc["dist-tags"]?.latest, - versions: Object.keys(doc.versions ?? {}), - description: doc.description, - modified: doc.time?.modified, - }; - } -} diff --git a/modules/verdaccio/index.ts b/modules/verdaccio/index.ts deleted file mode 100644 index 0c23d9e..0000000 --- a/modules/verdaccio/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -// verdaccio's events. The tool runtime imports this once the broker is bound. -// -// Emits (novox/hq ADR 0041/0042): -// module.verdaccio.package.published — a new package version was published to the registry -// -// A genuinely useful signal: a package was just published, so anything on the mesh that pins, -// mirrors or announces dependency releases can react without polling the registry. Verdaccio has -// no publish webhook, so the module discovers it by diffing the package list's latest versions. -// -// The polling is deliberately unhurried: a publish a minute late is still the event, whereas -// hammering the registry for immediacy nobody asked for is not. - -import { emit } from "@novox/mesh-sdk/events"; -import { VerdaccioClient } from "./client.js"; - -const verdaccio = VerdaccioClient.fromEnv(); - -// The latest version we have seen per package name. Primed silently on the first look so a registry -// that was already populated when this started does not announce its whole catalog as freshly -// published. -const latest = new Map(); -let primed = false; - -async function pollPackages(): Promise { - const packages = await verdaccio.listPackages(); - for (const pkg of packages) { - if (!pkg.version) continue; - const known = latest.get(pkg.name); - if (known !== pkg.version) { - // A name we have not seen, or a name whose latest version moved — both are a publish. - if (primed) await emit("module.verdaccio.package.published", { name: pkg.name, version: pkg.version }); - latest.set(pkg.name, pkg.version); - } - } - primed = true; -} - -const tick = (fn: () => Promise, everyMs: number): void => { - const run = (): void => void fn().catch((err) => console.error(`[verdaccio] ${err}`)); - setInterval(run, everyMs); - run(); -}; -tick(pollPackages, 60_000); - -console.log("[verdaccio] watching the registry for newly published packages"); diff --git a/modules/verdaccio/module.json b/modules/verdaccio/module.json deleted file mode 100644 index 5a161d4..0000000 --- a/modules/verdaccio/module.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "module": "verdaccio", - "version": "1", - "slug": "verdacc", - "capabilities": [ - "container-runtime" - ], - "emits": [ - "module.verdaccio.package.published" - ], - "own-secrets": { - "broker": "/var/lib/mesh/verdaccio/broker" - }, - "listens": [ - { - "port": 4873, - "protocol": "tcp", - "from": "mesh", - "why": "the package registry, for installs and publishes" - } - ], - "resources": [ - { - "id": "mesh-state", - "type": "directory", - "path": "/var/lib/mesh/verdaccio", - "mode": "0700" - }, - { - "id": "conf", - "type": "directory", - "path": "/services/verdaccio/conf", - "mode": "0755", - "owner": "10001:10001" - }, - { - "id": "storage", - "type": "directory", - "path": "/services/verdaccio/storage", - "mode": "0700", - "owner": "10001:10001" - }, - { - "id": "config", - "type": "file", - "path": "/services/verdaccio/conf/config.yaml", - "mode": "0644", - "content": "storage: /verdaccio/storage\nauth:\n htpasswd:\n file: /verdaccio/conf/htpasswd\n max_users: 10\nuplinks:\n npmjs:\n url: https://registry.npmjs.org/\npackages:\n \"**\":\n access: $all\n publish: $authenticated\n proxy: npmjs\nserver:\n keepAliveTimeout: 60\n maxBodySize: 10mb\nmiddlewares:\n audit:\n enabled: true\nlog:\n type: stdout\n format: pretty\n level: http\n" - }, - { - "id": "server", - "type": "container", - "name": "verdaccio", - "image": "verdaccio/verdaccio@sha256:7b067a47ae51fb9dff3dcdce60ec0a2cbd7650c208cb4b9f6d37cb1b09b39d43", - "ports": [ - "4873" - ], - "volumes": [ - "/services/verdaccio/storage:/verdaccio/storage", - "/services/verdaccio/conf:/verdaccio/conf" - ] - }, - { - "id": "runtime-config", - "type": "file", - "path": "/var/lib/mesh/verdaccio/config.json", - "mode": "0600", - "content": "{}\n", - "merge": "json" - }, - { - "id": "runtime", - "type": "container", - "name": "mesh-verdaccio", - "network": "host", - "volumes": [ - "/var/lib/mesh/verdaccio/broker:/run/secrets/broker:ro", - "/var/lib/mesh/verdaccio/config.json:/run/config/config.json:ro" - ], - "env": { - "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_VERDACCIO_URL": "http://127.0.0.1:4873", - "MESH_VERDACCIO_CONFIG_FILE": "/run/config/config.json" - }, - "restart-on": [ - "runtime-config" - ], - "artifact": "runtime" - } - ], - "requires": [ - "route" - ], - "contributes": { - "route": { - "label": "npm", - "port": 4873 - } - }, - "binds": { - "route": "/var/lib/mesh/verdaccio/route.json" - }, - "provides": [ - { - "name": "npm-package-registry", - "scope": "mesh" - } - ], - "build": { - "on": [ - { - "arg": "BUILD_BASE", - "module": "mesh-tools", - "artifact": "build" - }, - { - "arg": "RUNTIME_BASE", - "module": "mesh-tools", - "artifact": "runtime" - } - ], - "artifacts": [ - { - "name": "runtime", - "kind": "image", - "from": "Dockerfile" - } - ] - } -} diff --git a/modules/verdaccio/package.json b/modules/verdaccio/package.json deleted file mode 100644 index a3c21eb..0000000 --- a/modules/verdaccio/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@novox/module-verdaccio", - "version": "0.1.0", - "description": "verdaccio — private npm registry. Its API client, tools and events live here (novox/hq ADR 0039).", - "type": "module", - "private": true, - "dependencies": { - "@novox/mesh-sdk": "^0.1.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.6.0" - } -} diff --git a/modules/verdaccio/tools/index.ts b/modules/verdaccio/tools/index.ts deleted file mode 100644 index 5632bef..0000000 --- a/modules/verdaccio/tools/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -// verdaccio's tools — its own code (novox/hq ADR 0039), importing verdaccio's own client. They -// return structured data; the mesh serves them through the sdk's tool harness. - -import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; -import { VerdaccioClient } from "../client.js"; - -export function getVerdaccioTools(verdaccio: VerdaccioClient): ToolDefinition[] { - return [ - { - name: "verdaccio_list_packages", - description: "List every package hosted on the private npm registry, with each one's latest version.", - input: {}, - run: async () => { - const packages = await verdaccio.listPackages(); - return { count: packages.length, packages }; - }, - }, - { - name: "verdaccio_package_info", - description: "Details of one package on the registry: its latest tag, all published versions, and description.", - input: { name: { type: "string", description: "the package name, e.g. '@novox/mesh-sdk'" } }, - run: async (args) => verdaccio.getPackageInfo(String(args.name)), - }, - ]; -} - -// The tools exist only when a registry URL is configured; otherwise verdaccio contributes none -// rather than failing the whole runtime. -registerModuleTools("verdaccio", (env) => { - try { - return getVerdaccioTools(VerdaccioClient.fromEnv(env)); - } catch { - return []; - } -}); diff --git a/modules/verdaccio/tsconfig.json b/modules/verdaccio/tsconfig.json deleted file mode 100644 index 3677859..0000000 --- a/modules/verdaccio/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true - }, - "include": ["client.ts", "index.ts", "tools/index.ts"] -}