Repository navigation
Commit 2972097
docs(security): state the RLS write check as the gate enforces it in ADR-0066 and the data skill (#20298)
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 `using` the read filter and `check` the write filter,
with no stand-in rule, no per-row statement and no refusal rule. Both
now say what `main` enforces, in the wording the schema already carries.
#20268 landed the non-governed half (the `rls.zod.ts` overview and
describe texts, the docs pages); this PR is the governed half and waits
on the maintainer's hand (Tier H: `docs/adr/**` and `skills/**`). 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 `check` on a `select` / `delete` policy 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' `using`
predicates 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 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 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-anchors` holds 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 declare `check`; when
none does, the ones that declare `using` (the platform ownership floor
kept exactly when the pre-image gate kept it, `keepOwnershipFloor`).
`computeWriteCheckFilter` then hands that set to `compileFilter` with
the `check` clause, which OR-combines.
### `skills/objectstack-data/rules/security.md` — the "Row-Level
Security (RLS)" paragraph and its example comments
The paragraph now states, in this order:
1. `using` admits rows — what a `select` policy lets the caller read,
and the existing rows an `update`/`delete` policy lets it change or
remove; on a read the applicable `using` OR-combine, then AND into the
query.
2. `check` is judged on every row an `insert`/`update` writes (array
inserts and `multi: true` included; one failing row refuses the write),
chosen per operation: when any applicable policy declares `check`, only
those decide (OR-combined); else each applicable `using` stands in.
3. A non-blank `check` on a `select`/`delete` policy is refused.
4. The default posture, in the schema overview's words ("Default deny,
among the policies that apply … when none applies the policies restrict
nothing (the tenant wall still applies)"), plus the one clause the
review of the non-governed half named: an `update`/`delete` target with
no write-class `using` is bounded by the caller's `select` policies (the
by-id pre-image gate derives its scope from the caller's SELECT
narrowing when no write-class `using` applies; not for `insert`, not
under the read-side superuser bypass).
The example's two comments (`// read scope` / `// write scope`) now read
`// rows readable / targetable` and `// every row written`.
Wording follows the `RowLevelSecurityPolicySchema.using` / `.check`
describe texts and the `rls.zod.ts` overview as landed in `3f86dc52`; no
second phrasing of the same fact was introduced. No issue or PR number
appears in the skill text (`check:doc-authoring` refuses one under
`skills/`).
### Paying the token ceiling
`check:skills-token-ratchet` holds `security.md` at 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:
- the `permissions`-vs-`permissionSets` bullet no longer repeats the
code comment two lines above it (the refusal text, the
`ObjectStackDefinitionSchema` source and "never a silent drop" stay);
- the `permission_set_id` warning no longer says "record id" twice;
- the RLS source line cites `rls.zod.ts` once (policy shape, grammar,
`check` composition) instead of `permission.zod.ts` again (already cited
under RBAC);
- the owner-scoping bullet, the `requiredPermissions` paragraph (its
enforcer was already named under `maskingRule`), 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`):
| surface | lines before → after | tokens before → after |
|---|---|---|
| `skills/objectstack-data/rules/security.md` | 214 → 216 | 2543 → 2541
(ceiling 2543, headroom 2) |
| `skills/objectstack-data/**` (17 files) | 3807 → 3809 | 40934 → 40932
|
| `skills/**/SKILL.md` (10 files) | 4402 → 4402 | 51903 → 51903 |
| whole `skills/` tree (65 files) | 13427 → 13429 | 155990 → 155988 |
| ratchet "bundle total (whole shipped tree)" | — | 153970 → 153968 |
| `docs/adr/0066-unified-authorization-model.md` | 114 → 114 | 5096 →
5224 (no token ratchet on `docs/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 --commands` off the merge base at `4b330f38`:
29 commands; every exit code captured before any pipe; `--ran`
reconciliation: "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.md` is 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` → 0
- `node scripts/check-adr-links.mjs` → 0; `--self-test` → 0
- `node scripts/check-ci-filter-parity.mjs` → 0
- `node scripts/check-closing-keyword-parity.mjs` → 0; `--self-test` → 0
- `node scripts/check-comment-mask-corpus.mjs` → 0
- `node scripts/check-doc-route-spelling.mjs --advisory` → 0;
`--self-test` → 0
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions` →
first run exit 3 (PREREQUISITE NOT MET: `@objectstack/formula` and
`@objectstack/lint` not built — a refusal, not a measurement); after
`turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint` under `os-verify-lock.sh` (VERDICT
command-exit 0, held 240s) → 0
- `pnpm 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` → 0
Reverse verification of the ADR anchors (the gate lists only findings,
so resolution was proven by failure): with `writeCheckPolicies` mutated
to `writeCheckPoliciesNOPE` in the committed ADR,
`check-adr-symbol-anchors` exits 1 with `[unresolved-symbol]
docs/adr/0066-unified-authorization-model.md:93`; restored with `git
checkout HEAD -- PATH`, `git hash-object` of the path equals the HEAD
blob (`01878adb…`) and `git diff HEAD` is empty.
Package tests / typecheck: none owed — the diff touches no `packages/**`
file, so there is no ① dependency closure and no ② package suite; the
whole-repo `pnpm lint` sweep is CI's.
Changeset: `skip-changeset` applies. Both paths are outside every
published package: 0 of the 69 non-private `package.json` manifests list
`skills/`, `docs/adr` or a parent path in `files[]`; positive control —
`RowLevelSecurityPolicySchema` is found in
`packages/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 under
`packages/` (exit 1). `docs/adr/**` is on the fast track (never
published).
## Acceptance notes
- The dispatch order's suggested ADR phrasing named "the `using` of the
applicable insert-class policies" as the stand-in. The code is wider:
`writeCheckPolicies` runs for `insert` and `update` alike (an `all`
policy included), and when no applicable policy declares `check`, every
applicable policy's `using` stands in for that operation. The landed
text says "each applicable policy's `using`", matching
`RowLevelSecurityPolicySchema.using` ("on an insert or an update, when
no applicable policy for that operation declares `check`, each
applicable policy's `using` also stands in"). The triage note's "an
insert policy's `using` stands in when no `check` is declared" is one
instance of that rule, not the whole rule.
- The example policy `org_isolation` (a hand-written `organization_id ==
current_user.organization_id` select policy) sits beside the file's own
Multi-tenancy section ("⛔ never `single` + 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-expressions` refuses (exit 3) on a fresh worktree
until `@objectstack/formula` and `@objectstack/lint` are built; its
refusal text names the fix. Not a finding.
## 维护者速读(草稿)
**改了什么**:两处受管文本各改一段。ADR-0066「优先级/组合语义」第 3 项那一句,从「同一对象/操作的多条行策略 OR
合并(任一匹配即放行)」改为:读侧按 `using` OR 合并;写侧(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](https://claude.ai/code/session_01MjvgiFAmjHqsxy1XLiVYfH)_
Co-authored-by: objectstack-fleet[bot] <noreply@anthropic.com>1 parent 9daced0 commit 2972097
2 files changed
Lines changed: 43 additions & 41 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
| 23 | + | |
| 24 | + | |
27 | 25 | | |
28 | | - | |
29 | | - | |
30 | | - | |
| 26 | + | |
| 27 | + | |
31 | 28 | | |
32 | 29 | | |
33 | 30 | | |
34 | 31 | | |
35 | 32 | | |
36 | 33 | | |
37 | 34 | | |
38 | | - | |
| 35 | + | |
39 | 36 | | |
40 | 37 | | |
41 | 38 | | |
42 | 39 | | |
43 | 40 | | |
44 | 41 | | |
45 | 42 | | |
46 | | - | |
47 | | - | |
| 43 | + | |
| 44 | + | |
48 | 45 | | |
49 | 46 | | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
54 | 50 | | |
55 | | - | |
| 51 | + | |
56 | 52 | | |
57 | 53 | | |
58 | 54 | | |
| |||
69 | 65 | | |
70 | 66 | | |
71 | 67 | | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
76 | 81 | | |
77 | 82 | | |
78 | 83 | | |
| |||
81 | 86 | | |
82 | 87 | | |
83 | 88 | | |
84 | | - | |
85 | | - | |
| 89 | + | |
| 90 | + | |
86 | 91 | | |
87 | 92 | | |
88 | 93 | | |
| |||
97 | 102 | | |
98 | 103 | | |
99 | 104 | | |
100 | | - | |
101 | | - | |
| 105 | + | |
| 106 | + | |
102 | 107 | | |
103 | 108 | | |
104 | 109 | | |
| |||
109 | 114 | | |
110 | 115 | | |
111 | 116 | | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
118 | 121 | | |
119 | 122 | | |
120 | 123 | | |
| |||
135 | 138 | | |
136 | 139 | | |
137 | 140 | | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
143 | 145 | | |
144 | 146 | | |
145 | 147 | | |
| |||
190 | 192 | | |
191 | 193 | | |
192 | 194 | | |
193 | | - | |
194 | | - | |
| 195 | + | |
| 196 | + | |
195 | 197 | | |
196 | 198 | | |
197 | 199 | | |
| |||
208 | 210 | | |
209 | 211 | | |
210 | 212 | | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
0 commit comments