Skip to content

Cartridge Selection and the Dual-Tier Entitlement Model — ADR-132

Cartridge Selection and the Dual-Tier Entitlement Model — ADR-132

One ruling from Phase 4 (2026-08-09, owner brief), wiring OwnsAsync to room mechanics and closing the membership question ADR-085 deliberately left open.

It amends ADR-085 rather than superseding it, and the distinction is not procedural. That record rules on two things: that the membership model is an open product decision — which this closes — and that departure and erasure are two verbs that must never become one operation, which is still correct, still load-bearing, and would be retired along with everything else if this record superseded it. docs-and-adr-workflow.md §3 defines Amends as narrowing one clause while the target stays Accepted. That is precisely this.


ADR-132 — The Owner Is the Wallet, the Game Master Is the Authority, and Entitlement Is Checked in Two Tiers

Status: Accepted · Date: 2026-08-09 · Amends 085 (its openness clause only; the departure/erasure ruling stands unchanged), Depends-on 097 (the entitlement surface and its five clauses, including clause 4’s separation of authorisation from disclosure), Depends-on 131 (the opaque PlayerId and the linkage this reverses direction on), Depends-on 033 (the mailbox discipline that decides how the activation check is dispatched), Depends-on 078 (the erasure-by-construction property whose new consequence is recorded here) · interacts with 128, 094, M-027

Context

Phases 3a–3c built identity and entitlement and left one gap recorded twice: nothing consumed an entitlement decision, because no room-level cartridge selection existed to hang one on. The Product Owner’s dual-tier model supplies it — a base cartridge bound to the room and paid for by its owner, and content modules loaded as feature flags whose entitlement any participant may supply when the owner subscribes.

Three things in the brief could not be built as written, and each is a constraint the corpus already owned.

Decision

1. The durable identifiers are PlayerId and CartridgeUrn, and the cartridge is bound at creation with no method that changes it. Decision 1’s “cannot be hot-swapped” is enforced by the absence of a call site rather than by a check that refuses one (P3). A reflection test asserts that no public method other than Room.Create accepts a CartridgeUrn.

RoomCreated gained two optional members rather than required ones. Widening a positional record with required members fails to deserialise every stream written before this record — including the recorded-session corpus ADR-065 replays in CI, where the break surfaces as a mass failure three layers from its cause. A pre-ADR-132 stream replays as unprovisioned, which is the honest state and the one the gate refuses to guess about.

2. Owner and game master are separate, and the owner is a member with no authority. Three questions that one “permission” concept would merge: may this principal pay for it (Room.Owner), may they command it (RoomMemberRole.GameMaster), may they see it (ViewerRole, untouched by this record). ADR-097 clause 4 predicts the merge by name. A group that buys a system and hands the screen to whoever is refereeing tonight is the common case, so ownership grants nothing mechanical — the owner is admitted as a Player.

Exactly one game master at a time, and reassignment demotes the previous holder in the apply rather than the decision, so a replay reproduces it. Two simultaneous mechanical authorities is a rules-adjudication conflict with no tiebreak: ADR-094 resolves opposed rolls against a policy-setting authority, and two of them makes the policy ambiguous at the moment it is read. Co-GMs are a widening with their own ADR, not a set this record leaves open.

3. The Tier A check is DISPATCHED, not awaited in OnActivateAsync. The brief asked for the check inside activation. The outcome it wanted — a locked room whose ALC never boots — is unchanged; how the check arrives is not.

Awaiting a database round trip in activation means that when the entitlement store is slow, every room activation blocks. Orleans’ activation timeout then fails the activation, and the next message retries it — a hot loop of failing activations against a dependency that is already struggling, at exactly the moment it can least afford it. ADR-033 forbids unbounded I/O holding the mailbox for this reason, and the hydration saga three methods away had already made this call: set a flag, fire the work, answer immediately, refuse commands with an explicit reason. AwaitingEntitlement and Locked join Hydrating and Failed on the same pattern.

