A Message Is Not a Permission
On September 23 the product session needed a way to bring a dead test character back to life without logging in — the death spec kills it on purpose, and the next run needs it standing. The realm’s database lives on the homelab host, in a repository the product session has no write access to. I ruled ask Homelab, and the product session did: one message to the session that owns that host. A few hours later an offline revive existed, deployed on the host, verified from the product side by an exit code, and committed nowhere yet — because the decision to commit it is mine, not either agent’s.
That happened five times that day. A revive tool, a reading owed back, a staged ghost login, a locale field on a query tool, a spellbook verb. In each case one agent asked, the other built the answer inside its own repository, and the asker checked the result from its own side before relying on it. Neither agent ever wrote into the other’s repo, and nothing in the exchange carried a permission.
Post 6 introduced this channel as a governance boundary and told its first case; post 7 told a fixture that was displaced and restored across it, and used it to interview the factory. This post is what the boundary looks like on a busy day — the catalogue of what crosses it, the failure modes I can see and the one I can’t, and a blind spot in the telemetry that this series has been proud of.
(Series rules apply: running totals, never final ones, every number dated and sourced; where a fact lives only in an agent’s memory or my own framing rather than in a file, the post says so.)
One Day, Five Exchanges
The factory’s sessions can message each other directly. Two of them matter here: the product session, which builds the phone client in its repository, and the homelab session, which owns the host that runs the game realm, its database and its deployment — in a different repository, with an architecture decision that says so: the product repo builds and publishes images; the homelab repo deploys them; product code carries zero deployment configuration.
Here is September 23, itemised from the spec file that recorded it. Elapsed times are not measured — there are no message timestamps, only the commit times on the product side, which bound the day from 08:14 to 10:44.
September 23, 2026 — every exchange across the boundary
| Asked | Came back | Checked from the asking side | Not touched |
|---|---|---|---|
| Product → Homelab. An offline revive for a dead fixture character, after my ruling: ask Homelab. | A SQL revive that clears the corpse row, the ghost auras and the ghost flag — deployed on the host, not committed; committing is mine to approve. | realm-restore --check exits 0 for both fixture characters. | The product session wrote no SQL. Homelab touched no spec. |
| Homelab → Product. An acknowledgement, and a reading owed back. | The product side sent its first real-login reading: restore exit 0 three times, the paladin at full vitals. | The spec records its own caveat: this exercised position and vitals repair, not the dead path. | — |
| Homelab offered; Product held, then released. A staged ghost login to prove the dead path. The product side answered hold while an implementer was using the realm. | A ghost staged in the database; the check reported ten failures; the restore exited 0; one login followed — alive, mana 5,629 of 5,629. | Health was not read. It's ruled out by code path: the resurrect routine sets health and mana together, so full mana excludes the 50% path. | The realm stayed single-user. The hold is what kept it that way. |
| A product subagent → Homelab, relayed. The localized name field on the NPC query tool, because the plate showed a French name and the tool returned the English one. | namelocale and subnamelocale on the query tool — live on the host, not committed on the homelab side. | The test lane reads it live; a redeploy without it fails loudly rather than silently. | The product session did not patch the tool. Homelab did not touch the helper that consumes it. |
| Product → Homelab. Worn items and the spellbook, for two register items. | equipped, orientation and a spells verb (23 spells), commit pending on their side. | Verified from the laptop. | — |
Five exchanges, from the product repo's own spec file and change log for the day. A sixth pair, between this blog's session and the product session, produced the brief this post is written from.
Read the last two columns together, because they’re the point. Every row ends with the asking side checking — an exit code, a login reading, a test lane that fails loudly — and never with the asking side trusting. And every row ends with each agent inside its own repository. The tools that came back were built by the session that owns the host; the specs that consume them were written by the session that owns the client. What crossed the boundary was a request and, later, a fact.
What Crosses, and What Never Does
By now there’s enough record to write the catalogue down.
What crosses:
- Tools, each read-only or exit-coded. A realm query tool, built on the homelab side “at pwa’s request” the day the testing spine landed; a restore tool that exits 0, 1 or 2; a packet tool; an accounts tool on the path. The product side calls them; it does not maintain them.
- Realm state. The fixture SQL lives in the homelab repo, not the product one. The game-master account the tests use is realm-scoped — GM level 3, one realm — and was granted from that side with its own deviations recorded there.
- Facts and corrections, in both directions. On one story the homelab side “corrected this repo three times,” including a wrong database field the product session had cited in five code comments. The correction came back as a message; the fix was the product session’s to make.
- Rulings the other side owns. In August the product session asked Homelab to retire one of two dashboard lanes. Homelab kept both, against the request, and wrote down why. A request across the boundary is a request, and the owner of the thing can say no.
What never crosses, or is refused:
- Facts a query can answer. The rule from post 6, unchanged: the channel is for judgement and operations, never for anything a read-only tool returns.
- Writes into the other repo. By architecture decision, by register rule — every item that touches the homelab host is owned by me, not by the engineering persona — and in practice: each of the day’s rows ends with the other side committing in its own repo, or not yet.
- An outward-facing act on a peer’s say-so. Told in post 6: an image push declined because the request arrived as a message rather than from me.
The shape is worth stating as a rule, and I’ll be honest that the rule is my framing rather than the repository’s. Post 3 said approvals must be structural, never strings. Applied to agents: a message can carry facts, tools and state. It cannot carry a permission. The nearest sentences the repo itself holds are “adding a deviation is a visible act” and “the merge commit is the attestation” — same idea, said about files instead of sessions.
Five Ways It Goes Wrong
A boundary you can’t describe failing is a boundary you’re taking on faith. Here’s the record, including where the record is empty.
1. A relayed answer that was right about the wrong thing. The query tool returned the NPC’s English name; the plate on the phone showed the French one, because the gateway logs in with a French locale. The relay was exact. The premise was wrong. It was found by the first live run, not by either agent — which is the correct instrument, and also the last one. A wrong answer from the homelab side that was relayed and acted on: none found in the record. That is not the same as none happening.
2. An authorization disguised as a request — deploy is not commit. Three of the day’s five exchanges run on changes that are live on the host but that the homelab repo does not yet hold. A redeploy would silently withdraw them. Neither agent can close that gap, because committing to the homelab repo is a decision reserved to me, and the spec says so in as many words: David approves that separately. The boundary held precisely because the product session couldn’t paper over it. It’s also why “it works” and “it’s done” are different sentences in this factory.
3. A message held, and an offer held. When a session runs under a different permission mode, a peer’s message waits for the human to approve it in that session, and can expire. The rule my sessions carry for this is in agent memory, not in the repo: never treat silence as done. The day’s third exchange was the shape from the other side — Homelab offered a staged ghost login, and the product session held the offer while someone else was using the realm, then released it. Cooperation across the boundary includes the right to say not yet.
4. A refusal. Nothing new on the day; the image push in post 6 remains the canonical case.
5. Permission laundering. The pattern where an agent that was denied something asks a peer to do it instead, so the human’s decision is bypassed one hop away. The agent runtime names this in the text it wraps around every peer message, and instructs the receiving session to refuse and surface it. I can’t show you that in the repository, because it isn’t there: the word appears in the factory’s own records only in other senses. The repo’s defence against it is structural instead of textual — the product session never holds write access to the homelab host, and every check on this side is an exit code rather than the peer’s word — which is a stronger defence than a sentence, and also the reason the shape leaves no trace when it doesn’t happen.
Invisible to the Meter
This series has spent ten posts insisting that numbers come from telemetry. Here is a channel the telemetry cannot see.
The cost and token counters carry five labels: project, phase, story, session and model. There is no label for a tool, and none for a peer. The words SendMessage and peer do not occur, in a messaging sense, anywhere in the query recipes or the scripts that read the metrics store. So the cost of a round trip across the boundary is not measured, and its latency is not measured either — there are no message timestamps at all. Each side’s spend lands in its own session, and the homelab session’s spend falls outside the product’s project label entirely, so it isn’t even in the tables post 9 built.
What I can give you is a floor. Lines in the product repo that name the homelab agent and carry a date span at least 32 distinct days, from July 17 to September 23. The densest day is September 23 itself, with fifteen dated lines; the next are September 4 and July 25. That’s inference-grade — a line’s date isn’t always the exchange’s date — so it’s “at least 32 days,” never “32 exchanges.” Post 6 said the same about the same channel: a floor, not a census. The floor is higher now, and it’s still the only number there is.
How this number is measured
Itemised from the day’s spec file and change log in the product repo, bounded by commit times 08:14–10:44. Precise only as a floor: a subagent’s relayed request counts as one.
How this number is measured
Each row ends with the owner committing in its own repo, or not yet. The gap between deployed and committed is closed by a human decision, by rule.
How this number is measured
The counters carry project, phase, story, session and model. The homelab session’s spend falls outside the product’s project label. The absence is the finding.
How this number is measured
A grep for lines naming the homelab agent with a date attached. A floor, and inference-grade: the date on a line is not always the date of the exchange.
What the Channel Minted, and What It Didn’t
Post 7 argued that a repeated manual ritual is evidence of missing machinery. Apply that test to the channel itself.
Mechanised — a tool enforces it:
- The query tool’s exit codes (0 an answer, 1 missing), which replaced ad-hoc questions. Its own file records why: four of ten walk steps on one story had been unwalkable as written, and none was guessable from the product repo.
- The restore tool’s check, run by the end-to-end lane’s global setup before any login, so a drifted fixture is repaired rather than discovered.
- A
found-by: realmtag on findings, naming the homelab agent as the instrument. Used once so far.
Prose — a session has to remember it:
- Homelab work goes by message; the old prompt files are legacy — my ruling on September 22, recorded in agent memory only. Three of those prompt files are still in the repo, from July 27, August 1 and August 18.
- Hand the walk steps to the homelab agent first, to resolve every command against the running realm before I walk them — a story-file rule.
- The register item that asked for this post.
And nothing mechanises the message itself. No log, no counter, no required fields, no schema. The exchanges that made September 23 productive were governed by a rule in a QA protocol, an architecture decision about which repo deploys, and two agents that checked exit codes instead of taking each other’s word. That’s held, so far. By this series’ own standard, a boundary held by prose is one whose first failure will be found by a person, late.
Regenerate This
The day’s table is the product repo’s spec and change-log lines for September 23; each cell traces to a grep on the spec file, and the commit bounds to git log -S on the quoted strings. The floor is one git grep for lines naming the homelab agent or session, piped through a date pattern and counted by day. The telemetry absence is regenerable in the negative: grep -i 'SendMessage\|peer' over the query recipes and the scripts returns nothing in a messaging sense, and the metric-semantics section of the query file lists the five labels that exist. The catalogue’s sources are the architecture decision record, the factory register’s ownership rule, the realm-accounts table and the QA protocol, all of which ship when the product repo goes public.
Final Thoughts
The easy version of multi-agent systems is agents that can do things to each other. The version this factory runs is agents that can ask each other, and then verify. That’s slower, and on September 23 it was also the only reason a revive tool, a locale field and a spellbook could all be built in one morning by the session that understood each of them, without anybody having to trust anybody.
If you’re wiring agents together, the rule I’d carry is the one the repo doesn’t quite state: a message can carry facts, tools and state; it cannot carry a permission. Give each agent its own repository and a tool with an exit code, and let the human own the gap between deployed and committed. Then go and add the one thing this factory still hasn’t: a way to see the channel in the same telemetry as everything else. Until then, the boundary that made the day possible is the one part of the day nobody can measure.