Repository navigation
feat(lint)!: membership key declarations, and the row policies that read them, are judged when authored - #25550
Conversation
…read them when authored (#25234) Adds validateMembershipKeyDeclarations (user/value field, declared twice, resolver collision) and teaches validateRlsPredicateEnforceability the membership-key refusals: a key's filter judged as an RLS clause, a key tested against a column of another target, `==` on a declared key, and (at the save door) a key nobody owns. The RLS rule now also runs for object writes, and the publish gate reads the registered resolver's keys. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 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 fa36efd3cbac99e314f8893fe04adef3ddc11187 && git checkout fa36efd3cbac99e314f8893fe04adef3ddc11187
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 cf991db317782291a2a5be83eafd822638b766cc && git checkout -B drift-repro 673ebfd4dcb592c8501c2ee3ccc25e85a6582056 && git merge --no-ff cf991db317782291a2a5be83eafd822638b766cc
node scripts/docs-audit/affected-docs.mjs --json 673ebfd4dcb592c8501c2ee3ccc25e85a6582056
|
…et their own door rows (#25234) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
席位核对 · ACCEPT ·
|
Contract reviewServed-tier: Reviewed at 2026-10-23T23:54Z. Inputs: card #25234 (body and all 12 comments: design 6344270438, ruling 6355640929, landing plan 6370632236, the three os-dev-reports), PR #25550 (body, 20 files, net diff against ① Derived judgmentsThe ruling's assignment to PR 3 (6355640929 §3: refusals 5–7 and 11–13, registration for
Host fact. Beyond the 13, implied by the ruled registration and declared in the changeset's BREAKING section: (a) an object write's Narrowing census (what a new refusal could now reject, and whether it still passes):
Docs edit in cf991db ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
Seat correction · 2026-10-23T23:55Z: the ACCEPT above names its contract review with an empty id. The record is comment Generated by Claude Code |
Requested by Tesla · project thread
Closes #25234
Clause-②: no (narrowing)
Metadata-protocol: narrowed
PR 3 of 3 of ruling A on #25234 (ruling 6355640929, design 6344270438, landing plan 6370632236): the authoring refusals. PR 1 (#25513) declared
ObjectSchema.membershipKeys; PR 2 (#25528) stages them per request, and every miss fails closed. This PR refuses, where the metadata is written, the declarations that cannot resolve and the policies that read a key in a way that matches no rows: refusals 4 to 7 and 11 to 13 of the design table, plus refusal 3 at the save door, and the runtime-gate registration forobjectwrites.Refusals
membership-key-declared-twicedefineStackalready refuses it in one stack)membership-key-resolver-collisionuserFieldmissing, or not a single-valued user reference (user, orlookup/master_detailtosys_user, notmultiple)membership-key-user-field-invalidvalueFieldmissing, or not a reference (lookup,master_detail,user)membership-key-value-field-invalidfilterthat does not lower, names a column the roster lacks, compares with a list-holding column, is refused by the engine's judge, or reads anycurrent_uservaluerls-predicate-*ids, atobjects[N].membershipKeys.KEY.filterbuyer in current_user.portal_supplier_ids)rls-predicate-membership-key-targetx in current_user.KEYthat no kernel key, declaration or registered resolver ownsrls-predicate-unknown-user-variableunknowable==rls-predicate-unenforceable(the kernel list-key type fault)Refusals 5 and 6 judge the roster object as merged with its extensions (
objectExtensions, by priority). A column two packages write at the deciding priority is left unjudged. Refusal 11 reads the key's target from the declaration'svalueFieldreference, only when exactly one object declares the key. It accepts the column that references that target, andidon the target object itself.The save door
validateMembershipKeyDeclarationsis a new gating rule,CLI_AND_RUNTIME,runtimeTypes: ['object'].validateRlsPredicateEnforceabilitynow declaresruntimeTypes: ['permission', 'object']. On an object write it judges the object's key filters anddefaultRowScope. Through the differential gate, it also refuses an object write that drops a field or a key a stored policy reads. A stored policy that already failed is not blamed on the write.assertRuntimeAuthoringRulesreads the registered resolver'skeysper publish fromRLS_MEMBERSHIP_RESOLVER_SERVICE,[]when none is registered (protocol.ts#rlsMembershipResolverKeys).runRuntimeAuthoringRulestakes them asrlsMembershipResolverKeysand hands the rules amembershipKeyOwnerscontext: the resolver's keys, and which live objects declare which key. The declarations are read from the un-narrowedcontext.objects, so the package closure cannot hide a key. The written stack is authoritative for the objects it carries. With no service registry, or a resolver whosekeysis not a list of strings, no ownership is judged, as on the CLI.os explainNew entries for the four
membership-key-*ids andrls-predicate-membership-key-target. Each says how a key resolves and that every miss fails closed.rls-predicate-unenforceablenames the filter path.rls-predicate-unknown-user-variablegains three paragraphs: a declared key under==; the CLI-versus-save-door split for an unowned key in a membership position; and the caller-free filter. All verdicts and hints are one line of at most 200 characters (pinned).Pins moved, each with its reason in the diff
runtime-gate.object-writes.test.ts: the object-door roster and the clean-writerulesRungain the two rules, as ruled joins.validate-rls-predicate-enforceability.engine-judge.test.ts:runtimeTypesis['permission', 'object'].validate-rls-predicate-enforceability.test.ts: theos explaintext the verdict moved to now says "not reported byos validate" where it said "never reported", because the save door now reports it.packages/cli/test/rls-policy-authoring-admission.test.ts(realObjectQLhost): the app-stagedcurrent_user.holidayscases register a resolver owningholidays, which is how an app stages a key. A new case pins refusal 12 end to end: with no resolver, the CLI accepts and the save door answers 422 withrls-predicate-unknown-user-variable.Tests
validate-membership-key-declarations.test.ts: 17, refusals 3 to 6, both doors, merged-object judging, an unjudged priority tie,os explain, verdict length.validate-rls-predicate-enforceability.membership-keys.test.ts: 24, refusals 7, 11, 12 and 13, each with a CONTROL, both doors, the object write that drops a key a stored policy reads (refused) or keeps it (published),os explain, verdict length.protocol.runtime-authoring-gate.test.ts: 4 host cases. No resolver: refused, nothing written. A resolver owning the key: published. A resolver owning other keys: refused. No registry, a resolver withoutkeys, or non-stringkeys: published.Ablations, each restored byte-identically from a saved copy:
findings.pushsites in the declarations rule disabled in turn: 1, 6, 2, 1, 4 and 3 red.undefined: the two refusing host cases red, the two publishing ones green.Suites:
@objectstack/lint: 158 files, 7237 passed.tsc --noEmitexit 0. dts build withOS_SKIP_DTSunset: 6/6 declaration files.@objectstack/metadata-protocol: 261 files, 21457 passed, 19 skipped, before the 4 host cases; those 4 pass.tsc --noEmitexit 0.@objectstack/clirls-policy-authoring-admission.test.ts: 40 passed.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 71 families on this head (1,972 changed lines, under the human-merge threshold). All 71 were run one at a time underos-verify-lock.sh, exit codes captured before any pipe, after a dts build of the@objectstack/cliclosure; so were 58 more the longer listing names. All exit 0 but the following.d16c4a71:check:docs-transcript-driftfound four docs transcripts printing "author-time rules (51)". Thevalidateregistry now derives 52 (the declarations rule), so the four lines read 52.check-closing-target-claim,check-partof-closing-keyword,check-single-claim-pathsandcheck-governed-queue-guard. CI runs them with the PR.check-partof-closing-keywordwas also run with this body asPR_BODYand passes.Changeset
.changeset/25234-membership-key-authoring-refusals.md:minoron@objectstack/lint,patchon@objectstack/metadata-protocol, BREAKING with FROM, TO, Unchanged and Remedy, ADR-0087not-required (no-migration-prescription).pnpm check:adr-0087-registrationpasses.🤖 Generated with Claude Code
https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339