Skip to content

spec(rls): a membership key cannot yield "the person records I am" — valueField must be a reference column, never id, so an employee, agent or customer roster cannot give its user current_user.my_employee_ids #31129

Closed

Description

@objectstack-fleet

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

Filing gate: ① a named location. This came up in the #30913 ruling analysis (in the project files at issue-30913/decision-analysis.md §6), which was read against origin/main 5f00374492. ⛔ Not a claim. Triage sets the grade and the lane.
class: b · reach: every app whose people are records (HR employees, service-desk agents, portal customers) and whose managers or leads must see their people's rows. Read from the spec, not measured on a running server.
type: finding

Business reason

In HR, service-desk and portal apps, the person is a row: employee e05, agent a04, customer cus_03. A row-level rule such as "a manager sees the assets of their department" needs current_user.my_department_ids. That works today: the key is declared on employee with userField: 'user' and valueField: 'department'. But a rule that needs which employee I am, for example "I see leave requests whose employee is me" or "a lead sees tickets whose agent reports to me", needs current_user.my_employee_ids. That is the id of the roster row itself, and the shape refuses it.

The only routes left are owner_id on every such row, or a separate (user, employee) junction table. Real apps in these domains do not model either. The scenario runner (#30913, ruled Q2 = C) leaves the manager and portal views to the build's own platform means, so this gap shows up there as AI-built apps that cannot express a common rule.

Where (origin/main 5f00374492)

Wanted (a design question for the spec lane; not decided here)

Decide whether a membership key may take valueField: 'id' (the set of the caller's own roster rows), or whether another documented route covers "the records I am".

  • If the answer is no, the RLS guide and the data skill should say so and name the route. Today an author finds out only at publish.
  • If yes: the refusal list loses its id exclusion, and the skill gains the "person is a record" example.

Level: spec (RLS contract). Clause-②: to be decided by the spec lane. Metadata-protocol: to be decided by the spec lane.

Dedupe (open and closed): "membershipKeys valueField" and "membership key roster valueField id" (11 hits) turned up the original design (#25234, closed), its docs and skill cards (#26423, #26297, #26184), catalog cards (#22682, #20297, #15196, #15204) and QA run records. None raises the self-row case. New.

Related: #30913, #25234, #30914.

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:specDomain lane — seat card indexed by label:pm:seat
    enhancementNew feature or request
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 7, 2026
  2. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:spec · decision · 2026-11-07T13:56Z

    ⛔ Not a claim.

    • An RLS membership key cannot yield "the records I am": valueField must be a reference column, never id (rls.zod.ts, about L760). So an employee, agent or customer roster cannot give its user current_user.my_employee_ids, and rules like "I see leave requests whose employee is me" have no route except owner_id or a junction table. Read from the spec, not measured.
    • p2, lane domain:spec. It changes what an RLS contract accepts, which is a security-boundary question, so triage puts it to the maintainer rather than leave it to the lane. finding removed, needs-user-decision.

    维护者速读

    • 要裁什么: 行级权限(RLS)的「成员键」能不能取 valueField: 'id',也就是返回「我本人对应的那几行记录」(例如员工表里我自己那一行)。
    • A(推荐):允许。 这个键只产出 userField 等于当前用户的那些行的 id,本身不多放任何数据;规则拿它授多少权,和其他键一样由作者决定。改动是去掉现有的 id 排除,再给技能补一个「人就是一条记录」的示例,并走契约评审。
    • B:不允许。 在 RLS 指南和数据技能里写明,并给出替代办法(每行加 owner_id,或建一张「用户↔员工」关联表)。现在作者要到发布时才会发现这条路走不通。

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Ruling: batch #T962 item 3 · letter A · maintainer 「同意」 2026-11-07T14:47Z

    Path: none

    Presented in the triage seat's session as batch 1 item 3. The maintainer answered 「同意」 to A: allow a membership key to take valueField: 'id', the caller's own roster rows. The key yields only rows whose userField is the caller, so it grants nothing by itself; what a rule grants with it stays the author's choice.

    Execution:

    • domain:spec removes the id exclusion in rls.zod.ts (the membership-key contract) and adds tests. Contract review is required, and it confirms that id is not mistaken for a reference column elsewhere.
    • domain:skills then adds the "the person is a record" example to the data skill and the RLS guide.

    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 7, 2026
  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2eHDXrsKgGAUHq956U

    Claim: PM loop round 1 · 2026-11-07T14:53Z
    Session: session_017ihL2bjT2KeFnLTtWu2yPW (card thread; dispatched by seat session session_017ihL2eHDXrsKgGAUHq956U)
    Account: marchtian
    Branch: claude/project-thread-ph4s7x
    Worktree: the card thread's own container checkout
    Domain: domain:spec
    Seat: domain:spec#4
    File surface: packages/spec/src/security/rls.zod.ts (membership-key contract prose, valueField JSDoc/.describe()), packages/lint/src/validate-membership-key-declarations.ts and the authoring-rule text in packages/lint/src/authoring-rules.ts, packages/lint/src/validate-rls-predicate-enforceability.ts (what a key's set references when valueField is id), packages/plugins/plugin-security/src/object-membership-keys.ts (resolver, only if it needs to read id), tests in those packages, regenerated references, one changeset. ⛔ skills/** and the RLS guide (the domain:skills follow-up).
    Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), reason: Clause-② yes widening on the published schema, built at the default tier and reviewed at CONTRACT_REVIEW_TIER by the seat; full Fable review per the maintainer's standing spec rule, effort: default
    Clause-②: yes
    Metadata-protocol: added
    Responsibility: n/a — not a defect card (ruled widening, 6708428544)
    Thread-read: 6708428544
    Premise: holds
    Serial constraints cleared: none (no open claim or PR on these files; disjoint from #31035, #31094, #31099)

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

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2bjT2KeFnLTtWu2yPW

    os-dev-report

    {
      "issue": 31129,
      "status": "done",
      "branch": "claude/project-thread-ph4s7x",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31174",
      "head": "bad5140f",
      "session": "session_017ihL2bjT2KeFnLTtWu2yPW",
      "premise_still_valid": true,
      "summary": "A membership key may take valueField: 'id' (the caller's own roster rows). The exclusion lived in lint (refusal 6, id resolved untyped) and the spec prose, not in a Zod refine; both lifted. One exported predicate isRosterOwnId (driver-provisioned id only; an authored field named id is judged by its type) feeds the declaration judge and the refusal-11 target judge, which now reads such a key's target as the declaring object. The resolver projects id once; dedupe, the 1,000 bound, the Layer 0 wall and fail-closed misses are the existing path.",
      "tests": "at bad5140f: lint 197 files / 8340 tests pass; plugin-security 248 files / 5799 pass, 45 skipped; metadata-core 25/966 pass; spec object-membership-keys 27/27; lint, plugin-security, spec typecheck exit 0. Acceptance (real SecurityPlugin + ObjectQL + SQLite): u_x sees lr_1, lr_4 only; u_y sees lr_2; cross-org employee row dropped. Ablations via ablation-replace.mjs (mutation proven on disk, restored to HEAD blob): primaryKey accept off -> 2 accept tests red; target set off -> refusal cases red; resolver projection reverted -> acceptance red.",
      "gates": "dispatch-gates --commands at bad5140f: 108 families; 104 exit 0 first pass; 4 exit 3 (prerequisites: teardown self-test fixture commit, client-react dist, i18n build closure, full dist for dual-build-cjs-loads) rerun after building -> all exit 0. --ran: 108 run, 0 NOT-MEASURED. spec check:generated exit 1 (check:docs stale) -> --fix exit 0, committed. eslint --no-inline-config on touched .ts exit 0. PR check-runs: in_progress at report time.",
      "line_budget": "size M; 12 files incl. 2 regenerated reference pages; ~+420/-30, mostly tests",
      "deviations": [
        "File surface: packages/lint/src/rule-explanations.ts (the two membership-key explanations) was edited although the Claim lists only authoring-rules.ts text; authoring-rules.ts needed no change (it never stated the reference-only rule).",
        "Merged origin/main once (cbe20ab0) before opening; main has moved 5 commits since, not merged again (no conflict reported)."
      ],
      "files_changed": [
        ".changeset/31129-membership-key-own-rows.md",
        "content/docs/references/data/object.mdx",
        "content/docs/references/security/rls.mdx",
        "packages/lint/src/rule-explanations.ts",
        "packages/lint/src/validate-membership-key-declarations.ts",
        "packages/lint/src/validate-membership-key-declarations.test.ts",
        "packages/lint/src/validate-rls-predicate-enforceability.ts",
        "packages/lint/src/validate-rls-predicate-enforceability.membership-keys.test.ts",
        "packages/plugins/plugin-security/src/object-membership-keys.ts",
        "packages/plugins/plugin-security/src/object-membership-keys.test.ts",
        "packages/spec/src/data/object-membership-keys.test.ts",
        "packages/spec/src/security/rls.zod.ts"
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: domain:skills follow-up (seat files it) · skills/objectstack-data + RLS guide 'the person is a record' example, per the ruling; PR body line Skills-docs: follow-up owed"
      ],
      "security_findings": "none",
      "dogfood_battery": "n/a — no `!` in the PR title and Clause-② carries the (widening) arm",
      "mcp_calls": "3 — mcp__github__issue_read (get, get_comments x2); no write tool",
      "api_writes": "3 relay dispatches, 4 REST writes: pr_create (POST /pulls draft + POST /issues/31174/assignees), PR comment 6709361136 (Fable verdict), this report comment. git push not counted.",
      "fable_review": "round 1 full diff PASS with should-fix (2 should-fix + 4 nits; both should-fix and 2 nits fixed, 2 nits left with reason); round 2 delta PASS. Verdict comment 6709361136 on PR #31174."
    }
  8. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 7, 2026
  9. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2eHDXrsKgGAUHq956U

    Landed · domain:spec#4 · 2026-11-07T16:53Z

    reading value
    PR #31174 merged 2026-11-07T16:52:49Z through the queue, merge commit 3c1eb635a07744541c11dc809c1336e6692d83f0, reviewed head bad5140f28 (contract review PASS + ACCEPT on that head)
    origin/main contains 3c1eb635a

    State: card closed completed by Fixes; pm:dispatched removed. Skills-docs follow-up: #31176 (domain:skills, Blocked-by #31129, now unblocked).

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions