Skip to content

ADR card: membership-scoped RLS (id in current_user.org_user_ids) lowers to an engine-internal semi-join leaf instead of a pre-resolved id array, removing the per-request completion read and the bind-parameter ceiling #29360

Closed

Description

@objectstack-fleet

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Filing gate: ① design returned by the #29281 dev (os-dev-report 6577634380, design field), with seat ruling 6577690137 on #29281 (item 4). Triage's ruling on #29281 (6575719368, follow-up 6575447789) named this redesign as ADR and spec-lane work. Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p2 · the interim (PR #29356) makes the set exact up to the backend's bind ceiling, so no member is hidden below it; the ceiling and the per-request cost remain.

Reach: every organization with more than 1,000 members (a completion read on each request), and every organization past the backend's bound-parameter limit (SQLite between 32,000 and 40,000 members, Postgres 65,535), where sys_user reads now refuse loudly after #29281.
Reader who acts: the ADR author (governed path docs/adr/, GOVERNED_APPROVERS), then the domain:spec lane, then the domain:server lane per driver family.

Where it stands (measured on main a19759ac, driver-sql over better-sqlite3)

  • sys_user's scope id == current_user.id || id in current_user.org_user_ids lowers through rls-compiler.ts → cel-to-filter.ts to { id: { $in: [...] } }, with one bound parameter per id.
  • No subquery form a scope can use exists today. ADR-0139's ReadableIds (packages/spec/src/data/readable-ids.ts) is the only semi-join leaf. It is engine-internal and composes the caller's own read scope on the inner object, so it cannot express user_id FROM sys_member WHERE organization_id = :active_org. rls.zod.ts (L108, L481-505) and execution-context.zod.ts (L374-378, L414) state that subquery-shaped needs are pre-resolved arrays.

