Compare commits

..
13 Commits
Author SHA1 Message Date
mesh-admin efff54157b Merge pull request 'The seven say what the runtime loads from their tools bundle (hq ADR 0192)' (#243) from fix/0192-the-seven-say-what-the-runtime-loads into main 2026-10-03 13:58:27 +00:00
jochen 2000ec3f48 The seven say what the runtime loads from their tools bundle (hq ADR 0192)
None declares a tools list, so the composer had nothing saying the runtime loads from the bundle,
and delivered it nowhere: built, recorded, never sent. loads names tools/index.js.
2026-10-03 15:58:19 +02:00
mesh-admin 7c800705bf Merge pull request 'Seven tool containers move to bundles the node's runtime serves, given their words (hq ADR 0192)' (#242) from feat/0192-seven-tool-containers-move into main 2026-10-03 13:45:46 +00:00
jochen 19511012f8 Seven tool containers move to bundles the node's runtime serves, given their words (hq ADR 0192)
baserow, confluence, gitlab, jira, letta, searxng and unifi: each runtime container's
environment becomes its tools bundle's env, mount targets folded back into the host paths they
came from; baserow and letta reach their service on the published port rather than a container
network name. The container, base images, Dockerfile and the module's own bus credential go,
and the state directory where only that credential lived. Tool code is unchanged: every client
is built from the environment the contributor is handed.
2026-10-03 15:35:00 +02:00
jschoubben 0e8ad3a8e1 Merge pull request 'dnsmasq: restart on the mesh hosts region, which it reads only at start' (#241) from fix/dnsmasq-rereads-the-names-region into main 2026-10-03 13:29:29 +00:00
jschoubben b43e405947 dnsmasq: restart on the mesh's hosts region, which it reads only at start
The names region of /etc/hosts is written by mesh-wireguard and dnsmasq answers from it, but read
it once at start: after the mesh stopped publishing public names (hq ADR 0191) every machine's
hosts file was right and every resolver still answered mail.novox.be with a tunnel address. The
config comment that says so changes the file, which restarts dnsmasq once everywhere.
2026-10-03 15:29:28 +02:00
mesh-admin d5269c8662 Merge pull request 'fail2ban's tools are a bundle the node's runtime serves; its container goes (hq to-be 38 WP4)' (#240) from feat/fail2ban-tools-as-a-bundle into main 2026-10-03 13:07:35 +00:00
jochen aa5bf7d5ef fail2ban's tools are a bundle the node's runtime serves; its container goes (hq to-be 38 WP4)
The second holder follows the packet filter: the container, its base images, the Dockerfile,
and the bus credential and state directory only the container read are gone; the tools are a
TypeScript bundle node-tools loads. The daemon's socket answers only to root, so the client
runs through sudo without a prompt where the runtime's account is not root, naming sudo's
absence or refusal by how it failed; client and daemon are the one package the module declares.
2026-10-03 15:07:21 +02:00
mesh-admin 510de183b1 Merge pull request 'The packet filter's tools are a bundle the node's runtime serves; its container goes (hq to-be 38 WP4)' (#239) from feat/wp4-the-packet-filter-moves into main 2026-10-03 11:14:33 +00:00
jschoubben 38be3ba7f0 Merge pull request 'ssh-client: own ~/.ssh, not the openssh package' (#238) from fix/ssh-client-owns-no-package into main 2026-10-03 10:43:46 +00:00
jschoubben a19638d112 ssh-client: own ~/.ssh, not the openssh package
A package has one owning module, and sshd already owns openssh on every node — assigning
ssh-client was refused for both declaring it, and the refusal left every node unresolvable
until it was unassigned. Owning it was wrong besides: unassigning ssh-client would have
removed the package sshd serves from. The client binary ships in the package sshd holds.
2026-10-03 12:43:35 +02:00
jschoubben 1cbddeb104 Merge pull request 'ssh-client module: the mesh owns ~/.ssh, config from the hub (to-be 29)' (#237) from feat/ssh-client-module into main 2026-10-03 10:40:13 +00:00
jschoubben 301aeda5f3 ssh-client module: the mesh owns ~/.ssh, config from the hub (to-be 29)
Requires openssh; creates ~/.ssh (0700, owned by the operator account via
${machine:account}); writes every other node's Host block (HostName + User
<account>) into a marked region of ~/.ssh/config (home-scoped, into:block), so
`ssh <node>` reaches each peer as the right account and the operator's own
config is kept. Universal-tier: assigned wherever a person logs in; a node with
no account gets no config.
2026-10-03 11:58:05 +02:00
22 changed files with 178 additions and 505 deletions
-24
View File
@@ -1,24 +0,0 @@
# baserow'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
WORKDIR /app/modules/baserow
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/baserow/dist /app/modules/baserow/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.
ENV MESH_TOOL_MODULES=/app/modules/baserow/dist/tools/index.js
+14 -36
View File
@@ -25,8 +25,7 @@
"postgres-database": "${dir:state}/database.secret" "postgres-database": "${dir:state}/database.secret"
}, },
"own-secrets": { "own-secrets": {
"admin": "${dir:state}/admin.secret", "admin": "${dir:state}/admin.secret"
"broker": "${dir:mesh-state}/broker"
}, },
"listens": [ "listens": [
{ {
@@ -92,45 +91,24 @@
"mode": "0600", "mode": "0600",
"content": "{\n \"password\": \"${secret:admin}\",\n \"host\": \"${bound:route:name}\"\n}\n", "content": "{\n \"password\": \"${secret:admin}\",\n \"host\": \"${bound:route:name}\"\n}\n",
"merge": "json" "merge": "json"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-baserow",
"network": "baserow",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_BASEROW_URL": "http://baserow:80",
"MESH_BASEROW_CONFIG_FILE": "/run/config/config.json"
},
"restart-on": [
"runtime-config"
],
"artifact": "runtime"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_BASEROW_URL": "http://127.0.0.1:${port:80}",
"MESH_BASEROW_CONFIG_FILE": "${dir:mesh-state}/config.json"
}
} }
] ]
} }
-24
View File
@@ -1,24 +0,0 @@
# confluence'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
WORKDIR /app/modules/confluence
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/confluence/dist /app/modules/confluence/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.
ENV MESH_TOOL_MODULES=/app/modules/confluence/dist/tools/index.js
+14 -40
View File
@@ -3,16 +3,9 @@
"version": "1", "version": "1",
"slug": "confl", "slug": "confl",
"own-secrets": { "own-secrets": {
"token": "${dir:state}/token", "token": "${dir:state}/token"
"broker": "${dir:mesh-state}/broker"
}, },
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -26,46 +19,27 @@
"merge": "json", "merge": "json",
"content": "{}", "content": "{}",
"mode": "0600" "mode": "0600"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-runtime-confluence",
"network": "host",
"volumes": [
"${dir:state}/config.json:/run/config/config.json:ro",
"${dir:state}/token:/run/secrets/token:ro",
"${dir:mesh-state}/broker:/run/secrets/broker:ro"
],
"env": {
"MESH_CONFLUENCE_TOKEN_FILE": "/run/secrets/token",
"MESH_CONFLUENCE_CONFIG_FILE": "/run/config/config.json",
"MESH_BROKER_FILE": "/run/secrets/broker"
},
"artifact": "runtime"
} }
], ],
"capabilities": [ "capabilities": [
"container-runtime" "container-runtime"
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_CONFLUENCE_TOKEN_FILE": "${dir:state}/token",
"MESH_CONFLUENCE_CONFIG_FILE": "${dir:state}/config.json"
}
} }
] ]
} }
File diff suppressed because one or more lines are too long
-23
View File
@@ -1,23 +0,0 @@
# fail2ban's runtime: the tool runtime, carrying the intrusion prevention's verbs and the client they
# speak through.
#
# Built from this module's own directory and nothing else (novox/hq ADR 0069). Two bases, named in
# module.json's `build.on`: the image this is compiled in and the image it runs in.
ARG BUILD_BASE
ARG RUNTIME_BASE
FROM ${BUILD_BASE} AS build
WORKDIR /app/modules/fail2ban
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
# The daemon runs on the machine, declared by this module; what runs here is only its client, which
# speaks to the daemon over the socket the machine shares into this container (novox/hq ADR 0179).
# The package brings the client and the daemon together; the daemon is never started here.
RUN apt-get update \
&& apt-get install -y --no-install-recommends fail2ban \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/modules/fail2ban/dist /app/modules/fail2ban/dist
ENV MESH_TOOL_MODULES=/app/modules/fail2ban/dist/tools/index.js
+41 -9
View File
@@ -5,12 +5,15 @@
// prevention seat's four verbs (ADR 0179). The daemon's state is fail2ban's, not the mesh's: the // prevention seat's four verbs (ADR 0179). The daemon's state is fail2ban's, not the mesh's: the
// mesh composes the jails and never writes the ban list. // mesh composes the jails and never writes the ban list.
// //
// Spoken through fail2ban-client over the daemon's socket, which the machine shares into this // Spoken through fail2ban-client over the daemon's socket. Client and daemon come from the one
// runtime; so the client here is the one from the runtime's own package and the daemon is the // package this module declares on the machine, and the socket is root's: root is the module's
// machine's, and the two meet at /var/run/fail2ban/fail2ban.sock. // concern (ADR 0175 §4), and the runtime loading this bundle runs as the operator's account (to-be
// 38 WP4), so the client is run through sudo without a prompt where the account is not root.
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import { accessSync, constants } from "node:fs";
import { isIP } from "node:net"; import { isIP } from "node:net";
import { delimiter, join } from "node:path";
import { promisify } from "node:util"; import { promisify } from "node:util";
const execFileP = promisify(execFile); const execFileP = promisify(execFile);
@@ -18,16 +21,44 @@ const execFileP = promisify(execFile);
/** A command runner, so the verbs can be tested without a daemon. */ /** A command runner, so the verbs can be tested without a daemon. */
export type Runner = (cmd: string, args: string[]) => Promise<string>; export type Runner = (cmd: string, args: string[]) => Promise<string>;
export const execRunner: Runner = async (cmd, args) => { /** The command as it is run: as given when this process is root, else through sudo without a
* prompt. The daemon's socket answers only to root. */
export function escalated(cmd: string, args: string[], uid: number | undefined = process.getuid?.()): [string, string[]] {
if (uid === 0) return [cmd, args];
return ["sudo", ["-n", cmd, ...args]];
}
/** Whether a tool is on this machine: an executable of that name on the path, or where the
* system keeps its administration. */
export function installed(tool: string, path: string = process.env.PATH ?? ""): boolean {
const dirs = [...path.split(delimiter), "/usr/sbin", "/sbin", "/usr/bin"].filter((d) => d !== "");
return dirs.some((dir) => {
try { try {
const { stdout } = await execFileP(cmd, args, { maxBuffer: 16 * 1024 * 1024 }); accessSync(join(dir, tool), constants.X_OK);
return true;
} catch {
return false;
}
});
}
export const execRunner: Runner = async (cmd, args) => {
if (!installed(cmd)) throw new Error(`${cmd} is not installed on this machine`);
const [program, argv] = escalated(cmd, args);
try {
const { stdout } = await execFileP(program, argv, { maxBuffer: 16 * 1024 * 1024 });
return stdout; return stdout;
} catch (err) { } catch (err) {
const e = err as { code?: string | number; stderr?: string; stdout?: string; message?: string }; const e = err as { code?: string | number; stderr?: string; stdout?: string; message?: string };
const said = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim(); const said = `${e.stdout ?? ""}${e.stderr ?? ""}`.trim();
if (e.code === "ENOENT") throw new Error(`${cmd} is not in this runtime`); // What failed is named by how it failed: sudo missing is a spawn error, sudo refusing speaks
if (/Failed to access socket path|Is fail2ban running/i.test(said)) { // on its own stderr line, and the rest is the client's own answer.
throw new Error("fail2ban is not running on this machine, or its socket is not shared with this runtime"); if (program === "sudo") {
if (e.code === "ENOENT") throw new Error(`${cmd} needs root, and sudo is not installed here for the runtime's account to escalate with`);
if (/^sudo:/m.test(said)) throw new Error(`${cmd} needs root and the runtime's account may not run it without a prompt: ${said}`);
}
if (/Failed to access socket path|Is fail2ban running|Permission denied to socket/i.test(said)) {
throw new Error("fail2ban is not running on this machine, or its socket does not answer the runtime's account");
} }
// fail2ban-client's own last line is the one a person reads ("Sorry but the jail 'x' does not exist"). // fail2ban-client's own last line is the one a person reads ("Sorry but the jail 'x' does not exist").
const lines = said.split("\n").map((l) => l.trim()).filter(Boolean); const lines = said.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -76,7 +107,8 @@ export class Fail2banClient {
this.run = run; this.run = run;
} }
static fromEnv(_env: NodeJS.ProcessEnv = process.env): Fail2banClient { /** The daemon as this machine has it, through its own client. */
static onThisMachine(): Fail2banClient {
return new Fail2banClient(); return new Fail2banClient();
} }
+6 -38
View File
@@ -19,9 +19,6 @@
"tools": [ "tools": [
"fail2ban_settings" "fail2ban_settings"
], ],
"own-secrets": {
"broker": "${dir:mesh-state}/broker"
},
"jailing": { "jailing": {
"into": "/etc/fail2ban/jail.d/mesh.conf", "into": "/etc/fail2ban/jail.d/mesh.conf",
"filter-into": "/etc/fail2ban/filter.d" "filter-into": "/etc/fail2ban/filter.d"
@@ -56,12 +53,6 @@
"path": "/var/run/fail2ban", "path": "/var/run/fail2ban",
"mode": "0755" "mode": "0755"
}, },
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "jail-local", "id": "jail-local",
"type": "file", "type": "file",
@@ -118,40 +109,17 @@
"action-dualchain", "action-dualchain",
"composed-jails" "composed-jails"
] ]
},
{
"id": "runtime",
"type": "container",
"name": "mesh-fail2ban",
"artifact": "runtime",
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"/var/run/fail2ban:/var/run/fail2ban"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker"
}
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
]
} }
] ]
} }
+1 -1
View File
@@ -12,7 +12,7 @@
"typescript": "^5.6.0" "typescript": "^5.6.0"
}, },
"scripts": { "scripts": {
"build": "tsc client.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist", "build": "tsc client.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --rootDir . --outDir dist",
"test": "node --test --experimental-strip-types 'test/*.test.ts'" "test": "node --test --experimental-strip-types 'test/*.test.ts'"
} }
} }
+9 -1
View File
@@ -2,7 +2,7 @@
// on the control node on 2026-10-02 (novox/hq ADR 0179). // on the control node on 2026-10-02 (novox/hq ADR 0179).
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { Fail2banClient, parseBans, parseJailStatus, type Runner } from "../client.ts"; import { Fail2banClient, escalated, installed, parseBans, parseJailStatus, type Runner } from "../client.ts";
const STATUS = "Status\n|- Number of jail:\t2\n`- Jail list:\trecidive, sshd\n"; const STATUS = "Status\n|- Number of jail:\t2\n`- Jail list:\trecidive, sshd\n";
const RECIDIVE = const RECIDIVE =
@@ -104,3 +104,11 @@ test("a jail's settings are read from the daemon's listings", async () => {
logpath: [], journalmatch: "_SYSTEMD_UNIT=sshd.service + _COMM=sshd", logpath: [], journalmatch: "_SYSTEMD_UNIT=sshd.service + _COMM=sshd",
}); });
}); });
test("the client runs as given by root and through sudo without a prompt by anyone else", () => {
assert.deepEqual(escalated("fail2ban-client", ["status"], 0), ["fail2ban-client", ["status"]]);
assert.deepEqual(escalated("fail2ban-client", ["set", "sshd", "banip", "198.51.100.7"], 1000),
["sudo", ["-n", "fail2ban-client", "set", "sshd", "banip", "198.51.100.7"]]);
assert.equal(installed("sh"), true);
assert.equal(installed("no-such-client-of-the-mesh"), false);
});
+1 -1
View File
@@ -55,7 +55,7 @@ export function getFail2banTools(fail2ban: Fail2banClient): ToolDefinition[] {
]; ];
} }
const fail2ban = Fail2banClient.fromEnv(); const fail2ban = Fail2banClient.onThisMachine();
// The seat's verbs under the seat's name: the runtime serves them on the seat's subjects where this // The seat's verbs under the seat's name: the runtime serves them on the seat's subjects where this
// module holds it (ADR 0159, 0160). The module's own under its own. // module holds it (ADR 0159, 0160). The module's own under its own.
registerModuleTools("node-intrusion-prevention", () => getSeatVerbs(fail2ban)); registerModuleTools("node-intrusion-prevention", () => getSeatVerbs(fail2ban));
-24
View File
@@ -1,24 +0,0 @@
# gitlab'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
WORKDIR /app/modules/gitlab
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/gitlab/dist /app/modules/gitlab/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.
ENV MESH_TOOL_MODULES=/app/modules/gitlab/dist/tools/index.js
+14 -40
View File
@@ -2,16 +2,9 @@
"module": "gitlab", "module": "gitlab",
"version": "1", "version": "1",
"own-secrets": { "own-secrets": {
"token": "${dir:state}/token", "token": "${dir:state}/token"
"broker": "${dir:mesh-state}/broker"
}, },
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -25,46 +18,27 @@
"merge": "json", "merge": "json",
"content": "{}", "content": "{}",
"mode": "0600" "mode": "0600"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-runtime-gitlab",
"network": "host",
"volumes": [
"${dir:state}/config.json:/run/config/config.json:ro",
"${dir:state}/token:/run/secrets/token:ro",
"${dir:mesh-state}/broker:/run/secrets/broker:ro"
],
"env": {
"MESH_GITLAB_TOKEN_FILE": "/run/secrets/token",
"MESH_GITLAB_CONFIG_FILE": "/run/config/config.json",
"MESH_BROKER_FILE": "/run/secrets/broker"
},
"artifact": "runtime"
} }
], ],
"capabilities": [ "capabilities": [
"container-runtime" "container-runtime"
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_GITLAB_TOKEN_FILE": "${dir:state}/token",
"MESH_GITLAB_CONFIG_FILE": "${dir:state}/config.json"
}
} }
] ]
} }
-24
View File
@@ -1,24 +0,0 @@
# jira'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
WORKDIR /app/modules/jira
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/jira/dist /app/modules/jira/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.
ENV MESH_TOOL_MODULES=/app/modules/jira/dist/tools/index.js
+14 -40
View File
@@ -2,16 +2,9 @@
"module": "jira", "module": "jira",
"version": "1", "version": "1",
"own-secrets": { "own-secrets": {
"token": "${dir:state}/token", "token": "${dir:state}/token"
"broker": "${dir:mesh-state}/broker"
}, },
"resources": [ "resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh"
},
{ {
"id": "state", "id": "state",
"type": "directory", "type": "directory",
@@ -25,46 +18,27 @@
"merge": "json", "merge": "json",
"content": "{}", "content": "{}",
"mode": "0600" "mode": "0600"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-runtime-jira",
"network": "host",
"volumes": [
"${dir:state}/config.json:/run/config/config.json:ro",
"${dir:state}/token:/run/secrets/token:ro",
"${dir:mesh-state}/broker:/run/secrets/broker:ro"
],
"env": {
"MESH_JIRA_TOKEN_FILE": "/run/secrets/token",
"MESH_JIRA_CONFIG_FILE": "/run/config/config.json",
"MESH_BROKER_FILE": "/run/secrets/broker"
},
"artifact": "runtime"
} }
], ],
"capabilities": [ "capabilities": [
"container-runtime" "container-runtime"
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_JIRA_TOKEN_FILE": "${dir:state}/token",
"MESH_JIRA_CONFIG_FILE": "${dir:state}/config.json"
}
} }
] ]
} }
-24
View File
@@ -1,24 +0,0 @@
# letta'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
WORKDIR /app/modules/letta
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/letta/dist /app/modules/letta/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.
ENV MESH_TOOL_MODULES=/app/modules/letta/dist/tools/index.js
+14 -36
View File
@@ -26,8 +26,7 @@
}, },
"own-secrets": { "own-secrets": {
"server-password": "${dir:state}/server-password.secret", "server-password": "${dir:state}/server-password.secret",
"openai-api-key": "${dir:state}/openai-api-key.secret", "openai-api-key": "${dir:state}/openai-api-key.secret"
"broker": "${dir:mesh-state}/broker"
}, },
"listens": [ "listens": [
{ {
@@ -84,45 +83,24 @@
"mode": "0600", "mode": "0600",
"content": "{\n \"password\": \"${secret:server-password}\"\n}\n", "content": "{\n \"password\": \"${secret:server-password}\"\n}\n",
"merge": "json" "merge": "json"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-letta",
"network": "letta",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_LETTA_URL": "http://letta:8283",
"MESH_LETTA_CONFIG_FILE": "/run/config/config.json"
},
"restart-on": [
"runtime-config"
],
"artifact": "runtime"
} }
], ],
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_LETTA_URL": "http://127.0.0.1:${port:8283}",
"MESH_LETTA_CONFIG_FILE": "${dir:mesh-state}/config.json"
}
} }
] ]
} }
-24
View File
@@ -1,24 +0,0 @@
# searxng'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
WORKDIR /app/modules/searxng
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/searxng/dist /app/modules/searxng/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.
ENV MESH_TOOL_MODULES=/app/modules/searxng/dist/tools/index.js
+14 -36
View File
@@ -8,8 +8,7 @@
"secret": { "secret": {
"path": "${dir:mesh-state}/secret", "path": "${dir:mesh-state}/secret",
"taken": "at-start" "taken": "at-start"
}, }
"broker": "${dir:mesh-state}/broker"
}, },
"listens": [ "listens": [
{ {
@@ -91,25 +90,6 @@
"path": "${dir:mesh-state}/config.json", "path": "${dir:mesh-state}/config.json",
"mode": "0600", "mode": "0600",
"content": "{}\n" "content": "{}\n"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-searxng",
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_SEARXNG_URL": "http://127.0.0.1:${port:8080}",
"MESH_SEARXNG_CONFIG_FILE": "/run/config/config.json"
},
"restart-on": [
"runtime-config"
],
"artifact": "runtime"
} }
], ],
"requires": [ "requires": [
@@ -125,23 +105,21 @@
"route": "${dir:state}/route.json" "route": "${dir:state}/route.json"
}, },
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_SEARXNG_URL": "http://127.0.0.1:${port:8080}",
"MESH_SEARXNG_CONFIG_FILE": "${dir:mesh-state}/config.json"
}
} }
] ]
} }
+21
View File
@@ -0,0 +1,21 @@
{
"module": "ssh-client",
"version": "1",
"resources": [
{
"id": "ssh-dir",
"type": "directory",
"path": "${machine:account-home}/.ssh",
"owner": "${machine:account}",
"mode": "0700"
}
],
"facts": {
"ssh-config": {
"path": ".ssh/config",
"home": true,
"shared": true,
"template": "# The mesh's Host blocks — every other node, so `ssh <node>` reaches it as the\n# right account. This region is replaced whenever a node joins, leaves or is\n# renamed; the rest of this file is yours and is kept untouched.\n{{range .Machines}}{{if ne .Name $.Node}}\nHost {{.Name}} {{.FQDN}}\n HostName {{.FQDN}}\n{{if .Account}} User {{.Account}}\n{{end}}{{end}}{{end}}"
}
}
}
-24
View File
@@ -1,24 +0,0 @@
# unifi'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
WORKDIR /app/modules/unifi
COPY . .
RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts \
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
FROM ${RUNTIME_BASE}
COPY --from=build /app/modules/unifi/dist /app/modules/unifi/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.
ENV MESH_TOOL_MODULES=/app/modules/unifi/dist/tools/index.js
+13 -35
View File
@@ -122,25 +122,6 @@
"mode": "0600", "mode": "0600",
"content": "{\n \"site\": \"default\",\n \"password\": \"${secret:controller}\"\n}\n", "content": "{\n \"site\": \"default\",\n \"password\": \"${secret:controller}\"\n}\n",
"merge": "json" "merge": "json"
},
{
"id": "runtime",
"type": "container",
"name": "mesh-unifi",
"network": "host",
"volumes": [
"${dir:mesh-state}/broker:/run/secrets/broker:ro",
"${dir:mesh-state}/config.json:/run/config/config.json:ro"
],
"env": {
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_UNIFI_URL": "https://127.0.0.1:${port:8443}",
"MESH_UNIFI_CONFIG_FILE": "/run/config/config.json"
},
"restart-on": [
"runtime-config"
],
"artifact": "runtime"
} }
], ],
"requires": [ "requires": [
@@ -158,27 +139,24 @@
"route": "${dir:state}/route.json" "route": "${dir:state}/route.json"
}, },
"own-secrets": { "own-secrets": {
"broker": "${dir:mesh-state}/broker",
"controller": "${dir:mesh-state}/controller" "controller": "${dir:mesh-state}/controller"
}, },
"build": { "build": {
"on": [
{
"arg": "BUILD_BASE",
"module": "mesh-tools",
"artifact": "build"
},
{
"arg": "RUNTIME_BASE",
"module": "mesh-tools",
"artifact": "runtime"
}
],
"artifacts": [ "artifacts": [
{ {
"name": "runtime", "name": "tools",
"kind": "image", "kind": "bundle",
"from": "Dockerfile" "language": "typescript",
"entrypoints": [
"tools/index.js"
],
"loads": [
"tools/index.js"
],
"env": {
"MESH_UNIFI_URL": "https://127.0.0.1:${port:8443}",
"MESH_UNIFI_CONFIG_FILE": "${dir:mesh-state}/config.json"
}
} }
] ]
} }