Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:services·area:access·pm:blocked. Route A: the wrap rule moves besidetemporalStorageFormin@objectstack/core, and the check folds itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-01T20:52Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21109
Why p2. It fails closed: a write the stored form would admit is refused, and nothing is let through. The named producer is a legacy client that sends a scalar to a multi-valued field. Read and write give two answers on one row, but no boundary is crossed.
Ruling: route A, home
@objectstack/core(triage's; overturnable by the maintainer).- The wrap rule moves to
packages/core/src/utils/, besidetemporalStorageForm, the precedent the card names. - Both consumers already depend on
@objectstack/coreat runtime (@objectstack/objectqland@objectstack/plugin-security, read at this write). ⛔ Not@objectstack/spec/data: spec holds the predicate (isMultiValueField), and core holds the storage-form transforms. - The record validator calls it from there, and
storedFormCheckJudgefolds it. One rule, shared by the write door and the check. ⛔ No second copy. - ⛔ Not B, which would add a runtime engine dependency to
plugin-security. ⛔ Not C, which covers only half the path.
Routing.
domain:servicesowns the defect (the RLS write check) and the fold. The move intopackages/coreand the validator's call site aredomain:enginefiles, declared in the claim as its cross-lane surface and named in the PR before they are edited.Why blocked. The fold point
storedFormCheckJudgearrives with PR #21235 (Fixes #21109, in flight; read at this write).Pins: the card's.
tags: 'x'is admitted undercontains('x')andtags: 'xy'is still refused, on both driver families. Read and write give one answer on the same row.
Generated by Claude Code
- The wrap rule moves to
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T21:40Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21238-multivalue-wrap-core
Worktree:objectstack-issue-21238
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface:packages/core/src/utils/: the multi-valued wrap rule moves here besidetemporalStorageForm, as one exported function with its unit tests, plus its export from@objectstack/core's entry. This is adomain:enginesurface, declared cross-lane as triage's routing (5940327789) orders.packages/objectql/src/validation/record-validator.ts:normalizeMultiValueFieldscalls the moved rule. No behaviour change at the write door. This is adomain:enginesurface, declared cross-lane.packages/plugins/plugin-security/src/rls-check-stored-form.ts:storedFormCheckJudgefolds the rule for declared multi-valued columns, plus its tests.- A changeset.
⛔ No second copy of the rule. Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(default tier; a move plus a fold, security-adjacent).
Clause-②: no
Thread-read: 5940327789
Release frompm:blocked, double check: ① the latest transition comment (5940327789) carries the line-startBlocked-by: #21109, and [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 is closedcompleted(PR fix(plugin-security): the RLS write check judges a date / datetime / time column in its stored form #21235 merged asef96c9ed). ② The card has no newer merged PR of its own. Re-derived for new blockers againstorigin/mainatef96c9ed: none of the open PRs touchespackages/core/src/utils/,record-validator.ts,rls-check-stored-form.tsorsecurity-plugin.ts. The only in-flight claim namingplugin-securityis 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, forzero-set-deny-baseline.test.ts, which is disjoint.
Selection:priority:p2(triage5940327789, route A, home@objectstack/core). It is the lane's next free card. plugin-security: an org member reading a colleague's sys_user row is served the Admin-group fields (last_login_ip/at, failed_login_count, locked_until, ban_*, phone_number) — and through sys_activity, their sign-in history #21237 is waiting for triage's direction, and automation: a host's per-kernel scheduled-work refusal is reported with the deployment sentence —SCHEDULED_WORK_DISABLED_REASONtells a free-plan tenant to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true(the #19834 seam has no reason slot) #21110 is waiting for the maintainer.
Direction quoted to the dev from triage
5940327789: "The wrap rule moves topackages/core/src/utils/, besidetemporalStorageForm… The record validator calls it from there, andstoredFormCheckJudgefolds it. One rule, shared by the write door and the check. ⛔ No second copy." Pins:tags: 'x'is admitted undercontains('x')andtags: 'xy'is still refused, on both driver families; read and write give one answer on the same row.
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21238,
"status": "done",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"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": "Route A is implemented in draft PR #21253 (Fixes #21238, Clause-②: no), head 8f7c43b, base main be5a83c. The premise was re-measured at be5a83c through ObjectQL.insert + SecurityPlugin + driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check. tags: 'x' under contains('x') was 403 while a system write stored ["x"] and the read showed it, and a by-id update was 403 the same way. The card graded this fail-closed only, but the negated form fails OPEN on main: under !record.tags.contains('x'), tags: 'x' was admitted and stored ["x"] while the read hid it. A policy that forbids tagging x was passed by sending a scalar. The predicate update was already right, because the engine wraps before that seam. Change: the per-value wrap rule moved to @objectstack/core as multiValueStorageForm (packages/core/src/utils/multi-value-storage-form.ts, beside temporalStorageForm, exported from the root entry). objectql's normalizeMultiValueFields keeps its column selection (SKIP_FIELDS, system, readonly, isMultiValueField) and calls the core rule. The write door is byte-for-byte unchanged: validator suite 917 before = 917 after, and a 2750-case differential probe against the verbatim base body found 0 diffs, with a positive control at 1. storedFormCheckJudge now puts each declared multi-valued column's post-image value through the core rule, on every image it judges. The column set is declaredMultiValueColumns: the spec's isMultiValueField over the declaration's type + multiple, never values. No copy of the rule lives in plugin-security. Comparands are deliberately left as written, because the read pairs none with the wrap: $contains / $notContains take a member, and every scalar comparison on such a column is refused 400 by the read. Measured side effect, stated in the PR and changeset: under scalar-comparison policies (==, !=, in, an ordering), which the read refuses 400, 'x' used to get the opposite verdict of ['x'] and now gets the same one. That the check evaluates those policies at all is out_of_scope_findings[0]. Docs: no sentence made false.",
"tests": "Run under os-verify-lock, at 242331e. The TS sources are identical to the final head 8f7c43b, which differs only in changeset text. (1) Core: vitest run --maxWorkers=2, both projects, 79 files / 2199 passed; multi-value-storage-form.test.ts has 16 cells. (2) objectql validator dir (src/validation/): 17 files / 917 passed BEFORE, with record-validator.ts restored from be5a83c (restore proven blob == HEAD 7455fdd0, git diff HEAD empty), and 17 files / 917 passed AFTER. objectql full suite, all 361 test files in three chunks: 122 files / 2528 + 121 / 1985 + 118 / 2574 = 7087 passed. (3) Differential probe (scratch tsx, never committed): base normalizeMultiValueFields (verbatim be5a83c:599-614) vs the new one, over 5 schema shapes x 22 field names x 25 values = 2750 cases, 0 diffs. CONTROL=1 plants one difference and reads diffs=1. (4) plugin-security: rls-check-stored-form.test.ts has 61 cells (38 before + 23 new). The full suite is 157 files, 3406 passed / 23 skipped (the #21235 report read 3383 at its head; this run adds 23 cells; the base count at be5a83c was NOT MEASURED). (5) Typecheck, core / objectql / plugin-security: all exit 0, check:test-typecheck OK (debt 4 / 234 / 0, held in the shrink-only ledgers). (6) Premise and red-on-main: the new pins at base (98b0e5b over base sources) gave 8 failed / 50 passed: the scalar-admitted tags / owners x2, !contains x2, by-id x2. A temporary probe file (deleted) printed write / stored / read per cell on both families, at base and at head, including the scalar-comparison table in the PR. (7) Ablations, on committed head a049a41 through scripts/ablation-replace.mjs in WRAP mode. Each anchor hit 1 -> 0, and each restore was proven: blob == HEAD 88931797 / 6b7d1004, git diff HEAD empty. A1, the fold removed (storedFormImage called without multiValue): 9 red = tags / owners 'x' admitted x2, !contains x2, by-id x2, 1 unit. The predicate update stays green (the engine wraps before that seam). A2, the wrap applied to a non-declared text column: 8 red = 3 text controls x2 + 2 unit. A3, the judge admits everything: 25 red = 12 negative pins x 2 drivers + 1 unit. A4, the core rule stops wrapping, rebuilt into dist: ablation-dist-preflight found the marker in packages/core/dist/index.js and index.cjs. Core unit 7 red, objectql record-validator.test.ts 1 red (through core dist), plugin-security file 16 red. A4 restore: core rebuilt, preflight --absent OK, whole-tree clean, 16 / 126 / 61 green. (8) Narrowed lint at 8f7c43b: eslint --no-inline-config --format json over the 6 changed .ts files, 6 files, 0 errors, 0 warnings, none ignored. Population: eslint.config.mjs:971 /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. eslint.config.mjs:327-329 enables no type-aware linting, so no untouched file's verdict can move. The full pnpm lint is CI's. (9) Control-byte self-scan over the 7 changed files: none. Docs grep: content/docs/ (outside releases/, references/) and skills/** for the scalar wrap and for contains / multi-valued under a check found no false sentence. Positive controls: validation-rules.mdx:473 and permissions/rls.mdx:64, both still true.",
"mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, PR #21235 and its dev report, 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 became POST /repos/objectstack-ai/objectstack/pulls (#21253, draft). Read-back: 11855 bytes sent, 11855 stored, identical (run 36937088475). (2) label-write --issue 21253 --assign os-bill became POST /repos//issues/21253/assignees. Read-back matches (run 36937157695). The labels size/m, documentation, tests and tooling are another actor's and were left alone. (3) This os-dev-report comment, through post-stamped --comment=21238, becomes POST /repos//issues/21238/comments. git push (not REST): 6 pushes (the empty-branch probe, then 5 commits), none forced. git fetch --shallow-since=2026-06-26 origin main (a read, deepening the shared clone for check:engine-split-ratio). No label write beyond the assignee: the dispatch named none, and skip-changeset does not apply.",
"open_questions": [
{
"question": "The Clause-② line. The claim and the dispatch fixClause-②: no, and the PR body and changeset carry it verbatim. But this diff adds one @objectstack/core root export (multiValueStorageForm). It also widens the RLS accept set (tags: 'x' under contains now admitted) while narrowing a fail-open (!contains). The line answers whether a card widens the accept set or enlarges the public surface. Precedent is split. The move this card copies (temporalStorageForm into core, e5cf27d) shipped underClause-②: no (narrowing), with core minor. a11faee, with three new core exports, declaredClause-②: yes (widening). #21235, whose accept-set widening is the same kind, declaredno. check-widening-tells' T3 reads only packages/spec/api-surface, so no gate will flag a core export either way.",
"options": [
"A. KeepClause-②: no(as dispatched). The export is a relocated rule with one consumer pair, the accept-set change is conformance to ruling A as in #21235, and the move precedent isno.",
"B. Re-declareClause-②: yes (widening)in the PR body line and the changeset (a seat write). The PR then takes the contract-review tier, as a11faee did for new core exports."
],
"recommendation": "A, on the four axes. Business: the export serves exactly the two measured consumers (the write door and the check) and adds no capability an author can write. Long-term: route A copies temporalStorageForm, which wasno, and an accept-set change that makes the check agree with the stored row is the ruling-A conformance #21235 declaredno. Anti-AI-error: the honest-declaration argument points to B, because T3 is spec-only and cannot check a core export. That is the trade-off for the seat. Startup: A adds no review tier to a move. The bumps already follow the dispatch rule: core minor (export), plugin-security minor (accept set), objectql patch."
}
],
"out_of_scope_findings": [
"class: b · Seam: core:JSON_COLUMN_INCOMPATIBLE_OPERATORS (the read's refusal set on a JSON-stored column) → runtime: @objectstack/formula matchesFilterCondition as the RLS write check (plugin-security rls-check-stored-form.ts storedFormCheckJudge) · reach: the ObjectQL.insert door, measured at be5a83c and 8f7c43b on driver-sql (better-sqlite3) and driver-sqlite-wasm, as a member resolving a permission set, using == check, on atagsfield. Under record.tags != 'x' and !(record.tags in ['x']), the member write of ['x'] (and now 'x') is ADMITTED and stored ["x"], while the read under the same policy answers INVALID_FILTER / 400. Under record.tags == 'x' and record.tags in ['x'], the write is 403 while the read is 400. Under record.tags > 'a', both are 400 (the matcher already refuses orderings there). · contract: core json-column-operator-refusal.ts module note, 'Two faces, one rule': the in-process faces (objectql's per-aggregation filter, driver-memory's filter gate) refuse these operators on the same declared fields, and the write check is an in-process face that evaluates them instead. Whether the permission-set authoring door refuses such a policy is NOT MEASURED. · family: the RLS write/read agreement family of #21109 and this card. Route it to that family's closing card if one exists, not a single-point card. · dedupe words: RLS check scalar comparison multi-valued JSON column INVALID_FILTER · matchesFilterCondition JSON_COLUMN_INCOMPATIBLE_OPERATORS write check · tags != policy admitted read refused",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes 2): security-plugin.ts:3294, the comment above the storedFormCheckJudge call, still names only date / datetime / time. It defers to rls-check-stored-form.ts, which now states the multi-valued half. The file is outside this claim's surface.",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes 3): boundary. The write door skips system / readonly multi-valued columns, and the check's declaration (declaredComparisonColumns: type + multiple) does not carry those flags. Only a hook's own scalar write to such a column is judged wrapped while it is stored as written. A caller's value there is stripped before the engine's seams. Inference from the engine's strip order, not measured."
],
"hypotheses": {
"H1": "Held at be5a83c on both families (the PR's premise table). tags: 'x' under contains('x') was 403 while the system write stored ["x"] and the read showed it. Plus a fail-open the card did not grade: !contains('x') admitted 'x' while the read hid the stored ["x"].",
"H2": "Done. The per-value rule moved, and the validator's column selection did not (SKIP_FIELDS, system, readonly, isMultiValueField stay in record-validator.ts). The blank skip (isMissing) moved with the rule, since it decides a value's form. The predicate stays the spec's isMultiValueField. Byte-for-byte: 917 = 917 validator tests, 7087 objectql tests, and a 0-diff differential over 2750 cases with a positive control.",
"H3": "Done for the image, on every path: the insert seam, the by-id image, and the by-id and predicate update seams. Declared types come from the object's metadata via declaredComparisonColumns, never from values (A2 pins it). The comparand half does not apply, because the read pairs no comparand with the wrap. Measured: the read refuses ==, !=, in and orderings on the column 400, and contains takes a member.",
"H4": "Delivered. The card's pins run on both families, plus 'xy' refused, the list spelling, !contains, select multiple, by-id and predicate updates, and two text controls. #21235's 38 temporal cells are still green. A1 (fold removed: the scalar stays refused) and A2 (wrap on a non-declared column) are both red as predicted. A3 shows every negative pin can fail. A4 shows both faces read the one core rule through dist."
},
"gates": {
"head": "8f7c43bf",
"derived": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 70 families (tree 8f7c43b, 7 paths vs merge base be5a83c; --repo checked against origin). The list is identical to the derivation at 242331e.",
"reconciliation": "dispatch-gates --ran (with exit codes) at 8f7c43b: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)",
"exit_0": 70,
"notable": "First sweep at 242331e. check:engine-split-ratio exited 2 (shallow-clone refusal) and exited 0 after its own remedy, git fetch --shallow-since=2026-06-26 origin main. check:dual-build-cjs-loads and check:i18n exited 3 (PREREQUISITE NOT MET) and exited 0 after a full turbo build (72/72). check:dts-closure and check:sourcemap-no-sources-content were re-measured after that build (17 -> 72 and 68 built packages swept). The final sweep at 8f7c43b is all exit 0: check:nul-bytes OK (9911 files), check:engine-double-contract OK, check:type-check-debt OK. AGENTS.md stayed unmodified throughout (git status clean after every turbo-driven gate).",
"ci": "in_progress (not waited on, per the dispatch contract)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs the 5000 human-merge threshold (dispatch-gates at 8f7c43b): under",
"deviations": [
"Clause-② staysno, verbatim from the claim and the dispatch, although the diff adds a core export and widens the RLS accept set. This is raised as open_questions[0], not chosen silently.",
"Bumps follow the dispatch rule ('patch unless an export or accept set widens'): @objectstack/core minor (one new root export), @objectstack/plugin-security minor (accept set widens), @objectstack/objectql patch (no behaviour change). The release group is fixed, so the release takes minor.",
"No comparand half for multi-valued columns. The read pairs none (measured: scalar comparisons are 400 at the read, and contains takes a member). Only the post-image is put into the form.",
"The pins extend the existing rls-check-stored-form.test.ts (two fields on its object, cells in its table, two update cells, a unit describe). Its per-driver describe was renamed from 'one temporal row' to 'one stored row', which renames the title prefix of the 38 existing cells. That is cosmetic, and every cell's body is unchanged.",
"The check cannot mirror the write door's system / readonly column skip, because the declaration it reads lacks those flags and declared-comparison-columns.ts is outside the claimed surface. This is documented as a boundary in the module note and the PR (Acceptance notes 3), not a breach.",
"Two temporary probe test files were created in packages/plugins/plugin-security/src and deleted before any commit: one for the premise, and one for the scalar-comparison table at base and at head. The base reading for the table restored rls-check-stored-form.ts from be5a83c without staging, then checked out HEAD again (blob == HEAD 88931797, diff empty).",
"The objectql full suite ran as three explicit file-list chunks (122 / 121 / 118 files), so each fit inside the foreground cap.",
"The tests and typechecks ran at 242331e. The final head 8f7c43b changes only the changeset's text. The gate union and the narrowed lint were rerun at 8f7c43b.",
"The dispatch named main as ef96c9e. The branch point is be5a83c, one commit later (#21239, driver-sql MySQL create / bulkCreate), with no overlap. origin/main has since moved to 3dc33b2 (#21222), with no overlap with the 7 changed files. No merge was made.",
"The deepening fetch for check:engine-split-ratio (shallow-since 2026-06-26) wrote shared .git state (the shallow boundary and refs/remotes/origin/main). It is additive history only."
],
"files_changed": [
"A .changeset/21238-multi-value-wrap-one-rule.md",
"M packages/core/src/index.ts",
"A packages/core/src/utils/multi-value-storage-form.test.ts",
"A packages/core/src/utils/multi-value-storage-form.ts",
"M packages/objectql/src/validation/record-validator.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.test.ts",
"M packages/plugins/plugin-security/src/rls-check-stored-form.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim revision (Clause-② line) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T22:52Z · amends claim5941247880The seat answers the dev's open question in
5942236028with B. The claim'sClause-②: nowas wrong and is corrected toClause-②: yes (widening).- Governing text:
references/lanes/spec.md: 「放宽接受集或扩大公开面的卡,不论多小,即条款②」 and 「按yes申报恒不是错误」. PR fix(core,objectql,plugin-security): the RLS write check judges a lone scalar on a declared multi-valued field as the list it is stored as #21253 adds one root export to@objectstack/core(multiValueStorageForm). It is reachable from the package's entry types and addressable throughexports, so it is a published public surface (contract-review.md: the published surface is read from the package'sexports). Enlarging it is Clause-②, however small. - Not the reason: the RLS check's admission change.
execution-duties.md: 「负边界:运行时权限/安全行为变更不是条款②」. That change executes triage's route A under ruling A and stays on the security floor as ruled. - Precedent: a11faee (new
@objectstack/coreexports) declaredyes (widening). e5cf27d declaredno (narrowing)for thetemporalStorageFormexport; under the spec-lane rule above, that was an under-declaration and is not followed. - Consequence: an isolated
CONTRACT_REVIEW_TIERreview is owed on the patched head before enqueue (Clause-②: yesis one of the three contract faces). Semver is unchanged:yestakes at leastminor, and core is alreadyminor.
Generated by Claude Code
- Governing text:
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21238,
"status": "done",
"round": "patch round 1 (Clause-② claim revision, answer B)",
"branch": "claude/issue-21238-multivalue-wrap-core",
"pr": "#21253",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (subagent; the dispatching session's id)",
"head": "e559c9ac",
"summary": "The changeset's declaration line is now exactlyClause-②: yes (widening). Its prose says the widening is the new @objectstack/core root export multiValueStorageForm, and that the plugin-security admission change is a security-floor behaviour change, not the declared widening. The bumps are unchanged: core minor, plugin-security minor, objectql patch. The yes arm requires at least one moved package at minor or above, and core minor satisfies it. One commit (e559c9a, model-free trailer pair), a normal push. The PR body was NOT patched by this run: see deviations[0]. The ready body is at the scratchpad path in deviations[0].",
"gates": {
"head": "e559c9ac",
"derived": "dispatch-gates --repo objectstack-ai/objectstack --commands at e559c9a: 70 families, the list identical to the one at 8f7c43b. check:api-surface is NOT derived (no packages/spec path in the diff).",
"run": "node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0 (1 non-breaking changeset seen) · --self-test: exit 0 · node scripts/check-changeset-no-major.mjs --base origin/main: exit 0 (the level axis is NOT APPLICABLE locally, since there is no pull_request payload) · --self-test: exit 0 · node scripts/check-empty-changeset.mjs --base origin/main: exit 0 · --self-test: exit 0 · pnpm check:changeset-gate-self-tests: exit 0 · pnpm check:nul-bytes: exit 0 (9911 files) · pnpm check:objectui-changeset: exit 0 · pnpm check:pm-changeset-deadline-census: exit 0",
"level_axis_offline": "check-changeset-no-major --base origin/main --event, with a pull_request payload carrying the patched body (line 2Clause-②: yes (widening)): exit 0, 'declares clause-② yes (widening), and it grades a package whose packages//src/ it moves at minor or above'. The same with the live body (line 2Clause-②: no): exit 0, 'not declared'.",
"not_rerun": "The other 60 derived families were not re-run at e559c9a. The only change since 8f7c43b (all 70 exit 0) is changeset prose, +2 / -2 lines.",
"ci": "in_progress (not waited on)"
},
"line_budget": "359 changed lines (+324 / -35, 7 files) vs merge base be5a83c at e559c9a (dispatch-gates): under",
"files_changed": [
"M .changeset/21238-multi-value-wrap-one-rule.md (this round: the Clause line + two prose clauses)"
],
"mcp_calls": "0",
"api_writes": "1 relay dispatch: this comment, through post-stamped --comment=21238, became POST /repos//issues/21238/comments. git push (not REST): 1 normal push (8f7c43b -> e559c9a). No issue_patch was sent (deviations[0]).",
"deviations": [
"CONFLICT, raised and not chosen silently: the patch order's step 3 (issue_patch on PR #21253's body) conflicts with the dev definition: 'PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写', and the definition prevails over dispatch words on a conflict. So no body write was made. The seat's one write is ready: /tmp/claude-0/-home-user-objectstack/648b83a3-c5c3-520e-b41a-19ee2af1efbe/scratchpad/issue-21238/pr-body-patched.md (12176 bytes). It is the live body (read back at e559c9a and byte-identical to what was sent) with three edits. Line 2:Clause-②: no->Clause-②: yes (widening). Line 33, the Changes bullet: the core minor is marked as the declared widening, and the plugin-security admission change as a security-floor change, not Clause-②. Line 94, Acceptance notes 4: rewritten to say the line declares the new core export, the claim'snowas revised on #21238, and the families re-ran green at e559c9a. It keepsFixes #21238on line 1 and the session-URL footer. There is no closing keyword next to another card number, no less-than character, no control byte, and the level axis passes over it offline (gates.level_axis_offline). Until it is written, CI readsClause-②: nofrom the body, and the level axis stands down green."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsACCEPT(席位复核,入队前仍欠契约级复核) · PR #21253 @
e559c9ac·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T22:58Z依据:报告
5942236028与补丁轮5942306414,以及 diff。- 形态: draft,目标
main,首行Fixes #21238,第 2 行Clause-②: yes (widening)。正文这三行(第 2、33、94 行)由本席经 relay 改写,回读一致。dev 按自己的定义不改 PR 正文,只把正文备好交给本席,这是角色分工,不是越权。全文扫过,没有关闭关键词挨着别的卡号。页脚为会话 URL。 - 范围: 7 个文件,359 行,全部在认领面内。
@objectstack/core新模块与入口导出,以及 objectql 记录校验器的调用点,都是已声明的跨车道面。没有任何规则副本。 - 写入门不变的证据: 校验器测试前后都是 917;对原函数逐字比对的 2750 例差分 0 处不同,正对照为 1;objectql 全套 7087 通过。
- 消融: A1 到 A4 都按预期变红(A4 经 dist 证明写入门与检查读的是同一条 core 规则)。门禁 70/70 exit 0,补丁轮后 changeset 各族重跑 exit 0。
- dev 多测出的问题:
!contains('x')下发送标量'x'原先能通过(写入时放行,读却看不到这一行),现已关闭。
按 PR #21192 新规,逐句核了 changeset 与 diff:
- 三行表(
contains时 403 而读可见;!contains时放行而读隐藏;按 id 更新时 403 而读可见)与 dev 在be5a83cf的前提实测一致。 - "the image's value on every field the object declares multi-valued goes through the same rule the write door stores it by" 对应
storedFormCheckJudge经declaredMultiValueColumns调multiValueStorageForm,一致。 - "A lone scalar now gets exactly the verdict its stored list gets, on the insert, a by-id update and a predicate update" 与 H3 及 pin 一致。
- 标量比较类策略的副作用("used to get the opposite of the verdict … now gets the same one")如实写明。读侧仍以 400 拒收这些策略,这一问题已另立 RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254。
- "Unchanged:" 四条(非多值字段、列表 / null / 空串 / 对象、比较值保持原样、拒收的 code 与 status)与 diff 及对照格一致。
- core 段 "wraps a string, a number or a boolean into a one-member list and returns every other value as the same value" 对应
multi-value-storage-form.ts及其 16 格单测,一致。 Clause-②: yes (widening)指 core 新增的根导出;plugin-security 的接受集变化属于安全地板,不属于 Clause-②,与认领修订一致。
- 文档: dev 检索了
content/docs/**与skills/**,以validation-rules.mdx:473和permissions/rls.mdx:64作正对照,没有句子被改成假。docs-drift 对core/src/index.ts没有锚点,本席认可 dev 的人工检索。
仍欠: 隔离的
CONTRACT_REVIEW_TIER复核(Clause-②: yes),已经起跑,结论落在 PR 上。PASS 在案、全部 check 转绿之后才入队。
Generated by Claude Code
- 形态: draft,目标
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsContract review recorded: PASS (
5942463292on PR #21253,CONTRACT_REVIEW_TIER,Local-runs: none, heade559c9ac) ·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T23:12Zneeds:contract-reviewwas removed in the same act. The seat's disposition of each escalated or noted item:- ③3 (the check cannot mirror the write door's
system/readonlyskip):Acceptance noteson the PR, no card. The reviewer confirmed that no caller can reach it: a caller's value on such a column is stripped before the engine's seams. The one remaining case, a hook's own scalar write, is unmeasured, and the filing gate ① files only measured reach. If a later card measures it, it folds into RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254's family. - ③2 (scalar comparisons on a multi-valued column that the read refuses): filed as RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254.
- ③4 (the stale comment above the
storedFormCheckJudgecall insecurity-plugin.ts):Acceptance notes; a comment only. It rides the next PR that edits that file (RLS: the write check evaluates a scalar comparison (!=, ==, in) on a declared multi-valued / JSON-stored column that the read refuses 400, so a policy the read cannot run admits writes #21254 is the likely carrier).
Landing waits for every check on
e559c9acto finish green.
Generated by Claude Code
- ③3 (the check cannot mirror the write door's
- added a commit that references this issue
on Oct 7, 2026
立卡门 ①:有具名落点与复现的产品缺陷。
finding类别 b(写检查与读在同一行上给出两个答案)。reach:引擎写入门实测(REST 数据门走的就是这条ObjectQL.insert路径),具名生产者是记录校验器自己点名的旧客户端:它们给多值字段发一个标量。动手的读者:分诊定级并定车道。修复要把
@objectstack/objectql里的一条规则移到共享位置(domain:engine的包),再在plugin-security里折叠(domain:services),是跨车道的父子卡形态。查重:
mcp__github__search_issues查 "RLS check multi-valued scalar wrap stored form normalizeMultiValueFields contains 403",5 条命中(含 closed),都不是本题。最近的是 #20355 / #20347(跨比较类)、#20212(比较值形状),都已关闭。来源
5938708535)。5939908710(PR fix(plugin-security): the RLS write check judges a date / datetime / time column in its stored form #21235)实测这个条件不满足,所以没有建折叠:normalizeMultiValueFields(packages/objectql/src/validation/record-validator.ts:599)不在@objectstack/objectql的任何入口导出,而plugin-security对 objectql 只有 devDependency。domain:engine#2携带到 [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(5935014513,出自 #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 group 3b 的报告5934956420)。reach:ObjectQL.insert配SecurityPlugin配 SQL 驱动(better-sqlite3),在62b90d74与11f8dae1上实测。策略为check: record.tags.contains('x'),字段tags声明为多值:tags: 'x'(标量):403 PERMISSION_DENIED;["x"],同一策略的读能看到它。这是失败即拒(fail-closed):不会放行一行读看不见的数据。
方向(供分诊参考,不是裁决)
dev 给的三条路线,推荐 A:
@objectstack/core,或@objectstack/spec/data里与isMultiValueField并列),objectql 的记录校验器从那里调用;随后在plugin-security的storedFormCheckJudge(PR fix(plugin-security): the RLS write check judges a date / datetime / time column in its stored form #21235)里折叠进去。这与temporalStorageForm已经采用的形状相同:一条规则,写入门与检查共用。⛔ 不另写第二份。@objectstack/objectql导出这个函数,并把 objectql 提升为plugin-security的运行时依赖。会加一条沉重的包依赖,而 plugin-security 是有意不依赖引擎的。Pins(若采纳 A):
tags: 'x'在contains('x')下放行,tags: 'xy'仍拒,两个驱动族都覆盖;读与写对同一行给出同一答案。依赖
在 PR #21235(#21109)落地之后进行:折叠点
storedFormCheckJudge由它引入。Generated by Claude Code ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