4. An unprovisioned room skips the gate; a provisioned room with no gate locks. The first is not a loophole: a pre-ADR-132 stream records no cartridge, so there is no paid cartridge to protect and none to load. The second is the fail-closed half — the gate dependency is optional so a test host driving unprovisioned rooms needs no entitlement wiring, and a production host that provisioned rooms and forgot the gate must not silently run paid cartridges for free.

5. The activation gate carries no grant hint, and the creation filter does. §2.5’s fresh-read hatch exists for a buyer who purchased in this session; an activation has no connection and therefore no hint, so a replica miss is authoritative there. The just-purchased case is caught at creation, which does have a principal. This is why there are two checks over one rule and why that is not duplication: only the gate is authority. Removing the filter costs a caller a clear error at creation; removing the gate lets a room run a cartridge nobody paid for.

6. Content modules are feature flags, pooled behind the owner’s subscription. Any member may supply a module’s entitlement provided the owner holds urn:descent:sub:premium; without it the pool is the owner alone, which is how “players can only use modules they personally own” is expressed in one code path rather than two.

7. M-F-03 — the premium subscription is inert in production and will remain so. ADR-M-027 defers recurring billing, ADR-121 is Conditional on a recurring-billing surface existing, and Q-086/Q-087 are pending on the same. The Marketplace cannot grant a subscription entitlement at all, so Tier B’s pooling branch is unreachable until that lands. The code is built and exercised; the product behaviour is not available. Recorded as a finding because a reader who saw the tests pass would otherwise conclude the feature works.

8. M-F-04 — §3 and ADR-097 disagree about caching entitlement, and this record resolves it. §3’s prohibition table forbids caching entitlement state “beyond a room’s lifetime”, which permits caching within one; ADR-097’s alternatives list rejects “cache entitlements per room activation” outright. Those two sentences cannot both be applied literally, and this epic is the first thing that had to. Resolution: the module set is derived on demand, held only in T0 activation state, never persisted and never in a snapshot. Revocation reaches it at the next resolution — a bounded staleness this record states rather than claims does not exist.

9. ADR-131’s reverse lookup is built, and the reversal is recorded. That record said of player-id-to-subject: “there is no legitimate caller for it”, and added no index. Tier A is one and could not have been foreseen: a room activating has no connection principal — the Activation Paradox — so the only route from a stored owner to an entitlement question runs backwards through the linkage.

Alternatives Considered and Why Rejected

  • A durable check inside OnActivateAsync, as briefed. Rejected on clause 3’s mechanism.
  • Superseding ADR-085. Rejected: it would retire the departure/erasure ruling, which is correct and unrelated to the openness this closes.
  • Making the owner the game master by default. Rejected as clause 2’s merge, and it would additionally make ADR-128’s GameMasterIntentAuthority grant command authority to a billing entity who may not be at the table.
  • A set of game masters. Rejected as clause 2 records: no tiebreak, and ADR-094 needs one policy-setting authority.
  • Storing the owner’s Marketplace account id on the room, avoiding the reverse lookup. Rejected outright: it puts a linkable identifier into T0 state, which ADR-078 forbids.
  • Caching an “entitlement verified” fact on the room. Rejected by §3 and revocation-blind.
  • Retrying Unavailable inside the activation. Rejected: it holds an activation open against a dependency that is down, and Orleans already has the right recovery — an idle room deactivates and the next message activates it fresh, which asks again.
  • Recording which member supplied a pooled module. Rejected: nothing needs it, and it would be a new class of retained data with an ADR-079 question attached.

