Decision 0076
0076. A narrowing kept at one door is not a narrowing
Architecture decision record 0076: 0076. A narrowing kept at one door is not a narrowing
Status: accepted. Closes a leak in ADR-0061’s guarantee, found by the question ADR-0069 wrote down.
Context
ADR-0061 settled that an integration — an editor plugin, another agent, anything that said
integration.hello — takes its own authority down and can never put it back. PLAN.md §3.11 states
what that buys: an integration sees machine and project, the impersonal half, and not what the
owner told this machine or what belongs to a conversation it is not in.
That was built, and tested, at memory.search — the method a client calls. A test asserts that an
integration listing memories is shown the machine layer and not the owner’s.
There are two doors into the memory store. The other is memory.recall, the tool, which the
model calls during a turn on that client’s behalf. It narrowed by nothing at all: it took a query,
returned eight results from every layer, and put them in the model’s context — from which they
reach whoever asked the question.
So an integration that could not read the owner layer could ask a question whose answer was assembled out of it. No attack is needed: a mind that reaches for what it remembers before answering is a mind working correctly.
The promise was true of the mechanism a reviewer looks at and false of the machine. That is the shape ADR-0069 named, and it was found by asking its question of the memory store: what does this rest on that it does not check itself, and who else has to have been careful for it to be right? The tool rested on nobody ever calling it for a principal that should see less.
Decision
One definition, and both doors call it. Layer::readable_by(&Principal) lives on the layer, in
tacid-protocol, where both the server and the toolbox can reach it; the copy in server.rs is
gone. A rule with two implementations is a rule with one that is wrong.
A tool context carries who the turn is for. Context had the policy, the turn’s trust, the
clock and whether the owner had confirmed — everything about what a turn may do, and nothing
about who it is for, which is a different question and the one that decides what it may be shown.
It carries a principal now, and the agent sets it from the principal it already had.
The default is the narrowest answer. A Context built without a principal is built for a
remote agent — the least any principal may see. That is wrong for the machine’s own agent and safe
for everything, which is the way round a default should be wrong: a caller who forgets shows less
rather than more.
Consequences
memory.recallnow returns the same setmemory.searchwould for that principal, and the test that proves it drives the whole agent loop rather than the tool: a scripted mind asks for what it remembers, and what comes back differs by who the turn was for. Verified by breaking it.- Trust and readability stay separate, and both travel. A turn may believe a memory and not be allowed to see it, or see it and not believe it; conflating them would have made the fix a taint rule, which is a different mechanism for a different failure.
- This is the only tool with a per-principal answer today, and the rest are impersonal by construction: facts read the kernel, logs read the machine’s own journal, the filesystem tools go through the policy’s paths and the scribe. A tool that gains a per-principal answer later has somewhere to ask now, which is most of the point of putting the principal in the context rather than special-casing one tool.
Alternatives rejected
Filter in the store. Store::recall could take a principal. It is the wrong layer: the store
is one person’s memory on disk and knows nothing about who is connected, and giving it a principal
would put an authorisation decision in a database wrapper — where the next reader would not look
for it.
Give the tool the same may_read function by copying it. What was already there, once. The
copy is how the two doors came apart.
Cap the integration’s turn at the session layer instead. Simpler-sounding and wrong: the machine layer is exactly what an editor plugin should be able to use — “this laptop throttles at eighty” is why the integration exists. The line is personal against impersonal, not narrow against wide.