DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-232-unsessioned-agent-actions-sentinel-and-baseline-v0_1.md

CR-2026-232 — Unsessioned Agent Actions: Sentinel and Baseline — v0.1

Version: v0.1 Date: 2026-08-21 Status: Drafted, not pre-flighted. Markdown primary — the consumer is Claude Code. Implements: CR-2026-231 §3a, **ruled in scope by the Operator 2026-08-21.** Independent of: CR-2026-231 §6, which remains unruled. Nothing here depends on it — the sentinel rides ActorRef into memory_events.provenance, which §6 does not touch. Runtime direction screen: No conflict — this records an existing state more legibly and widens nothing. No new act becomes possible; no actor gains authority. It converts a silent gap into an enumerated one. Grounded on: CC's CR-2026-231 Gate 1 pre-flight census (29 agent-ActorRef construction sites across 22 files, engine 22bec5d); dev-database counts of 221 actor_kind='agent' events across 79 actor ids against 14 agent contributor rows. Cites: standing-notes/loomworks-standing-note-agent-accountability-v0_1 (STANDING) §1, §6; standing-notes/loomworks-standing-note-a-test-that-pins-a-literal-cannot-follow-a-ruling-v0_1; foray-reference/loomworks-contribution-credential-event-log-bypass-v0_1; O1, O2, O5, O10; R1 (allowlist-as-data).


Plain-language summary

Most agent actions in the record are not the shaping agent. They are agents acting over the HTTP API with a bearer token — fifteen-plus places in the routers — plus the seed-induction agent and the render specialists. None of them holds a session, so after CR-2026-231 they all write a blank where the grant should be.

The Operator has ruled these agents in scope of the accountability commitment. That settles the important half: they are not excluded, the commitment simply is not met for them yet. This change request does not meet it — that needs grants at the API boundary, which is separate work. What it does is stop the gap being invisible.

Two mechanisms. A sentinel written in place of the missing session, so a reader can tell this path has no grant yet from this write forgot one. And a baseline — the enumerated list of sites that write the sentinel, with a test that fails when a new one appears. The list may shrink and must not grow. It is the mypy baseline's discipline applied to an accountability gap: a number that is allowed to be non-zero and not allowed to rise.

The three populations are listed separately, because they are blocked on different things and will be discharged by different work.


1. What the ruling settles, and what it does not

Settled: API-acting agent contributors are inside loomworks-standing-note-agent-accountability-v0_1 §1. Every agent action carries a responsible human and that responsibility is auditable — for them too.

Therefore, stated as a system property rather than managed as an exposure: the accountability commitment is currently unmet for agent actions taken over the HTTP bearer-token path, the seed-induction path, and the render path. Those actions record who acted and do not record what authorized them.

Not settled by this change request, and not attempted here: meeting it. A grant at the API boundary is separate work with its own design question — what a session means for an agent that arrives per-request holding a token.

Why the ruling matters more than the mechanism. Ruling them out of scope would have been the cheaper answer and would have written an exclusion into the record permanently. Ruling them in makes this debt rather than design, and debt is the kind of thing that gets paid.

2. The connected gap, named because it is the same one

The responsible human behind an API agent is whoever holds the bearer token. That token is a contribution credential — and foray-reference/loomworks-contribution-credential-event-log-bypass-v0_1 records that the credential lifecycle writes via raw SQL into its own table, reaching neither the event log nor any FORAY reserved location.

So the chain from token to human is unrecorded at both ends. The action does not record its grant; the grant's issuance is not recorded either. §3a and the credential bypass are one gap seen from two directions, and neither is closed here — but the CR should say they are the same gap so they are not discharged separately and half-way.

3. The sentinel

3.1 Add to memory/base.py, beside LEGACY_UNRESOLVED_ACTOR_ID and following its documentation pattern exactly:


UNSESSIONED_ACTION_ID: UUID = UUID("00000000-0000-0000-0000-0000000005e5")

Document it as: written into ActorRef.session_id when an agent acts on a path where no session exists. Not an exclusion — a recorded absence. These agents are inside the accountability commitment (Operator ruling 2026-08-21); the commitment is unmet for them. A reader can tell "this path has no grant yet" from "this write lost one", which a bare None cannot. It never resolves to an agent_sessions row, by construction — any query joining session_id to that table must exclude it explicitly. When the enumerated baseline at §4 reaches empty, this constant has no writer and it and the baseline are removed together.

