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.
This commit is contained in:
+78
@@ -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.
|
||||||
@@ -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 <table>` 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; <the caller's statement>; 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.
|
||||||
Reference in New Issue
Block a user