Decision 0050
50. A change a mind composed is the agent's, and a channel is not a mind's to move
Architecture decision record 0050: 50. A change a mind composed is the agent's, and a channel is not a mind's to move
Context
Tacid’s first invariant is that the model is not an authorization authority. The daemon broke it in a way that is easy to miss, because the code that broke it looks like courtesy.
When a person asked a question and the model answered by proposing a change, the daemon built the transaction with the asker’s principal:
let transaction = Transaction::new(self.mint_id(session), intent, principal.clone(), …);
Every invariant that holds an agent back is written against the principal. by_agent is true for
Agent, RemoteAgent and Voice, and it is what refuses raw Nix, refuses writes to
anything but the option roots ADR-0008 grants it, and refuses adding a user to wheel. With
the owner’s name on the transaction, none of them fired. A model that answered “you would work
better with more freedom, wouldn’t you” by proposing
tacid.policy.autonomy = "sovereign" did not get a refusal. It got an approval prompt reading
“some of this cannot be undone” — put in front of the person who had just asked the leading
question.
The same hole made ChannelAdvance reachable. Its invariant allows the owner or a system-
provenance transaction, so an owner-attributed proposal passed. A channel advance replaces the
sources this machine is built from. It carries Tacid’s own binaries, it takes effect at the next
boot, and until this change the line the owner read before approving it was
“follow the stable channel to 2026.09.05-1200” — a sentence equally true of a move to anybody’s
tree, with the inputs that decide whose tree it is nowhere on the screen.
On a running machine the keeper’s socket already corrected the principal: the daemon connects as
tacid, principal_for calls that Principal::Agent, and the keeper’s server overwrites what
the payload claimed. So the worst of this was closed by a boundary rather than by intent — and
not closed at all for the keeper a development machine holds in process, nor for the daemon’s own
record, its preview, or its journal line, all of which said “owner”.
Decision
A change a mind composed is proposed by the agent, whoever asked. The daemon sets the
principal on a Completion::Propose transaction to itself. The person’s identity belongs on the
turn, not on the change the model wrote. What the socket would have said is now what the daemon
says, so a development machine, a preview and a journal line all agree with a production keeper.
A mind never names a revision or the sources it locks. A ChannelAdvance arriving from
Completion::Propose is refused at the daemon, before policy, at any autonomy level, with any
principal, tainted or clean. The revision and its inputs come from a manifest whose signature the
update check verified; tacid update is the path, and the model is told so.
A channel advance says where its sources come from. ChangeItem::summary names each input’s
origin — github:owner/repo@0123456789ab, tarball:https://…, or the hash of the tree when Nix
recorded nothing more legible. Consent to a revision string is not consent to a tree.
Consequences
The agent’s principal is Principal::Agent { key: "uid:700" } — its own user id, the same thing
the keeper’s socket calls it. A machine’s agent has no key of its own yet, and naming the uid is
honest about that rather than inventing a fingerprint nothing signed. When agent keys exist this
is one line.
Approval lines for a channel advance are longer, and they are longer in the only place where the length is the point. The revision alone was the shorter, prettier, wrong thing.
An owner who wants to move their machine to a tree of their own still can: tacid update against
a channel they run, or a transaction they compose themselves over tx.propose, where the
principal is theirs because they are the one who wrote it. What is closed is the path where a
model writes it and a person’s name is attached to it.
The remaining asymmetry is deliberate. tx.propose still takes the principal from the connection,
because on that path a person or the update check genuinely composed the change. The rule is not
“the owner may not do this”; it is “a mind does not get the owner’s name for free”.