The precedent is exact. LEGACY_UNRESOLVED_ACTOR_ID exists because one path genuinely cannot name an identity, and its comment gives the reasoning this borrows: a reader can tell we do not know who from this specific person, which a fresh uuid4() could never do. It is a sentinel, not a fabrication, and it carries its own removal condition.

3.2 At each enumerated site in §4, construct the agent ActorRef with session_id=UNSESSIONED_ACTION_ID.

3.3 — None becomes a defect. After this change, three states are distinguishable on an agent write: a real session id, the sentinel, and None. None on an agent-kind actor is now a bug rather than a silence, and §5's test is what makes that enforceable.

4. The baseline

4.1 — allowlist-as-data, per R1. A module-level frozen structure, not conditionals scattered across the routers. The R1 reasoning applies in mirror image: SANCTIONED_SYSTEM_WRITERS is data because adding to it is an authority grant; this is data because adding to it is an admission that another path writes without a grant. Both deserve CR-level scrutiny; neither should be reachable by a one-line edit nobody reads.

4.2 — grouped by blocker, not flat. The three populations are stuck on different things and will be discharged by different work. A flat list would hide that.

| Group | Sites | Why it has no session | Discharged by | |---|---|---|---| | A — HTTP bearer-token agents | 15+ routers plus api/deps.py:1156 | The actor is an agent contributor row, not an agent_identity. There is nothing to mint against | Grants at the API boundary — unscoped | | B — seed induction | engagement/seed_induction.py:215, engagement/creation.py:575 | It holds a seeded identity (seed_induction, migration 0113) and simply does not mint | Smallest of the three. It could mint today | | C — render specialists | render_specialist_bootstrap.py ×4 | Per-binding identity from render_specialist_binding, which per-role session minting cannot express | standing-notes/loomworks-scoping-per-binding-session-minting-v0_1 |

Group B is worth stating plainly: it is not blocked. It has an identity, and minting is one call. It is listed as debt rather than fixed here because CR-2026-232's job is enumeration, not closure — but it is the one that could be discharged next week rather than next quarter.

4.3 — credit/specialists.py:76 and commit_orchestration.py:103/153 are excluded and the exclusion is recorded: the first is a module constant unused at runtime, the second two create contributor rows rather than ActorRefs. Recorded so the next census does not re-litigate them.

5. The test that makes it a baseline

5.1 An AST scan over src/ for ActorRef(...) constructions with kind="agent", comparing the discovered set against the §4 structure. Fails when a site appears that is not enumerated. Shrinking the list is a passing change; growing it fails until the CR that grows it says why.

5.2 Assert no agent-kind ActorRef construction leaves session_id unset — every one carries either a real session or the sentinel.

5.3 — pin against the constant, not the literal. Per the literal-pinning standing note: assert ref.session_id == UNSESSIONED_ACTION_ID, never the UUID string. A test pinning the literal cannot follow a ruling.

5.4 — the discriminating question this baseline answers. Is this absence deliberate? Before: unanswerable. After: the sentinel says yes, the enumeration says which population, and None says no.

6. What this does not do

7. Acceptance

  1. Every agent-kind ActorRef construction in src/ carries either a real session id or UNSESSIONED_ACTION_ID. None carries None.
  2. The AST test fails when an un-enumerated agent site is added — verified by adding one temporarily and confirming the failure, then removing it.
  3. The baseline is grouped by blocker as §4.2, with each group's discharge path named.
  4. The sentinel never appears in agent_sessions, and any query joining session_id to that table excludes it.
  5. The system property at §1 is recorded in the record, not only in this CR — a short standing note stating the commitment is unmet for the three populations and that they are in scope.
  6. Suite green with the delta accounted for; ruff clean; mypy against the unchanged baseline; no migration.

8. Gates

Halt and report at each. Explicit path staging; no git add -A; halt before push.


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-232 — Unsessioned Agent Actions: Sentinel and Baseline — v0.1 — 2026-08-21