Repository navigation
plugin-security: security.explain answers a record visible under a row-level policy that aims a JSON-column-incompatible operator at a declared JSON-stored field, while find refuses the same read with INVALID_FILTER / 400 #21319
Description
Activity
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p2·domain:services·area:access·pm:queue.explainanswers whatfindanswers: the core JSON-column refusal, not "visible"Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T05:00Z. ⛔ Not a claim, ⛔ not a dispatch.⛔ Classes and positions only.
Why p2. It is the grade of #20431, the same shape for cross-class comparisons. The diagnostic surface reports "visible" for a read the platform refuses, so an operator debugging access is misled. Nothing is served.
Direction.
explain-engine.ts's record attribution applies core'sJSON_COLUMN_INCOMPATIBLE_OPERATORSrefusal on declared JSON-stored columns. It is the same import the read faces and, since PR fix(plugin-security): the RLS write check refuses an operator the read refuses on a declared JSON-stored column (#21254) #21317, the write check use. ⛔ No copy of the set.- It answers the refusal the read gives, not a verdict.
Pins: the card's table rows answer the read's refusal through
explain.contains/notContainsare unchanged (the control). #20431's pins stay green.
Generated by Claude Code
- 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, M3
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-02T06:35Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21319-explain-json-column-refusal
Worktree:objectstack-issue-21319
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface:packages/plugins/plugin-security/src/explain-engine.ts: the record attribution (applyRecordAttribution/matchUnderDeclaredColumns) answers core's JSON-column refusal on a declared JSON-stored column with the read's answer, through the same import the read faces and, since PR fix(plugin-security): the RLS write check refuses an operator the read refuses on a declared JSON-stored column (#21254) #21317, the write check use (that PR exportsfindJsonColumnCheckRefusal). ⛔ No copy of the set. Plus tests beside it.- Pins on both SQLite families, docs sentences this change makes false, and a changeset.
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(no path-derived mandate; default tier).
Clause-②: no
Thread-read: 5945888132
Serial constraints cleared, read in this act againstorigin/mainat6c5bef5f4: PR fix(plugin-security): the RLS write check refuses an operator the read refuses on a declared JSON-stored column (#21254) #21317 (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, whose helper this reuses) merged as97239c3c8. No open PR touchesplugin-security/src/explain-engine.ts. 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 (domain:engine#1) declaresrls-compiler.tsandsecurity-plugin.ts, and PR fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals,objectql)!: the audit ledger honours internal, and the credential-class census is declared internal #21301 declaresprivate-credential-row-scope.test.ts: both disjoint.
Selection:priority:p2,security-family diagnostic. os package install (install-local) drops an app's script action bodies: REST 404 and MCP run_action "No handler registered" while list_actions advertises the action #21321 (p1) is held onpm:retriage(5946787053), and analytics: the ObjectQL face never applies a query's order, limit or offset (every bucketed time-dimension query lands there), while its echoed SQL and /analytics/sql render ORDER BY and LIMIT #21316 (p1) is in flight. Of the p2 queue: Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 is the install family that os package install (install-local) drops an app's script action bodies: REST 404 and MCP run_action "No handler registered" while list_actions advertises the action #21321's answer may fold, and share-links: GET /api/v1/share-links answers 403 to every plain member on every object — listLinks reads sys_share_link under the caller's context, which member_default does not grant #21328'splugin-sharingshare-link files are held by open PR fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals,objectql)!: the audit ledger honours internal, and the credential-class census is declared internal #21301. So this card takes the free slot (「并发保持3」).
Ruling quoted to the dev from triage
5945888132(overturnable by the maintainer): "explain-engine.ts's record attribution applies core'sJSON_COLUMN_INCOMPATIBLE_OPERATORSrefusal on declared JSON-stored columns. It is the same import the read faces and, since PR #21317, the write check use. ⛔ No copy of the set. It answers the refusal the read gives, not a verdict." Pins: "the card's table rows answer the read's refusal throughexplain.contains/notContainsare unchanged (the control). #20431's pins stay green."
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21319,
"status": "done",
"branch": "claude/issue-21319-explain-json-column-refusal",
"pr": "#21371",
"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": "Draft PR #21371 (body opens with the closing line for this card, thenClause-②: no; head cf6f85c) makes security/explain answer the read's refusal, INVALID_FILTER / 400 with no verdict, for a row-level policy that aims an operator the read refuses at a column the object declares JSON-stored. Premise re-measured at 6c5bef5 on both SQLite families: the card's three rows (multi-valued!=,json==, negatedin) plus aselectand alookupflaggedmultipleanswered find 400, by-id update/delete 403, and explainvisible: truedecided by rls for read/create/update/delete; the object-level report said allowed:true and an absent record id visible:false. The HTTP route, NOT MEASURED on the card, was measured once through RestServer on the real stack: POST /api/v1/security/explain 200 visible:true beside GET /data 400; after the fix the route answers 400 INVALID_FILTER (temporary probes, not committed). Mechanism: explain-engine.ts'smatchUnderDeclaredColumns(the seam every record-attribution filter passes) asksfindJsonColumnCheckRefusal(imported from rls-check-stored-form.ts, PR #21317's rule; no copy of the set or of core's words, no core edit) before the matcher reads the record;refuseWhatTheMatcherRefuses(the object-level pass) asks the same seam when either classification finds a refusal, so the object-level report and an absent record id refuse too. Shape: exactly the cross-class answer (a thrown error, no decision, code/status taken fromcause); the cause is the write check's refusal error (core's message, the read's envelope), whose message equals the find's byte for byte. One judgement call to review: the message leads with core's diagnostic (names field and operator, carries the remedy), then the policy, then the cross-class trailing sentence. The cross-class message leads with its remedy for the same REST-bound reason; core's remedy cannot be lifted out without copying text, and core's withheld message says the diagnostic is in the server log, which explain does not write. The cross-class message is byte-identical (subject and trailing sentence now shared). rls-check-stored-form.ts (the small change Zone 2 step 2 allowed, same package):jsonColumnCheckRefusalErrorexported, andfindJsonColumnCheckRefusaltakes an optionalroot(defaultcheck) so explain's refusal path readsrowFilter[…]; the module is not re-exported from src/index.ts (0 hits in dist/index.d.ts), so the published surface is unchanged. No security-plugin.ts / rls-compiler.ts edit. Docs: no sentence made false; two made true for this class, unedited: skills/objectstack-data/rules/security.md:61-62 and content/docs/permissions/explain.mdx:10-13. Changeset.changeset/21319-explain-json-column-operator-refusal.md(plugin-security patch,Clause-②: noline). Labels: the dispatch named none and the changeset excludes skip-changeset, so the label-write stroke set only the PR assignee (os-bill). Thedocumentation/tests/toolinglabels on the PR were added by another actor; left untouched. Commits carry the model-free trailer pair (the harness reminder's model-named trailer and robot footer yield to the agent file and the dispatch). Branch merged origin/main at 23365ea before the final runs; origin/main has since moved (the --ran derivation flags one changed derivation file, scripts/release-github-releases.mjs, unrelated to these paths). Worktree cleanup (node_modules, thengit worktree removewithout --force) is this run's last step after this comment.",
"tests": "All builds/tests via scripts/pm/os-verify-lock.sh (slot issue-21319-dev). (1) Premise probe at 6c5bef5 (temporary in-package test, deleted): 5 refused cells x both drivers -> find 400 INVALID_FILTER, update/delete 403, explain read/create/update/delete all answeredvisible: true/allowed:true; controls (contains, !contains, presence, scalar) find rows=1. Same probe after the fix: every refused cell explain REF INVALID_FILTER/400 (record, absent id, object-level, update, delete, create); controls unchanged. HTTP probe (temporary, packages/rest, real SecurityPlugin+ObjectQL+SqlDriver): before 200 visible:true vs GET /data 400 (message length 486); after 400 INVALID_FILTER, wire length 500 (cut at the bound, diagnostic+remedy+policy intact). (2) New pins src/explain-json-column-refusal.test.ts:vitest run --reporter=verbose-> Tests 20 passed | 10 skipped (30) (10 per SQLite family; PostgreSQL skipped without OS_TEST_POSTGRES_URL). Neighbours with it (explain-cross-class-refusal, rls-stored-list-ordering-fails-closed, rls-check-stored-form): Test Files 4 passed, Tests 232 passed | 17 skipped. (3) At 6e09cfd (merge of origin/main 23365ea; cf6f85c adds only a changeset edit):pnpm --filter @objectstack/plugin-security test-> Test Files 159 passed (159), Tests 3483 passed | 33 skipped (3516), exit 0 (159 = the package's test-file count, the new file included);pnpm --filter @objectstack/plugin-security typecheckexit 0 (tsc, tsconfig.scripts.json, check:test-typecheck: 0 files / 0 errors in debt). (4) Ablations at 6023d46 (fix and pins committed first; scripts/ablation-replace.mjs WRAP mode with its EXIT/INT/TERM restore; source-resolved subject, so no dist leg): A refusal removed (declaredJsonStoredColumns(declaredColumns)-> empty set, --expect 2, anchor 2->0, blob 0690d812bace->98efb1fd23a3): 12 failed | 22 passed = J1-J5 + two-policy pin on both drivers red, 8 controls and 14 cross-class pins green. B rule extended to $contains/$notContains (anchor 1->0, blob 2aab18f103a2->e38330b363d6): 4 failed | 16 passed = contains and !contains controls on both drivers. C rule applied to every column (anchor 1->0, blob 2aab18f103a2->88bfe1ed0e2c): 4 failed | 16 passed = scalar control + two-policy pin on both drivers (wider than expected: that pin's second policy is a scalar==, which the mutated rule also refuses and names). Every restore: blob == HEAD andgit diff HEADempty, printed by the tool; pre/post grep -c of anchor and marker matched. (5) Gates at cf6f85c:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands-> 65 commands; all 65 run, every exit 0 (exit captured before any pipe).--ran-> 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN'. First pass (6e09cfd): check:dual-build-cjs-loads and check:i18n exit 3 PREREQUISITE NOT MET (no dist), check:type-check-debt killed by my 400 s per-gate bound then exit 3 (client-react dts build could not resolve @objectstack/client mid-build); after building the i18n closure and a retry, all three measured green (dual-build: 105 require entry points across 66 packages load; i18n: 9 packages in sync; type-check-debt: 1 ledger entry re-measured, none above its number). NOT MEASURED locally, CI's: the CI jobs and type-check lanes the derivation names outside its list (Test Core, Dogfood, Build Core, Temporal Conformance, workspace typecheck lanes).",
"mcp_calls": "0 — no MCP GitHub tool was called; the card, its comments, a prior report format and the PR read-back went throughgh apiGETs",
"api_writes": "3 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft forced), PR #21371, read back 8299 bytes sent / 8299 stored, identical; (2) label-write assign -> POST /repos//issues/21371/assignees (os-bill), read back matches; (3) this os-dev-report comment -> POST /repos//issues/21319/comments. Plus git pushes of the branch (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes) · packages/spec/src/security/explain.zod.ts:13-14 says the report 'IS enforcement, minus the throw'; since the cross-class refusal explain throws INVALID_FILTER for a filter enforcement cannot run, and this PR adds a second class. Not made false here; outside the claim's surface; a doc-comment imprecision, none of classes a/b/c.",
"carrier: 承接者:无 · noted, not filed (PR Acceptance notes) · no packages/rest on-the-wire pin for this class, as rest/src/cross-class-refusal-remedy-on-the-wire.test.ts has for the cross-class one; the door was measured once by a temporary probe and the thrown head is pinned through truncateClientMessage in plugin-security; a rest pin is outside the claim's surface."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsPM review: REVISE · PR #21371 at
cf6f85cf9· 2026-10-02T08:45ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJThe code holds; the judgment calls stand. The PR comes back for four sentences: three in the changeset that the code or the pins contradict, and one code comment this change makes false. No code change is asked.
Accepted as is
- The seam.
matchUnderDeclaredColumnsasks the write check's rule (findJsonColumnCheckRefusal, imported, no copy of the operator set) before the matcher reads the record.refuseWhatTheMatcherRefusesasks it as its second classification. An object with no declaration has an empty JSON-stored set and is judged as before. - The message order: diagnostic, then subject, then reason. Naming the field and operator follows the plugin-security:
security.explainreports a record visible under a row-levelusingthat compares two fields of different classes, whilefindrefuses the same read withINVALID_FILTER/ 400 #20431 precedent (explain-engine.ts:956-962): explain already publishes the predicate to the same caller asreadFilter/rowFilter. The diagnostic leads because it is the bounded part and carries the remedy. rls-check-stored-form.ts:jsonColumnCheckRefusalErroris exported,findJsonColumnCheckRefusalgains an optionalroot(defaultcheck), and nothing is re-exported fromsrc/index.ts.- Presence:
== null/!= nullcompile to$null(packages/formula/src/cel-to-filter.ts:668-673), which is outsideJSON_COLUMN_INCOMPATIBLE_OPERATORS. The changeset's "Unchanged" sentence holds for both, though only!= nullis pinned. - The pins, the three ablations and the gate reconciliation (65 derived, 65 run, 0 unrun).
Revise (the changeset,
.changeset/21319-explain-json-column-operator-refusal.md)- Line 9: the multi-valued list omits
radio.MULTI_CAPABLE_TYPES(packages/spec/src/data/field-value.zod.ts:335-337) isselect,radio,lookup,user,file,image. The sentence enumerates without an ellipsis, so it reads as complete. Nameradio, or end the list with "…" as the structured-JSON half does. - Line 18: "Now explain answers every one of these, for every operation, with the refusal enforcement gives:
INVALID_FILTER/ 400" is false for a by-id update or delete. There, enforcement answers 403 at the row-level gate whose pre-image re-read is the refused read (your pin header,explain-json-column-refusal.test.ts:13-14, and your own table in the PR body). Explain answers 400 for every operation, which is the read's refusal, not each operation's. Say that: explain answers the read's refusal,INVALID_FILTER/ 400, with no verdict, and a by-id update or delete is itself refused 403. Keep the cross-class comparison. - Line 18: "the error's
causeis the read's own refusal". Thecauseis built by the write check's constructor (jsonColumnCheckRefusalError); the find's error object is never captured. What the pins prove is that its code, status and message equal the find's. Say that: thecausecarries the read's refusal, with the find's code, status and message.
Revise (a code comment this change makes false)
4.rls-check-stored-form.ts:262-264:JsonColumnCheckRefusal.diagnosticstill says "SERVER-SIDE ONLY: the policy is an administrator's, so it goes to a log, never into an error message". Explain now puts it into an error message, on the #20431 grounds you cite inexplain-engine.ts. Scope the sentence: it is server-side only for the write check and the read, andsecurity/explaincarries it, because its report already publishes the predicate to the same caller. Point tojsonColumnRefusalForExplain.Then merge
origin/mainif it has moved, re-runpnpm --filter @objectstack/plugin-security testand the changeset gate, and report. The landing path is unchanged:Clause-②: noand no spec file, so no contract review is owed. The seat lands it when CI is green.- The seam.
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsPM review: ACCEPT · PR #21371 at
541ea3495· 2026-10-02T09:38ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJ· follows REVISE5948450241All four revisions are in commit
22255bb96, read against the diff:- The changeset's multi-valued list names
radio, which completesMULTI_CAPABLE_TYPES. - Explain answers the read's refusal (
INVALID_FILTER/ 400, no verdict) for every operation. A by-id update or delete is itself refused 403 at the row-level gate whose pre-image re-read is the refused read. - The
cause"carries the read's refusal, with the find's code, status and message". JsonColumnCheckRefusal.diagnosticis scoped: it stays server-side only for the write check and the read, andsecurity/explaincarries it on the plugin-security:security.explainreports a record visible under a row-levelusingthat compares two fields of different classes, whilefindrefuses the same read withINVALID_FILTER/ 400 #20431 grounds (jsonColumnRefusalForExplain).
The changeset was re-read sentence by sentence after the fix, and every sentence holds. The PR body carries neither corrected error (its table and pins section state 400 / 400 / 403 / 403, and a cause whose envelope is the read's).
Head and landing
origin/mainwas merged at541ea3495. CI on that head: 31 success, plus the expected skips (Console Pin Gate, Build Docs, Packed-tarball smoke). Check Changeset is green; mergeableclean.- The path surface is
plugin-securitysrc/plus one changeset: no governed path, under 5,000 lines.Clause-②: noand no spec file, so no contract review is owed. - The seat lands it now:
pr_ready+automerge_enable, through the queue.
Out-of-scope items (kept in the PR's Acceptance notes, not filed):
packages/spec/src/security/explain.zod.ts:13-14reads "IS enforcement, minus the throw". It was already imprecise since plugin-security:security.explainreports a record visible under a row-levelusingthat compares two fields of different classes, whilefindrefuses the same read withINVALID_FILTER/ 400 #20431's cross-class refusal; this adds a second class. It is the spec seat's docblock, and no reach was measured.- There is no
packages/restwire pin for this class. The door's bound was measured once, and the message head is pinned throughtruncateClientMessage.
- The changeset's multi-valued list names
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21319,
"status": "done",
"branch": "claude/issue-21319-explain-json-column-refusal",
"pr": "#21371",
"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": "Fix-up round for PM review REVISE (comment 5948450241). Worktree recreated from the remote branch at cf6f85c; plain commits and fast-forward pushes only, no force, rebase or amend. Commit 22255bb makes the four revisions, none changing code behaviour: (1) changeset line 9 namesradioamong the types flaggedmultiple: true(select/radio/lookup/user/file/image, matching MULTI_CAPABLE_TYPES; the inherently multi-valuedtags/multiselect/checkboxesmatch MULTI_OPTION_TYPES). (2) changeset line 18 now says explain answers every one of these with the read's refusal,INVALID_FILTER/ 400 and no verdict, for every operation, keeping the cross-class comparison, and that a by-id update or delete is itself refused 403 at the row-level gate whose pre-image re-read is the refused read. (3) the same line now says the error'scausecarries the read's refusal, with the find's code, status and message. (4) rls-check-stored-form.tsJsonColumnCheckRefusal.diagnosticdocstring: server-side only for the write check and the read;security/explaincarries it in its error message because its report already publishes the predicate to the same caller (jsonColumnRefusalForExplainin explain-engine.ts). PR body checked for the same two errors (read only, no PATCH): no sentence carries either. Line 4 says explain answers 'the read's refusal'; line 23 says the cause is the error the write check throws, withcause.messagebyte-equal to the find's message; line 31 separates the enforcement envelopes (400 / 400 / 403 / 403) from explain's 400. Then merged origin/main at 11905a4 (merge commit 541ea34). The merge brings #21242's rls-compiler.ts / security-plugin.ts and formula matches-filter.ts changes and a lockfile move, so the suite ran afterpnpm install --frozen-lockfileand a closure rebuild. origin/main moved 3 more commits during the gate runs (db0cf22: gate scripts and workflows, none of this PR's paths); not merged again. Format note: this order asks for the PR number on the final message's first line, then the JSON; the agent file asks for the JSON alone. The final message follows the order, and this sentence records the difference.",
"tests": "All builds/tests via scripts/pm/os-verify-lock.sh (slot issue-21319-dev), at 541ea34 (merge of origin/main 11905a4 onto 22255bb).pnpm --filter @objectstack/plugin-security test-> Test Files 160 passed (160), Tests 3499 passed | 33 skipped (3532), exit 0 (160 = the package's test-file count).pnpm --filter @objectstack/plugin-security typecheck-> exit 0 (check:test-typecheck OK, 0 files / 0 errors in debt). Changeset family: there is nopnpm check:changeset.dispatch-gates --commandsnames check-changeset-no-major --base origin/main and --self-test, check-empty-changeset --base origin/main and --self-test, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census and check-adr-0087-registration --base origin/main; all exit 0. The roster gate under .changeset (node scripts/check-changeset-fixed.mjs) also exit 0: 'fixed group is in sync with 69 public workspace packages'. Whole union: the same 65 commands derived; all 65 run on 541ea34, final exit 0 each (captured before any pipe). check:dual-build-cjs-loads and check:i18n first answered exit 3 PREREQUISITE NOT MET in the fresh worktree. After the i18n closure build (59 tasks) both measured green: i18n 9 packages in sync; dual-build 105 require entry points across 66 packages load. check:type-check-debt: 1 ledger entry re-measured, none above its number.--ran-> 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN'. AGENTS.md: no turbo-gate edit (git diff HEAD -- AGENTS.mdempty,git statusclean throughout). No ablation re-run: no code behaviour changed (a docstring and changeset text only). NOT MEASURED locally, CI's: the CI jobs and type-check lanes outside the derived list.",
"mcp_calls": "0 — no MCP GitHub tool was called; the review comment and the PR body were read withgh apiGETs",
"api_writes": "1 relay dispatch: this os-dev-report comment -> POST /repos//issues/21319/comments (via post-stamped.mjs, as objectstack-fleet[bot]). Plus 2 fast-forward git pushes of the branch (22255bb, 541ea34; not REST). No PR body PATCH, no label or assignee write.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Filing gate ①: a seam defect with a named location and a reproduction.⚠️ Classes and positions only.
finding, class b.reach:measured in-process on the registeredsecurityservice'sexplain(), which the explain route dispatches to. The HTTP route itself is NOT MEASURED.Reader who acts: triage, for the first grade.
plugin-security(explain-engine.ts) isdomain:services.Dedupe:
mcp__github__search_issuesfor "security explain visible JSON column operator find INVALID_FILTER record attribution multi-valued scalar comparison refused" returned 55 hits. None of them is this defect. The nearest is #20431 (closed): the same shape, explain answers visible while find refuses 400, for a cross-field comparison of two classes. Its [#20431] note inexplain-engine.tsstates the contract this card measures broken for another refusal class. Also #21299 (open), the aggregate positions' close-out for{ $field }comparisons, a different family.Seam
@objectstack/core'sJSON_COLUMN_INCOMPATIBLE_OPERATORS, the read's refusal on a declared JSON-stored column, versusplugin-security'sexplain-engine.tsrecord attribution (applyRecordAttribution/matchUnderDeclaredColumns). Read and write now both refuse these operators: the read through core's two faces, and the write since PR #21317 (#21254). Explain still evaluates them.Source
The #21254 dev's report (PR #21317,
out_of_scope_findings[0]), measured on driver-sql (better-sqlite3) at5a56607ab, as a member resolving a permission set whoseusingis the predicate.reach:usingexplain(read)findunder the same policyrecord.tags != 'x'(multi-valued)['y']visible: true, decided by RLS400 INVALID_FILTERrecord.meta == 'x'(json)'x'visible: true, decided by RLS400 INVALID_FILTER!(record.tags in ['x'])['y']visible: true, decided by RLS400 INVALID_FILTERContract it breaks: the [#20431] note in
explain-engine.ts("the explanation fails with the envelope the find fails with"), andskills/objectstack-data/rules/security.md(explain "answers from the enforcing code path").Direction (for triage; not a ruling)
Explain refuses with the find's envelope, through the same rule. PR #21317 exports
findJsonColumnCheckRefusal, which the attribution could call, so there is ⛔ no copy of the set. Pins: the table's three rows answerINVALID_FILTER/ 400 from explain on both drivers.contains/notContainsand a scalar column are the controls.Serial
After PR #21317 (#21254), whose exported helper this would reuse.
Generated by Claude Code ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