From b13ef1be81a406aad659825e190509f693b890fe Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 00:02:51 +0200 Subject: [PATCH 1/4] Issues 192 and 193: the console reaches a person only by hand; the store's read-only query is not 192: no provision says where the console is, its port was never assigned, and nothing owns a person's agent configuration since the predecessor left. 193: the query verb wraps the caller's text in a read-only transaction the text can end, and its rows come back keyed by BEGIN. --- .../00-report.md | 78 +++++++++++++++++++ .../00-report.md | 53 +++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md create mode 100644 04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md diff --git a/04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md b/04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md new file mode 100644 index 0000000..68b31b6 --- /dev/null +++ b/04-ISSUES/192-the-meshs-tools-reach-a-person-only-by-a-registration-made-by-hand/00-report.md @@ -0,0 +1,78 @@ +--- +status: open +opened: 2026-10-02 +located-in: [mesh-catalog modules/mesh-console, mesh-controller cmd/mesh-controller/plan.go (port assignment)] +fixed-by: +amended-design: +--- + +# 192 — The mesh's tools reach a person only by a registration made by hand + +## What was observed + +A design session on a workstation had none of the mesh's tools. The console was running on that +machine and answering on its loopback port. It was reached over the bus as the console's account, and +listed every running module's tools and every seat's verbs +([ADR 0152](../../02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md)). What was missing +was the registration that tells the person's coding agent where the console is. That registration had +been made by hand, once, while migrating the machine, and scoped to the one project directory it was +made in. Every session started anywhere else had no mesh tools. Nothing said so: the agent simply +offered no mesh tools, and the session fell back to a pull-request link for a person to open by hand. + +The predecessor did this job itself: it wrote its tool server into the agent's user configuration on +every machine. Migrating removed that entry, as it should have, and no module took the job over. + +## Why this is here + +Three gaps, each of which would have stopped a module from doing it even if one existed. + +**1. The console tells nobody where it is.** Its definition listens on a port and provides nothing. +A module that wanted to point an agent at the console has no requirement it could name, so it +would have to write the address into its own definition as a literal. That is exactly what +[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) and +[ADR 0155](../../02-DECISIONS/0155-a-definition-names-no-installation-and-how-that-is-checked.md) +remove. + +**2. The console's port is one its definition chose.** The definition names a port, and the mesh +never assigned one: no port assignment exists for the console on any machine. The plan assigns a +machine port only to a port a container publishes through a mapping, "without one the software binds +what it binds". The console runs on the host network with no mapping, but it reads its listening +address from `${port:…}`, so the mesh could move it and does not. That is a module choosing a +machine port, which [ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md) exists to +prevent, through a gap in how the rule is applied rather than a decision against it. A module that +reads its port from the mesh should be assigned one like any other. + +**3. Nothing in the mesh owns a person's agent configuration.** No catalogue module writes the agent's +settings, its tool-server registrations, or the rules and skills the predecessor delivered. On the +four machines these are hand-kept, or left over from the predecessor, or missing. + +## What a fix looks like (not decided) + +- **The console provides its endpoint.** A provision, working name `mesh-tools`, served as the URL on + the machine port the mesh gives it. The console listens only on loopback, so the provider must be on + the consumer's own machine. Co-location already chooses it + ([ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md)), and a machine with no + console refuses the consumer, naming the provision. +- **A module for the coding agent requires it** and writes the registration into the agent's + system-wide managed settings. The agent reads tool servers from a `managedMcpServers` key there. That + file is the machine's rather than a user's, so the module owns it whole and no home directory is + named. People keep their own registrations beside it. The agent's separate *exclusive* managed + file is the wrong one: it blocks every registration a person makes and hides the hosted connectors. + The agent's per-user file is rewritten by the agent continuously and sits in a home directory, + which would make its path an operator value. These facts come from the agent's documentation + (managed MCP and managed settings pages), not yet verified on a machine. +- **The same module owns the rest of the agent's configuration** the predecessor delivered: managed + settings and the rules, skills and instructions every session reads. Each declared setting carries + a default (ADR 0164, + proposed on its own branch), so one configuration serves every machine and one machine may differ. + +## Open questions + +- **Is the agent's configuration one module or several?** Tool registration, managed settings, and + the instruction files have different readers and change at different rates. +- **Whose machine port is the console's?** Should a host-network container that reads its port from + `${port:…}` be assigned one, or should a machine-only listener keep its declared number? The second + needs a decision, because ADR 0038 does not allow it today. +- **Credentials.** The console's authority is the machine's login (ADR 0152). A registration that + reaches it carries no secret today. If the console ever listens beyond loopback, the registration + needs one, from the vault. diff --git a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md new file mode 100644 index 0000000..810a6a7 --- /dev/null +++ b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md @@ -0,0 +1,53 @@ +--- +status: located +opened: 2026-10-02 +located-in: [mesh-catalog modules/postgres/client.ts (readOnlyQuery)] +fixed-by: +amended-design: +--- + +# 193 — The store seat's read-only query is read-only by convention, and its answer is unreadable + +## What was observed + +Asking the store seat's `query` verb for a count through the console returned this. Rows are each +wrapped in an object under a key named `BEGIN`: the column name, then the value, then the word +`ROLLBACK`. A query returning nothing gave the column name and `ROLLBACK` alone. The answer to +`select count(*) as n from ` was: + +> `rows: [ {BEGIN: "n"}, {BEGIN: "46"}, {BEGIN: "ROLLBACK"} ]` + +A reader can work it out. A program cannot, and a query with two columns loses which value belongs to +which. + +## Why this is here + +**The cause is the same line that makes the query read-only.** The holder's tool sends +`BEGIN TRANSACTION READ ONLY; ; ROLLBACK;` to the command-line client as one +string. The client prints a command tag for each of the three statements, and the parser takes the +first line, `BEGIN`, as the header. + +**And it is not read-only.** [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) +decided the store seat's `query` verb is "one read-only statement against one database". The only +thing enforcing that is the wrapping transaction, and the caller's statement is pasted inside it as +text. A statement that begins by ending the transaction (a commit, then anything) runs whatever +follows it outside the read-only transaction, with the holder's own role, the administrative one that creates every +consumer's role and database. A rule stated in a decision and enforced by string concatenation is enforced by nothing. + +*This is read from the code, not tried against the live store, and it should not be tried there.* +The lab bed is where it gets proven. + +Every caller with `invokes` on the store seat's `query` can do this. The console has `invokes: ["*"]`, +so that includes anyone logged in on a machine running the console. + +## What a fix looks like + +- **One statement, refused otherwise.** Send the caller's statement alone, through the client's + single-statement path (the extended protocol takes one statement per call and refuses more). The + read-only property then comes from the session, not from text around the statement. +- **Read-only by role, not by transaction.** Run the verb as a role that can only read, granted + `pg_read_all_data`, not as the administrative role. A statement that escapes every wrapper still cannot write. +- **Rows as rows.** Parse the client's output with the column names it returns, or use a driver + instead of the command-line client, so a row is an object keyed by its columns. +- **The check 0159 lacks:** a test that sends a commit followed by a write and asserts the write is + refused and nothing changed. Another asserts a two-column row comes back keyed by both columns. -- 2.54.0 From 696957aa5ec0145477d2be38405b9b651c054642 Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 00:09:38 +0200 Subject: [PATCH 2/4] Issue 193: proven on a throwaway server, fixed for postgres by mesh-catalog PR 209; mssql has the same hole --- .../00-report.md | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md index 810a6a7..4b8afbb 100644 --- a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md +++ b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md @@ -2,7 +2,7 @@ status: located opened: 2026-10-02 located-in: [mesh-catalog modules/postgres/client.ts (readOnlyQuery)] -fixed-by: +fixed-by: [mesh-catalog PR 209 (postgres; open)] amended-design: --- @@ -51,3 +51,30 @@ so that includes anyone logged in on a machine running the console. instead of the command-line client, so a row is an object keyed by its columns. - **The check 0159 lacks:** a test that sends a commit followed by a write and asserts the write is refused and nothing changed. Another asserts a two-column row comes back keyed by both columns. + +## Proven, 2026-10-02 + +On a throwaway server — the same engine image, no network, reached over a socket — the module's code +from the catalogue's main branch ran `COMMIT; COPY (select 1) TO PROGRAM ''` and **the +command ran on the database host** as the server's own user. `COMMIT; DROP TABLE t` executed the drop +outside the read-only transaction; the wrapper's own trailing rollback happened to undo it, which a +caller ending their statement with a commit of their own would get past (not tried). Nothing was tried +against the live store. + +The fix (mesh-catalog PR 209) runs the caller's statement as a login granted `pg_read_all_data` and +nothing else, read-only by its role and its session, with a password the mesh mints as one of the +module's own secrets; without that password the call is refused rather than run as the admin. On the +same throwaway server every escape above, and `SET ROLE`, `RESET SESSION AUTHORIZATION`, turning +read-only off, creating a table, altering the role and reading a server file, is refused; a plain +select comes back keyed by its columns. One attempt — turning the transaction's read-only off, then +deleting — got past the first layer and was stopped by the second, which is why both exist. + +**Not answered by the statement-count fix proposed above.** The command-line client sends one string +in one message, so several statements still arrive together. They are harmless as the reader, and +refusing them is left to whoever moves the module to a driver. + +## The same hole, elsewhere + +The `mssql` module wraps a caller's statement the same way (`BEGIN TRANSACTION; … ROLLBACK;` as its +administrator) for its own `mssql_query` tool. It holds no seat, but the console may call it. It +needs the same change: a login that can only read, and no wrapper as text. Open. -- 2.54.0 From afbfd5f29da067e2905ef540d94babd86788128e Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 00:25:48 +0200 Subject: [PATCH 3/4] Issue 193: mssql's variable substitution and shell commands, proven and fixed by mesh-catalog PR 210 --- .../00-report.md | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md index 4b8afbb..70e6b2a 100644 --- a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md +++ b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md @@ -2,7 +2,7 @@ status: located opened: 2026-10-02 located-in: [mesh-catalog modules/postgres/client.ts (readOnlyQuery)] -fixed-by: [mesh-catalog PR 209 (postgres; open)] +fixed-by: [mesh-catalog PR 209 (postgres; open), mesh-catalog PR 210 (mssql; open)] amended-design: --- @@ -73,8 +73,27 @@ deleting — got past the first layer and was stopped by the second, which is wh in one message, so several statements still arrive together. They are harmless as the reader, and refusing them is left to whoever moves the module to a driver. -## The same hole, elsewhere +## The same hole, elsewhere — and two worse ones -The `mssql` module wraps a caller's statement the same way (`BEGIN TRANSACTION; … ROLLBACK;` as its -administrator) for its own `mssql_query` tool. It holds no seat, but the console may call it. It -needs the same change: a login that can only read, and no wrapper as text. Open. +The `mssql` module wrapped a caller's statement the same way (`BEGIN TRANSACTION; … ROLLBACK;` as its +administrator) for its `mssql_query` tool. Its command-line client added two holes of its own. Both +were proven on a throwaway server, running the client the way the module ran it: + +- **It substitutes `$(NAME)` from its environment into the caller's text**, and the administrator's + password is in that environment. Selecting it as a string returned the password. +- **It reads a line beginning `:!!` as a command that starts a program**, in the container that holds + the administrator's password and the module's bus credentials. Its switch for refusing such commands + makes the shipped version ignore the statement entirely, so the switch cannot be the guard. + +None of it was reachable on the live mesh, for a reason that is a defect of its own: the runtime image +never installed the client, so every mssql tool failed (`spawn sqlcmd ENOENT`). The fix (mesh-catalog +PR 210) installs the client at a pinned digest and runs the caller's statement as a login that can +connect and read and do nothing else. Substitution is off. The statement must be one line, placed after +the module's own text, so no line of it can begin a command; a line break is refused before the client +starts. On the throwaway server, writes, `xp_cmdshell`, impersonating the administrator, and joining +the administrators' role were all refused, and the variable came back as the literal text. + +**The general lesson**, worth more than either module: *a command-line client is an interpreter with +its own syntax, and a caller's text handed to it is a program in that syntax as well as in SQL.* A +module that passes a caller's text to a client has two languages to defend, and a transaction drawn +around the text defends neither. -- 2.54.0 From 24aeb203f76a0fb2fb10d3ab7d9c6d09b86c149a Mon Sep 17 00:00:00 2001 From: jochens Date: Fri, 2 Oct 2026 00:35:03 +0200 Subject: [PATCH 4/4] Issue 193 resolved: both readers live on every machine, checked by asking each copy who it is --- .../00-report.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md index 70e6b2a..45c3b18 100644 --- a/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md +++ b/04-ISSUES/193-the-store-seats-read-only-query-is-read-only-by-convention/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-10-02 located-in: [mesh-catalog modules/postgres/client.ts (readOnlyQuery)] -fixed-by: [mesh-catalog PR 209 (postgres; open), mesh-catalog PR 210 (mssql; open)] +fixed-by: [mesh-catalog PR 209 (postgres), mesh-catalog PR 210 (mssql)] amended-design: --- @@ -97,3 +97,16 @@ the administrators' role were all refused, and the variable came back as the lit its own syntax, and a caller's text handed to it is a program in that syntax as well as in SQL.* A module that passes a caller's text to a client has two languages to defend, and a transaction drawn around the text defends neither. + +## Resolved, 2026-10-02 + +Both pull requests merged, built and pushed to the two machines that run each module. Checked live, on +every copy, by asking each one who it is: + +- the store seat's `query`, and postgres's own tool on each machine, answer as the reader login — + not a superuser, in a read-only transaction — with rows keyed by their columns; +- mssql's tool, on each machine, answers as its reader login, outside the administrators' role, and + returns `$(SQLCMDPASSWORD)` as the literal text it is. Its tools work for the first time. + +The escapes themselves were tried only on the throwaway servers above; on the live mesh the check is +the identity a statement runs as, which is what makes every escape a statement that the login cannot do. -- 2.54.0