Beds read the catalogue: one loader, eight beds converted, the rest declared (hq issue 073) #41
@@ -0,0 +1,102 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { catalogueIsPresent, catalogueDir } from "./integration/harness.ts";
|
||||
|
||||
/**
|
||||
* A bed installs a catalogue module by reading the catalogue, never by carrying a copy.
|
||||
*
|
||||
* The beds used to build the manifests they install inline, as literals taken from the catalogue
|
||||
* when each bed was written. The copies did not move when the catalogue did: six modules were
|
||||
* converted to file-delivered secrets and not one bed ran the converted shape, because every bed
|
||||
* ran its own copy (novox/hq 04-ISSUES/073). "Proven in the lab" then meant "the copy was proven".
|
||||
*
|
||||
* So: a manifest literal in a bed that names a catalogue module is refused, unless the bed is
|
||||
* listed below with the reason it still carries one. The list is the debt, and it only shrinks.
|
||||
*
|
||||
* What this reads: `module: "<name>"` and `"module": "<name>"` with a `version` close behind, in
|
||||
* test/integration/*.test.ts, against the catalogue's directory names. A bed that hid the name
|
||||
* behind a computed string would pass — this is a fence, not a proof, and the reviewer of a bed
|
||||
* that builds a manifest inline is the proof.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Beds that still carry an inline copy of a catalogue module's manifest, and why. Three reasons
|
||||
* recur, and each names the work that removes the entry:
|
||||
*
|
||||
* BESIDE the catalogue's module CLAIMS the foundation's container (postgres claims mesh-store,
|
||||
* lavinmq mesh-broker) and adopts it in place; the bed raises a second one beside the
|
||||
* foundation's instead. Reading the catalogue changes what the bed raises — it would
|
||||
* adopt — and the bed's assertions with it.
|
||||
* WEARING the bed proves a mesh mechanism (a grant, a credential, a restart, a route) with a
|
||||
* module cut down to the shape the mechanism needs — no upstream server, a secret in the
|
||||
* environment, a requirement edge removed — and gives it a catalogue name. It is a mesh
|
||||
* test wearing a catalogue module's name. It should carry a name of its own, or read the
|
||||
* catalogue and meet the module's real requirements.
|
||||
* DIFFERS a module bed whose copy differs from the catalogue in more than the lab may rewrite
|
||||
* (an image, a port, an address). Reading the catalogue is the fix and needs a run.
|
||||
*/
|
||||
const STILL_CARRIED: Record<string, { modules: string[]; why: string }> = {
|
||||
"assigned-catalogue-apps.test.ts": { modules: ["postgres", "mongodb", "unifi", "marrytts"],
|
||||
why: "BESIDE (postgres); DIFFERS (unifi takes its credentials from the environment, mongodb and marrytts drop listens)" },
|
||||
"assigned-catalogue-media.test.ts": { modules: ["sonarr", "radarr"],
|
||||
why: "DIFFERS: both drop the route requirement the catalogue declares, and take their API keys from the environment" },
|
||||
"assigned-catalogue-small.test.ts": { modules: ["postgres", "minio", "redis", "plex"],
|
||||
why: "BESIDE (postgres); DIFFERS (minio's root password by env-file, redis minting its own secret instead of the vault's, plex without its server)" },
|
||||
"assigned-model-usage.test.ts": { modules: ["postgres"], why: "BESIDE" },
|
||||
"assigned-two-node-db.test.ts": { modules: ["redis", "baserow", "letta"],
|
||||
why: "DIFFERS: redis mints its own secret, baserow drops its route requirement, letta drops its ports" },
|
||||
"lavinmq-bed.test.ts": { modules: ["lavinmq", "amqp-ping"],
|
||||
why: "BESIDE (lavinmq, with a bootstrap step and a data directory the catalogue has not got); DIFFERS (amqp-ping names its entrypoint)" },
|
||||
"assigned-grafana.test.ts": { modules: ["grafana"], why: "WEARING: the sidecar alone, no Grafana, no route" },
|
||||
"assigned-plex.test.ts": { modules: ["plex"], why: "WEARING: the sidecar alone, no Plex, the token in the environment" },
|
||||
"assigned-redis.test.ts": { modules: ["redis"], why: "WEARING: its own secret, a lab seal key in the environment" },
|
||||
"assigned-sonarr.test.ts": { modules: ["sonarr"], why: "WEARING: the sidecar alone against a forged config.xml" },
|
||||
"mesh-grant-end-to-end.test.ts": { modules: ["redis"], why: "WEARING: a grant mechanism test" },
|
||||
"minio-grant-end-to-end.test.ts": { modules: ["minio"], why: "WEARING: a grant mechanism test, the root password by env-file" },
|
||||
"postgres-grant-end-to-end.test.ts": { modules: ["postgres"], why: "WEARING: a grant mechanism test, the superuser by env-file" },
|
||||
"provider-on-backend-network.test.ts": { modules: ["redis"], why: "WEARING: a network mechanism test" },
|
||||
"provider-uses-mesh-credential.test.ts": { modules: ["redis"], why: "WEARING: a credential mechanism test" },
|
||||
"runtime-restart-on-config.test.ts": { modules: ["grafana"], why: "WEARING: a restart mechanism test" },
|
||||
"route-forwarding.test.ts": { modules: ["route-proxy", "hello-web"],
|
||||
why: "WEARING: route-proxy without its certificate authority, hello-web with the route shape ADR 0066 replaced" },
|
||||
"mesh.test.ts": { modules: ["postgres", "builder", "umami"],
|
||||
why: "WEARING: a postgres with no resources, a builder that builds itself, an umami that is another module of that name" },
|
||||
};
|
||||
|
||||
const beds = resolve(import.meta.dirname, "integration");
|
||||
|
||||
test("a bed that installs a catalogue module reads the catalogue", (t) => {
|
||||
const absent = catalogueIsPresent();
|
||||
if (absent) {
|
||||
// Said, not silent: a check that cannot see the catalogue has checked nothing.
|
||||
t.skip(`cannot check — ${absent}`);
|
||||
return;
|
||||
}
|
||||
const names = new Set(readdirSync(catalogueDir(), { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && existsSync(resolve(catalogueDir(), d.name, "module.json")))
|
||||
.map((d) => d.name));
|
||||
|
||||
const offences: string[] = [];
|
||||
for (const file of readdirSync(beds).filter((f) => f.endsWith(".test.ts")).sort()) {
|
||||
const text = readFileSync(resolve(beds, file), "utf8");
|
||||
const found = new Set<string>();
|
||||
// A manifest literal: the module's name with its version close behind it. A `module:` key
|
||||
// elsewhere (a table of what to register, a grant entry) has no version and is not one.
|
||||
for (const m of text.matchAll(/(?:^|[\s{,])(?:"module"|module)\s*:\s*"([a-z0-9.-]+)"[^}]{0,160}?(?:"version"|version)\s*:/g)) {
|
||||
if (names.has(m[1]!)) found.add(m[1]!);
|
||||
}
|
||||
const declared = STILL_CARRIED[file];
|
||||
for (const name of [...found].sort()) {
|
||||
if (declared?.modules.includes(name)) continue;
|
||||
offences.push(`${file}: an inline manifest for the catalogue's '${name}'`);
|
||||
}
|
||||
for (const name of declared?.modules ?? []) {
|
||||
if (!found.has(name)) offences.push(`${file}: declared as still carrying '${name}', and it does not — remove the declaration`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(offences, [],
|
||||
`a bed carries a copy of a catalogue manifest; read it with catalogueModule() from the harness:\n ${offences.join("\n ")}`);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -62,7 +62,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "anthropic-bed";
|
||||
const MACHINE = "anchor";
|
||||
@@ -194,8 +194,6 @@ after(async () => {
|
||||
test("model access refreshes on the manager node and delivers only the access token, never the refresh token", {
|
||||
skip, timeout: 1_500_000,
|
||||
}, async () => {
|
||||
const managerImage = pinned("mesh-runtime-anthropic-manager");
|
||||
const consumerImage = pinned("mesh-runtime-anthropic-consumer");
|
||||
|
||||
// --- the licence, and the manager as its holder ------------------------------------------------
|
||||
// The manager module is a HOLDER, named the licence's manager. It is delivered the refresh token;
|
||||
@@ -207,37 +205,15 @@ test("model access refreshes on the manager node and delivers only the access to
|
||||
await mesh(`licence use personal ${MACHINE} anthropic-manager`);
|
||||
|
||||
// --- the manager module, deployed so the host delivers its bound facts --------------------------
|
||||
// Inline manifest mirroring the committed module.json: model-access holder, refresh token bound as a
|
||||
// sealed secret, no node-key mount. The scheduled container installs as present state (ADR 0053);
|
||||
// the test drives adopt/refresh directly for a deterministic flow rather than waiting on cron.
|
||||
const managerManifest = JSON.stringify({
|
||||
module: "anthropic-manager",
|
||||
version: "1",
|
||||
requires: ["model-access"],
|
||||
binds: { "model-access": "/var/lib/mesh/anthropic-manager/model.json" },
|
||||
secrets: { "model-access": "/var/lib/mesh/anthropic-manager/refresh-token" },
|
||||
"own-secrets": { broker: "/var/lib/mesh/anthropic-manager/broker" },
|
||||
emits: ["module.anthropic-manager.usage.read"],
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/anthropic-manager", mode: "0700" },
|
||||
{ id: "out", type: "directory", path: "/var/lib/mesh/anthropic-manager/out", mode: "0700" },
|
||||
{
|
||||
id: "refresh", type: "container", name: "mesh-anthropic-manager-refresh",
|
||||
image: managerImage, network: "host", schedule: "*/9 * * * *",
|
||||
args: ["run", "/app/modules/anthropic-manager/dist/refresh/index.js"],
|
||||
volumes: ["/var/lib/mesh/anthropic-manager:/run/state"],
|
||||
env: {
|
||||
MESH_ANTHROPIC_LICENCE: "personal",
|
||||
MESH_ANTHROPIC_TOKEN_ENDPOINT: "http://127.0.0.1:9099/token",
|
||||
MESH_ANTHROPIC_USAGE_ENDPOINT: "http://127.0.0.1:9099/usage",
|
||||
MESH_MODEL_ACCESS_SECRET_FILE: "/run/state/refresh-token",
|
||||
MESH_MODEL_ACCESS_BIND_FILE: "/run/state/model.json",
|
||||
MESH_ANTHROPIC_ACCESS_OUT: "/run/state/out/access-token",
|
||||
MESH_ANTHROPIC_GRANT_OUT: "/run/state/out/grant.json",
|
||||
MESH_ANTHROPIC_USAGE_OUT: "/run/state/out/usage.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
// The catalogue's own manifest (novox/hq 04-ISSUES/073): model-access holder, refresh token bound
|
||||
// as a sealed secret, no node-key mount. The scheduled container installs as present state (ADR
|
||||
// 0053); the test drives adopt/refresh directly for a deterministic flow rather than waiting on
|
||||
// cron. The one lab rewrite: the OAuth endpoints point at the stub this bed raises below.
|
||||
const managerManifest = catalogueModule("anthropic-manager", held, {
|
||||
env: { refresh: {
|
||||
MESH_ANTHROPIC_TOKEN_ENDPOINT: "http://127.0.0.1:9099/token",
|
||||
MESH_ANTHROPIC_USAGE_ENDPOINT: "http://127.0.0.1:9099/usage",
|
||||
} },
|
||||
});
|
||||
await must(`printf %s ${quote(managerManifest)} > /tmp/anthropic-manager.json && docker cp /tmp/anthropic-manager.json mesh-controller:/anthropic-manager.json`);
|
||||
await mesh(`module add /anthropic-manager.json`);
|
||||
@@ -329,32 +305,8 @@ test("model access refreshes on the manager node and delivers only the access to
|
||||
assert.match(submitted, /sealed to 1 holder/, submitted);
|
||||
|
||||
// --- 5. deliver: deploy the consumer and push; it gets the sealed access token -------------------
|
||||
const consumerManifest = JSON.stringify({
|
||||
module: "anthropic-consumer",
|
||||
version: "1",
|
||||
requires: ["model-access"],
|
||||
binds: { "model-access": "/var/lib/anthropic-consumer/model.json" },
|
||||
secrets: { "model-access": "/var/lib/anthropic-consumer/access-token" },
|
||||
"own-secrets": { broker: "/var/lib/mesh/anthropic-consumer/broker" },
|
||||
emits: ["module.anthropic-consumer.usage.session"],
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/anthropic-consumer", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/anthropic-consumer", mode: "0700" },
|
||||
{ id: "claude-home", type: "directory", path: "/var/lib/anthropic-consumer/claude", mode: "0700" },
|
||||
{
|
||||
id: "apply", type: "container", name: "mesh-anthropic-consumer-apply",
|
||||
image: consumerImage, network: "host", schedule: "*/9 * * * *",
|
||||
args: ["run", "/app/modules/anthropic-consumer/dist/apply/index.js"],
|
||||
volumes: ["/var/lib/anthropic-consumer:/run/state"],
|
||||
env: {
|
||||
MESH_MODEL_ACCESS_SECRET_FILE: "/run/state/access-token",
|
||||
MESH_MODEL_ACCESS_BIND_FILE: "/run/state/model.json",
|
||||
MESH_CLAUDE_CREDENTIALS_FILE: "/run/state/claude/.credentials.json",
|
||||
MESH_CLAUDE_IDENTITY_FILE: "/run/state/claude/.claude.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// The catalogue's own manifest (novox/hq 04-ISSUES/073).
|
||||
const consumerManifest = catalogueModule("anthropic-consumer", held);
|
||||
await must(`printf %s ${quote(consumerManifest)} > /tmp/anthropic-consumer.json && docker cp /tmp/anthropic-consumer.json mesh-controller:/anthropic-consumer.json`);
|
||||
await mesh(`module add /anthropic-consumer.json`);
|
||||
await mesh(`module issue anthropic-consumer --node ${MACHINE}`);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -35,7 +35,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "audit-node";
|
||||
const MACHINE = "anchor";
|
||||
@@ -145,26 +145,10 @@ after(async () => {
|
||||
test("the mesh assigns the audit logger, and it consumes over the account the mesh delivered", {
|
||||
skip, timeout: 900_000,
|
||||
}, async () => {
|
||||
// The assigned-module manifest (mesh-catalog), its runtime image the ID the machine holds.
|
||||
const manifest = JSON.stringify({
|
||||
module: "audit-logger",
|
||||
version: "1",
|
||||
consumes: ["#"],
|
||||
"own-secrets": { broker: "/var/lib/audit-logger/broker" },
|
||||
resources: [
|
||||
{ id: "state", type: "directory", path: "/var/lib/audit-logger", mode: "0700" },
|
||||
{ id: "trail", type: "directory", path: "/var/lib/audit-logger/trail", mode: "0700" },
|
||||
{
|
||||
id: "run", type: "container", name: "mesh-audit-logger", image: pinned("mesh-runtime-audit"),
|
||||
network: "host",
|
||||
volumes: [
|
||||
"/var/lib/audit-logger/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/audit-logger/trail:/trail",
|
||||
],
|
||||
env: { MESH_BROKER_FILE: "/run/secrets/broker", AUDIT_LOG: "/trail/audit.log" },
|
||||
},
|
||||
],
|
||||
});
|
||||
// The catalogue's manifest (novox/hq 04-ISSUES/073). Its runtime artifact is the image this
|
||||
// scenario stocks under the module's slug, `mesh-runtime-audit` — built by scripts/build-runtime-image.sh
|
||||
// before build-module-runtime.sh generalised it, and named as it was.
|
||||
const manifest = catalogueModule("audit-logger", held, { artifacts: { runtime: "mesh-runtime-audit" } });
|
||||
await must(`printf %s ${quote(manifest)} > /tmp/audit.json && docker cp /tmp/audit.json mesh-controller:/audit.json`);
|
||||
await mesh("module add /audit.json");
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -49,7 +49,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "catalogue-mqtt";
|
||||
const MACHINE = "anchor";
|
||||
@@ -158,101 +158,11 @@ test("the mesh assigns mosquitto: a run-once step seeds dynsec before the broker
|
||||
skip, timeout: 1_500_000,
|
||||
}, async () => {
|
||||
// mosquitto's dynsec config: the plugin refuses to start unless dynamic-security.json holds an
|
||||
// admin client, so the store MUST be seeded first. The `run-once` bootstrap container is declared
|
||||
// BEFORE `server` (the broker) and reuses the module's runtime image; the host runs it to
|
||||
// completion, then starts the broker. The runtime `server`/`runtime` shape mirrors the committed
|
||||
// manifest, with images pinned to what this scenario serves by digest.
|
||||
const mosquittoConf =
|
||||
"persistence true\n" +
|
||||
"persistence_location /mosquitto/data\n\n" +
|
||||
"log_dest stdout\n" +
|
||||
"log_type warning\n" +
|
||||
"log_type error\n" +
|
||||
"log_type notice\n\n" +
|
||||
"# Every client authenticates; identities and their per-topic ACLs are managed\n" +
|
||||
"# at runtime by the dynamic security plugin, whose store the plugin itself owns.\n" +
|
||||
"allow_anonymous false\n" +
|
||||
"plugin /usr/lib/mosquitto_dynamic_security.so\n" +
|
||||
"plugin_opt_config_file /mosquitto/data/dynamic-security.json\n\n" +
|
||||
"# MQTT listener\n" +
|
||||
"listener 1883\n\n" +
|
||||
"# MQTT-over-WebSockets listener\n" +
|
||||
"listener 8081\n" +
|
||||
"protocol websockets\n";
|
||||
|
||||
const manifest = JSON.stringify({
|
||||
module: "mosquitto",
|
||||
version: "1",
|
||||
provides: [{ name: "mqtt-topic", scope: "mesh" }],
|
||||
serves: { "mqtt-topic": {} },
|
||||
emits: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
|
||||
// The events entrypoint subscribes to its own lifecycle events (an audit log), so it consumes
|
||||
// them too — declared, or the foundation never makes the queue the runtime binds (ADR 0046).
|
||||
consumes: ["module.mosquitto.topic.provisioned", "module.mosquitto.topic.deprovisioned"],
|
||||
receives: { "mqtt-topic": "/var/lib/mosquitto-module/grants/mesh.json" },
|
||||
grants: { "mqtt-topic": "/var/lib/mosquitto-module/grants" },
|
||||
"own-secrets": {
|
||||
admin: "/var/lib/mosquitto-module/admin.secret",
|
||||
broker: "/var/lib/mesh/mosquitto/broker",
|
||||
},
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/mosquitto", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/mosquitto-module", mode: "0700" },
|
||||
{ id: "grants-dir", type: "directory", path: "/var/lib/mosquitto-module/grants", mode: "0700" },
|
||||
// The broker runs as uid 1883, so the shared data directory it seeds into and persists to is
|
||||
// its own.
|
||||
{ id: "data", type: "directory", path: "/services/mosquitto/data", mode: "0700", owner: "1883:1883" },
|
||||
{
|
||||
id: "server-conf", type: "file", path: "/var/lib/mosquitto-module/mosquitto.conf",
|
||||
mode: "0600", owner: "1883:1883", content: mosquittoConf,
|
||||
},
|
||||
{ id: "net", type: "network", name: "mosquitto" },
|
||||
// THE run-once step: seed dynsec offline, once, before the broker. It reuses the runtime image
|
||||
// (`mesh-tools run <bootstrap>` imports mosquitto's bootstrap entrypoint, which writes the
|
||||
// admin client into dynamic-security.json and chowns it to the broker's uid, then exits). It is
|
||||
// declared BEFORE `server`; the host runs it to completion and requires exit 0 before starting
|
||||
// the broker.
|
||||
{
|
||||
id: "bootstrap", type: "container", name: "mosquitto-bootstrap",
|
||||
image: pinned("mesh-runtime-mosquitto"), "run-once": true,
|
||||
volumes: [
|
||||
"/services/mosquitto/data:/mosquitto/data",
|
||||
"/var/lib/mosquitto-module/admin.secret:/run/secrets/admin:ro",
|
||||
],
|
||||
env: {
|
||||
MESH_PROVISION_MQTT: "mosquitto:1883",
|
||||
MESH_PROVISION_ADMIN_USER: "mesh-admin",
|
||||
MESH_PROVISION_PASSWORD_FILE: "/run/secrets/admin",
|
||||
MESH_DYNSEC_FILE: "/mosquitto/data/dynamic-security.json",
|
||||
},
|
||||
args: ["run", "/app/modules/mosquitto/dist/bootstrap/index.js"],
|
||||
},
|
||||
{
|
||||
id: "server", type: "container", name: "mosquitto", image: pinned("eclipse-mosquitto"),
|
||||
network: "mosquitto", ports: ["1883", "8081"],
|
||||
volumes: [
|
||||
"/services/mosquitto/data:/mosquitto/data",
|
||||
"/var/lib/mosquitto-module/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "runtime", type: "container", name: "mesh-mosquitto",
|
||||
image: pinned("mesh-runtime-mosquitto"), network: "mosquitto",
|
||||
volumes: [
|
||||
"/var/lib/mesh/mosquitto/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mosquitto-module/grants:/var/lib/mosquitto-module/grants:ro",
|
||||
"/var/lib/mosquitto-module/admin.secret:/run/secrets/admin:ro",
|
||||
],
|
||||
env: {
|
||||
MESH_BROKER_FILE: "/run/secrets/broker",
|
||||
MESH_RECEIVES: "/var/lib/mosquitto-module/grants/mesh.json",
|
||||
MESH_PROVISION_MQTT: "mosquitto:1883",
|
||||
MESH_PROVISION_ADMIN_USER: "mesh-admin",
|
||||
MESH_PROVISION_PASSWORD_FILE: "/run/secrets/admin",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// admin client, so the store MUST be seeded first. The catalogue's manifest declares a `run-once`
|
||||
// bootstrap container BEFORE `server` (the broker), reusing the module's runtime image; the host
|
||||
// runs it to completion, then starts the broker. The manifest is the catalogue's own, its runtime
|
||||
// artifact the image this scenario stocked (novox/hq 04-ISSUES/073).
|
||||
const manifest = catalogueModule("mosquitto", held);
|
||||
|
||||
await must(`printf %s ${quote(manifest)} > /tmp/mosquitto.json && docker cp /tmp/mosquitto.json mesh-controller:/mosquitto.json`);
|
||||
await mesh("module add /mosquitto.json");
|
||||
|
||||
@@ -38,7 +38,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, deriveTheFilterOn } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, deriveTheFilterOn, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -51,7 +51,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "model-usage-bed";
|
||||
/** The node that carries the postgres provider and the model-usage consumer. anchor carries only the
|
||||
@@ -190,7 +190,9 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
||||
}, async () => {
|
||||
// ================================================================================================
|
||||
// THE MANIFESTS — postgres verbatim from two-node-db (its server publishes 5432 so the consumer
|
||||
// reaches it), and model-usage the committed catalogue shape with its images pinned.
|
||||
// reaches it): a SECOND postgres beside the foundation's store, which the catalogue's postgres would
|
||||
// instead claim and adopt in place. Still an inline copy, declared in beds-read-the-catalogue.test.ts
|
||||
// (novox/hq 04-ISSUES/073). model-usage is the catalogue's.
|
||||
// ================================================================================================
|
||||
const postgresManifest = JSON.stringify({
|
||||
module: "postgres",
|
||||
@@ -233,45 +235,12 @@ test("usage events are upserted into model-usage's store — latest-per-key, bot
|
||||
],
|
||||
});
|
||||
|
||||
// --- model-usage: requires postgres-database, owns a provisioned store, consumes module.*.usage.*,
|
||||
// runs a run-once migrate then the long-lived event consumer. Both containers on the host network so
|
||||
// they reach the granted postgres (at the provider's address the mesh writes) and the broker. ------
|
||||
const modelUsageManifest = JSON.stringify({
|
||||
module: "model-usage",
|
||||
version: "1",
|
||||
// `mesh_laptop_model-usage` is 23 chars, over the 20 an S3 access key keeps (ADR 0049); a short
|
||||
// slug makes the consumer identity `mesh_laptop_usage` (17). db/role/`as` all derive from it.
|
||||
slug: "usage",
|
||||
capabilities: ["container-runtime"],
|
||||
requires: ["postgres-database"],
|
||||
contributes: { "postgres-database": { name: "model_usage" } },
|
||||
binds: { "postgres-database": "/var/lib/model-usage/database.json" },
|
||||
secrets: { "postgres-database": "/var/lib/model-usage/database.secret" },
|
||||
consumes: ["module.*.usage.*"],
|
||||
"own-secrets": { broker: "/var/lib/mesh/model-usage/broker" },
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/model-usage", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/model-usage", mode: "0700" },
|
||||
// The connection string carries the password, so it reaches the runtime as a file the mesh
|
||||
// templates (novox/hq ADR 0086), the shape the catalogue's manifest has.
|
||||
{
|
||||
id: "database-url", type: "file", path: "/var/lib/model-usage/database.url", mode: "0600",
|
||||
content:
|
||||
"postgresql://${bound:postgres-database:as}:${secret:postgres-database}@" +
|
||||
"${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n",
|
||||
},
|
||||
{
|
||||
id: "runtime", type: "container", name: "mesh-model-usage",
|
||||
image: pinned("mesh-runtime-model-usage"), network: "host",
|
||||
volumes: [
|
||||
"/var/lib/mesh/model-usage/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/model-usage:/run/state",
|
||||
"/var/lib/model-usage/database.url:/run/secrets/database-url:ro",
|
||||
],
|
||||
env: { MESH_BROKER_FILE: "/run/secrets/broker", DATABASE_URL_FILE: "/run/secrets/database-url" },
|
||||
},
|
||||
],
|
||||
});
|
||||
// --- model-usage: the catalogue's own manifest (novox/hq 04-ISSUES/073). It requires
|
||||
// postgres-database, owns a provisioned store, consumes module.*.usage.*, and its runtime is on the
|
||||
// host network so it reaches the granted postgres (at the provider's address the mesh writes) and
|
||||
// the broker. Its slug keeps the consumer identity under the 20 characters an S3 access key allows
|
||||
// (ADR 0049). ------------------------------------------------------------------------------------
|
||||
const modelUsageManifest = catalogueModule("model-usage", held);
|
||||
|
||||
async function addIssueAssign(name: string, manifest: string): Promise<void> {
|
||||
await must("anchor", `printf %s ${quote(manifest)} > /tmp/${name}.json && docker cp /tmp/${name}.json mesh-controller:/${name}.json`);
|
||||
|
||||
@@ -29,7 +29,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -42,7 +42,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "tools-confluence";
|
||||
const MACHINE = "anchor";
|
||||
@@ -155,35 +155,9 @@ test("the mesh assigns confluence: its tools-only runtime comes up and serves th
|
||||
// confluence is tools-only and outbound-only: no service, no listener, no provisioner — just a
|
||||
// broker-bound runtime serving the module's tools. Its own token is a mesh-minted own-secret; the
|
||||
// lab has no real Confluence, so the token points at nothing — and that is the case under test: the
|
||||
// runtime must serve every tool regardless. The runtime container name and shape mirror the
|
||||
// committed manifest, with the image pinned to what this scenario serves by digest.
|
||||
const manifest = JSON.stringify({
|
||||
module: "confluence",
|
||||
version: "1",
|
||||
"own-secrets": {
|
||||
token: "/var/lib/confluence/token",
|
||||
broker: "/var/lib/mesh/confluence/broker",
|
||||
},
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/confluence", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/confluence", mode: "0700" },
|
||||
{ id: "config", type: "file", path: "/var/lib/confluence/config.json", merge: "json", content: "{}", mode: "0600" },
|
||||
{
|
||||
id: "runtime", type: "container", name: "mesh-runtime-confluence",
|
||||
image: pinned("mesh-runtime-confluence"), network: "host",
|
||||
volumes: [
|
||||
"/var/lib/confluence/config.json:/run/config/config.json:ro",
|
||||
"/var/lib/confluence/token:/run/secrets/token:ro",
|
||||
"/var/lib/mesh/confluence/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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// runtime must serve every tool regardless. The manifest is the catalogue's own, its runtime
|
||||
// artifact the image this scenario stocked (novox/hq 04-ISSUES/073).
|
||||
const manifest = catalogueModule("confluence", held);
|
||||
|
||||
await must(`printf %s ${quote(manifest)} > /tmp/confluence.json && docker cp /tmp/confluence.json mesh-controller:/confluence.json`);
|
||||
await mesh("module add /confluence.json");
|
||||
|
||||
@@ -28,7 +28,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -41,7 +41,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "tools-gitlab";
|
||||
const MACHINE = "anchor";
|
||||
@@ -154,35 +154,9 @@ test("the mesh assigns gitlab: its tools-only runtime comes up and serves the fu
|
||||
// gitlab is tools-only and outbound-only: no service, no listener, no provisioner — just a
|
||||
// broker-bound runtime serving the module's tools. Its own token is a mesh-minted own-secret; the
|
||||
// lab has no real GitLab, so the token points at nothing — and that is the case under test: the
|
||||
// runtime must serve every tool regardless. The runtime container name and shape mirror the
|
||||
// committed manifest, with the image pinned to what this scenario serves by digest.
|
||||
const manifest = JSON.stringify({
|
||||
module: "gitlab",
|
||||
version: "1",
|
||||
"own-secrets": {
|
||||
token: "/var/lib/gitlab/token",
|
||||
broker: "/var/lib/mesh/gitlab/broker",
|
||||
},
|
||||
resources: [
|
||||
{ id: "mesh-state", type: "directory", path: "/var/lib/mesh/gitlab", mode: "0700" },
|
||||
{ id: "state", type: "directory", path: "/var/lib/gitlab", mode: "0700" },
|
||||
{ id: "config", type: "file", path: "/var/lib/gitlab/config.json", merge: "json", content: "{}", mode: "0600" },
|
||||
{
|
||||
id: "runtime", type: "container", name: "mesh-runtime-gitlab",
|
||||
image: pinned("mesh-runtime-gitlab"), network: "host",
|
||||
volumes: [
|
||||
"/var/lib/gitlab/config.json:/run/config/config.json:ro",
|
||||
"/var/lib/gitlab/token:/run/secrets/token:ro",
|
||||
"/var/lib/mesh/gitlab/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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
// runtime must serve every tool regardless. The manifest is the catalogue's own, its runtime
|
||||
// artifact the image this scenario stocked (novox/hq 04-ISSUES/073).
|
||||
const manifest = catalogueModule("gitlab", held);
|
||||
|
||||
await must(`printf %s ${quote(manifest)} > /tmp/gitlab.json && docker cp /tmp/gitlab.json mesh-controller:/gitlab.json`);
|
||||
await mesh("module add /gitlab.json");
|
||||
|
||||
@@ -237,14 +237,104 @@ export async function assertUniversalInvariants(
|
||||
/** The packet filter genesis installs on the control-node, by the name the catalogue gives it. */
|
||||
export const FILTER_MODULE = "nftables";
|
||||
|
||||
/** The catalogue's manifest for a module, under either spelling of MESH_LAB_CATALOG: the modules
|
||||
* directory, or the checkout that holds it. */
|
||||
export function catalogueManifest(module: string): string {
|
||||
const dir = process.env["MESH_LAB_CATALOG"] ?? "";
|
||||
for (const candidate of [resolve(dir, module, "module.json"), resolve(dir, "modules", module, "module.json")]) {
|
||||
if (existsSync(candidate)) return candidate;
|
||||
// --- the catalogue: a bed installs a module by reading its manifest, never by carrying a copy ----
|
||||
|
||||
/**
|
||||
* The catalogue's `modules/` directory: MESH_LAB_CATALOG under either spelling (the checkout, or
|
||||
* its modules directory), else the checkout beside this one, the way the main layout has it.
|
||||
*
|
||||
* Beds used to build the manifests they install inline, as literals copied from the catalogue when
|
||||
* each bed was written. The copies did not move when the catalogue did, so a catalogue change was
|
||||
* proven nowhere — and a bed that installs a copy proves the copy (novox/hq 04-ISSUES/073). A bed
|
||||
* reads the catalogue, or it does not install a catalogue module; `beds-read-the-catalogue.test.ts`
|
||||
* refuses an inline copy that names one.
|
||||
*/
|
||||
export function catalogueDir(): string {
|
||||
const named = process.env["MESH_LAB_CATALOG"];
|
||||
const candidates = named
|
||||
? [resolve(named, "modules"), resolve(named)]
|
||||
: [resolve(process.cwd(), "..", "mesh-catalog", "modules")];
|
||||
for (const dir of candidates) {
|
||||
if (existsSync(resolve(dir, "mesh-controller", "module.json"))) return dir;
|
||||
}
|
||||
throw new Error(`no manifest for ${module} under MESH_LAB_CATALOG=${dir || "(unset)"}`);
|
||||
throw new Error(
|
||||
`no catalogue: MESH_LAB_CATALOG=${named ?? "(unset)"} and nothing at ${candidates.join(", ")}`);
|
||||
}
|
||||
|
||||
/** Whether a catalogue is where a bed will look — for a skip guard, which says so instead of failing. */
|
||||
export function catalogueIsPresent(): string | false {
|
||||
try { catalogueDir(); return false; } catch (err) { return (err as Error).message; }
|
||||
}
|
||||
|
||||
/** The catalogue's manifest for a module, as a path. */
|
||||
export function catalogueManifest(module: string): string {
|
||||
const path = resolve(catalogueDir(), module, "module.json");
|
||||
if (!existsSync(path)) throw new Error(`no manifest for ${module} at ${path}`);
|
||||
return path;
|
||||
}
|
||||
|
||||
/** What the lab may rewrite in a catalogue manifest, and nothing else. */
|
||||
export interface ForTheLab {
|
||||
/**
|
||||
* The image repository each build artifact was built as on this workstation, by artifact name.
|
||||
* A module's own runtime is `mesh-runtime-<module>` by default — what `scripts/build-module-runtime.sh`
|
||||
* tags and what the scenarios stock; a bed names it only where the scenario stocks another name.
|
||||
* An artifact this does not name is refused: the bed must say what stands in for the builder.
|
||||
*/
|
||||
artifacts?: Record<string, string>;
|
||||
/**
|
||||
* Host-port remaps by container id, where one machine carries modules whose published ports
|
||||
* collide — `{ server: { "8080": "8090:8080" } }`. The container side never changes.
|
||||
*/
|
||||
ports?: Record<string, Record<string, string>>;
|
||||
/**
|
||||
* Environment a container gets in the lab that it does not get in the mesh — an address the bed
|
||||
* stands up in place of a real upstream, and nothing else. Merged over the manifest's own.
|
||||
*/
|
||||
env?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A catalogue manifest as a machine in the lab can run it: the mesh's build section gone (the lab
|
||||
* stocks images rather than building), each artifact replaced by the image the machine holds for it,
|
||||
* every image pinned to what the machine holds or the upstream digest the catalogue pins, and the
|
||||
* declared lab rewrites applied. Everything else is the catalogue's, verbatim — which is the point.
|
||||
*/
|
||||
export function catalogueModule(module: string, held: HeldImage[], lab: ForTheLab = {}): string {
|
||||
const m = JSON.parse(readFileSync(catalogueManifest(module), "utf8")) as {
|
||||
resources?: { id: string; type: string; image?: string; artifact?: string; ports?: string[]; env?: Record<string, string> }[];
|
||||
build?: unknown;
|
||||
};
|
||||
const artifacts: Record<string, string> = { runtime: `mesh-runtime-${module}`, ...(lab.artifacts ?? {}) };
|
||||
for (const r of m.resources ?? []) {
|
||||
if (r.type !== "container") continue;
|
||||
if (typeof r.artifact === "string") {
|
||||
const repository = artifacts[r.artifact];
|
||||
assert.ok(repository,
|
||||
`${module}'s container '${r.id}' names the "${r.artifact}" artifact, which the mesh would ` +
|
||||
`build. The lab does not build: the bed must say which stocked image stands in for it ` +
|
||||
`(artifacts: { ${r.artifact}: "<repository>" }).`);
|
||||
const reference = referenceFor(held, repository);
|
||||
assert.ok(reference,
|
||||
`${module}'s "${r.artifact}" artifact is ${repository} and this scenario stocked no such ` +
|
||||
`image. Add it to the scenario's images: and build it (scripts/build-module-runtime.sh ${module}).`);
|
||||
r.image = reference;
|
||||
delete r.artifact;
|
||||
} else if (typeof r.image === "string") {
|
||||
r.image = onTheMachine(r.image, held);
|
||||
}
|
||||
const remap = lab.ports?.[r.id];
|
||||
if (remap && Array.isArray(r.ports)) r.ports = r.ports.map((p) => remap[p] ?? p);
|
||||
const env = lab.env?.[r.id];
|
||||
if (env) r.env = { ...(r.env ?? {}), ...env };
|
||||
}
|
||||
delete m.build;
|
||||
return JSON.stringify(m);
|
||||
}
|
||||
|
||||
/** Whether a manifest's runtime dials the broker — the module then needs a scoped broker account. */
|
||||
export function needsBrokerAccount(manifest: string): boolean {
|
||||
return manifest.includes("MESH_BROKER_FILE");
|
||||
}
|
||||
|
||||
function shellQuote(s: string): string {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -39,7 +39,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "local-model-bed";
|
||||
const MACHINE = "anchor";
|
||||
@@ -153,47 +153,13 @@ after(async () => {
|
||||
test("a node hosting a model answers model-access, and the consumer is handed its endpoint", {
|
||||
skip, timeout: 1_500_000,
|
||||
}, async () => {
|
||||
const ollamaImage = pinned("ollama/ollama");
|
||||
|
||||
// The provider: ollama runs the model server and `provides: ["model-access"]` at node scope, serving
|
||||
// its port and model. It mints nothing — provides/serves are declaration the mesh reads, so there is
|
||||
// no runtime container, only the server.
|
||||
const ollamaManifest = JSON.stringify({
|
||||
module: "ollama",
|
||||
version: "1",
|
||||
capabilities: ["container-runtime"],
|
||||
provides: [{ name: "model-access", scope: "node" }],
|
||||
listens: [{ port: 11434, protocol: "tcp", from: "machine", why: "local consumers reaching the model server" }],
|
||||
serves: { "model-access": { port: 11434, model: "llama3.2" } },
|
||||
resources: [
|
||||
{ id: "state", type: "directory", path: "/services/ollama", mode: "0700" },
|
||||
{
|
||||
id: "server", type: "container", name: "ollama",
|
||||
image: ollamaImage, network: "host", env: { OLLAMA_HOST: "0.0.0.0:11434" },
|
||||
volumes: ["/services/ollama:/root/.ollama"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// The consumer: it requires model-access and is answered by the local node. No secret (the local
|
||||
// server is keyless), only the bound endpoint facts, templated into an openai.env the mesh writes.
|
||||
const consumerManifest = JSON.stringify({
|
||||
module: "local-model-consumer",
|
||||
version: "1",
|
||||
slug: "local",
|
||||
requires: ["model-access"],
|
||||
binds: { "model-access": "/var/lib/local-model-consumer/model.json" },
|
||||
resources: [
|
||||
{ id: "state", type: "directory", path: "/var/lib/local-model-consumer", mode: "0700" },
|
||||
{ id: "config", type: "directory", path: "/var/lib/local-model-consumer/config", mode: "0700" },
|
||||
{
|
||||
id: "openai-env", type: "file", path: "/var/lib/local-model-consumer/config/openai.env", mode: "0600",
|
||||
content:
|
||||
"OPENAI_BASE_URL=http://${bound:model-access:at}:${bound:model-access:port}/v1\n" +
|
||||
"OPENAI_MODEL=${bound:model-access:model}\nOPENAI_API_KEY=local\n",
|
||||
},
|
||||
],
|
||||
});
|
||||
// Both manifests are the catalogue's own (novox/hq 04-ISSUES/073). The provider: ollama runs the
|
||||
// model server and `provides: ["model-access"]` at node scope, serving its port and model. It mints
|
||||
// nothing — provides/serves are declaration the mesh reads, so there is no runtime container, only
|
||||
// the server. The consumer requires model-access and is answered by the local node: no secret (the
|
||||
// local server is keyless), only the bound endpoint facts, templated into an openai.env the mesh writes.
|
||||
const ollamaManifest = catalogueModule("ollama", held);
|
||||
const consumerManifest = catalogueModule("local-model-consumer", held);
|
||||
|
||||
await addAssign("ollama", ollamaManifest);
|
||||
await addAssign("local-model-consumer", consumerManifest);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath, HOST_PATH } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine } from "./harness.ts";
|
||||
import { labIsUsable, destroyAll, foundationBundle, onTheMachine, catalogueModule, catalogueIsPresent } from "./harness.ts";
|
||||
import type { HeldImage } from "../../src/pinning.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
@@ -37,7 +37,7 @@ const skip = !capability.usable
|
||||
? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle)
|
||||
? "MESH_LAB_BUNDLE is not set to a foundation bundle (mesh-host examples/)"
|
||||
: false;
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "openai-bed";
|
||||
const MACHINE = "anchor";
|
||||
@@ -156,7 +156,6 @@ after(async () => {
|
||||
test("a static-key model-access licence delivers the operator's API key to the consumer, unchanged", {
|
||||
skip, timeout: 1_500_000,
|
||||
}, async () => {
|
||||
const consumerImage = pinned("mesh-runtime-openai-consumer");
|
||||
|
||||
// --- the licence, a record with vendor openai (static-key) -------------------------------------
|
||||
// No manager: a static-key licence has none (mesh-controller refuses `licence manager` on it). The
|
||||
@@ -172,33 +171,11 @@ test("a static-key model-access licence delivers the operator's API key to the c
|
||||
await mesh(`licence key personal --file /openai-key`);
|
||||
|
||||
// --- deploy the consumer -----------------------------------------------------------------------
|
||||
// Inline manifest mirroring the committed module.json: a model-access holder whose delivered key
|
||||
// The catalogue's own manifest (novox/hq 04-ISSUES/073): a model-access holder whose delivered key
|
||||
// arrives at its secret path. The scheduled apply container installs as present state (ADR 0053) and
|
||||
// its image is pulled at apply (schedule-pull); the test drives apply directly for a deterministic
|
||||
// flow rather than waiting on cron.
|
||||
const consumerManifest = JSON.stringify({
|
||||
module: "openai-consumer",
|
||||
version: "1",
|
||||
requires: ["model-access"],
|
||||
binds: { "model-access": "/var/lib/openai-consumer/model.json" },
|
||||
secrets: { "model-access": "/var/lib/openai-consumer/api-key" },
|
||||
resources: [
|
||||
{ id: "state", type: "directory", path: "/var/lib/openai-consumer", mode: "0700" },
|
||||
{ id: "config", type: "directory", path: "/var/lib/openai-consumer/config", mode: "0700" },
|
||||
{
|
||||
id: "apply", type: "container", name: "mesh-openai-consumer-apply",
|
||||
image: consumerImage, network: "host", schedule: "*/5 * * * *",
|
||||
args: ["run", "/app/modules/openai-consumer/dist/apply/index.js"],
|
||||
volumes: ["/var/lib/openai-consumer:/run/state"],
|
||||
env: {
|
||||
MESH_MODEL_ACCESS_SECRET_FILE: "/run/state/api-key",
|
||||
MESH_MODEL_ACCESS_BIND_FILE: "/run/state/model.json",
|
||||
MESH_OPENAI_ENV_FILE: "/run/state/config/openai.env",
|
||||
MESH_OPENAI_CREDENTIALS_FILE: "/run/state/config/auth.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const consumerManifest = catalogueModule("openai-consumer", held);
|
||||
await must(`printf %s ${quote(consumerManifest)} > /tmp/openai-consumer.json && docker cp /tmp/openai-consumer.json mesh-controller:/openai-consumer.json`);
|
||||
await mesh(`module add /openai-consumer.json`);
|
||||
await mesh(`module issue openai-consumer --node ${MACHINE}`);
|
||||
|
||||
Reference in New Issue
Block a user