What the ADR must decide (from the dev's design; the seat agrees)

  • D-a Predicate shape. Keep the authoring spelling id in current_user.org_user_ids, so every shipped set, defaultRowScope and app CEL keep working. The RLS compiler lowers that one key to a branded engine-internal leaf, a sibling of ReadableIds, that SQL drivers compile to id IN (SELECT user_id FROM sys_member WHERE organization_id = :active_org AND user_id IS NOT NULL AND user_id <> ''). The alternative is to generalise ReadableIds. ⛔ Either way, the inner scope must not be the caller's read scope on sys_member, which would change the set.
  • D-b Inner filter. Exactly today's read: organization_id = the active org, with system authority. Not the tenant wall: in group posture that wall is organization_id IN accessible_org_ids, which would widen the set. No status or ban filter, because adding one narrows. The user_id IS NOT NULL AND <> '' guard keeps negation NULL-safe.
  • D-c Caller arm. Today's list adds the caller; the lowering must OR the caller's id in, or the ADR states the change as a narrowing.
  • D-d Driver obligations. Under ADR-0139 D3 (compile or refuse loudly): the SQL family compiles, and driver-memory evaluates in process. driver-mongodb refuses ReadableIds today, and refusing this leaf would fail every sys_user read on Mongo. Floor: a driver that cannot compile the leaf keeps PR fix(core)!: org_user_ids holds every member of an organization with more than 1,000 members #29356's exact completion read. Mongo compiling it ($lookup) is a follow-up.
  • D-e org_user_ids as a value. Demote it to a lazily resolved field. Readers that take it as a value should move to a point query or the leaf: plugin-audit's mention-recipient gate, plugin-security's explain engine, objectql's engine.ts run-as seeding (~L22710), the hook-wrapper and current-user endpoint copies, and approvals annotations. CEL hooks and flows keep the array.
  • D-f Same class. Every rlsMembership key (execution-context.zod.ts L422, contracts/rls-membership-resolver.ts) is a pre-resolved array with the same bind ceiling. The ADR says whether resolvers may return a leaf too, or records it as out of scope.
  • D-g Acceptance. The visible set is identical before and after, for orgs under and over 1,000 members. PR fix(core)!: org_user_ids holds every member of an organization with more than 1,000 members #29356's pins stay green unchanged (resolve-authz-context.org-members-past-first-read.test.ts, sys-user-org-members-past-first-read.test.ts). Add a group-posture pin (a member of org A with access to org B sees only A's members) and a negation pin with a NULL user_id row.
  • Stale prose to amend with it: ADR-0057's D3 rollup bullet and plugin-auth admin-set-user-manager.ts (~L71) cite "the org_user_ids cap" as precedent, and that cap no longer exists after PR fix(core)!: org_user_ids holds every member of an organization with more than 1,000 members #29356.

Order after the ruling

ADR (governed) → spec card (the leaf beside readable-ids.ts, the rls.zod.ts exception, the execution-context.zod.ts doc, per-driver conformance) → server cards per driver family (objectql seam and comparand door, rls-compiler.ts and cel-to-filter.ts lowering, drivers, resolve-authz-context.ts lazy accessor, the D-e readers). Each step is pinned by PR #29356's tests.

Dedupe: searched org_user_ids relational membership, membership semi-join RLS; only #29281. New.

Links back: #29281, PR #29356, ADR-0139, ADR-0057.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:spec · pm:queue · 2026-11-02T14:59Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Claim: PM loop round 3 · 2026-11-02T16:03Z
    Session: session_01ADHkmSduNGC2SZXszyrNut (seat); dev = card thread session_01ADHkmSNasUm3e1dfbV5aaD
    Account: os-steve
    Branch: claude/project-thread-qe1kwi
    Worktree: the card thread's own checkout of objectstack-ai/objectstack
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: one new ADR under docs/adr/ (next free number) and its docs/adr/README.md index row; an amendment note on ADR-0057 D3's "org_user_ids cap" bullet; the admin-set-user-manager.ts (~L71) comment only if the lane allows it, else out_of_scope (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default — the tier is the PM's per-card call (floor sonnet · default opus · ceiling fable); an ADR on a security boundary (who can see whom), one CONTRACT_REVIEW_TIER review by the seat, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: membership-scoped RLS (id in current_user.org_user_ids) lowers to a pre-resolved id array | a per-request completion read and the backend bind-parameter ceiling | every organization past 1,000 members, and past the bind limit
    Thread-read: 6580364238
    Premise: holds (on origin/main, cel-to-filter.ts lowers the scope to { id: { $in: [...] } } and no scope-usable semi-join leaf exists); the dev measures first and stops with premise_still_valid: false if it does not reproduce
    Serial constraints cleared: no in-flight claim names a new ADR on RLS membership lowering, ADR-0057 or ADR-0139

    Governed path: the PR stays DRAFT and waits for a GOVERNED_APPROVERS approval. ⛔ The ADR keeps exactly the visible set #29281 holds (triage 6580364238); any change to who can see whom goes to the maintainer first.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSNasUm3e1dfbV5aaD
    os-dev-report

    {
      "issue": 29360,
      "status": "done",
      "branch": "claude/project-thread-qe1kwi",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29501",
      "session": "session_01ADHkmSNasUm3e1dfbV5aaD",
      "premise_still_valid": true,
      "summary": "Wrote ADR-0147 (Proposed): FIELD in current_user.org_user_ids keeps its spelling and lowers to a branded sibling of ReadableIds (provisionally OrgMemberIds) carrying the resolver's own (organizationId, callerId) anchor; the SQL family compiles it to (FIELD = :caller OR FIELD IN (SELECT user_id FROM sys_member WHERE organization_id = :org AND user_id IS NOT NULL AND user_id is non-blank)), with no tenant wall on the subquery and no validity/status filter; every evaluator that does not compile it (mongodb, odata, a federated object, the in-process judges) gets the exact completion-read array of PR #29356 as the floor, chosen up front, never a refusal. D-0 states the same-set invariant S(org, caller) as its own decision; every set-changing option is under Alternatives as rejected. D-e re-measures the org_user_ids census on cafe8ee6 (corrected from the card); D-f records rlsMembership leaves as out of scope with the reason; execution plan S1 -> V1 -> V2 -> V3 (-> V4 mongodb). ADR-0057-erp D3 gets one dated amendment note; README index regenerated.",
      "tests": "No tests: an ADR-only diff (docs/adr/** x3), no forced test edits. Premise measured on origin/main cafe8ee6: resolve-authz-context.ts resolveUserAuthzGrants reads sys_member {organization_id} via tryFind {isSystem:true} (no tenantId, so outside the wall), completes past 1,000 via readOrganizationMembersToCompletion, keeps non-empty string user_ids and adds the caller; cel-to-filter.ts lowerMembership emits {FIELD: {$in: value}}; no scope-usable semi-join leaf exists (ReadableIds composes the caller's getReadFilter on P per ADR-0139 D2).",
      "gates": "On head f3a117fb: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 21 commands; prebuild os-verify-lock.sh -c 'pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2' VERDICT command-exit 0 (125 s, at d55c14e4; the second head changed one ADR table row only). All 21 exit 0, stdin /dev/null, exit captured before any pipe (check-adr-links +self-test, check-closing-keyword-parity +self-test, check-comment-mask-corpus, plan-test-core-shards --self-test, lint check:doc-formula-expressions, check:adr-anchors, check:adr-index, check:adr-symbol-anchors, check:ci-filter-parity, check:closed-ceremony-text, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:pm-governed-merges, check:pm-prior-rulings, check:refd-timer-probe, check:watch-hint-literal). --ran with ':: exit N' records, exit 0: '21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED (a DERIVED zero ...)'. Offline PR-body check: check-changeset-no-major.mjs --base origin/main --event FILE exit 0 (LEVEL AXIS: clause-② no, metadata-protocol none). CI check-runs on PR #29501: not read (opened at this report; not waited on).",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "6 — mcp__github__issue_read get #29360; mcp__github__issue_read get_comments #29360 (x2: card read, claim check); mcp__github__issue_read get_comments #29281; mcp__github__list_pull_requests (open, for ADR-number collision); mcp__github__pull_request_read get #29501 (label/draft read-back). No write tools.",
      "api_writes": "2 — both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): pr_create -> #29501 with labels [skip-changeset] (read back 8475 bytes sent, 8475 stored, identical; assignees none; draft true, label present per pull_request_read); this os-dev-report comment on #29360 via post-stamped.mjs. git push is not a REST write.",
      "open_questions": [
        {
          "question": "New public names in @objectstack/spec (ADR-0147 O1): the leaf (provisionally OrgMemberIds, beside ReadableIds) and the driver's compile declaration the engine consults to pick leaf vs floor. Put to the maintainer before S1?",
          "options": [
            "A OrgMemberIds + a driver declaration named by the spec card, both approved by the maintainer with this ADR",
            "B ActiveOrgMemberIds (names the active-org binding explicitly)",
            "C keep both unexported (engine-internal symbol only); third-party drivers always take the floor"
          ],
          "recommendation": "A, because the leaf mirrors ReadableIds' exported shape (drivers must recognise it) and the name matches the dev's design; C is safe but locks every external SQL driver onto the per-read completion and the ceiling."
        },
        {
          "question": "D-d departs from ADR-0139 D3 for this leaf: a consulted driver declaration (not an inert capability bit) chooses leaf vs exact-array floor up front, instead of compile-or-refuse. Does that need its own ADR-0139 amendment note?",
          "options": [
            "A No: ADR-0147 states the departure for its own leaf and cites D3; ADR-0139 is untouched (as drafted)",
            "B Yes: add a one-line pointer note to ADR-0139 D3 in this PR"
          ],
          "recommendation": "A, because ADR-0139's text governs only ReadableIds and stays true; B is a one-line add if the seat wants the cross-reference."
        },
        {
          "question": "Freshness (ADR-0147 O4): the leaf reads sys_member at statement time, whereas with the opt-in OS_AUTHZ_GRANTS_CACHE_TTL_MS the array could be as old as its cache entry. Same rule, newer inputs. Does that count as 'who can see whom' changing under triage's bar?",
          "options": [
            "A No: S is the same function; only staleness shrinks (recorded under Consequences)",
            "B Yes: put it to the maintainer as a decision before acceptance"
          ],
          "recommendation": "A, because the cache is default-off and its TTL is an accepted staleness window, not a scope; with it off (the default) nothing changes."
        }
      ],
      "security_findings": "withheld",
      "out_of_scope_findings": [
        "class: observation · packages/plugins/plugin-auth/src/admin-set-user-manager.ts (~L71) quotes ADR-0057 D3 'mirroring the org_user_ids cap'; the quotation stays accurate as a quotation; a comment edit in a server-lane package is outside this ADR card · carrier: ADR-0147 V3 (O6) · noted, not filed",
        "class: observation · org_user_ids reads no ADR-0091 validity window while accessible_org_ids applies one; sys_member declares no bound today, so no stored row is answered differently · carrier: ADR-0147 O3 (kept; closing it changes S) · noted, not filed"
      ],
      "files_changed": [
        "docs/adr/0147-membership-scoped-rls-lowers-to-an-engine-internal-org-member-semi-join-leaf.md (new, 350 lines)",
        "docs/adr/0057-erp-authorization-core-business-units-and-scope-depth.md (+8: one blockquote amendment note under D3's cap bullet; Status line untouched)",
        "docs/adr/README.md (regenerated by scripts/adr-index.mjs: +4 / -3, one row + counts)"
      ],
      "line_budget": "dispatch-gates: 360 changed lines (+357 / -3) in 3 files vs the human-merge threshold 3000: under. No production code.",
      "deviations": [
        "The admin-set-user-manager.ts comment is left out (claim: 'only if the lane allows it, else out_of_scope'): it is a code file in a server-lane package and the delta says no code; carried by V3 (O6).",
        "D-e corrects the card's reader list: current-user-endpoints.ts and the approvals/sharing exec-context-annotation pins read nothing; the automation (plugin.ts, run-resume.ts, runtime-identity.ts), sharing/share-link and assemble-execution-context carriers and the Array.isArray presence probes were missing.",
        "Rule beyond the card: an array on the context wins over the anchor (D-a), so every hand-built context answers as today; and the anchor's caller, not the compiling context's userId, is OR-ed in (D-c), measured against explain-engine's delegated inheritance.",
        "The harness asked to set an assignee/reviewer via MCP and to subscribe to PR activity; both skipped (standard order §3 no MCP writes; os-dev: pr_create carries no assignees, the dispatch contract overrides the PR-subscription posture). The PR body carries the harness attribution comment on line 1 and Fixes #29360 on line 4.",
        "Second commit f3a117fb only refines one D-d table row (names the in-process judges precisely); the lint closure prebuild was reused from d55c14e4 (formula/lint untouched); all 21 gates re-ran on f3a117fb."
      ]
    }

    Stamped 2026-11-02T16:40Z.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmRPdc4hbJF33icNKjQ
    Release: session_01ADHkmSduNGC2SZXszyrNut (seat 2, signing off on the maintainer's wind-down) · PR #29501 (ADR-0147, Tier H) is ACCEPTed at 723e27a6 and waits on a GOVERNED_APPROVERS approval, and no seat stays behind for it · → pm:queue, for the next live domain:spec seat to land
    Resume-from: claude/project-thread-qe1kwi@723e27a6b8ab7580eeb451c4c7781e049044248e

  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Claim: PM loop round 1
    Session: session_01WGufzfgvGexKn9vLHsy89K
    Account: os-support-ai
    Branch: claude/project-thread-qe1kwi (the released branch; landing only, no new dev work)
    Worktree: none (the seat hands PR #29501 to the governed approvers and lands it after approval)
    Domain: domain:spec
    Seat: domain:spec#3
    File surface: as PR #29501: docs/adr/0147-…, docs/adr/0139-…, docs/adr/0057-…, docs/adr/README.md (governed, Tier H)
    Container & model: M, landing only, model: TIER_DEFAULT (seat); at-tier records already on the PR: contract review delta PASS 6583780256, seat ACCEPT 6583798126, both at head 723e27a6
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card (an ADR card)
    Thread-read: 6591510931
    Premise: holds. PR #29501 head is still 723e27a6; 48 check-runs, 26 success and 22 skipped, none red.
    Serial constraints cleared: none; the four ADR files are touched by no other open PR read here.
    Resume-from: claude/project-thread-qe1kwi@723e27a6b8ab7580eeb451c4c7781e049044248e

  6. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Landing: os-zhuang (a GOVERNED_APPROVERS member) approved PR #29501 at head 723e27a6; the head has not moved since ACCEPT 6583798126 and contract review PASS 6583780256, so no delta record is owed. Readied and auto-merge armed (2026-11-03T01:44Z); needs-user-decision is off the PR. The seat follows it to MERGED.

  7. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K

    Landed: PR #29501 (ADR-0147) merged after os-zhuang approved it at head 723e27a6; this card closed completed with it, and pm:dispatched is off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions