Repository navigation
[Decision] #20822 F7: retiring formula's whole-day copy — the RLS write check judges the raw post-image, so deleting the copy refuses writes the same policy's read shows #21109
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsRuling: batch #261 item 5 · letter A · maintainer 「其他四张同意」 2026-10-01T14:16Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. Batch #261 was presented in the live director chat with this card's options A–D. The director recommended A, with fallback B, and ⛔ never D. The maintainer agreed with item 5 as presented: 「其他四张同意」.The ruling
A: the RLS write check judges the row as it will be stored.
- Before
matchesFilterConditionjudges acheckclause, the columns of the write's post-image that are declareddate/datetime/timepass through@objectstack/core'stemporalStorageForm. That is the same rule every driver applies when it writes (packages/core/src/utils/filter-tokens.ts:118). - The write check and the read then give one answer for one row, as PostgreSQL's
WITH CHECKjudges the new row after the column type is applied. - After that lands, F7's copy (
lteBound,packages/formula/src/matches-filter.ts:867) is deleted mechanically. Until then it stays exactly as it is. - Not taken: B (lower the date columns in the RLS compile seam; leaves the
{ $field, addDays }half), C (a permanent copy; an ADR-0053 amendment), and D (delete now, which splits read from write).
Readings (
maind34aa58a2a, 12:47Z)lteBoundis atmatches-filter.ts:867; F7's copy is live.- plugin-security's tests drive
matchesFilterCondition(controlled-by-parent-*.test.ts), the write check's evaluator. - No date-typed
checkclause in the examples. The body's own re-check command globsexamples/**/*.permission*.ts, which matches 0 files, so its zero was not a reading. Re-measured withgit grep -n -E '\b(using|check)\s*:' origin/main -- examples, which finds 2usingand 1check(examples/app-showcase/src/security/permission-sets.ts:116,owner == current_user.email). None is date-typed. The premise holds, and the body's command is rewritten in this act. - Prior rulings read: ADR-0053 D-D1 (amended) items 5, 7 and 9; ruling 5902355785 D4 (b). Thread: none on this card, and #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's report 5926452254.
四棱(本裁决新记录)
- ① 长远:写检查判「将要存在的那一行」,与读一致,对标 PostgreSQL RLS
WITH CHECK在列类型转换之后判新行;删副本随之成为机械操作。C 是永久副本,D 留下读写分歧。 - ② 拉动:仓内零日期写检查(重测),但 SDK 用
Date写日期字段是常见写法;A 落地前 F7 照留,今天放行的写照样放行。 - ③ 防 AI:原始形态与落库形态的差别作者看不见,A 从结构上消除;D 的 403 响亮但误导(像是策略写错)。
- ④ 不扩散:一张 security 车道的卡,不改 ADR、不加门禁。
- 只看①选 A;②③④ 是否翻转:否。
Execution parameters (ruled here; no further decision card)
- This card becomes the carrier.
needs-user-decision→pm:queue, anddomain:engine→domain:services: the fix lands inpackages/plugins/plugin-security, and core'stemporalStorageFormis read, not changed. Level M,mode:subagent. Clause-②: no(no schema or published-contract change). Apatchchangeset. If the claim measures a write that is admitted today and refused after (a$gt/$lton the boundary day, whose stored row the read does not show), it is named in the changeset as the read/write agreement this ruling buys.- Pins: the dev's probe table on #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 (5926452254) becomes the pins.- Through ObjectQL plus SecurityPlugin plus a SQL driver, a member writes
due_onon the boundary day as an ISO instant, as aDate, and as a plain date: admitted, with F7's copy disabled in the pin to prove the new path carries it. - One row is read and written under the same policy, and both give the same answer.
- A
datetimeand atimecolumn, one row each.
- Through ObjectQL plus SecurityPlugin plus a SQL driver, a member writes
- Step 2, F7's deletion: filed by the claiming seat at ACCEPT, in
domain:engine, as #5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's follow-up. It deleteslteBoundand moves its 24 direct-call cases onto lowering. ⛔ Not before this card's PR is onmain. - Fork clause: if
temporalStorageFormturns out not to be the form every driver stores (a driver lowers differently), the claim stops and reports that on this card. ⛔ It does not pick one driver's form.
Generated by Claude Code
- Before
- added and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsCarried note from
domain:engine#2: the same raw-versus-stored split on a declared multi-valued column, measured by #20822 group 3bdomain:engine#2(seat post #20966) ·session_01Ujdtvqs7ree7WyQmEDwEnG· 2026-10-01T15:46Z. ⛔ Not a claim; this card's claim decides whether the fold carries it.-
From #5930 step 4 (
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822's group 3b report 5934956420,out_of_scope_findings[0]. PR fix(objectql,driver-mongodb,formula): having compiles the whole-day bound it is handed; $contains asks membership on a JSON-stored field (#20822 group 3b) #21196 is in review. It movesformula's$containsto membership on a declared JSON-stored field (Q2 = C). -
Measured through
ObjectQL.insert+SecurityPlugin+SqlDriver(better-sqlite3 and sqlite-wasm), undercheck: record.tags.contains('x')on atagsfield:- a member's insert of
tags: 'x', a scalar, answers403 PERMISSION_DENIED; - the write door would have stored
['x'], and the same policy's read shows a stored['x'].
The check judges the raw post-image before the write door wraps the scalar. This is fail-closed: no write path admits a row the read hides, per the report's table.
- a member's insert of
-
Why here: ruling A (5933322270) says "the RLS write check judges the row as it will be stored", and its fold names
date/datetime/timecolumns. The scalar wrap of a declared multi-valued column is the same raw-versus-stored split on another column class. The claim that executes A can fold the declared multi-valued wrap into the same pre-check step, or name why not. -
Pin, if folded:
tags: 'x'undercontains('x')is admitted, andtags: 'xy'stays refused.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T19:13Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21109-rls-check-stored-form
Worktree:objectstack-issue-21109
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface:packages/plugins/plugin-security/src/: the RLS write check's step that runs beforematchesFilterConditionjudges acheckclause (insecurity-plugin.tsand/orrls-compiler.ts, wherever the post-image is handed to the evaluator), plus its tests.@objectstack/core'stemporalStorageForm(packages/core/src/utils/filter-tokens.ts) is consumed, not changed.- Dogfood pins under
packages/qa/dogfood/test/for the door-level reading, and a changeset.
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates--tierat62b90d74: no path-derived mandate; default tier, because this is a security-boundary execution of a ruling).
Clause-②: no (the write check's accept set moves to what the stored row satisfies, as ruling A orders. If the build measures a refusal that A does not reach, it reports instead of widening.)
Thread-read: 5935014513
Serial constraints cleared, read in this act againstorigin/mainat62b90d74: plugin-security: security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079 (PR fix(plugin-security,spec)!: a non-system caller that carries a principal and resolves no permission set gets the deny baseline at object admission and at the row scope #21217) merged as62b90d74, and this card waited behind it. None of the open PRs touchesplugin-security, and spec(forms): retirepublicPicker— anonymous public forms stop taking lookup / master_detail / user fields, and the anonymous picker route goes (ruling E on #21079) #21180's PR feat(spec,rest,lint)!: retire the form field's publicPicker and the anonymous lookup route (#21180) #21222 does not. No openpm:dispatchedclaim declaresplugin-security/src.area:api: no other card in flight.
Selection:priority:p2under ruling A (batch 🔗 Broken links detected in documentation #261 item 5,5933322270, maintainer 「其他四张同意」). It is the lane's next free card.
Fold decision on
domain:engine#2's carried note (5935014513): folded, with one condition. A declared multi-valued column's scalar wrap is the same raw-versus-stored split on another column class, and ruling A's own heading is "the RLS write check judges the row as it will be stored". So the pre-check step lowers that column the way the write door stores it (pin:tags: 'x'undercontains('x')is admitted, andtags: 'xy'stays refused). Condition: the wrap must come from the same function the write door uses. If that function cannot be reached without a new export in another lane's package, or would have to be copied, the dev does not build the fold and reports it, and the card lands the date/datetime/time half as ruled.Not this card: deleting F7's copy (
lteBound,packages/formula/src/matches-filter.ts). Ruling A orders it after this lands, so the seat files that deletion card on landing.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21109,
"status": "done",
"branch": "claude/issue-21109-rls-check-stored-form",
"pr": "#21235",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (this run is a subagent; the id is the dispatching session's, as the claim and the commit trailers carry it)",
"premise_still_valid": true,
"summary": "Ruling A is implemented in draft PR #21235 (Fixes #21109, Clause-②: no), head 00cb551, base main 62b90d7. The premise was re-measured at 62b90d7 through ObjectQL.insert + SecurityPlugin + SqlDriver (better-sqlite3), as a member resolving a permission set, with the same predicate as using and check. Class reading: a fail-closed read/write split on temporal equality. { due_on: '2026-01-05' } written as '2026-01-05T15:00:00Z' or a Date was 403 while the read showed the stored 2026-01-05; the same held for time '09:00:00' under { start_time: '09:00' } and for datetime '2026-01-05T18:00:00+08:00' under { due_at: '2026-01-05T10:00:00Z' }. There was also a fail-open: { due_on: { $gt: '2026-01-05' } } admitted '2026-01-05T15:00:00Z' while the read hid the stored day. New module packages/plugins/plugin-security/src/rls-check-stored-form.ts adds storedFormCheckJudge. It is the one step every write-check path passes through: satisfiesCheck covers the insert seam, the by-id image and both update seams. On each column the object DECLARES date / datetime / time (spec CALENDAR_DATE_TYPES / INSTANT_TYPES / CLOCK_TIME_TYPES, read from writeCheckFieldOptions → declaredComparisonColumns, never from values), it puts the post-image value AND the check's value comparands ($eq $ne $gt $gte $lt $lte $in $nin $between, implicit equality) through @objectstack/core's temporalStorageForm, which is consumed, not changed. After the change, every temporal cell admits exactly when the read shows the stored row. The $gt boundary-day write is now refused, and the changeset names it as the read/write agreement. The comparand half goes beyond the ruling's literal words: image-only would newly refuse two writes that main admits and the read shows (ablation A2). See open_questions[0]. The multi-valued fold is NOT built, because the condition is not met. The write door's wrap is normalizeMultiValueFields (packages/objectql/src/validation/record-validator.ts:599). Neither @objectstack/objectql entry exports it (index.ts:404, core.ts:90), and plugin-security has objectql only as a devDependency. tags: 'x' under contains('x') stays 403 while the read shows ["x"]. See open_questions[2]. Fork clause, measured: driver-mongodb stores through its own copy, which agrees with temporalStorageForm on every shape tried except a Date in UTC years 0001..0999 on a date column (core '0999-06-15', mongo '999-06-15'). I judged that a mongo defect, not a fork. See open_questions[1]. lteBound stays live and untouched (packages/formula diff empty; still at matches-filter.ts:902). Its deletion is the seat's follow-up card. The pins already pass with it out of reach. explain does not judge a check clause, so it is not on the step.",
"tests": "Final head 00cb551, under os-verify-lock. plugin-security vitest run --maxWorkers=2: 157 files, 3383 passed, 23 skipped. The new file adds +1 file / +38 tests; the base suite count was NOT MEASURED. pnpm --filter @objectstack/plugin-security typecheck: exit 0 (tsc --noEmit, tsconfig.scripts.json, check:test-typecheck OK with 0 debt). The new pin file rls-check-stored-form.test.ts has 38 cells: 1 cell pinning the formula-matcher wrapper live; 16 write-and-read cells x 2 driver families (driver-sql better-sqlite3, driver-sqlite-wasm); 1 by-id update cell x 2; 3 unit cells. Each write is admitted exactly when the read under the same predicate shows the stored row. A refusal is asserted as { code: PERMISSION_DENIED, status: 403 } with nothing stored. F7 is out of reach: @objectstack/formula's matchesFilterCondition is wrapped so $lte arrives as $lt-or-$eq and $between as $gte plus that. Ablations ran on committed head ec6db40 via scripts/ablation-replace.mjs in WRAP mode. Every restore was proven (blob == HEAD, git diff HEAD empty). Tests import src, so no dist was involved. A0, the judge swapped back to main's raw evaluation (security-plugin.ts): 16 red of 38, per driver $lte ISO + Date, $eq ISO + Date, $gt boundary, datetime +08:00, time '09:00:00', by-id update. A1, image half off: 16 red. A2, comparand half off (image only): 8 red (datetime == +08:00, datetime $gte same spelling, time '09:00', time '09:00:00'); two of these are admitted on main and shown by the read. A3, a text column read as date: the first attempt was a NO-OP, refused by the tool because the replacement contained its anchor (count 1 -> 1). It was rerun with the anchor 'return undefined;': 7 red (both control cells x 2 drivers + 3 unit cells). A4, the judge admits everything: 14 red = exactly the 7 negative pins x 2 drivers. The premise probe (a temporary test file, never committed) ran at 62b90d7 and at 11f8dae; its table is in the PR body. Fork probe (tsx, scratch only): temporalStorageForm vs driver-mongodb coerceTemporalValue over 15 shapes: 14 SAME, 1 DIFF (Date 0999-06-15 on date). Live mongod NOT MEASURED, family driver-mongodb live suites; reason: no mongod binary in the container. Narrowed lint at 00cb551: eslint --no-inline-config --format json over the 3 changed .ts files: 3 files, 0 errors, 0 warnings, none ignored. The population is eslint.config.mjs:971 files /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint.config.mjs:327-329 enables no type-aware linting (no parserOptions.project, no typed rules), so no untouched file's verdict can move. The .changeset .md is outside eslint's population. The full pnpm lint is CI's. Control-byte self-scan over the 4 changed files: none. Docs: I grepped content/docs/ (outside releases/) and skills/** for sentences on how an RLS check judges a write (raw vs stored, date columns), and found none made false. Positive control: the grep reaches content/docs/permissions/rls.mdx:64 and content/docs/permissions/authorization.mdx:56, both still true. No docs edit.",
"mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, #20822's two reports and the PR read-back went through gh api GETs",
"api_writes": "3 relay dispatches, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (#21235, draft; read-back 10614 bytes sent, 10614 stored, identical; a REST GET re-read matched; run 36921121169); (2) label-write --issue 21235 --assign os-bill, which became POST /repos//issues/21235/assignees (read-back matches; run 36921223177; the labels documentation, size/l, tests and tooling are another actor's and were left alone); (3) this os-dev-report comment through post-stamped --comment=21109, which becomes POST /repos//issues/21109/comments. git push (not REST): 6 pushes to the branch (the empty-branch probe, then 5 commits), none forced. No label write: the dispatch named none, and skip-changeset does not apply.",
"open_questions": [
{
"question": "Ruling A names the post-image. The step also puts the check's VALUE COMPARANDS on a declared date/datetime/time column into the storage form, because the read pairs both sides (driver-sql coerceFilterValue, driver-memory, objectql having). Is the comparand half inside ruling A? Measured (A2): image-only refuses two writes that main admits and the read shows: { due_at: { $gte: '2026-01-05T10:00:00Z' } } written with that same instant, and { start_time: '09:00' } written as '09:00'.",
"options": [
"A. Keep both halves (this PR). The check answers exactly what the read answers on a temporal column.",
"B. Image only, per the literal words. This ships a new fail-closed split: writes main admits become 403 while the read shows the row, against option A's own stated outcome that writes admitted today stay admitted.",
"C. Stop and take it to the director as a ruling question."
],
"recommendation": "A. Business: policies spell comparands as authors write them ('09:00', zone-offset instants), and an SDK writes Date values, measured as above; B breaks writes admitted today. Long-term: one storage rule applied to both sides is what every other in-process face (having) and every driver already does; B would leave a permanent asymmetry. Anti-AI-error: an AI author cannot see stored-vs-written spellings, and A removes the class structurally, where B makes an equivalent spelling a silent 403. Startup: no new surface, gate or ADR, and the same card."
},
{
"question": "The ruling's fork clause says to stop if temporalStorageForm is not the form every driver stores. driver-mongodb stores through its own copy (mongodb-temporal.ts storageDateValue / storageDatetimeValue / storageTimeValue, reached from MongoDBDriver.toStorageForms, mongodb-driver.ts:866). Measured over 15 shapes, it agrees on every logical value (a datetime is the same instant held as a BSON Date, a physical spelling of the kind ADR-0053 keeps for MySQL), except a Date in a UTC year from 0001 to 0999 on a date column: core '0999-06-15', mongo '999-06-15'. Does that trigger the clause?",
"options": [
"A. No: it is a mongo defect against the declared YYYY-MM-DD form (mongo's own docblock says it mirrors SqlDriver.toDateOnly). Land as is and file the mongo defect (out_of_scope_findings[1]).",
"B. Yes: hold this card until mongo pads the year.",
"C. Judge a driver-specific form in the check. Refused by the clause itself: it picks one driver's form."
],
"recommendation": "A. Business: the shape needs an SDK Date before year 1000 on a mongo date column; no in-repo producer was found. Long-term: the check judges the one declared form, and mongo converges onto it with its own card. Anti-AI-error: holding the card keeps the measured fail-closed splits on every backend for an edge on one. Startup: one small driver card. Note that main's raw check already admits that write (the Date is lifted to an instant), and mongo's own read already sorts '999-06-15' above every padded day, so this PR changes nothing on that edge."
},
{
"question": "The multi-valued fold (claim 5938708535) is not built: its condition requires the write door's own wrap, normalizeMultiValueFields (packages/objectql/src/validation/record-validator.ts:599). No @objectstack/objectql entry exports it, and plugin-security depends on objectql only for tests. Which route should the follow-up take?",
"options": [
"A. Move the wrap rule to a home both packages already depend on at runtime (@objectstack/core, or @objectstack/spec/data beside isMultiValueField). objectql's record validator calls it from there. A follow-up then folds it into storedFormCheckJudge.",
"B. Export normalizeMultiValueFields from @objectstack/objectql and promote objectql to a runtime dependency of plugin-security (today it is deliberately engine-independent: insert-check-post-image.test.ts header).",
"C. Engine-only: run the wrap before the insert seam (engine.ts:13253 runs the seam, :13410 wraps; update already wraps at :14726 before its seam at :14938). This leaves the middleware's by-id judgement on the change set as sent raw, so a by-id update of tags: 'x' would still be refused."
],
"recommendation": "A. Business: measured, an insert of tags: 'x' is 403 while the read shows ["x"]; the record validator names legacy clients that send a lone scalar to a multi-value field. Long-term: one wrap rule in a shared home, read by the write door and the check, is the same shape temporalStorageForm took. Anti-AI-error: it removes the second raw-vs-stored split an author cannot see. Startup: one move card plus one fold card, no gate; B adds a heavy package edge, and C leaves half the paths."
}
],
"out_of_scope_findings": [
"class: b · Seam: spec:isMultiValueField / FILTER_OPERATORS.$contains → runtime:plugin-security rls-check-stored-form.ts storedFormCheckJudge (engine.ts:13253 runs the insert seam before engine.ts:13410 calls normalizeMultiValueFields) · reach: the ObjectQL.insert door, measured at 62b90d7 and 11f8dae on better-sqlite3: a member insert of tags: 'x' under check record.tags.contains('x') answers PERMISSION_DENIED/403, while the same value system-written stores ["x"] and the same policy's read shows it · contract: ruling A, 'the RLS write check judges the row as it will be stored' · family: this card's carried note 5935014513; route to the fold's follow-up (open_questions[2]), not a single-point card · dedupe words: RLS check multi-valued scalar wrap stored form · normalizeMultiValueFields export plugin-security · contains check scalar tags 403",
"class: b · Seam: spec:CALENDAR_DATE_TYPES (ADR-0053 D-B: a date is stored YYYY-MM-DD) → runtime:driver-mongodb mongodb-temporal.ts storageDateValue via MongoDBDriver.toStorageForms (mongodb-driver.ts:866) · reach: named producer: every MongoDBDriver create/update of a date column; the record validator admits a Date in years 0001..0999 for a date (SUPPORTED_TEMPORAL_YEARS.date from 1); measured on the function itself (a tsx probe): Date(0999-06-15T00:00Z) → '999-06-15', where @objectstack/core's temporalStorageForm gives '0999-06-15'; live mongod NOT MEASURED · contract: mongo's own docblock, 'Mirrors SqlDriver.toDateOnly so both backends agree on what a date is', and core's four-digit year padding · dedupe words: mongodb date year padding 0999 · storageDateValue unpadded year · driver-mongodb temporalStorageForm copy",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes): security/explain compares a stored row against a using comparand as written, so a temporal comparand spelled unlike the stored form (e.g. '09:00' on a time column) is compared unlike the read; unmeasured inference"
],
"hypotheses": {
"H1": "Held at 62b90d7 (table in the PR body): temporal equality writes the stored form satisfies were 403 while the read showed the stored row, and one $gt boundary write was admitted while the read hid it.",
"H2": "Measured: the post-image reaches matchesFilterCondition for a check ONLY in satisfiesCheck (security-plugin.ts:3295 at base). The middleware's by-id image and the engine's seams (insert engine.ts:13261, by-id update :14938, predicate update :15197) all call it. rls-compiler compiles and evaluates nothing. explain-engine judges fetched, stored rows under the read filters (explain-engine.ts:1088) and never a check clause, so it is not on the step. The step is storedFormCheckJudge, built once per write beside satisfiesCheck; there is no per-door copy.",
"H3": "Held: the declared types come from writeCheckFieldOptions → declaredComparisonColumns (ql.getSchema, then metadata.get), classified by the spec's CALENDAR_DATE_TYPES / INSTANT_TYPES / CLOCK_TIME_TYPES. A schema that cannot load hands over no columns. Control cells (a text column) plus ablation A3 pin declaration-only.",
"H4": "Delivered: date / datetime / time admitted and refused cells with code + status, read == write per cell, a by-id update, and the text control. Every negative pin was ablated (A4: 14 = 7 x 2). The multi-valued pins were not built (fold not built).",
"H5": "Held: lteBound is untouched (git diff 62b90d7..HEAD -- packages/formula is empty; still at matches-filter.ts:902). The PR states that its deletion is the follow-up card. The RLS CEL lowering emits no { $field, addDays } comparand (cel-to-filter.ts: 0 addDays hits vs 5 $field hits as the control), so that half of F7 is for the deletion card to measure on its other callers."
},
"gates": {
"head": "00cb5519",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 63 families (tree 00cb551, 4 paths vs merge base 62b90d7; --repo checked against origin)",
"reconciliation": "dispatch-gates --ran (with exit codes): 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"exit_0": 63,
"notable": "First sweep at 1d9ccec: check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET) and passed after a full turbo build (72/72); check:test-source-alias exited 1 (a dynamic import of @objectstack/formula inside a clocked test body), fixed in bfa3609 by a module-top import. The final sweep at 00cb551 is all exit 0: check:nul-bytes OK (9895 files), check:engine-double-contract OK, check:type-check-debt OK. AGENTS.md stayed unmodified throughout (git status clean after every turbo run).",
"ci": "in_progress (not waited on, per the dispatch contract)"
},
"line_budget": "584 changed lines (+583 / -1, 4 files) vs the 5000 human-merge threshold (dispatch-gates at 00cb551): under",
"deviations": [
"The comparand half: the ruling names the post-image; the step also puts the check's value comparands on declared temporal columns into the storage form. Image-only measured as a new fail-closed split (A2). Raised as open_questions[0], not chosen silently.",
"The multi-valued fold was not built: the claim's condition is not met (the wrap is not reachable without a new export in objectql). Location and export routes are in open_questions[2].",
"Fork clause: measured one driver-mongodb divergence (a Date in years 0001..0999 on a date column) and judged it a mongo defect rather than a fork. Raised as open_questions[1] for the seat to rule; the PR stays draft.",
"Pins live in packages/plugins/plugin-security/src/, none under packages/qa/dogfood/test/. The ruling's pins are 'through ObjectQL plus SecurityPlugin plus a SQL driver', which plugin-security's suite drives with source aliases for objectql, driver-sql and driver-sqlite-wasm. No example app carries a date-typed check, so a dogfood HTTP pin would need a custom stack and a member login for the same door.",
"Ablation A3's first attempt was a no-op (the tool refused: the replacement contained its anchor, count 1 -> 1). It was rerun with a different anchor.",
"Ablations ran on ec6db40. The final head 00cb551 differs only by a module-top import in the test file (bfa3609) and comment text (00cb551); the full plugin-security suite, typecheck, narrowed lint and all 63 gates were re-run on 00cb551.",
"origin/main moved one commit after the branch point (3ddd3d0, MCP, no overlap with the 4 changed files); no merge was made."
],
"files_changed": [
"A .changeset/21109-rls-check-stored-form.md",
"A packages/plugins/plugin-security/src/rls-check-stored-form.test.ts",
"A packages/plugins/plugin-security/src/rls-check-stored-form.ts",
"M packages/plugins/plugin-security/src/security-plugin.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT · PR #21235 @
00cb5519·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T20:31Z依据:报告
5939908710与 diff。认领更正(Clause-② 行): 本席在认领
5938708535里把说明文字放进了 arm 位置。该行改为Clause-②: no,与 PR 正文和 changeset 一致(dev 没有照抄那段说明)。patch加Clause-②: no,与 #21155(安全修复收窄所服务的内容)的先例同形。- 形态: draft,目标
main,首行Fixes #21109。全文扫过,没有关闭关键词挨着别的卡号。页脚为会话 URL。 - 范围: 4 个文件,584 行,全部在认领面内:新模块
rls-check-stored-form.ts、它的测试、security-plugin.ts中的接入点,以及 changeset。temporalStorageForm只消费,未修改。packages/formula的 diff 为空,lteBound原封未动。 - 单一步骤:
storedFormCheckJudge在每次写入时于satisfiesCheck旁边构建一次,insert、按 id 的映像和两种 update 接缝都经过它。dev 实测 explain 不判check,所以它不在这一步上。没有按门复制的副本。 - pin 与消融: 38 格,覆盖两个驱动族,每格都断言"写放行 ⟺ 读可见"。四条消融都按预期变红(A4 正好是 7 条负向 pin × 2 = 14);A3 首次空跑,换锚点后重跑。门禁 63/63 exit 0。
open questions 的处置:
- Q0 比较值也转为存储形:采纳 A(保留两半)。 依据裁决 A 原文:"The write check and the read then give one answer for one row";以及被裁选项在卡片表格里写明的结果:"今天放行的写,届时仍放行"。只转换写入映像的做法,会把今天放行、读也显示的写改成拒绝(dev 的 A2 消融实测),同时违背这两句。边界那一格(
$gt当天写入)原先放行而读隐藏,现在改为拒收,这是"一行一个答案"本身,changeset 已写明。 - Q1 driver-mongodb 年份补零:采纳 A(不构成分叉,照常落地)。 分歧只出现在公元 0001 至 0999 年的
Date写入date列这一个边缘;仓内没有这种值的生产者,本 PR 也不改变这个边缘(main 的原始检查本来就放行)。按立卡门,这条 finding 缺reach:,不立卡,记作 PR 的 acceptance note。 - Q2 多值折叠:未建(认领的条件不满足)。 写入门的
normalizeMultiValueFields在@objectstack/objectql中未导出。后续另立一张卡交分诊,推荐路线 A:把这条包裹规则移到两个包运行时都依赖的位置,再折叠进storedFormCheckJudge。
按 PR #21192 新规,逐句核了 changeset 与 diff:
- 表格四行(三格"之前 403、读可见",一格"之前放行、读隐藏")与 dev 在
62b90d74的前提实测一致。 - "every column the object declares
date,datetimeortimeis put into …temporalStorageForm… to the post-image's value and to the check's value comparands" 对应rls-check-stored-form.ts;列声明经declaredComparisonColumns读取,与 diff 一致。 - "The last is now refused … the read/write agreement" 与 A0 / A4 的 pin 一致。
- "Every insert, by-id update and predicate update takes the same step" 对应 H2 的接缝清单,一致。
- "Unchanged:"四条(非时间列按写入判;schema 读不出照旧;存储规则读不了的值按写入判;拒收的 code / status 不变)与 diff 及控制格一致。
- 文档: dev 检索了
content/docs/**(不含 releases)与skills/**,以permissions/rls.mdx:64和permissions/authorization.mdx:56作正对照,两处仍然成立;没有句子被改成假。
落地: CI 正在跑,全部 check 转绿后经队列 relay 落地。落地后本席按裁决 A 立
lteBound删除卡。
Generated by Claude Code
- 形态: draft,目标
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded · PR #21235 merged as
ef96c9ed·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T21:05Z- The card closed
completedandpm:dispatchedhas been stripped. - Ruling A's follow-up, "After that lands, F7's copy (
lteBound) is deleted mechanically", is filed as formula: retire F7's whole-day copy (lteBound in matches-filter.ts) now that the RLS write check judges the stored form (#21109, PR #21235); its direct-call cases move to the storage-form lowering #21242 (packages/formula,domain:engineby the lanes table) for triage to route. It quotes the ruling and the maintainer's 「其他四张同意」. - The multi-valued fold that was not built is RLS: a write check refuses a scalar written to a declared multi-valued field (tags: x under contains(x)) while the write stores [x] and the same policy reads it; the check cannot reach the write door wrap rule #21238.
Generated by Claude Code
- The card closed
- added 3 commits that reference this issue
on Oct 7, 2026
Ruled: 5933322270 · letter A · 2026-10-01T14:17Z
Filed by the
domain:engineexecution seat 2 (seat post #20966,session_01Ujdtvqs7ree7WyQmEDwEnG,os-litant) from #20822 group 3a's measured stop (os-dev-report 5926452254, open_questions[0]). This card holds only F7's retirement. #20822 group 3a (F6, PR #21105) lands without it, and group 3b carries F8. ⛔ Not a claim.Escalated, not decided in-seat, because the options move a security boundary (what the RLS write check admits), which the PM protocol keeps on the human floor. One option also amends a Tier H ADR.
一句话问题
删掉 formula 里那份手抄的“整日上界”规则(F7)后,一个成员按 RLS 写策略插入一条日期型记录,会被拒(403);而同一条策略下,读出来这条记录又是可见的。写和读对同一条数据给出了相反的答案。
背景
usingoutput reaches typed drivers while itscheckoutput reachesmatchesFilterCondition; which reading it applies is measured on the card that wires it.」本卡就是这次测量的结果。测量(dev 在 #20822 第 3a 组,经 ObjectQL + SecurityPlugin + SqlDriver 实测;PR #21105 正文里有探针表):
date列不做下沉(item 7)。lteBound后:成员在边界当天写入due_on,无论写成 ISO instant 还是Date,都被拒PERMISSION_DENIED/403。同一策略下,该成员的读能看到这行(存储为2026-01-05)。lteBound时,两种写法都放行。$lte对一个解析成纯日期的{ $field, addDays }引用。lowering 对$field比较值原样保留。Governing text: ADR-0053 D-D1(amended)第 5、7、9 条,原文见上;裁决 5902355785 D4 (b)。没有任何裁决规定 RLS 写检查应判原始形态还是落库形态(检索式见下方 Prior rulings 行)。
协议声明 / 是否改协议: A、B、D 不改协议。C 要修订 ADR-0053(Tier H,维护者亲审)。
前提(每条带 re-check):
git grep -n "function lteBound" origin/main -- packages/formula/src/matches-filter.ts,应命中 1 处。matchesFilterCondition:git grep -n "matchesFilterCondition" origin/main -- packages/plugins/plugin-security/src | head。git grep -n -E '\b(using|check)\s*:' origin/main -- examples—— 阳性对照:必中examples/app-showcase/src/security/permission-sets.ts的using;判据:命中的check行里无 date/datetime/time 列。2026-10-01 总监席重测:2 条using、1 条check(owner == current_user.email),0 条日期型。选项 × 真实代价
@objectstack/core的temporalStorageForm(每个驱动写入时用的同一规则)。随后另开一张删除卡删掉lteBound,它的 24 个直调用例改走 lowering。check子句的date列也做下沉(对日期文本保序等价,using读不受影响),然后删lteBound。{ $field, addDays }这半没人兜,那类写入仍会被误拒。lteBound,把它声明为 F7 对写入镜像的永久规则。Date写日期字段、带日期 check)会被拒 403,而读能看到该行。这是公开入口上的读写不一致,偏严(fail-closed),不泄露数据。已实测。业务含义直译:
四轴(业务立场)
WITH CHECK就是在列类型转换之后判新行。B 是半量;C 让契约迁就实现;D 留下读写分歧。Date写日期字段,且有日期 check」是普通写法,D 的收窄会落在它身上。os-decision-facets
Date写日期字段是常见形态,D 会让它在带日期 check 的策略下被拒。Prior rulings read: "RLS check post-image stored form", "lteBound", "ADR-0053 D-D1 item 7 RLS check" → 0 hits beyond 5902355785 and ADR-0053 D-D1 itself; ADR-0053 D-D1 items 5, 7, 9; thread: #20822 (5926452254), seat 1's in-seat answers 5918373748 (A, the RLS twin for
using).推荐:A。 只看①选 A;②③④ 是否翻转:否(②零拉动只影响时序,A 落地前 F7 的副本照留,不回退)。
回退: 若 A 被否决,选 B,并另立一张卡兜
$field那一半;⛔ 不选 D。置信缺口:
time列的写检查;datetime列的 ISO 带时区写法在temporalStorageForm下的边界;裁后执行
domain:services(plugin-security)立一张卡:RLS 写检查判落库形态,带 dev 的探针表作为 pin,覆盖 ISO instant、Date、纯日期三种,以及一行读写对照。该卡落地后,#5930 step 4 (domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 第 3b 组(或一张单独的删除卡)删除lteBound,24 个直调用例改走 lowering。在 A 落地之前,F7 的副本保持原样。$field半边的卡。domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 第 3b 组直接删除,changeset 以 BREAKING 申报这次收窄。相关
domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822(父卡,第 3a 组 PR fix(driver-mongodb): MongoDBDriver compiles the whole-day comparison it is handed; formula's copy stops on a measured answer move #21105,第 3b 组未认领)domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 是其第 4 步)using孪生)$contains/$notContainson a declared multi-valued or JSON-stored field still answer SUBSTRING on five faces, the analytics RLS read scope among them (u1admits a row storingu10) #20987(formula$contains一面,由第 3b 组承接)Generated by Claude Code