From 6615b5e3b359a98c69e4bdee1647d5ef12b7a0ae Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 22:29:41 +0200 Subject: [PATCH] plex, redis: module runtime containers + declare self-consumed events (ADR 0052/0046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plex gains a broker-bound tools/events runtime container (mesh-runtime-plex) alongside its server, and a never-throwing plex_reachable health probe. redis and postgres subscribe to their own lifecycle events in index.ts but declared no consumes — so the substrate never made the queue the runtime binds and it crashed on start (404). Declare the consume, as ADR 0046 requires. Proven end-to-end in the mesh-lab: assigned-plex and assigned-redis both green. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF --- modules/plex/client.ts | 14 ++++++++++++++ modules/plex/module.json | 16 ++++++++++++++++ modules/plex/tools/index.ts | 6 ++++++ modules/postgres/index.ts | 25 +++++++++++++++++++++++++ modules/postgres/module.json | 4 ++++ modules/redis/index.ts | 25 +++++++++++++++++++++++++ modules/redis/module.json | 4 ++++ 7 files changed, 94 insertions(+) create mode 100644 modules/postgres/index.ts create mode 100644 modules/redis/index.ts diff --git a/modules/plex/client.ts b/modules/plex/client.ts index a987767..78c2288 100644 --- a/modules/plex/client.ts +++ b/modules/plex/client.ts @@ -128,4 +128,18 @@ export class PlexClient { async refreshAll(): Promise { for (const library of await this.getLibraries()) await this.refreshLibrary(library.key); } + + /** + * A health probe that never throws: report whether the Plex server this client is pointed at + * answers, and identify it when it does. Every other call assumes the server is up; this is the + * one that tells the mesh whether it is, so a diagnosis does not start from a stack trace. + */ + async reachable(): Promise<{ reachable: boolean; url: string; server?: { name: string; version: string }; error?: string }> { + try { + const info = await this.getServerInfo(); + return { reachable: true, url: this.baseUrl, server: { name: info.name, version: info.version } }; + } catch (err) { + return { reachable: false, url: this.baseUrl, error: err instanceof Error ? err.message : String(err) }; + } + } } diff --git a/modules/plex/module.json b/modules/plex/module.json index b120077..bb9dc37 100644 --- a/modules/plex/module.json +++ b/modules/plex/module.json @@ -99,6 +99,22 @@ "/services/media/music:/music", "/services/media/audiobooks:/audiobooks" ] + }, + { + "id": "runtime", + "type": "container", + "name": "mesh-plex", + "image": "mesh-runtime-plex@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "host", + "volumes": [ + "/var/lib/mesh/plex/broker:/run/secrets/broker:ro", + "/services/plex/config:/var/lib/plex/config:ro" + ], + "env": { + "MESH_BROKER_FILE": "/run/secrets/broker", + "MESH_PLEX_URL": "http://127.0.0.1:32400", + "MESH_PLEX_DATA_DIR": "/var/lib/plex" + } } ] } diff --git a/modules/plex/tools/index.ts b/modules/plex/tools/index.ts index f175a7d..7a2115b 100644 --- a/modules/plex/tools/index.ts +++ b/modules/plex/tools/index.ts @@ -20,6 +20,12 @@ export function getPlexTools(plex: PlexClient): ToolDefinition[] { return { server, libraries, sessions, recentlyAdded: recent }; }, }, + { + name: "plex_reachable", + description: "Health probe: whether the Plex server answers, and which server it is. Never fails.", + input: {}, + run: async () => plex.reachable(), + }, { name: "plex_search", description: "Search across all Plex libraries — movies, shows, episodes, music.", diff --git a/modules/postgres/index.ts b/modules/postgres/index.ts new file mode 100644 index 0000000..a7bbe12 --- /dev/null +++ b/modules/postgres/index.ts @@ -0,0 +1,25 @@ +// postgres's events entrypoint, loaded by the per-node tool host (the provisioner container runs +// ./provisioner separately). The database lifecycle events are EMITTED from the provisioner, where +// the lifecycle actually happens (novox/hq ADR 0046/0047): +// module.postgres.database.provisioned — a consumer's database + owning role was created +// module.postgres.database.deprovisioned — that database was removed +// Here in the tool host we react to them, keeping a lightweight audit trail of who was granted a +// database and who lost one — observability the provider itself is best placed to log. + +import { on } from "@novox/mesh-sdk/events"; + +interface DatabaseEvent { + consumer: string; + database: string; + user?: string; +} + +await on("module.postgres.database.provisioned", async (e) => { + console.log(`[postgres] database provisioned for ${e.body.consumer} (db ${e.body.database})`); +}); + +await on("module.postgres.database.deprovisioned", async (e) => { + console.log(`[postgres] database deprovisioned for ${e.body.consumer} (db ${e.body.database})`); +}); + +console.log("[postgres] auditing database lifecycle events"); diff --git a/modules/postgres/module.json b/modules/postgres/module.json index a11feaf..5fe0d69 100644 --- a/modules/postgres/module.json +++ b/modules/postgres/module.json @@ -14,6 +14,10 @@ "module.postgres.database.provisioned", "module.postgres.database.deprovisioned" ], + "consumes": [ + "module.postgres.database.provisioned", + "module.postgres.database.deprovisioned" + ], "listens": [ { "port": 5432, diff --git a/modules/redis/index.ts b/modules/redis/index.ts new file mode 100644 index 0000000..870c886 --- /dev/null +++ b/modules/redis/index.ts @@ -0,0 +1,25 @@ +// redis's events entrypoint, loaded by the per-node tool host (the provisioner container runs +// ./provisioner separately). The cache lifecycle events are EMITTED from the provisioner, where the +// lifecycle actually happens (novox/hq ADR 0046/0047): +// module.redis.cache.provisioned — a consumer's ACL user + keyspace was created +// module.redis.cache.deprovisioned — that user was removed +// Here in the tool host we react to them, keeping a lightweight audit trail of who was granted a +// cache and who lost one — observability the provider itself is best placed to log. + +import { on } from "@novox/mesh-sdk/events"; + +interface CacheEvent { + consumer: string; + username: string; + keyspacePrefix?: string; +} + +await on("module.redis.cache.provisioned", async (e) => { + console.log(`[redis] cache provisioned for ${e.body.consumer} (user ${e.body.username})`); +}); + +await on("module.redis.cache.deprovisioned", async (e) => { + console.log(`[redis] cache deprovisioned for ${e.body.consumer} (user ${e.body.username})`); +}); + +console.log("[redis] auditing cache lifecycle events"); diff --git a/modules/redis/module.json b/modules/redis/module.json index 80d9f30..77b648d 100644 --- a/modules/redis/module.json +++ b/modules/redis/module.json @@ -14,6 +14,10 @@ "module.redis.cache.provisioned", "module.redis.cache.deprovisioned" ], + "consumes": [ + "module.redis.cache.provisioned", + "module.redis.cache.deprovisioned" + ], "serves": { "redis-cache": {} },