DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-120-actor-kind-read-contract-and-commit-gate-v0_1.md

CR-2026-120 — Actor-Kind Read Contract + Commit-Gate Enforcement — v0_1

Version. 0.1 Date. 2026-06-29 Repo. loomworks-engine Branch base. main @ d32c99b Author. Claude.ai (scoping + CR). Operator: Marvin Percival. Executor. Claude Code on DUNIN7-M4. Room. Memory (Pipeline-walk Finding 1 of 3).


Plain-language summary

The Companion writes memory the system then cannot read back. The write side accepts an actor kind of companion; one read serializer does not, so any assertion the Companion authored crashes the Memory room with a 422. That is why E0060's Memory room shows "Couldn't load this room" — all five of its assertions are Companion-authored.

This CR makes read match write: it adds companion to the read-side actor-kind types so Companion-authored memory displays correctly. Introduced once as a shared ActorKind alias so the two sides cannot drift apart again.

The same inspection found a security-relevant hole the display fix would otherwise make more reachable: the commit and discard gates block non-humans by checking kind == "agent", but a companion kind is not "agent", so it slips through. The seed commits Loomworks to Operator-authority — "the machine surfaces and signals; the Operator approves." The Companion is the machine. This CR closes the gate so a companion actor cannot commit or discard, making that principle structural rather than dependent on indirection elsewhere holding forever.

Two changes, one contract: companion is a first-class actor kind we display honestly, and precisely because it is a distinct kind, we enforce that it is a non-committing one.


Seed alignment


In scope

  1. Introduce a canonical ActorKind alias. A single Literal["contributor", "agent", "person", "companion"] referenced everywhere the driver-kind is constrained, so the set cannot drift across duplicated inline declarations again.
  2. Add companion to the read-side serializers (the live crash plus its siblings of the same bug class):
  1. Fix the stale ORM CheckConstraint at memory/events.py:79 (currently the 3-value set, shadowed by live migration 0057) so a schema regen / create_all cannot silently re-narrow the DB.
  2. Close the commit/discard gate hole. Change the human-only gates from "block == "agent"" to "block any non-human kind," explicitly excluding companion, mirroring the canonical tier-mapping at credit/authorization.py:27 (_AGENT_KINDS = {"agent", "companion"}):

Out of scope (Room-1 residue, recorded not actioned)


Step-0 inspect-first (mandatory before edits)

Before any change, CC reads each listed site at current HEAD and confirms the line still matches this CR's quoted state (d32c99b may have advanced). Two-pass discipline: inspect-and-report first, edit after confirmation. If any site differs from what is quoted here, halt and report the divergence rather than editing blind.

Specifically confirm:


Execution steps (halt-before-push; suite green at each commit)

Stage by explicit path. Per-step commits. Push is a separate, explicitly-authorized instruction — do not push until the Operator says so.

Step 1 — Canonical alias. Introduce ActorKind = Literal["contributor", "agent", "person", "companion"] in the appropriate shared location (co-locate with the domain ActorRef in memory/base.py, or a types module it can import without cycle — CC's judgment, report the chosen home). Reference it at the domain ActorRef.kind so the alias is anchored to the write authority. Commit. Suite green.

Step 2 — Read-side serializers. Repoint the five read-side kind/actor_kind Literals to include companion (via the alias where the field is the full driver-kind set; DecidedBySchema.kind additionally carries rule — preserve that, add companion to the actor portion). Sites: schemas.py:2525, 3756, 3788, 3864, 4292. Commit. Suite green.

Step 3 — Stale ORM constraint. Update memory/events.py:79 CheckConstraint to the 4-value set matching live migration 0057. No new migration needed (DB already correct); this aligns the ORM so a regen cannot re-narrow. Commit. Suite green.

Step 4 — Commit/discard gate enforcement. At each of assertions.py:204, assertions.py:491, creation.py:748, creation.py:802, deps.py:1044: change the non-human rejection from kind == "agent" to reject any kind in the non-human set ({"agent", "companion"}, referencing the credit/authorization.py pattern — ideally import or mirror _AGENT_KINDS so commit-gate and spend-tier share one definition of "non-human actor"). A companion actor must be rejected by these gates with the same error a human-only gate gives an agent. Commit. Suite green.

Step 5 — Tests. Add/extend tests:

Commit. Suite green.

HALT. Report terminal SHA and suite count. Await explicit push authorization.


Verification (runtime — exists is not works)

After merge, restart the dev server (serializer + gate logic load in-process), then verify against both probes:

E0060 (repair):

  1. Open E0060 Memory room. Confirm it now loads (HTTP 200, not 422) and the five held Companion-authored assertions render with kind: companion shown honestly.
  2. Confirm /assertion-groups and /contributors/me for E0060 no longer 500.

E0089 (clean loop + reproduction-then-fix):

  1. Add one note through the Companion (writes a companion-authored held assertion — the same shape that crashed E0060 from clean).
  2. Confirm E0089's Memory room now displays that note (the from-scratch reproduction is now fixed).

Gate enforcement (the security fix actually holds at runtime, not just in code):

  1. Confirm a companion-attributed actor cannot commit or discard through the gate — attempt the commit path with a companion ref and confirm it is rejected. This proves the Operator-authority enforcement is structural.

Pass condition: E0060 loads and renders; E0089 displays a freshly-added Companion note; companion ref is rejected at the commit/discard gate. Only then does Room 1 advance to Finding 2.


Risk notes