Repository navigation
Commit b531016
feat(lint)!: membership key declarations, and the row policies that read them, are judged when authored (#25550)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoJLr1A7TYy1k6cGE23eo9VQ)_
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 for `object` writes.
## Refusals
| # | What | Rule id | CLI | Save door |
|:--|:--|:--|:--|:--|
| 3 | one key declared on two objects | `membership-key-declared-twice`
| yes (`defineStack` already refuses it in one stack) | yes, reading the
live objects the written stack does not carry, outside the package
closure too |
| 4 | a key the registered resolver also owns |
`membership-key-resolver-collision` | not judged (no resolver visible) |
yes |
| 5 | `userField` missing, or not a single-valued user reference
(`user`, or `lookup` / `master_detail` to `sys_user`, not `multiple`) |
`membership-key-user-field-invalid` | yes | yes |
| 6 | `valueField` missing, or not a reference (`lookup`,
`master_detail`, `user`) | `membership-key-value-field-invalid` | yes |
yes |
| 7 | a key's `filter` that does not lower, names a column the roster
lacks, compares with a list-holding column, is refused by the engine's
judge, or reads any `current_user` value | the existing
`rls-predicate-*` ids, at `objects[N].membershipKeys.KEY.filter` | yes |
yes |
| 11 | a declared key tested against a column whose values can never be
the key's (`buyer in current_user.portal_supplier_ids`) | new
`rls-predicate-membership-key-target` | yes | yes |
| 12 | `x in current_user.KEY` that no kernel key, declaration or
registered resolver owns | `rls-predicate-unknown-user-variable` |
unchanged, `unknowable` | yes |
| 13 | a declared key under `==` | `rls-predicate-unenforceable` (the
kernel list-key type fault) | yes | yes |
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's `valueField` reference, only when exactly one
object declares the key. It accepts the column that references that
target, and `id` on the target object itself.
## The save door
- `validateMembershipKeyDeclarations` is a new gating rule,
`CLI_AND_RUNTIME`, `runtimeTypes: ['object']`.
- `validateRlsPredicateEnforceability` now declares `runtimeTypes:
['permission', 'object']`. On an object write it judges the object's key
filters and `defaultRowScope`. 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.
- **The host fact.** `assertRuntimeAuthoringRules` reads the registered
resolver's `keys` per publish from `RLS_MEMBERSHIP_RESOLVER_SERVICE`,
`[]` when none is registered (`protocol.ts#rlsMembershipResolverKeys`).
`runRuntimeAuthoringRules` takes them as `rlsMembershipResolverKeys` and
hands the rules a `membershipKeyOwners` context: the resolver's keys,
and which live objects declare which key. The declarations are read from
the un-narrowed `context.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 whose `keys` is not a list of
strings, no ownership is judged, as on the CLI.
## `os explain`
New entries for the four `membership-key-*` ids and
`rls-predicate-membership-key-target`. Each says how a key resolves and
that every miss fails closed. `rls-predicate-unenforceable` names the
filter path. `rls-predicate-unknown-user-variable` gains 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-write `rulesRun` gain the two rules, as ruled joins.
- `validate-rls-predicate-enforceability.engine-judge.test.ts`:
`runtimeTypes` is `['permission', 'object']`.
- `validate-rls-predicate-enforceability.test.ts`: the `os explain` text
the verdict moved to now says "not reported by `os validate`" where it
said "never reported", because the save door now reports it.
- `packages/cli/test/rls-policy-authoring-admission.test.ts` (real
`ObjectQL` host): the app-staged `current_user.holidays` cases register
a resolver owning `holidays`, 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 with `rls-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 without
`keys`, or non-string `keys`: published.
**Ablations, each restored byte-identically from a saved copy:**
- Refusal 12 arm off: 3 red. Refusal 11 arm off: 7 red. Refusal 13 swap
off: 2 red. Refusal 7 caller-read check off: 1 red. Refusal 7 judge pass
off: 6 red. Stack authority over the stored declarations off: 1 red.
- Each of the six `findings.push` sites in the declarations rule
disabled in turn: 1, 6, 2, 1, 4 and 3 red.
- The host's resolver read replaced by `undefined`: the two refusing
host cases red, the two publishing ones green.
**Suites:**
- `@objectstack/lint`: 158 files, 7237 passed. `tsc --noEmit` exit 0.
dts build with `OS_SKIP_DTS` unset: 6/6 declaration files.
- `@objectstack/metadata-protocol`: 261 files, 21457 passed, 19 skipped,
before the 4 host cases; those 4 pass. `tsc --noEmit` exit 0.
- `@objectstack/cli` `rls-policy-authoring-admission.test.ts`: 40
passed.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 71 families on this head (1,972
changed lines, under the human-merge threshold). All 71 were run one at
a time under `os-verify-lock.sh`, exit codes captured before any pipe,
after a dts build of the `@objectstack/cli` closure; so were 58 more the
longer listing names. All exit 0 but the following.
- One real finding, fixed in `d16c4a71`: `check:docs-transcript-drift`
found four docs transcripts printing "author-time rules (51)". The
`validate` registry now derives 52 (the declarations rule), so the four
lines read 52.
- Four are PR-context guards that judge nothing locally (exit 1 or 2
with "NOT WIRED" / "NOT MEASURED"): `check-closing-target-claim`,
`check-partof-closing-keyword`, `check-single-claim-paths` and
`check-governed-queue-guard`. CI runs them with the PR.
`check-partof-closing-keyword` was also run with this body as `PR_BODY`
and passes.
**Changeset** `.changeset/25234-membership-key-authoring-refusals.md`:
`minor` on `@objectstack/lint`, `patch` on
`@objectstack/metadata-protocol`, BREAKING with FROM, TO, Unchanged and
Remedy, ADR-0087 `not-required (no-migration-prescription)`. `pnpm
check:adr-0087-registration` passes.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01M4rJHRAZDFqLYQ9dHMH339
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 45f9962 commit b531016
20 files changed
Lines changed: 1868 additions & 122 deletions
File tree
- .changeset
- content/docs
- deployment
- getting-started
- ui
- packages
- cli/test
- lint/src
- metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
758 | 758 | | |
759 | 759 | | |
760 | 760 | | |
761 | | - | |
| 761 | + | |
762 | 762 | | |
763 | 763 | | |
764 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
477 | 477 | | |
478 | 478 | | |
479 | 479 | | |
480 | | - | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
481 | 483 | | |
482 | 484 | | |
483 | 485 | | |
| |||
543 | 545 | | |
544 | 546 | | |
545 | 547 | | |
546 | | - | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
547 | 553 | | |
548 | 554 | | |
549 | 555 | | |
| |||
735 | 741 | | |
736 | 742 | | |
737 | 743 | | |
738 | | - | |
| 744 | + | |
739 | 745 | | |
740 | 746 | | |
741 | 747 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
308 | | - | |
| 308 | + | |
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
393 | | - | |
| 393 | + | |
394 | 394 | | |
395 | 395 | | |
396 | 396 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
112 | | - | |
| 113 | + | |
113 | 114 | | |
114 | 115 | | |
115 | 116 | | |
| |||
119 | 120 | | |
120 | 121 | | |
121 | 122 | | |
122 | | - | |
| 123 | + | |
123 | 124 | | |
124 | 125 | | |
125 | 126 | | |
| |||
129 | 130 | | |
130 | 131 | | |
131 | 132 | | |
132 | | - | |
133 | | - | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
134 | 139 | | |
135 | 140 | | |
136 | 141 | | |
| |||
222 | 227 | | |
223 | 228 | | |
224 | 229 | | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
225 | 236 | | |
226 | 237 | | |
227 | 238 | | |
228 | | - | |
| 239 | + | |
229 | 240 | | |
230 | 241 | | |
231 | 242 | | |
232 | 243 | | |
233 | 244 | | |
234 | | - | |
| 245 | + | |
235 | 246 | | |
236 | 247 | | |
237 | 248 | | |
238 | 249 | | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
239 | 263 | | |
240 | 264 | | |
241 | 265 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
139 | 143 | | |
140 | 144 | | |
141 | 145 | | |
| |||
399 | 403 | | |
400 | 404 | | |
401 | 405 | | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
402 | 421 | | |
403 | 422 | | |
404 | 423 | | |
| |||
2263 | 2282 | | |
2264 | 2283 | | |
2265 | 2284 | | |
2266 | | - | |
2267 | | - | |
| 2285 | + | |
| 2286 | + | |
| 2287 | + | |
| 2288 | + | |
| 2289 | + | |
| 2290 | + | |
| 2291 | + | |
| 2292 | + | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
| 2299 | + | |
| 2300 | + | |
| 2301 | + | |
| 2302 | + | |
| 2303 | + | |
| 2304 | + | |
| 2305 | + | |
| 2306 | + | |
| 2307 | + | |
| 2308 | + | |
| 2309 | + | |
| 2310 | + | |
| 2311 | + | |
| 2312 | + | |
| 2313 | + | |
| 2314 | + | |
2268 | 2315 | | |
2269 | 2316 | | |
2270 | 2317 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
383 | 383 | | |
384 | 384 | | |
385 | 385 | | |
| 386 | + | |
386 | 387 | | |
387 | 388 | | |
388 | 389 | | |
389 | 390 | | |
390 | 391 | | |
391 | 392 | | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
392 | 409 | | |
393 | 410 | | |
394 | 411 | | |
| |||
0 commit comments