Repository navigation
docs(security): state the RLS write check as the gate enforces it in ADR-0066 and the data skill - #20298
Conversation
ADR-0066 item 3 said row policies for one object/operation are OR-combined for every operation. That holds on a read: RLSCompiler.compileFilter OR-combines the applicable policies' `using`. On an insert or update the check is chosen once per operation across the applicable policies first (writeCheckPolicies: the declared `check` predicates when any declares one, else each applicable policy's `using` standing in), and the chosen predicates OR-combine over every row written. The sentence now says both halves and cites the two enforcing symbols. The published data skill's RLS section called `using` the read filter and `check` the write filter with no stand-in rule. It now states what each clause admits per operation, that `check` judges every row an insert or update writes (array inserts and `multi: true` included), that `using` stands in when no applicable policy declares `check`, that a non-blank `check` on a `select` / `delete` policy is refused, and the default posture in the schema's own words. The token ceiling on that file has zero headroom, so the additions are paid by de-duplicating sentences the same file already states elsewhere. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH
Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flagsNo Implemented-by: VERDICT: PASS Generated by Claude Code |
维护者速读(终稿)— PR #20298改了什么:两处受管文本各改一段,不碰代码。ADR-0066「优先级/组合语义」第 3 项那一句,从「同一对象/操作的多条行策略 OR 合并(任一匹配即放行)」改为:读侧按各策略的 为什么改:这两段是 AI 写 RLS 策略时读的唯一说明。原文把 风险与代价(含回滚):纯文本,不改 schema、运行时或任何门禁。 席位意见:已逐句对 你要做的:在本 PR 上 Approve。之后由本席清标、翻 ready、挂 auto-merge 走队列落地。 Generated by Claude Code |
Fixes #20275
Clause-②: no
Two governed texts said the RLS write check wrong. ADR-0066's combination rule (item 3) called row policies "OR-combined (any matching policy admits the row)" for every operation; the published data skill's RLS section called
usingthe read filter andcheckthe write filter, with no stand-in rule, no per-row statement and no refusal rule. Both now say whatmainenforces, in the wording the schema already carries. #20268 landed the non-governed half (therls.zod.tsoverview and describe texts, the docs pages); this PR is the governed half and waits on the maintainer's hand (Tier H:docs/adr/**andskills/**). No code change.Provenance of the three facts the skill now states: every-row judging of inserts and updates landed in #19988 and #20012; the refusal of a non-blank
checkon aselect/deletepolicy in #20167; the per-operation default and the default-posture wording in #20268.What changed
docs/adr/0066-unified-authorization-model.md— item 3 of "Precedence / combination semantics"One sentence. It now reads: on a read, the applicable policies'
usingpredicates OR-combine (any matching policy admits the row); on an insert or update, the check is chosen once per operation across the applicable policies — the declaredcheckpredicates when any declares one, else each applicable policy'susingstanding in — and the chosen predicates OR-combine over every row written. The tenant-isolation clause and the superuser-bypass sentence are unchanged; Status, headings and the other items are untouched.The enforcing code is cited as symbol anchors, so
check:adr-symbol-anchorsholds them:packages/plugins/plugin-security/src/rls-compiler.ts#RLSCompiler.compileFilter— read side: OR-combines the applicable policies'using(its docblock: "Multiple policies for the same object/operation are OR-combined").packages/plugins/plugin-security/src/security-plugin.ts#writeCheckPolicies— write side: takes the applicable policies that declarecheck; when none does, the ones that declareusing(the platform ownership floor kept exactly when the pre-image gate kept it,keepOwnershipFloor).computeWriteCheckFilterthen hands that set tocompileFilterwith thecheckclause, which OR-combines.skills/objectstack-data/rules/security.md— the "Row-Level Security (RLS)" paragraph and its example commentsThe paragraph now states, in this order:
usingadmits rows — what aselectpolicy lets the caller read, and the existing rows anupdate/deletepolicy lets it change or remove; on a read the applicableusingOR-combine, then AND into the query.checkis judged on every row aninsert/updatewrites (array inserts andmulti: trueincluded; one failing row refuses the write), chosen per operation: when any applicable policy declarescheck, only those decide (OR-combined); else each applicableusingstands in.checkon aselect/deletepolicy is refused.update/deletetarget with no write-classusingis bounded by the caller'sselectpolicies (the by-id pre-image gate derives its scope from the caller's SELECT narrowing when no write-classusingapplies; not forinsert, not under the read-side superuser bypass).The example's two comments (
// read scope/// write scope) now read// rows readable / targetableand// every row written.Wording follows the
RowLevelSecurityPolicySchema.using/.checkdescribe texts and therls.zod.tsoverview as landed in3f86dc52; no second phrasing of the same fact was introduced. No issue or PR number appears in the skill text (check:doc-authoringrefuses one underskills/).Paying the token ceiling
check:skills-token-ratchetholdssecurity.mdat 2543 tokens with headroom 0. The RLS paragraph grew by 687 bytes and the two comments by 22; the difference is paid in the same file by removing sentences the file already states elsewhere — nothing moved to another file, the ceiling is untouched:permissions-vs-permissionSetsbullet no longer repeats the code comment two lines above it (the refusal text, theObjectStackDefinitionSchemasource and "never a silent drop" stay);permission_set_idwarning no longer says "record id" twice;rls.zod.tsonce (policy shape, grammar,checkcomposition) instead ofpermission.zod.tsagain (already cited under RBAC);requiredPermissionsparagraph (its enforcer was already named undermaskingRule), the platform-global paragraph and its blockquote lose filler words, no facts.Readings
Line/token budget (tokens =
ceil(utf8 bytes / 4), the ratchet's own unit;3f86dc52→4b330f38):skills/objectstack-data/rules/security.mdskills/objectstack-data/**(17 files)skills/**/SKILL.md(10 files)skills/tree (65 files)docs/adr/0066-unified-authorization-model.mddocs/adr/**)The +2 lines in the skill file are the natural wrapping of a longer paragraph; the gate that prices
skills/**is the token ratchet, and it went down.Gates — derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsoff the merge base at4b330f38: 29 commands; every exit code captured before any pipe;--ranreconciliation: "29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN … all 29 recorded an exit code and none of them is 3".node scripts/check-skills-token-ratchet.mjs→ 0 — "skills/objectstack-data/rules/security.mdis 2541 tokens (ceiling 2543; headroom 2)";--self-test→ 0 (65 cases)node scripts/check-adr-symbol-anchors.mjs→ 0 — "2125 anchors across 140 records resolve";--self-test→ 0node scripts/check-adr-links.mjs→ 0;--self-test→ 0node scripts/check-ci-filter-parity.mjs→ 0node scripts/check-closing-keyword-parity.mjs→ 0;--self-test→ 0node scripts/check-comment-mask-corpus.mjs→ 0node scripts/check-doc-route-spelling.mjs --advisory→ 0;--self-test→ 0pnpm --filter @objectstack/lint run check:doc-formula-expressions→ first run exit 3 (PREREQUISITE NOT MET:@objectstack/formulaand@objectstack/lintnot built — a refusal, not a measurement); afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintunderos-verify-lock.sh(VERDICT command-exit 0, held 240s) → 0pnpm check:adr-anchors→ 0 ·check:agent-test-spelling→ 0 ·check:corpus-claim-drift→ 0 ·check:cross-package-test-inputs→ 0 ·check:doc-authoring→ 0 ·check:driver-memory-census→ 0 ·check:gitlink-declared→ 0 ·check:nul-bytes→ 0 ·check:pm-governed-merges→ 0 ·check:pm-prior-rulings→ 0 ·check:refd-timer-probe→ 0 ·check:role-word→ 0 ·check:skill-compatibility→ 0 ·check:skill-frame-sync→ 0 ·check:skill-identifier-liveness→ 0 ·check:watch-hint-literal→ 0Reverse verification of the ADR anchors (the gate lists only findings, so resolution was proven by failure): with
writeCheckPoliciesmutated towriteCheckPoliciesNOPEin the committed ADR,check-adr-symbol-anchorsexits 1 with[unresolved-symbol] docs/adr/0066-unified-authorization-model.md:93; restored withgit checkout HEAD -- PATH,git hash-objectof the path equals the HEAD blob (01878adb…) andgit diff HEADis empty.Package tests / typecheck: none owed — the diff touches no
packages/**file, so there is no ① dependency closure and no ② package suite; the whole-repopnpm lintsweep is CI's.Changeset:
skip-changesetapplies. Both paths are outside every published package: 0 of the 69 non-privatepackage.jsonmanifests listskills/,docs/adror a parent path infiles[]; positive control —RowLevelSecurityPolicySchemais found inpackages/spec/dist/security/index.d.ts(grep exit 0) and the schema's describe phrase "decided per operation across the applicable policies" in 9 spec dist files (exit 0); negative — the new skill sentence "chosen per operation across the applicable" is in no built output underpackages/(exit 1).docs/adr/**is on the fast track (never published).Acceptance notes
usingof the applicable insert-class policies" as the stand-in. The code is wider:writeCheckPoliciesruns forinsertandupdatealike (anallpolicy included), and when no applicable policy declarescheck, every applicable policy'susingstands in for that operation. The landed text says "each applicable policy'susing", matchingRowLevelSecurityPolicySchema.using("on an insert or an update, when no applicable policy for that operation declarescheck, each applicable policy'susingalso stands in"). The triage note's "an insert policy'susingstands in when nocheckis declared" is one instance of that rule, not the whole rule.org_isolation(a hand-writtenorganization_id == current_user.organization_idselect policy) sits beside the file's own Multi-tenancy section ("⛔ neversingle+ your own RLS") and duplicates the Layer 0 wall. Left as is: not this card, and the token ceiling was paid without touching it. Observation only, nothing to file.check-doc-formula-expressionsrefuses (exit 3) on a fresh worktree until@objectstack/formulaand@objectstack/lintare built; its refusal text names the fix. Not a finding.维护者速读(草稿)
改了什么:两处受管文本各改一段。ADR-0066「优先级/组合语义」第 3 项那一句,从「同一对象/操作的多条行策略 OR 合并(任一匹配即放行)」改为:读侧按
usingOR 合并;写侧(insert/update)先按操作在适用策略中选出检查——有声明check的只用它们,否则每条适用策略的using顶上——再 OR 合并,逐行判定写出的每一行;并以符号锚引用compileFilter与writeCheckPolicies。发布技能包objectstack-data的 RLS 段改写为四句:using放行哪些行;check逐行判定 insert/update 写出的每一行(数组插入与multi: true包含)、按操作选定、无check时using顶上;select/delete策略上的非空check被拒;默认姿态(只在有策略适用时默认拒绝;无策略适用则不限制,租户墙照旧;update/delete 目标行在没有写侧using时受调用者的select策略约束)。示例块两行注释同步。为什么改:这两段是 AI 写 RLS 策略时读的唯一说明,原文把
check当成读过滤的对偶、且不写顶替规则,按它写出的策略与运行时真实执行不一致(NORTH-STAR 优先级规则 4:写给 AI 的文档与 skills 说错一句等于产品缺陷)。执行代码本身是对的,非受管文本已由 #20268 修正;本 PR 只改受管的两处,不动代码。风险与代价(含回滚):纯文本;不改 schema、不改运行时。
security.md的 token 上限为 2543、余量 0,新增内容以删除同文件重复句付账(2543 → 2541),上限未动、未挪内容到别的文件。29 个派生门禁全绿,ADR 符号锚经反向验证(改坏符号名即变红)。回滚即 revert 本 PR 的单个 commit,无迁移、无数据影响。席位意见:(留空)
你要做的:审阅两处措辞后在本 PR 上 Approve(Tier H);落地由席位执行。
Generated by Claude Code