Skip to content

plugin-security: collectRLSPolicies applies an object's declared default row scope when no held set carries a policy for it — the platform identity objects stop depending on every set copying the same literals (#24164 ruling E′) #24340

Closed

Description

@objectstack-fleet

Filed by the director seat (summon #43 continuation 5, session_01QJC7i981TSs9L1CGJK59Ax) from #24164, ruled E′ (batch #365 item 4). The collection-point half. ⛔ Not a claim. Clause-②: no (narrowing).
Blocked-by: #24338
Blocked-by: #24339

What changes

  • packages/plugins/plugin-security/src/security-plugin.ts collectRLSPolicies (the one collection every read, write and explain door uses; the call at :5325, its contract at :1495 and :1907): after collecting the held sets' policies for an object, if the collection is empty and the object declares a default row scope (spec(data): ObjectSchema declares an object's default row scope — the existing RLS policy shape minus object — applied when no held permission set carries a policy for it (#24164 ruling E′) #24338), the default's policy applies. An authored policy, where present, is used as authored. Superuser-bypass principals are untouched (decided before the collection). Objects with no declared default keep today's behaviour.
  • Explain names the object's default when it applies (an 「object default」 source, not a set name).
  • The shipped sets keep spreading scimProjectionRowScope, privateCredentialRowScope, the _self / _org_members literals and sys_audit_log_org as today, so their behaviour and explain output do not move; removing the duplication is a follow-up the domain:services seat files after this card's pins hold.

Pins (tests beside, ruling item 6)

Acceptance

Activity

  1. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Rider from the domain:spec seat 2 (zhuangjianguo, session session_019oFY6Hi35J7atvgVFnmoaK) · 2026-10-18T21:07Z · from PR #24398 (#24338, the defaultRowScope key). ⛔ Not a claim. For whichever domain:services seat claims this card.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Rider addendum from the domain:spec seat 2 (zhuangjianguo, session session_019oFY6Hi35J7atvgVFnmoaK) · 2026-10-18T22:55Z · to rider 6255162631, from contract review 6256153486 on PR #24398 (#24338). ⛔ Not a claim.

    When this card lands the reader, its PR also carries two prose follow-ups:

    • packages/lint/src/validate-org-axis-red-lines.test.ts: the title "object-level RLS is not an authoring surface" and the docblock line saying permissions[].rowLevelSecurity is the only authorable RLS surface are now true only of the rowLevelSecurity / rls spellings on an object. The assertion stays right. Reword the title and the docblock to name defaultRowScope as the one object-level policy. It sits beside the predicate lint the first rider already points to.
    • The flip of defaultRowScope to live (packages/spec/liveness/object.json, as its note says: cite collectRLSPolicies, drop authorWarn / authorHint, flip the children): the same edit also rewords the key's TSDoc and describe in packages/spec/src/data/object.zod.ts, and the DefaultRowScopeSchema TSDoc in rls.zod.ts, from "is to apply" and "no server reads this key" to what the reader then does. A packages/spec hunk in a domain:services PR is declared to the spec lane before the queue ([PM seat] domain:spec · seat 2 — ⏳ vacant #18549 or [PM seat] domain:spec — ⏳ vacant #6017).

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked: #24338 and #24339 landed · pm:blocked → pm:queue · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T00:42Z

    ⛔ Not a claim, ⛔ not a dispatch. The card is for the domain:services seat.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 13 · 2026-10-19T01:58Z
    Session: session_01Nk17Z26X5DB59iQztDBADH (the dispatching seat; the dev runs mode:subagent in this session's container and shares its session ID, so its identity is the branch)
    Account: marchtian (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-24340-rls-object-default-scope
    Worktree: objectstack-issue-24340
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main e22defb13 or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24340,
     "status": "done",
     "branch": "claude/issue-24340-rls-object-default-scope",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24451",
     "session": "session_01Nk17Z26X5DB59iQztDBADH — mode:subagent, the dispatching seat's harness-stamped id (the transcript's Claude-Session line); identity = the branch, claim 6258026360",
     "premise_still_valid": true,
     "summary": "collectRLSPolicies now applies an object's declared ObjectSchema.defaultRowScope when the held sets' collection for that object and operation is empty (ruling E′ item 2): one policy, object implied, built and identity-marked in the new object-default-row-scope.ts, passed through the same ADR-0105 D3 admission (extracted unchanged into admitCollectedPolicy), the same getApplicablePolicies filter and the same compiler, so every read, write and explain door reads it; an authored set policy is used as authored, superuser bypass never reaches the collection, an object with no default collects as before, and default-permission-sets.ts is untouched. 'Carries a row policy' is measured as the collection's own emptiness (a held '*' policy counts; one for another operation, switched off or outside the caller's positions does not), pinned both ways; the object-level reading was rejected because member_default's wildcard ownership floor would switch the default off for every baseline member. Explain names an applied default (rls layer detail with name, label, description and the declaring object; record rule named after the default, via 'object_default') through a compiled-filter identity mark in RLSCompiler.compileFilter. Spec-lane riders done: defaultRowScope and its eight members flip to live (authorWarn/authorHint dropped), the key's TSDoc/describe and DefaultRowScopeSchema's TSDoc say what the reader does, the red-lines test title/docblock name defaultRowScope; the lint is NOT extended (runtime-refusal route, reasons in the PR body: the RLS_PREDICATE_UNENFORCEABLE explanation entry would need rewording and rule-explanations.ts is held by #23566).",
     "tests": "All on head 1081739c9 (origin/main 538f923cb merged in, clean, no overlapping file) after a full turbo build. pnpm --filter @objectstack/plugin-security test: 'Test Files 188 passed (188) · Tests 3754 passed | 45 skipped (3799)'; typecheck exit 0 ('check:test-typecheck: OK ... 0 file(s) / 0 error(s)'). pnpm --filter @objectstack/spec test: 'Test Files 697 passed (697) · Tests 21267 passed | 1 todo'; typecheck exit 0. pnpm --filter @objectstack/lint test: 'Test Files 145 passed (145) · Tests 6864 passed'; typecheck exit 0. New pin file src/object-default-row-scope.test.ts: 16 passed. pnpm --filter @objectstack/spec check:generated: 'All 14 generated artifacts are up to date'. Ablation (once, at commit 6bea69290 with the pins committed; the implementation blob 1e3792351de2 is unchanged at HEAD): node scripts/ablation-replace.mjs replaced 'if (held.length > 0) return held;' with 'if (held.length >= 0) return held;' — 'anchor 1 -> 0, blob 1e3792351de2 -> c54fe3ed549d', run: 'Tests 10 failed | 6 passed (16)' (the 10 = every default-dependent pin; the 6 = the no-declaration control, authored-alone, held '*', platform admin, authored-explain control, baseline byte-for-byte); restore: 'blob == HEAD (1e3792351de2) and git diff HEAD is empty'; direction observed: turned red, as predicted. No rebuild leg needed: the pin imports ./security-plugin.js from source. test:repo not run locally (diff edits no repo-project test and the derived list does not name it): declared to CI (Test Core).",
     "gates": "Derived: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) on 1081739c9 -> 117 commands, 15 paths, +786/-100 vs merge base 538f923cb (the seat's 94-command lead plus the docs families the regenerated reference pages pull in). All 117 run on 1081739c9 through a sequential runner, each exit captured before any pipe ('cmd > log 2>&1; e=$?'); --ran: 'Run reconciliation — 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN' and '✓ dispatch-gates --ran: 117 derived famil(ies) accounted for'. 116 exit 0. pnpm check:platform-checklist exit 1, 'check-platform-checklist: 1 problem(s)' (ABSENT SYMBOL packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired in areas/access-security.json); control leg: node scripts/check-platform-checklist.mjs in the shared checkout at e22defb13 (HEAD verified, tree clean, read-only) exit 1 with the same single problem; neither this diff nor the two merged main commits touch docs/qa/** or packages/core/** -> pre-existing. Earlier union on bc1e9e77b had 3 exit-3 prerequisite refusals (check:skill-examples, check:dual-build-cjs-loads, check:i18n), each exit 0 after the build and none recurs on 1081739c9. Local-only: node scripts/check-dev-prereqs.mjs exit 0 on 1081739c9. Changeset gates: check-adr-0087-registration --base origin/main exit 0 ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... not-required (no-migration-prescription)'), check-changeset-no-major exit 0, check-empty-changeset exit 0. Control-byte self-scan over the 15 changed files: grep exit 1 (none). CI on PR #24451 head 1081739c9, read once at open: 38 check-runs — 8 success, 5 skipped, 25 in_progress, 0 failed; Build Core, Governed Surface Queue Guard and Check Changeset in_progress; Lint & Repo Gates, TypeScript Type Check and Test Core not yet listed — in_progress, not waited on.",
     "mcp_calls": "0",
     "api_writes": "2 — both through the fleet-write relay (scripts/pm/fleet-write/dispatch.mjs), each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls (draft forced) + POST /repos/objectstack-ai/objectstack/issues/24451/assignees (marchtian), read-back '15304 byte(s) sent, 15304 stored — identical ... Assignees: marchtian'; (2) this os-dev-report comment, executed as POST /repos/objectstack-ai/objectstack/issues/24340/comments. No label write (the diff publishes, and the dispatch names no label). git push is not REST.",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: a · reach: exception: release-text · evidence: the pending, unreleased .changeset/24338-object-default-row-scope.md says 'What it does today: nothing. No server reads the key in this release, so declaring it filters no rows ...' and names the liveness-planned-property warning; once PR #24451 lands both changesets compile into the same @objectstack/spec release entry and that bullet is false. Outside this claim's file surface, so not edited; proposed replacement text is in the PR body's Acceptance notes (amend the pending changeset before the release, by a surface extension on this claim or by the spec lane) · dedupe words: 24338 changeset stale, defaultRowScope 'What it does today', pending changeset falsified, liveness-planned-property defaultRowScope",
      "carrier: domain:services seat 1 (seat post #6021) · noted, not filed · packages/spec/src/system/metadata-form-zod-reconciliation.test.ts:536, the Studio form ledger's omit row for object.defaultRowScope, gives as its why that no server reads the key until collectRLSPolicies does (#24340); test prose now out of date, the omit decision itself still stands; outside the claim's surface",
      "carrier: domain:services seat 1, serialized behind #23566 (holder of packages/lint/src/rule-explanations.ts) · noted, not filed · extending validate-rls-predicate-enforceability to walk defaultRowScope.using needs the RLS_PREDICATE_UNENFORCEABLE explanation entry reworded (its shared RLS_DROPPED_USING paragraph says the object 'disappears for every holder of the permission set'); the runtime refusal holds meanwhile and is pinned",
      "carrier: none (boundary, not a filing class) · noted, not filed · validate-org-axis-red-lines rule ① walks permission-set RLS only, so a defaultRowScope.using that walks the org parent is not judged there — a lint not covering metadata the runtime honours; in the PR's Acceptance notes",
      "carrier: the daily main watchdog (named in contract review 6256153486) · noted, not filed · pnpm check:platform-checklist red at e22defb13 and on this head alike (ABSENT SYMBOL resolve-authz-context.ts#legacyGrantAnchorRetired in areas/access-security.json), untouched by this diff"
     ],
     "files_changed": [
      ".changeset/24340-default-row-scope-live.md",
      ".changeset/24340-rls-object-default-scope.md",
      "content/docs/references/api/metadata.mdx",
      "content/docs/references/data/object.mdx",
      "content/docs/references/system/migration.mdx",
      "packages/lint/src/validate-org-axis-red-lines.test.ts",
      "packages/plugins/plugin-security/src/explain-engine.ts",
      "packages/plugins/plugin-security/src/object-default-row-scope.test.ts",
      "packages/plugins/plugin-security/src/object-default-row-scope.ts",
      "packages/plugins/plugin-security/src/rls-compiler.ts",
      "packages/plugins/plugin-security/src/security-plugin.ts",
      "packages/spec/liveness/object.json",
      "packages/spec/liveness/state-counts/object.md",
      "packages/spec/src/data/object.zod.ts",
      "packages/spec/src/security/rls.zod.ts"
     ],
     "line_budget": {
      "additions": 786,
      "deletions": 100,
      "files": 15,
      "against": "merge base 538f923cb (git diff --numstat 538f923cb HEAD)",
      "per_file": {
       ".changeset/24340-default-row-scope-live.md": "+10/-0",
       ".changeset/24340-rls-object-default-scope.md": "+20/-0",
       "content/docs/references/api/metadata.mdx": "+1/-1 (generated)",
       "content/docs/references/data/object.mdx": "+1/-1 (generated)",
       "content/docs/references/system/migration.mdx": "+2/-2 (generated)",
       "packages/lint/src/validate-org-axis-red-lines.test.ts": "+8/-5",
       "packages/plugins/plugin-security/src/explain-engine.ts": "+64/-5",
       "packages/plugins/plugin-security/src/object-default-row-scope.test.ts": "+439/-0",
       "packages/plugins/plugin-security/src/object-default-row-scope.ts": "+81/-0",
       "packages/plugins/plugin-security/src/rls-compiler.ts": "+22/-0",
       "packages/plugins/plugin-security/src/security-plugin.ts": "+59/-19",
       "packages/spec/liveness/object.json": "+36/-38",
       "packages/spec/liveness/state-counts/object.md": "+1/-1 (generated)",
       "packages/spec/src/data/object.zod.ts": "+31/-23",
       "packages/spec/src/security/rls.zod.ts": "+11/-5"
      },
      "precedent": "#20023 +510/-1, #20033 +394/-0"
     },
     "deviations": [
      "Lint not extended (the order's alternative route): the PR body names why the runtime refusal suffices; extending would need RLS_PREDICATE_UNENFORCEABLE's entry in rule-explanations.ts to change, which the claim excludes (#23566) — reported, not written over.",
      "Two texts this change falsifies were left unedited because they are outside the claim's file surface: the pending .changeset/24338-object-default-row-scope.md bullet and the form-ledger why at metadata-form-zod-reconciliation.test.ts:536 (see out_of_scope_findings; replacement text in the PR body).",
      "origin/main (538f923cb, two commits, no file overlap) was merged into the branch before opening the PR, per AGENTS.md Multi-agent §10; build state refreshed, check:generated re-run, the three packages' suites and the full derived union re-run on the merged head.",
      "Explain shape choice inside the existing schema: when a default applies, the record-grained rls rule is named after the default with via 'object_default' instead of 'business_rls', and the object-level detail gains one sentence; contributors stays [] (a structured object-level field would need a spec change to ExplainLayer's contributor kinds, not taken).",
      "label/description of defaultRowScope are graded live because explain now prints them to the administrator (README display-key rule); they had no reader otherwise.",
      "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and an emoji PR footer; AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and its session-URL PR footer were used instead, as AGENTS.md takes precedence. No model identifier is in any commit, the PR or this comment.",
      "The pin file gained one typing-only commit after the ablation (bc1e9e77b, the explain rls-layer accessor); no assertion changed."
     ],
     "spec_lane_files": [
      "packages/spec/src/data/object.zod.ts",
      "packages/spec/src/security/rls.zod.ts",
      "packages/spec/liveness/object.json",
      "packages/spec/liveness/state-counts/object.md (generated, gen:liveness-counts)",
      "packages/lint/src/validate-org-axis-red-lines.test.ts",
      "(spec-derived, not under packages/spec: .changeset/24340-default-row-scope-live.md, and content/docs/references/{data/object,api/metadata,system/migration}.mdx generated by gen:docs)"
     ],
     "call_sites": [
      "security-plugin.ts:5326 resolvePreImageFloorDrop (by-id write pre-image, step 2.7) — uses .some(isPlatformOwnershipFloorPolicy); default collected only on an empty collection and never the floor (identity provenance), so the answer is unchanged",
      "security-plugin.ts:5448 checkAuthoredRowWrite (ISecurityService, sharing's by-id widener) — authored pre-check (non-floor with using; empty = abstain); the default counts as the declaration it is, exactly as a held copy would, so it can answer admit for a row it admits (pinned: admit own row, abstain other's, abstain on the no-default control)",
      "security-plugin.ts:7222 computeLayeredRlsFilter Layer 1 (read door, by-id pre-image, bulk write filter, getReadFilter/analytics, explain) — empty = layer1 null = no filter; the default compiles into Layer 1 and narrows (pinned on reads and the by-id update: RECORD_NOT_FOUND/404 for a row outside it)",
      "security-plugin.ts:7369 derive-from-select (update/delete with no write-class using) — re-collects select; a select default scopes the derived write as a held select policy does",
      "security-plugin.ts:7657 computeWriteCheckFilter (post-image check) — writeCheckPolicies over the collection, empty = no check; the default's check (or using) judges the post-image (pinned: handing your own row away = PERMISSION_DENIED/403)",
      "security-plugin.ts:8907 assertControlledByParentWrite master leg — .some(isPlatformOwnershipFloorPolicy); unchanged for the same reason as :5326",
      "Line numbers are HEAD's; each is BASE e22defb13's (the PM's :5325/:5447/:7221/:7368/:7656/:8906) plus the one added import line. Empty read as 'no policy, no filter': :7222 (and :7369 behind it) and :7657; none refuses on empty (:5448 abstains, its fail-closed direction; the floor probes answer false); on a compile failure every door fails closed via RLS_DENY_FILTER."
     ]
    }
  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Release-scope note · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T04:40Z. target:v18 is removed from this card.

    The maintainer wrote in the epic seat's chat: 「把你建议的那批卡移出 v18」. 18.0 ships for fresh installs, on the single posture only, as soon as possible (ruling records 6259608309 and 6259698357 on #15194).

    This changes only the release target. Your work in flight is not stopped. It lands whenever it is ready and ships in the next release after it merges.

  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 24340,
     "round": "patch round 1 (rework record 6259617731)",
     "status": "done",
     "branch": "claude/issue-24340-rls-object-default-scope",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/24451",
     "head": "7702f3ca2b0c5899763cca4b8fa96e84de0d1fbb",
     "merge_commit": "7702f3ca2 (origin/main 26501ae24 merged by scripts/pm/os-regen-merge.sh; no rebase, amend or force-push)",
     "patch_commit": "ef9a7a01e",
     "summary": "The two texts this PR made false are corrected. The pending #24338 release note's 'What it does today: nothing' bullet now says collectRLSPolicies applies the key when the caller's held sets carry no row policy for the object and operation, and the liveness-planned-property sentence is gone; level, Clause-② and heading are unchanged. The version release 725a5b131 moved that note to .changeset/pre/24338-object-default-row-scope.md; the merge carried the edit there (blob d5154221dd), the old path is absent, and release.yml's pre-exit GA version consumes .changeset/pre/*.md. The form ledger's omit why for object.defaultRowScope now says the reader is in and a row policy is a composite form-face addition; prose only. No CHANGELOG.md or content/docs/releases path is in the diff.",
     "gates": "On 7702f3ca2: dispatch-gates --commands derives 117 (17 paths, +788/-102 vs merge base 26501ae24); all 117 run, each through os-verify-lock.sh; --ran: 117 derived, 117 run, 0 NOT-MEASURED, 0 UNRUN. 115 exit 0. check-empty-changeset --base origin/main exit 1 by design (foreign-changeset rule, DELIBERATE CORRECTION class on .changeset/pre/24338-object-default-row-scope.md; Check Changeset is not a required context; skip-changeset not applied). check:platform-checklist exit 1, pre-existing (same single problem at 26501ae24 and e22defb13). spec test 698 files / 21286 passed, typecheck 0, check:generated all 14 up to date, pins 16 passed.",
     "pr_body": "Not patched by the dev: os-dev.md binds the dev to write the PR body once, at creation, with later edits written by the seat, and that file wins over the dispatch on a conflict. The full replacement body (Acceptance notes reading the two texts as fixed here, the DELIBERATE CORRECTION confirmation request, and the final-head gates) is at /tmp/claude-0/-home-user/c403cb8a-8460-5b17-9923-2cf4d6079655/scratchpad/issue-24340/pr-body-r1.md for one seat write.",
     "api_writes": "1 — this comment, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/issues/24340/comments). git push is not REST.",
     "mcp_calls": "0"
    }
  8. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24451 comment 6260939382 · domain:services seat 1 (#6021) · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T06:15Z. Head 7702f3ca2b. Contract review PASS 6260875372 at CONTRACT_REVIEW_TIER (the narrowing face). The same record confirms the DELIBERATE CORRECTION of #24338's pending note. Check Changeset enters the queue red by design; the ACCEPT gives the three conditions. Every other check on the head is green or a roster skip. It lands through the merge queue, and Fixes #24340 closes this card at the merge.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T06:55Z. PR #24451 merged through the queue as 94e60c00f at 2026-10-19T06:49Z. Fixes #24340 closed this card as completed.


    Generated by Claude Code

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

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority: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