Consequences (including negative)

  • Erasing a subject locks the rooms they own. The gate can no longer resolve them, and answers Unavailable. This is the intended product behaviour — an erased person’s paid room stops working — and it is a behaviour, not an implementation detail, which is why it is here and asserted by a test.
  • Tier B is inert (clause 7). The unpooled branch is the only reachable one today.
  • A room can be created and then lock. The creation filter and the gate can disagree if an entitlement is revoked between them. That is correct — revocation must win — and it means “created successfully” is not a promise that the room will run.
  • RoomCreated is now nullable in two members forever. Optional members cannot be made required later without the upcaster this avoided; the cost is that every consumer must handle an unprovisioned room, which is also the benefit.
  • The reverse lookup is bounded by convention, not structure. Nothing prevents another caller, and no test can distinguish a legitimate use from a curious one. What this record asks is that a new caller argues its case in a record, as this one did.
  • Q-078 remains pending, so the fresh-read fallback metric stays observed and not alarmed even though Tier A now drives it.

Rights-holders (ADR-079)

This record retains no new class of data. Membership is PlayerId-keyed and therefore already opaque under ADR-078/131; the cartridge urn is a product identifier and not personal data; and clause 8’s module set is derived, never persisted. Who can claim on the membership record besides the platform: the member, and honouring an erasure claim requires nothing new — severing their linkage under ADR-131 already renders their membership unattributable.

Enforcement

  • RoomMembershipTests.NoPublicSurfaceCanRebindTheCartridge — clause 1’s immutability, by reflection over the public surface.
  • RoomMembershipTests.TheOwnerIsAMemberButNotTheGameMaster and AMemberWhoIsNotTheOwnerMayNotAssignTheGameMaster — clause 2.
  • RoomMembershipTests.APreAdr132StreamReplaysAsUnprovisioned — clause 1’s optional members.
  • RoomEntitlementGateTests — clauses 4 and 5, including TheActivationGatePassesNoGrantHint and AnOwnerWithNoLinkageIsUnavailableRatherThanUnentitled (the erasure consequence).
  • ContentModuleResolverTests.WithoutTheSubscriptionAGuestsModuleIsNotPooled — clause 6, and the branch clause 7 says production takes.
  • ContentModuleResolverTests.WithoutTheSubscriptionTheMembersLibrariesAreNotQueried — the cost assertion a resolver that queried everyone and discarded the answers would fail.
  • Adr097EntitlementSurfaceTestsunchanged. Putting the gate in Application.Identity rather than Application.Rooms meant the disclosure-path ban needed no narrowing.

§11 Index Line

ADR-132: The dual-tier entitlement model, and the membership model ADR-085 left open. Owner, game master and viewer are three answers to three questions — may this principal PAY for it, may they COMMAND it, may they SEE it — and the owner is admitted as a Player with no mechanical authority, because a group that buys a system and hands the screen to tonight's referee is the common case. Exactly one game master, demoted in the APPLY so replay reproduces it, since ADR-094 needs one policy-setting authority. The cartridge is bound at creation with no method that changes it, and RoomCreated's two new members are OPTIONAL rather than required, because required ones fail to deserialise every pre-ADR-132 stream including ADR-065's replay corpus. Tier A's check is DISPATCHED rather than awaited in OnActivateAsync: awaiting a round trip in activation blocks every activation when the entitlement store is slow, Orleans' activation timeout then fails it, and the next message retries — a hot loop against a struggling dependency, which is what ADR-033 forbids and what the hydration saga beside it already solved with a flag. An unprovisioned room skips the gate (no cartridge, nothing to protect); a provisioned room with no gate LOCKS. The gate carries no grant hint and the creation filter does, because §2.5's hatch is for a buyer purchasing in-session and an activation has no connection — so there are two checks over one rule and only the gate is authority. Content modules are feature flags pooled behind the owner's subscription. Findings M-F-03 (the subscription is INERT until ADR-M-027 gives the Marketplace a recurring-billing surface) and M-F-04 (§3 permits caching entitlement within a room's lifetime while ADR-097 rejects a per-activation cache; resolved as derived T0-only state with bounded staleness stated). Erasing a subject locks the rooms they own, which is the intended behaviour. (Amends 085's openness clause only; its departure/erasure ruling stands. Depends-on 097, 131, 033, 078.)