From 4c7e438ea3c29ae9a8712d910515658f21f4df82 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 14:02:41 +0200 Subject: [PATCH 1/2] postgres serves the store seat's verbs, databases and query, and lists its tools (hq ADR 0159) Named as the seat names them so the runtime finds them by name; the same calls as its own tools. Its definition now lists its tools, which is what holding a seat with verbs demands at registration. Merge before the controller declares the verbs on the mesh-store seat. --- modules/postgres/module.json | 8 +++++++- modules/postgres/tools/index.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/postgres/module.json b/modules/postgres/module.json index 9a0a5cc..4ec69d9 100644 --- a/modules/postgres/module.json +++ b/modules/postgres/module.json @@ -133,5 +133,11 @@ "from": "Dockerfile" } ] - } + }, + "tools": [ + "databases", + "query", + "postgres_list_databases", + "postgres_query" + ] } diff --git a/modules/postgres/tools/index.ts b/modules/postgres/tools/index.ts index 51185be..d340f8b 100644 --- a/modules/postgres/tools/index.ts +++ b/modules/postgres/tools/index.ts @@ -8,6 +8,32 @@ import { PostgresClient } from "../client.js"; export function getPostgresTools(postgres: PostgresClient): ToolDefinition[] { return [ + // The store seat's verbs (novox/hq ADR 0159), served by this module wherever it holds the seat: + // named as the seat names them, so the runtime finds them by name, and answering with the same + // calls as this module's own tools below. A holder that is not the store serves nothing here; + // the bus admits the seat's subjects only to the holder. + { + name: "databases", + description: "The store seat's verb: every database the store holds, with its on-disk size.", + input: {}, + run: async () => ({ databases: await postgres.listDatabases() }), + }, + { + name: "query", + description: "The store seat's verb: one read-only statement against one database the store holds.", + input: { + database: { type: "string", description: "the database to query" }, + sql: { type: "string", description: "the SELECT (or other read-only) statement" }, + }, + run: async (args) => { + const database = String(args.database ?? ""); + const sql = String(args.sql ?? ""); + if (!database) throw new Error("query: database is required"); + if (!sql) throw new Error("query: sql is required"); + const result = await postgres.readOnlyQuery(database, sql); + return { database, command: result.command, rows: result.rows }; + }, + }, { name: "postgres_list_databases", description: "List the databases on the postgres server, with their on-disk size.", From c00dd0449434d5933661d5d045b50f676cd7036f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 15:19:35 +0200 Subject: [PATCH 2/2] postgres implements the store seat's verbs under the seat's name, and its claim says so (hq ADR 0160) The store's databases and query are registered under mesh-store, so the runtime serves them on the seat's subjects wherever postgres holds the seat and never lists them as postgres's own; the claim names them, so the mesh can judge the holder without postgres listing the seat's verbs among its tools. Scoped to what the store enables: creating a database stays postgres's tool. --- modules/postgres/module.json | 14 +++---- modules/postgres/tools/index.ts | 65 ++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/modules/postgres/module.json b/modules/postgres/module.json index 4ec69d9..d958817 100644 --- a/modules/postgres/module.json +++ b/modules/postgres/module.json @@ -10,7 +10,11 @@ "claims": [ { "name": "mesh-store", - "scope": "mesh" + "scope": "mesh", + "serves": [ + "databases", + "query" + ] } ], "capabilities": [ @@ -133,11 +137,5 @@ "from": "Dockerfile" } ] - }, - "tools": [ - "databases", - "query", - "postgres_list_databases", - "postgres_query" - ] + } } diff --git a/modules/postgres/tools/index.ts b/modules/postgres/tools/index.ts index d340f8b..90c048d 100644 --- a/modules/postgres/tools/index.ts +++ b/modules/postgres/tools/index.ts @@ -8,32 +8,6 @@ import { PostgresClient } from "../client.js"; export function getPostgresTools(postgres: PostgresClient): ToolDefinition[] { return [ - // The store seat's verbs (novox/hq ADR 0159), served by this module wherever it holds the seat: - // named as the seat names them, so the runtime finds them by name, and answering with the same - // calls as this module's own tools below. A holder that is not the store serves nothing here; - // the bus admits the seat's subjects only to the holder. - { - name: "databases", - description: "The store seat's verb: every database the store holds, with its on-disk size.", - input: {}, - run: async () => ({ databases: await postgres.listDatabases() }), - }, - { - name: "query", - description: "The store seat's verb: one read-only statement against one database the store holds.", - input: { - database: { type: "string", description: "the database to query" }, - sql: { type: "string", description: "the SELECT (or other read-only) statement" }, - }, - run: async (args) => { - const database = String(args.database ?? ""); - const sql = String(args.sql ?? ""); - if (!database) throw new Error("query: database is required"); - if (!sql) throw new Error("query: sql is required"); - const result = await postgres.readOnlyQuery(database, sql); - return { database, command: result.command, rows: result.rows }; - }, - }, { name: "postgres_list_databases", description: "List the databases on the postgres server, with their on-disk size.", @@ -59,6 +33,37 @@ export function getPostgresTools(postgres: PostgresClient): ToolDefinition[] { ]; } +// The store seat's verbs (novox/hq ADR 0159, 0160): the role's, not postgres's. Registered under the +// seat's name, so the runtime serves them on the seat's subjects wherever this module holds the +// seat and never lists them as postgres's own; scoped to what the store enables — asking what it +// holds and reading from it — so creating a database is postgres's tool and not the store's. +export function getStoreVerbs(postgres: PostgresClient): ToolDefinition[] { + return [ + { + name: "databases", + description: "Every database the store holds, with its on-disk size.", + input: {}, + run: async () => ({ databases: await postgres.listDatabases() }), + }, + { + name: "query", + description: "One read-only statement against one database the store holds.", + input: { + database: { type: "string", description: "the database to query" }, + sql: { type: "string", description: "the SELECT (or other read-only) statement" }, + }, + run: async (args) => { + const database = String(args.database ?? ""); + const sql = String(args.sql ?? ""); + if (!database) throw new Error("query: database is required"); + if (!sql) throw new Error("query: sql is required"); + const result = await postgres.readOnlyQuery(database, sql); + return { database, command: result.command, rows: result.rows }; + }, + }, + ]; +} + // The tools exist only when the server can be reached from the environment; without it, postgres // contributes none rather than failing the whole tool runtime. registerModuleTools("postgres", (env) => { @@ -68,3 +73,11 @@ registerModuleTools("postgres", (env) => { return []; } }); + +registerModuleTools("mesh-store", (env) => { + try { + return getStoreVerbs(PostgresClient.fromEnv(env)); + } catch { + return []; + } +});