Repository navigation
feat(spec): an object declares its default row scope, defaultRowScope, the RLS policy shape without object - #24398
Conversation
…pe without object WIP: schema, refinement sharing and pins; generated artefacts follow. Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…angeset, dropped-refinement sites Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ope; drill its ledger row Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ard and liveness counts Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
… yet Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…ject-default-row-scope
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d83d4a340f61f41be45eb9f77575d2f2a8839771 && git checkout d83d4a340f61f41be45eb9f77575d2f2a8839771
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d79dc4883c5698cf05680a545758bdbba0d8a7a2 e254e52d0a9feea667b7b2d35caf7676e33e573b && git checkout -B drift-repro d79dc4883c5698cf05680a545758bdbba0d8a7a2 && git merge --no-ff e254e52d0a9feea667b7b2d35caf7676e33e573b
node scripts/docs-audit/affected-docs.mjs --json d79dc4883c5698cf05680a545758bdbba0d8a7a2
|
席位核对 (seat check) · PR #24398 ·
|
…ppen, not as present fact Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…pe describe Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 (seat check) · PR #24398 ·
|
…one rule entry; the served object count moves to 46 Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
…on from the fail-safe Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
席位核对 (seat check) · PR #24398 ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, assignee zhuangjianguo. Line 1 of the body is Fixes #24338, line 2 is Clause-②: yes, and there is no other closing keyword. 26 files, +549/−83 against the merge base 1eae21f971. Not governed. The changeset is minor on @objectstack/spec and patch on @objectstack/metadata-core, with Clause-②: yes (widening). The seat applied the dev's eight body pairs; read back, the bytes are identical. |
| The key | ObjectSchema.defaultRowScope, optional: one policy of DefaultRowScopeSchema. It is the RLS policy's own member instances, without object, priority and tags. It reuses the same named refinement and the same alias table. object is refused by name, with guidance. The shape is ruling E′ item 1 as written: one policy (answer A, 6255131226). |
| Round 1 | The describe, the TSDoc on the key and on the schema, the changeset, the liveness note and the form ledger row now say what is to happen. They do not describe a reader or writer that exists. "Not applied yet: no server reads this key in this release" is said once, plainly. |
| Round 2 | Test Core 5/6 and 6/6 were red on 8964dfbbb and 62170b56e, read from the job logs. The cause was two domain:engine pins that enumerate ObjectSchema's keys. The seat extended the file surface to them (declared on #6367, 6255534103). OBJECT_REFERENCE_POSITIONS classifies defaultRowScope as one rule entry, the validations posture. The served object count moves 45 → 46. The new pin has three legs. Its third leg (a denied field named operation) is the one that tells the classification from the unclassified fail-safe. The dev's ablation showed the two-leg pin could not go red without it. Accepted as a deviation in the seat's favour. |
| Carried | The A answer and the merged predicate's divergence (when org_user_ids is unresolved) → #24341, rider 6255156624. The authoring-time lint covers only permissions[].rowLevelSecurity → #24340, rider 6255162631. |
| For the contract review to weigh | Is the describe as narrow as today's enforcement (Prime Directive #10)? Does DefaultRowScopeSchema refuse and accept exactly what the policy does, minus object? Is the metadata-core classification the right posture for a served object document? Is minor + (widening) right for the spec half and patch for metadata-core? |
| Local runs (dev, final head) | metadata-core 770 tests passed; the meta-types file 18 passed; metadata-core typecheck exit 0. The spec rounds: 696 files / 21246 tests passed on 8964dfbbb; round 1 re-ran its touched files, typecheck, check:generated (14/14) and check:doc-authoring. Gates: 112 derived, reconciled with --ran; 108 exit 0. Three are NOT MEASURED (dist prerequisites) and left to CI. check:platform-checklist is red on main too, and not from this diff. |
| CI | At the time of this record, on e254e52d0: 20 success, 7 skipped, 18 in progress (Test Core 1–6/6 among them). The landing waits for every check. |
| Landing | After a contract-review PASS and a fully green head: pr_ready then automerge_enable. The merge closes #24338. That unblocks #24341 (its only Blocked-by:) and moves #24340 one step closer: it still waits on #24339. |
Generated by Claude Code
Contract reviewServed-tier: Inputs read: card #24338 (body and all five comments: the claim Check-runs on ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Contract review adopted ·
Generated by Claude Code |
Fixes #24338
Clause-②: yes
The spec half of ruling E′ on #24164 (ruling comment 6250748677, items 1 and 7).
ObjectSchemagains one optional key,defaultRowScope: one row-level security policy, written like a permission set'srowLevelSecurityentry withoutobject, which the declaring object implies. Nothing reads the key yet. Its reader is the collection-point card #24340, its writers are the declarations card #24341, and the ADR note is #24339. No object that exists today changes behaviour: at the base commitb2e78701f7the worddefaultRowScopeoccurs nowhere in this tree, and nowhere in objectui at the.objectui-shapin0707b4ae37(git grepexit 1 on both, against asharingModelcontrol that hits on both).The shape, and why it is built this way
RowLevelSecurityPolicySchema(contract unchanged)DefaultRowScopeSchema(new,@objectstack/spec/security)name,label,description,object,operation,using,check,positions,enabled, plus thepriorityandtagstombstonesobject,priorityandtagsusing/check; nocheckon aselectordeletepolicyroles/roletopositions,withchecktocheck,condition/filter/wheretousingobject.omit({ object: true }). zod 4.6.5 throws on it:.omit() cannot be used on object schemas containing refinements(measured with a probe; the policy schema carries a.superRefine). So the new schema is rebuilt from the same three parts, each shared by reference, not copied: the policy schema's ownshapeminus the left-out keys, the refinement (now the named functionrefineRowLevelSecurityPolicy), and the alias table (now the constRLS_POLICY_ALIASES).objectis kept. Both rules read onlyoperation,usingandcheck. The schema has noobject-dependent default and noobjectalias. The one placeobjectstill appears is the inheritedcheckdescribe, which defines an "applicable" policy as one whoseobjectmatches; that text is shared, not forked.priorityandtagsareretiredKey()tombstones and accept no value. Leaving them out changes nothing the schema accepts: both are still refused, as unknown keys. It also keeps their upgrade text, which is about therowLevelSecurity[]path, off a surface that never had either key, and keeps two[RETIRED]rows out of a new authorable-surface entry. The input type is pinned at compile time as exactly the policy input type withobject,priorityandtagsomitted.RowLevelSecurityPolicySchemakeeps its contract; only its source moved. The inline.superRefinebody moved into the named function verbatim (the 45 lines were compared by a script before the swap: identical), and the alias object moved into the const. Its rows in the authorable-surface, authorable-defaults and api-surface shards and its dropped-refinement entry are untouched by this diff, and its existing tests pass. This is a deviation from the claim's "no change toRowLevelSecurityPolicySchemaitself", read as "no change to its contract": the.omitroute the claim names throws, and sharing the rule by name is the alternative to copying it.The key and its describe
defaultRowScope: DefaultRowScopeSchema.optional(), placed aftersharingModel. Its describe is published text that SDKs and AI authors read, so it states:rowLevelSecurityentry withoutobject, withobjectrefused;rowLevelSecuritypolicy."No tracker number appears in the describe, the guidance or the history sentence (
check:doc-authoring). The TSDoc beside the key and the schema names the cards.What else moves, each by its own tool
packages/spec/liveness/object.json):defaultRowScopeisplanned, withauthorWarnand anauthorHint. That is the pending-reader spelling theaction.outcomeMessagesrow used: authoring the key raises oneliveness-planned-propertywarning (probed: one finding on an object that declares it, zero on the control). The row is drilled into eightplannedchildren, becausecheck:livenessrefuses an undeclared container inheritance, and deferring topermission/rowLevelSecurityfails its equality check (the key sets differ byobject,priority,tags). Only the container carriesauthorWarn, so the warning fires once. The note gives the flip condition: goliveon the commit that lands plugin-security:collectRLSPoliciesapplies 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, citingcollectRLSPolicies, and dropauthorWarn/authorHintin the same edit.pnpm --filter @objectstack/spec check:generated --fixafter a build:authorable-surface/{data,security}.json,authorable-defaults/security.json,json-schema.manifest/security.json,api-surface/security.json,export-origins/security.json,declaration-map/security.json,liveness/state-counts/object.md, the reference pages undercontent/docs/references/**, and the strictness-ledger counts (docs/audits/2026-07-unknown-key-strictness-ledger.counts/security.md: one more strict site inrls.zod.ts).dropped-refinements.baseline.json(hand-edited by design, no generator): one new entry,security/DefaultRowScopeat the root, and the newdefaultRowScopesite added to the nine entries that embedObjectSchema(data/Object, the threesystem/*change-set shapes and fiveapi/*responses), as the build printed them;measuredmoved 239/771 to 240/781, whichdropped-refinements.test.tsholds equal to the entries. The refinement is the policy's own; it has no JSON Schema form, exactly as onsecurity/RowLevelSecurityPolicy, which is already listed.metadata-form-zod-reconciliation.test.tsrecords why: no server applies the key yet, so a form row would let an author declare a row restriction that filters nothing; its writers are to be the platform objects, declared in code, and none declares it yet. Revisit when the reader lands.composeStackswithobjectConflict: 'merge': the composer derives the refused fixed-shape config objects fromObjectSchema.shape, so it now refuses two stacks that declare a differentdefaultRowScopeon one object, likeaccessorpublicSharing, and the refusal text's list of config objects namesdefaultRowScopetoo. No composition changes today, because no stack declares the key. The two literal lists incompose-stacks-merge-{collection,config-object}-refusal.test.tsgained the key in shape order.@objectstack/metadata-core, patch round 2, commit91b2f39f3):OBJECT_REFERENCE_POSITIONSinobject-schema-fls-references.tsclassifiesdefaultRowScopeas one rule entry (ruleEntry, the per-entry testruleEntriesalready applies). When an object document is served to a caller who cannot read some fields, the whole policy is dropped if itsusing,checkor prose names one of them, and served unchanged otherwise: thevalidationsposture, because deleting onlyusingwould serve a policy that reads as restricting nothing. No behaviour moves until an object declares the key./meta/typescount (@objectstack/metadata-protocol, same commit): theobjectcontrol inprotocol.meta-types-degenerate-derivation.test.tsmoves 45 to 46, with its history sentence..changeset/24338-object-default-row-scope.md:minoron@objectstack/specandpatchon@objectstack/metadata-core, with the lineClause-②: yes (widening).Tests
packages/spec/src/data/object-default-row-scope.test.ts, the card's three cases plus a control:objectparses, withenabled: truedefaulted;objectis refused: one issue,unrecognized_keys, at pathdefaultRowScope, keys['object'], and the message carries the implied-object guidance;unrecognized_keys, keys['appliesTo']);defaultRowScope.Also: the same through the
objectmetadata write door (getMetadataTypeSchema('object')); both cross-field rules (no clause:customatdefaultRowScope;checkonselect:customatdefaultRowScope.check); the tombstoned keys refused as unknown keys; the members are the policy schema's own instances; and the compile-time type equality.Ablation (one, on the behaviour this diff adds): keep
objectin the members, throughnode scripts/ablation-replace.mjs(anchor 1 to 0, blobb68921414f53to26896837459d). Expected direction red, observed red and wider than theobjectcase alone: 8 of 10 failed, because a keptobjectis required, so every scope without it fails. The control and the compile-time case stayed green. Restored: blob equals the HEAD blobb68921414f53, andgit diff HEADis empty. No build is involved, because the test importssrcthrough a relative path.Reverse verification of the rebuilt
.d.tsdownstream: a temporary file inpackages/platform-objects/srccallingObjectSchema.create({ …, defaultRowScope: { name, operation: 'select', using } })typechecks (exit 0). A stale declaration file would refuse the key throughNoExcessObjectKeys. Appending a second call withoperation: 'nope'fails with TS2322 at the probe (exit 1). The probe was removed andgit statusis clean.Patch round 2, on the final head
e254e52d0, throughos-verify-lock.sh:pnpm --filter @objectstack/metadata-core test: 18 files, 770 tests passed (769 before plus the new pin).protocol.meta-types-degenerate-derivation.test.ts: 18 passed.pnpm --filter @objectstack/metadata-core typecheck(bothtsclegs): exit 0.object-schema-fls-references.test.ts(commite254e52d0) has three legs. Ausingnaming a denied field drops the scope. Ausingnaming only readable fields keeps it unchanged. A denied field named like the policy's own schema wordoperationkeeps it too, because in a classified position a key is a schema word, not a field reference.ruleEntryline, two runs throughnode scripts/ablation-replace.mjs. First run: only the key-coverage test went red, and the pin's first two legs stayed green. The unclassified fail-safe drops on a mention as well, so those legs could not tell the classification from it. That is why the third leg was added. Second run, with it: the pin and the coverage test both red (2 failed, 54 passed). Restored each time: blob equals HEAD,git diff HEADempty.Patch round 1 (describe and TSDoc wording only), on
62170b56e:pnpm --filter @objectstack/spec check:generated --fixrebuilt spec, regeneratedcontent/docs/references/{data/object,api/metadata,system/migration}.mdxand re-checked all 14 artefacts green.object-default-row-scope.test.tsandmetadata-form-zod-reconciliation.test.ts: 86 passed.pnpm --filter @objectstack/spec typecheck: exit 0.pnpm check:doc-authoring: exit 0 (9445 describe strings clean).Round-0 local runs, on
8964dfbbb(after mergingorigin/main), throughos-verify-lock.sh:pnpm --filter @objectstack/spec test: 696 files passed, 21246 tests passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0; the test layer compiles with no new errors.pnpm --filter @objectstack/spec check:generated --fix: all 14 generated artefacts up to date, measured against a dist built by that run.pnpm --filter @objectstack/spec exec vitest run --project repo src/security/rls-tags-retirement.test.ts: 14 passed (pre-merge head00929288f; the merge did not touch it).pnpm --filter @objectstack/platform-objects typecheckexit 0, and 8@objectstack/lintfiles that readObjectSchema.shapeor the liveness ledger: 1191 tests passed.Who imports
ObjectSchemain a way an added optional key could affectMeasured, not assumed. In this repo,
git grepforkeyof ServiceObject,keyof ObjectSchemaandkeyof z.inputof the object schema finds only spec's ownNoExcessObjectKeys, which an added key widens. The runtime enumerations ofObject.keys(ObjectSchema.shape)are the twocomposeStackspins, updated above, and the lint tests above, which stay green. Two more runtime pins enumerate the key set under spellings round 0's search missed, andTest Core(5/6, 6/6) went red on them at8964dfbbband62170b56e. One ispackages/metadata-core/src/object-schema-fls-references.test.ts("classify each new ObjectSchema key in OBJECT_REFERENCE_POSITIONS", which readsdeclaredKeys(ObjectSchema)). The other ispackages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts(the servedobjectproperty count). Both are fixed in this PR, in commit91b2f39f3:defaultRowScopeis classified as one rule entry, and the count moves to 46. Commite254e52d0adds the pin beside the classification. The seat extended the claim's file surface to these files in patch round 2. In objectui at the pin,object-schema-metadata-spec-derivation.test.tsasserts the document type equalsServiceObjectplus an empty client delta. That holds structurally for an added optional key, and its runtime half assertstoContain, not an exact key count.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) on the final heade254e52d0derives 112 families: the 111 it derived on8964dfbbband62170b56e, pluspnpm check:durability-log-level, whichmetadata-corebrings in (exit 0).--ranreconciliation one254e52d0: 112 accounted, 109 run, 3 NOT MEASURED, 108 exit 0. The changeset gates are among them:check-changeset-no-major,check-empty-changesetandcheck-adr-0087-registrationagainstorigin/main, all exit 0.e254e52d0(107 of 111 on8964dfbbband on62170b56e).pnpm --filter @objectstack/spec run check:skill-examples,pnpm check:dual-build-cjs-loads,pnpm check:lean-entry-closure: exit 3, PREREQUISITE NOT MET. They need builtclient-react, every package'sdist, andobjectql'sdist, which this worktree does not have. NOT MEASURED, and left to CI.pnpm check:platform-checklist: exit 1, and not from this diff.docs/qa/platform-checklist/areas/access-security.json:2823citespackages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired, a symbol that file does not declare atorigin/main(nor at the baseb2e78701f7). This diff touches neither file. By maintainer decision the gate is not in per-PR CI (lint.ymlnote), and its daily watchdog onmainis the reporting channel.pnpm check:dev-prereqs(local-only family): exit 0.Acceptance notes
extendcontributor that loosens a platform object's default (#24164 ruling E′) #24341. Under the ruled single-policy key,sys_user's two shipped literals (sys_user_self,id == current_user.id, andsys_user_org_members,id in current_user.org_user_ids, indefault-permission-sets.ts) cannot both move unrewritten, which that card's text asks. One||policy is row-equivalent whereverorg_user_idsresolves to an array, which the core resolver always produces (resolve-authz-context.tsseeds[]/[userId]). But the compiler drops a whole policy on any unresolved variable. Measured withcompileCelToFilter(@objectstack/formulasource): withorg_user_idsunresolved, the merged predicate is refusedunresolved-variable, whileid == current_user.idalone compiles to{ id: 'u1' }; withorg_user_ids: []the merged predicate compiles to an$orof the two arms. So for an execution context built withoutorg_user_ids, the merged policy loses the self arm, which two separate policies keep. Explain would also name one policy, not two. Details are in the dev report on spec(data):ObjectSchemadeclares an object's default row scope — the existing RLS policy shape minusobject— applied when no held permission set carries a policy for it (#24164 ruling E′) #24338. The seat answered A in6255131226: this PR keeps the single-policy shape.validate-rls-predicate-enforceability.tswalkspermissions[].rowLevelSecurity, so adefaultRowScope.usingthat does not lower is not refused at authoring time. Today that has no effect, because nothing reads the key. Carrier: plugin-security:collectRLSPoliciesapplies 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, before the reader applies the key.validate-org-axis-red-lines.test.tsis titled "object-level RLS is not an authoring surface" and asserts thatObjectSchemadeclares neitherrowLevelSecuritynorrls. It stays green, but its title is now only true of those two spellings.Generated by Claude Code