DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-228-own-key-ungated-path-v0_1.md

CR-2026-228 — the own-key ungated path — v0_1 — CLOSED

Version. 0.1 Date. 2026-08-20 Status. CLOSED. Engine bb2c806..e951e60. Filed and executed the same day. Named in CR-2026-227 v0_3 §6 and carried through v0_4–v0_6 and CR-2026-229/230 as one of the two things standing between the runtime and H0 acceptance 1. Vocabulary. Written under loomworks-standing-note-we-do-not-build-for-demonstration-v0_1. Runtime direction: this CR exists to repair O1. Also O6, O9. Cites. H0 acceptance 1; standing note O1; findings v0_2 (the plural-chokepoint finding); loomworks-standing-note-h0-tightening-guarantee-and-its-scope-v0_1; loomworks-scoping-per-binding-session-minting-v0_1.


1. Two defects, one chokepoint

Defect 1 — the ungated path. gated_room_complete returned before consulting the gate when the room was on an own key. The docstring said "unmetered, ungated". Unmetered was correct; ungated was the defect. An agent working on an own-key engagement reached a model through no named chokepoint at all — so O1's coverage guarantee had a hole, and there was nowhere to attach a label even once labels existed.

Own-key means the customer pays. It does not mean nobody is watching. The two were conflated in one early return.

Defect 2 — the gate saw the requester, never the worker. Found while fixing the first. RoomSpendContext carried triggered_by — the person who asked for the work, or the system actor that auto-triggered it — as actor_kind, and since W1 as actor_id too. The agent doing the work is a different ActorRef entirely (self.agent_actor, kind="agent").

So the spend chokepoint was carrying the requester's UUID, and "every agent action at the named chokepoints carries (agent UUID, session id)" could not have been true however carefully the session flowed. W1 and W2 both threaded a session id to a gate that was naming the wrong actor beside it.

2. The fixes

The gate is asked rather than assumed. The early return is gone; key_source is passed through instead of being hardcoded to "system"; the own-key branch is now a decision the gate returned rather than an assumption the caller made.

The fix is smaller than predicted, and the reason is worth recording. CR-2026-227 v0_5 §6 estimated this as a change to "what gated means at the spend chokepoint," touching metering semantics. It was not, because gate_room_spend already returned the right answerGateDecision(metered=False, refused=False) for own-key. The gate had always known. The defect was never asking it. No existing own-key test changed outcome, which is the confirmation.

agent_actor_id names the acting agent, distinct from actor_id, populated on the shaping path.

The labelling invariant makes acceptance 1 structural. An action that names its agent must name the run that agent was granted, or it raises UnlabelledAgentActionError before the model is called. "Impossible, not just unusual" is now a property of the code path rather than a description of current behaviour.

3. The invariant's shape, and why it is not stricter

**It fires on a half-labelled action — agent_actor_id present, session_id absent. A path presenting no agent at all passes.**

That is deliberate, and it is the design decision in this CR:

The chosen shape leaves the absence visible. The render path is unlabelled and unrefused, and that state is recorded as a system fact in two standing notes rather than being papered over by either choice. A test asserts the invariant's shape for exactly this reason, so a later reader does not "tighten" it into an outage.

4. Acceptance 1 — what is now true, and what is not

True: on the shaping path, an agent action reaches the spend chokepoint carrying its agent UUID and its session id, and cannot proceed without both. Every path through gated_room_complete is gated. The own-key hole in O1's coverage is closed for all callers.

Still false, and here is why, plainly:

The render path presents no agent to the chokepoint at all. A render specialist acts under a per-binding identity from render_specialist_binding, mints no session, consults no grantability check, and passes no agent_actor_id. So "an unlabeled agent action is impossible" remains false of the runtime, by one route rather than two.

Its cause is not a missing patch. It is that two identity grains do not line up — sessions mint per role, specialists hold identity per binding — and the SEED ruling, correct on the evidence available, does not reach actors created at runtime when an operator declares a render type. Closing it is loomworks-scoping-per-binding-session-minting-v0_1: named design work with a real blocker, not to be improvised to close faster.

One consequence stated where it belongs, not only here: freeze_agent_identity can be invoked against the rendering identity and returns successfully while stopping nothing, because nothing downstream consults it. A control that reports success without acting. See loomworks-standing-note-h0-tightening-guarantee-and-its-scope-v0_1.

So: acceptance 1 went from false by two routes to false by one. The remaining route is the render path, and it is the last one.

5. Evidence

Suite 3901 → 3909 passed, 68 skipped, 0 failed; the +8 is exactly the new test file, and no existing own-key test changed outcome. Ruff clean. mypy 536 errors, all covered by the 336-entry baseline, baseline untouched. No migration.

6. Also recorded

Per Operator direction, the conftest registry now names process-local state as a family: the identity roster cache and CR-2026-230's in-process silence dedupe ledger. The ledger is not a defect — its worst case under multiple processes is a duplicate alarm, never a missed one, which is the right direction for a tightening. The family is named because it has already produced one real ordering-dependent bug, and the next arrival should be recognised on sight. Two questions to ask of it: does a test's outcome depend on whether it is warm, and does anything reset it between tests?