ADR-0034: Section 29 as an audited seam rather than a design¶
- Status: Accepted
- Date: 2026-07-31
- Related: Sections 8 (tools), 9 (policy and approvals), 11.1 (context assembly), 11.2 (trust labels), 16 (the HTTP API), 22 (security baseline), 27 (sessions, runs, turns), 29 (multi-device operation and the shared core), Milestone 10, ADR-0004 (the Postgres run queue), ADR-0005 (the tool execution pipeline), ADR-0010 (live event transport), ADR-0011 (multi-device and the shared core), ADR-0017 (layered approval and inbound-surface security), ADR-0020 (the context engine), ADR-0021 (tool execution, effect watermarking, and MCP), ADR-0023 (the run loop), ADR-0024 (the composition root), ADR-0028 (the HTTP API surface)
- Detailed design:
docs/plan/multi-device-and-surfaces.md
Context¶
Section 29 was the last part of the engineering plan that no
specification expanded. The readiness review recorded it precisely:
eighty-seven lines, ADR-0011, four inbound consuming references, and
no expansion, with the section's core claim defensible on its own
because PostgreSQL is the source of truth and devices are API
clients. What it introduced beyond that claim was the Device
concept and four named ports, and none of the four had a contract.
The review's closing sentence named the gap as a placement problem:
the Device model itself still had no home.
The obvious response is the one every other gap got — write the specification. Here that response is wrong, and the plan says so twice. Section 29.8 defers the Device concept, presence, device-scoped tool routing, and notifications to a milestone with concrete use cases, "exactly as memory and MCP are deferred". The plan's sequencing table puts inbound surfaces and pairing at Milestone 10 and beyond as a row of its own. A document that wrote the four port contracts would be building the deferred thing, and would do it without the concrete use cases the deferral is waiting for.
The corpus also already made the smaller version of this decision and
made it deliberately. tool-system.md opens a "Device-scoped tools"
section, reserves ExecutionTarget.kind = "device" and the device.
domain, and states that device tools are "a reserved seam, not a
design". It then lists four things it deliberately does not decide
and says they "belong with the cross-device work" — work that had
no document to belong to.
So the question was not whether to expand Section 29 but what a document can usefully say about a subject the plan has correctly deferred. There is one thing, and it is the thing a deferral silently assumes: that the deferred work will be additive when it arrives. A deferral is only safe if that assumption is true, and nobody had checked it against the fourteen specifications written since ADR-0011 was accepted.
Decision¶
- A seventeenth document that declares no gates.
multi-device-and-surfaces.mdjoinsbootstrap-and-composition.mdanddevelopment-toolchain.mdas a specification whose hard-gates section is absent rather than empty. It states no requirement that code could violate because it specifies no behaviour, so the gate census, the fourteen gate-declaring specifications, and the milestone map are all unchanged by it. The count that moves is the number of detailed-design documents, from sixteen to seventeen. - The document is an audit, not a design. Its deliverable is the verification that the deferred work lands additively: the eight places the corpus already holds a device-shaped hole and needs no edit, the five places it does not, and what each of the five will cost whoever resolves it. No method signature appears in it.
- Placement is decided; contracts are not. A deferral does not
require its subject to be homeless, and placement is answerable
from a rule the corpus already states — a port lives in the
module named for the capability it abstracts, not for the
component that calls it.
DeviceRegistryandDevicePresenceare repository ports;DeviceChannelsits with the MCP ports it is explicitly modelled on;NotificationServiceneeds aports/module of its own; and the fourth item of Section 29.6 is an edit to two existing components rather than a port at all. TheDeviceis a domain model with a table, because a device outlives every session. - Per-device scopes are an intersection stamped at submission,
not a second evaluation path. The scope set is captured at
submission and stamped on the run, and
PrincipalResolver .for_runreads the stamp and never a table, so narrowing a device is the intersection of two sets computed once. The policy engine is never told that a device exists. This is the finding that most changes what Milestone 10 costs. - Per-device scopes are a subset of the fifteen strings, never a
device.prefix. The scope vocabulary is closed with exactly one escape, formcp.<server id>, and a hard gate asserts it. Adevice.scope prefix would mean rewriting that rule and that gate for no gain, and it would collide conceptually with thedevice.tool-name domain, which already exists and means something unrelated. - A Surface is the
Devicemodel with an empty capability set, not a second model. It serves no device-scoped tool; it originates messages. Two models would mean two presence mechanisms for one property. What does not come along is the session-key resolver, which is the one genuinely new mechanism in Section 29, becausesessionshas no external key and no unique index beyondid. - Advertisement stays pinned; a device that attaches mid-session
is not advertised until the next one. This resolves the
question
tool-system.mdreserved by name. It is decided on the precedent already in that document rather than on new reasoning: an MCP server whose catalog changes mid-session has the change "recorded and not applied", because a third party who can rewrite the prefix can invalidate a tenant's prompt cache at will, and a device connecting and disconnecting is the same exposure. The cost — a capability the user has to open a new session to reach — is recorded as an open question rather than buried. - Section 29.5's "queue or reject" is reject. ADR-0004's
partial unique index makes queueing impossible at the database
level and
runtime-loop.mdalready settled it. A second device on a busy session is not a special case of the single-active-run-per-session rule; it is the ordinary case with a second client. - Nothing in the document is scheduled before Milestone 10.
Section 29.8's own 0.1 obligation is already discharged: reads
and writes are principal-scoped and served from the core, and a
second client attaching and replaying is
gate.api.replay_exactat Milestone 5.
Consequences¶
- Sections 29 through 31 all carry an outward cross-reference paragraph now, and the readiness review's last named section-level gap closes. No part of the engineering plan is unexpanded.
- Milestone 10 acquires a five-item list of what it will actually
cost, each item a schema or vocabulary decision rather than a
surprise: a third registration source, device lifecycle events
against a
NOT NULLsession column, a fourth suspension kind, no client attribution on a write, and a notification port with no mechanism behind it. tool-system.mdgains a pointer to where its four reserved questions are audited, and the ordinal in itstool.device_offlinesentence is corrected — the availability table grew rows for MCP authentication after that sentence was written.- Section 29 gains its outward paragraph, which records two conflicts resolved in the specifications' favour rather than in the plan's. This is the same shape as ADR-0030's correction to Section 30.2 and weakens no requirement: both resolutions were already made elsewhere, and the paragraph reports them.
- The corpus now has three gate-less specifications, which is enough to make the pattern legible: a document earns gates by specifying behaviour, and a document that places, audits, or tools the corpus does not.
NotificationServiceis now recorded as the largest open design question in the area rather than as a solved item in a port list. The corpus'sLISTEN/NOTIFYtransport is explicitly not it.
Alternatives considered¶
- Writing the four port contracts anyway: rejected. Section 29.8
defers exactly those four, and the deferral is reasoned — the
contracts depend on a transport and an attach handshake that
depend on use cases nobody has. A contract written without them
would be guessed, and a guessed Protocol in
ports/is worse than no Protocol, because the composition root would be built against it. - Folding the audit into
tool-system.md: rejected. That document already carries the device seam it owns — the target kind, the reserved domain, the offline outcome — and explicitly hands the rest elsewhere. Per-device scopes, the session-key resolver, notifications, and event placement are not tool-system subjects, and adding them would make the largest specification in the corpus the owner of a subject it declined. - Adding the surface half to
http-api-and-streaming.md: rejected. That document states that it "adds nothing to the API surface that Section 16 did not already put there", and pairing endpoints and a session-key resolver would be new surface. The constraint is load-bearing and worth more than the convenience. - Declaring gates for the additive properties: rejected, and it
was tempting. A structural gate asserting that
DEVICEappears in the forced-trust table would be checkable today. But it would duplicate hard gate 2 oftool-system.md, which already covers the wholesourceenum, and a gate that restates another gate makes the registry a worse map of the system. The audit's findings are properties of other documents' gates, not new ones. - Reopening ADR-0011: rejected. Nothing in ADR-0011 is wrong. Its six decisions stand, its four ports stand, and its scope paragraph is the deferral this ADR is built on. This is a companion rather than a supersession.
- Waiting for Milestone 10 to do the audit: rejected, and this is the whole argument for the document existing. An audit run at Milestone 10 finds the same five problems, but it finds them while somebody is trying to ship, one at a time, each blocking work in progress. Run now, it costs a reading pass and produces a list. The additive assumption is also load-bearing before Milestone 10: it is the reason nine other documents were allowed to leave device handling as a reserved word.
- A
device.scope namespace: rejected. See decision 5. The closed vocabulary and its gate are worth more than the symmetry with the tool-name domain, and the symmetry is misleading anyway, since the two namespaces would mean different things. - Treating Surfaces as a separate subject with a separate document: rejected. They share the registry, the presence model, and the scope intersection with devices, and separating them would duplicate all three. What is genuinely surface-only — the session-key resolver and pairing — is two sections, not a document.