DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-229-w2-grantability-v0_1.md

CR-2026-229 — W2: grantability at the front — v0_1 — W2 CLOSED

Version. 0.1 Date. 2026-08-20 Status. CLOSED. Third CR of the H0 lane (loomworks-cr-brief-h0-native-habitat-v0_1.md §2 W2). Engine 9f31cb7..f786b83 (work, merge f786b83). Predecessors: CR-2026-226 (W7), CR-2026-227 (W1, closed at v0_6). Next: W4, then CR-2026-228, then demo assembly. Runtime direction: no conflict. O2 — no credential is created anywhere to make grantability work, asserted by test. O6GrantabilityRefusal carries decider, version and deciding condition, and passes the deciding gate's own voice through rather than restating it. O10 — this is the obligation W2 most directly implements: the widening function refuses every non-human actor outright, so no machine actor has a path to widen its own authority under any condition. Also serves O5, O8, O9. Rulings cited. R2 — discharged, see §2. R1; the 2026-08-20 rulings on CR-2026-227's Q1–Q5 and its natural-key + SEED ruling; the DR-01 amendment (boundaries-record d829a2a); DR-01 as amended, DR-02, DR-05, DR-15, DR-16 as re-scoped, DR-18.


1. What W2 asked for, and what it means

> Session start consults the agent's standing: active? not frozen? … the H0 stand-in is a single authoritative grantability check at session mint — one place, so the freeze semantics ("grant nothing to this identity") have a real switch to flip. No per-agent credentials are created anywhere.

The whole of W2 is the words "one place." The check itself is thin — does a seeded identity exist for this role, and is it active. Its value is not what it computes; it is that there is exactly one of it, so that when GRANTHA arrives it replaces one function, and so that freeze has one thing to act on.

A second grantability condition anywhere would be a second thing to freeze, and freeze would stop meaning what it says. That is asserted, not asserted-in-prose: a test greps the source and requires check_grantability to have exactly one caller.

2. R2 discharged — the sentence the Boundaries handoff owed

Ruling R2 (loomworks-handoff-step-4-v0_1.md) required this CR to carry the corrected recommendation so the Boundaries seed inherits it at ratification. It is carried, in the module's own docstring:

> The standing recommendation was "build on stele-agentic-id"; ruling R2 re-scoped it to adopt its model — the clamp/revoke two-lane semantics — not its code.

Nothing is imported from stele-agentic-id. What was adopted is the shape: two lanes, asymmetric on purpose.

3. What landed

agents/grantability.pycheck_grantability(role), freeze_agent_identity(...), unfreeze_agent_identity(...), GrantabilityRefusal, AgentCannotWidenAuthorityError.

The tightening lane — instant, machine-permitted, cascading. freeze_agent_identity sets status='frozen', ends every live session of that identity in the same call, and reloads the roster before returning so the very next check refuses. Any actor may call it, including a machine one — DR-15: a tightening never waits for a human, and an agent able to stop another agent is a direction the design wants cheap. Effective at the gate immediately, per DR-18.

The widening lane — human-only, deliberate, recorded. unfreeze_agent_identity requires a human actor and a non-empty rationale — the CR-2026-127/128 confirm-step pattern, O10's named template. A machine cannot reach past it under any condition; there is no condition. retired is terminal and is not reachable from here.

Mint and at-use both consult the one gate. require_live_session re-checks, so a freeze between mint and use bites at use — belt to the cascade's braces, and it matters if a row-kill ever fails while the status change succeeds.

No credentials (R-H03, O2). Asserted by a test that greps agents/ for the credential vocabulary. Grantability is a state question about a row, not a token to present — an agent proves nothing; the substrate looks its identity up.

W1's owed mint. produce_shape now mints at the front of the work and closes on every exit — success, spend-pause, exception — but only when it minted: a caller-supplied session is a grant we are only borrowing. A refused grant raises ShapingNotGrantedError carrying the verdict.

The demo script is under test, as far as W2 reaches: mint → work → stop → freeze → unfreeze → walk the record, with the walk asserting one identity, three sessions, and their three distinct ends (signed_out, killed, live). The warn step is W4's.

4. Acceptance 1 — the pair, tracked

H0 acceptance 1: "every agent action at the named chokepoints carries (agent UUID, session id); an unlabeled agent action is impossible, not just unusual." It is the demo's headline claim and the only one with two open dependencies. Tracked as a pair from the W1 close.

| Dependency | State at W1 close | State at W2 close | |---|---|---| | W2's mint | Open — carriage-complete, mint-incomplete | Closed for the shaping path. produce_shape mints, works, closes. Partially open elsewhere — see below. | | CR-2026-228, own-key path | Open | Open, unchanged. Sequenced before demo assembly. |

In the same terms used at the W1 close: acceptance 1 is now grant-complete on the demo path and still gated on two things — one path and one CR.

The honest remainder on the mint side. The shaping room mints; the rendering path does not yet. That is not an oversight, it is the per-binding identity question already accepted at the W1 close: render specialists hold durable per-(engagement, declared render type) identity from render_specialist_binding, which a seeded per-role roster does not express, so there is no role for a specialist to mint under. The render dispatch agent does hold a roster identity and could mint. Deferred rather than guessed: minting the rendering room under the rendering role would attach a session to an identity that is not the one acting, which trades a true statement for a tidy one. Raised for W4/demo assembly to sequence, not resolved here.

So the demo's "work" step is a shaping job, and it is fully granted, sessioned and walkable. If the demo intends to render, that path needs the same decision first, and it should be made deliberately rather than on the day.

5. The engagement-state premise — checked, and it holds

Per Operator direction: if W2's grantability work introduced an engagement-state gate at mint, agent_sessions.engagement_id's completeness declaration had to be revisited rather than inherited.

It did not, and the declaration stands. W2's grantability consults the agent's standing — active? not frozen? — exactly as the work order words it. No engagement-state condition was added at mint, so the premise the delete choice was reasoned from is intact: mint still has no engagement-state gate, a candidate engagement can still carry sessions, and active_only would still be a false claim.

Recorded so a later reader does not have to re-derive it: should any future CR make mint refuse on engagement state, CANDIDATE_DISCARD_POLICIES[("agent_sessions", "engagement_id")] must be revisited in the same change. **The declaration is delete because of an absence, and an absence is the fragile kind of premise** — nothing about the policy row itself would look wrong on the day the premise stops holding.

6. One thing found by building — the third instance

A fragility I introduced in W1 and caught here. The identity roster is a module-global cache, and W1 loaded it only in the api_client fixture. The full suite was green. Two files failed when run alonetest_shape_carry_forward_regression (8 failed) and test_shape_confirmation_closures (11 failed) — because whether the cache happened to be warm depended on test order.

A green full suite was not evidence the tests passed; it was evidence that something earlier had warmed a global. Now loaded autouse per test, on its own connection rather than the test's db session, and reset after each test — because a test that freezes an identity mutates status, the rollback restores the database, and nothing restores a module global.

This is the same family as the standing note filed at the W1 close: a suite's silence is not information until you know which kind of silence it is.

7. Evidence

Suite 3876 → 3888 passed, 68 skipped, 0 failed; the +12 is exactly the new test file, and the three previously order-dependent files now pass in isolation. Ruff clean. mypy 536 errors, all covered by the 336-entry baseline, baseline file untouched. No migration — W2 adds no schema; status and its constraint landed in 0113.

The date. 2026-09-09 holds. Remaining: W4, CR-2026-228, demo assembly.