Uh oh!
There was an error while loading. Please reload this page.
fix(verify): make the RLS by-id-write class reachable; re-decide the two authz-matrix rows on their own evidence - #7975
Conversation
…reading as passes The `verify --rls` runner reported 0 HOLES over a probe that could not, by construction, observe the class it claims to prove. - The probe persona was a bare `signUp()` member holding no object grants, so `checkObjectPermission` answered 403 before record scope was ever consulted. Measured on the stock showcase: 11 of 13 "consistent" verdicts were that object-gate 403; only 2 were a record-scope 404. - A `showcase_account` auto-record 400 cascaded into four further skips (8 of 23 objects skipped), and the summary line still read "0 HOLES". The probe is now the persona the class needs: object read+edit plus an owner-scoped SELECT-only narrowing, registered at boot so its policies are on the resolution path. That is deliberately the authoring shape that was the hole, so every object of every verified app is now a live regression guard for the by-id write-scope derivation. Reachability is MEASURED per object (a LIST 403 reports `probe-blocked`, never a pass), an unsatisfiable admin create falls back to adopting an existing row so one failure no longer cascades, and the report separates PROVEN objects from NOT-PROVEN ones in both the structured summary and the formatted output.
…over the runner's new not-proven statuses ADR-0049's point is the RECORD telling the truth, so both matrix rows were re-decided on their own evidence rather than downgraded by assertion. `rls-by-id-write` stays `enforced`, but its enforcement citation was incomplete in a way that mattered: the named pre-image re-read is a no-op under select-only authoring, which was #7665. Ablating the write-scope derivation while leaving the pre-image re-read in place turns 16 of 20 probed showcase objects into `rls-hole`, so the row now names both halves. The shared proof file is NOT vacuous for this row — since #7665/PR #7792 it carries a dedicated select-only block whose member set grants full CRUD, so its refusals are the record gate. `controlled-by-parent` stays `enforced` on its own dedicated proof, whose fixture likewise grants full CRUD on both objects. Neither row was downgraded, so the mirroring showcase `permission-sets.ts` comment is accurate as shipped and is left alone. The runner's unit oracle gains the two statuses that stop a not-proven object reading as a pass (`probe-blocked`, and skip/adoption accounting).
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also reference the affected code. These are read-only:
|
hotlong
commented
Aug 12, 2026
PM review — The dispatch's sharpest lead was a hypothesis, and it was checked rather than takenI handed this card a specific lead: The dev verified the lead instead of implementing it. That matters: taking my framing on faith would have downgraded a row that is genuinely enforced — a false claim in the opposite direction, one that would have made #7665's fix look unlanded. ⛔ Do not inherit a PM's lead; measure it. Done exactly right here. Both rows keep The finding the card did not contain
(i): the probe was made harder, not friendlierThe thing I most wanted to be impossible here was tuning the probe to preserve a green. The opposite happened:
And the 0 holes AFTER is backed by an ablation: 16 The CI-gate question was answered, not dodged. Out-of-scope, recorded rather than quietly fixedThe ledger checks only that a cited proof file exists ( Also honest about what the new green does not cover: the probe cannot reach narrowing authored on a position it does not hold, stated in the module header so the result is not over-read; and Enqueueing once CI lands. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7685 — items (i) and (ii) only. Item (iii) is objectstack-ai/objectui#4446 and is not touched here.
Cross-links #7665 (the defect whose acceptance criterion 2 this probe implements) and #1994 (the invariant).
(i)
verify --rls— the measurement first, because it decides (ii)objectstack verify --rlsreported0 HOLESon both example apps. Neither number was evidence.The probe was answered by the OBJECT gate, never by record scope. The persona was a bare
signUp()member holding no object grants, socheckObjectPermissionrefused with 403 before the row-level gate was consulted — and the runner banked that 403 asrls-consistent.A skip read as a pass, and cascaded. One
showcase_accountauto-record 400 skipped that object and every object with a required relation to it, while the summary line still said0 HOLES.What
verify --rlsreports BEFORE and AFTERThe BEFORE "consistent" counts are the finding, not the baseline: 11 of the 13 showcase verdicts and all 4 crm verdicts were
GET 403— the object gate — and only 2 showcase objects (showcase_inquiry,showcase_private_note) reached record scope with aGET 404. On the other 15, no platform regression could have producedrls-hole.AFTER, every consistent verdict is a record-scope
GET 404+PATCH 403with the row unchanged as ground truth. The 3 remaining showcase objects are honestly unprovable by this runner and are printed as such: one has no plain-text field to mutate, two are read-only federated objects.Changes
operation: 'select'only — deliberately the authoring shape that was the hole, so every object of every verified app becomes a live regression guard for the by-id write-scope derivation. It is registered at boot (rlsProbeSecurity) rather than written as a baresys_permission_setrow, because the evaluator's last-resort DB loader hydrates the object bits but notrowLevelSecurity— a row-only probe set would have granted the grants and silently dropped the narrowing.probe-blocked— a distinct status that is never a pass — so a persona that silently failed to receive its grants cannot masquerade as a proof.summarygainsproven/unproven/probeBlocked,RlsReport.unprovenlists every object the run did not exercise with its reason,RlsReport.probenames the persona, and the formatted output prints an explicit "this run is not a clean bill of health" line whenever anything went unproven.Reverse verification
Ablating the #7665 write-scope derivation in
plugin-security— while leaving the #1994 pre-image re-read fully in place — and rebuilding the plugin'sdist:rls-hole,hardFailures: 16, exit 1, each reported asGET 404 … yet MUTATED it by id (PATCH 200)The ablation was reverted and the plugin rebuilt before committing; the branch contains no
plugin-securitychange.CI gate risk: none
ci.yml'sdogfood-verifyjob runsverify --app <app> --rlsfor both example apps and gates throughdogfood-gate. Both apps exit 0 after this change, so nothing is being suppressed to keep CI green and no blocker decision is needed.(ii)
authz-conformance.matrix.ts— both rows survive, on their own evidenceDecided per row after the measurement, per ADR-0049. Neither row was downgraded, so — per the ruling — the mirroring comment in the showcase's
permission-sets.tsis left alone: it is accurate as shipped.rls-by-id-write(:39)enforced— the card is stalerls-fixture.dogfood.test.tsis not vacuous for this row despite sharing the file withrls-read: since #7665 / merged PR #7792 it carries a dedicated select-only block (:107–172) whose member set grants full CRUD onrls_note, so its refusals are the record gate, never the object gate — it asserts the by-id PATCH refused with the row unchanged and that an in-scope write still lands. Second, independent measurement: the newly-reachable probe, 20/23 + 6/6 proven, 16 holes under ablation.enforcement+noteonly —stateuntouchedcontrolled-by-parent(:48)enforced— the card is stalecontrolled-by-parent.dogfood.test.tsis a dedicated proof, andfixtures/cbp-fixture.tsgrants the member full CRUD on both objects, so every refusal it asserts is the derived record gate. It asserts derived read denial, derived by-id write denial with admin ground truth, and that a note under a member-owned master stays readable and writable (not over-blocked). Independently, the probe measuresshowcase_invoice_line— a realcontrolled_by_parentdetail — consistent, and it flips torls-holeunder the ablation.noteonly —stateuntouchedThe one substantive record correction:
rls-by-id-write'senforcementnamed only the pre-image re-read, which the ablation proves is a no-op on its own under select-only authoring — the 16 holes appeared with that re-read fully present. The row now names both halves. Nocoverskey was added or orphaned;AuthzStateis unchanged.Verification
pnpm -w typecheck— clean, 126/126.pnpm lint— clean.pnpm check:type-check-debt— OK, none above its recorded number (nothing raised).node scripts/check-verify-stand-in-erasure.mjs— OK; the newprovisionRlsProbePersonais classified inNOT_A_STAND_INwith its reason, as the reconciliation requires.@objectstack/verifytests — 23 passed.rls-runner.test.ts(extended),authz-conformance.test.ts,rls-fixture.dogfood.test.ts,controlled-by-parent.dogfood.test.ts— 27 passed.origin/mainmerged and all of the above re-run afterwards.rls-runner.test.tsis the runner's detector-liveness oracle — the live fixture can no longer plant the hole — so it gains cases forprobe-blocked, target adoption, and the proven/unproven accounting.Out of scope, recorded rather than fixed
checkLedger,packages/verify/src/conformance.ts:77), never that it proves the row. That is the mechanism behind this card's premise and it is unchanged here — a general fix touches ~30 rows.positions: ['contributor']rules); reaching those needs a per-declared-position persona. Stated in the module header so the green is not over-read.showcase_project_membershipis skipped for having no plain-text field to mutate. Widening the probe-field set to the text family risks a sanitizer or amin/maxrejection reading as a falserls-consistent, so it was left alone deliberately.Generated by Claude Code