MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3506

Planning bug: a card specified a write + its guarding read inside an existing transaction without checking that the transaction's SECURITY BINDING admits either

Done
Description

Filed by motir run MOTIR-3500 (parent-run, guard #4) on 2026-08-25. The plan defect is already corrected — the plan cmt9b9y3p001ei4ph9145oz38 proposes the missing prerequisite and re-scopes MOTIR-3501, which is blocked. This card is the telemetry.

The defect

MOTIR-3501 specified, in its own words:

read the org's workspaces through a repository count/list method (a read that guards a write, so it takes the tx) … when there is exactly one, create that WorkspaceMembership … via workspaceMembershipRepository.create(..., tx) — same transaction

Both operations were folded into organizationsService.addMember's existing withOrgContext transaction. That transaction binds app.user_id + app.organization_id and no app.workspace_id — and under row-level security neither operation does what the card assumed. Measured against real Postgres on origin/main @ d32892bd:

TRUE org workspace count = 2
SEEN under withOrgContext(founder) = 1
INSERT under org ctx -> DriverAdapterError: new row violates row-level security policy
                        for table "workspace_membership"

Why it survived authoring

The card was written by reading the service, and the service reads correctly. Everything the card cites is real: addMember exists, withOrgContext exists, workspaceRepository has a tx-taking count and list, workspaceMembershipRepository.create takes a tx. The policies that refuse the work are in prisma/migrations/, a directory the card never opens, and RLS is invisible at the call site by design.

The write half fails loudly and would have been caught in the first minute of building. The read half is the expensive one: it returns a plausible wrong number rather than an error. workspace's RLS answers "which workspaces may this user SEE?"; the card's predicate asks "how many does this ORG have?". Substituting the first for the second returns 1 where the truth is 2, which is indistinguishable from a small org — so the count-1 arm would fire in a two-workspace org whose inviting admin belongs to one of them, creating exactly the membership the card's own AC 3 forbids.

And AC 3's test would have passed anyway. The re-pointed fixture it describes creates both workspaces from one owner, which puts the actor in both, so the actor-scoped read returns 2 and the arm correctly does not fire — for the wrong reason. The card ships green with the defect in it. (The re-scope now pins the fixture: the acting admin must be in exactly one of the two.)

The finding worth keeping — a THIRD unreconciled clause

MOTIR-3500's premise is that organization-tier.md §5 and §6 were never reconciled at count = 1. Neither clause ever says whose count. §6's "count ≥ 2" reads as the organization's; the shipped gate ShellTierNav.tsx:103 counts listUserWorkspaces(session.user.id) filtered to the active org — the viewer's. They coincide exactly when the viewer belongs to every workspace in the org, which is the founder case, which is why nobody noticed.

Both answers are right in their own place and only one is right in each: the membership arm must read the org (whether an invitee is stranded cannot depend on their inviter's memberships) and the disclosure arm must read the viewer (a door nobody can walk through should not be drawn). MOTIR-3501 AC 5 now requires the amendment to say so.

The gap this leaves in the corpus

The gap was already written downlib/repositories/githubRepoRepository.ts:285-305 says "'the org's workspaces' IS NOT READABLE FROM A BACKGROUND PATH … Widening workspace's RLS instead would be a cross-tenant access change, which is not this card's to make." It sits in a comment explaining a workaround, where nothing planning MOTIR-3501 would ever read it.

plan-rules/core.md gate 2 (preconditions verified against shipped reality) covers does this symbol exist. Both symbols existed. What no gate asks is does the security context this card folds its work into ADMIT that work — a question whose answer lives one layer below every file the card cites.

Acceptance criteria

  1. plan-rules/core.md gate 2 gains a limb — or a sibling gate is added — requiring that a card which places a write, or a read that guards one, inside an existing transaction / security context names the policy or binding that admits it, verified against the migration rather than the call site.
  2. That limb states the asymmetry explicitly: a refused write fails loudly and a denied read returns a plausible subset, so the read is the half to verify, and a criterion whose predicate is a COUNT is the carrier to look for.
  3. It names the fixture consequence: a test whose actor happens to see the whole population passes for the wrong reason, so a card asserting a scoped count owes a fixture in which the actor's view and the true population differ.
  4. The rule is stated generically enough to fire outside this tenancy model (any per-request access scoping, not only Postgres RLS with GUCs).

Context refs

  • MOTIR-3501 — the mis-specified card, its blocking comment carrying the full probe evidence, and the re-scope
  • Plan cmt9b9y3p001ei4ph9145oz38 — the correction awaiting approval
  • motir-meta/prompts/plan-rules/core.md gate 2 — where the limb lands
  • motir-core lib/organizations/context.ts, lib/workspaces/context.ts:209, lib/repositories/githubRepoRepository.ts:285-305
  • motir-core prisma/migrations/20260810001000_tenant_root_insert_policies/migration.sql:110, 20260527134009_add_workspace_rls/migration.sql

Repo: the fix is a rule change in motir-meta (prompts/plan-rules/core.md). No criterion names a path in another repository — the motir-core refs above are evidence, not deliverables.