You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
driver-mongodb: translateFilter passes a { $field } cross-field reference through as a literal document, so record.s != record.t matches every row (an RLS using read widens) #19949
Filed by the domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357) from the out-of-scope findings of the #19886 stage-2c dev (report 5806886759, class a). ⛔ Filed unassigned and unlabelled: routing and grading are triage's. ⛔ Not a claim.
The defect
compileCelToFilter lowers a field-to-field comparison such as record.s != record.t to { s: { $ne: { $field: 't' } } }: a declared cross-field reference (FieldReferenceSchema). packages/drivers/driver-mongodb/src/mongodb-filter.ts has no $field handling. The seat grepped packages/drivers/driver-mongodb/src on origin/mainaeaaa44292 and found no $field arm in the filter translation, so translateFilter emits the reference verbatim, as a literal sub-document.
Measured by the #19886 stage-2c dev at 10efa63fb1, through translateFilter plus mingo 7.2.4 as the proxy (live mongod NOT MEASURED), over the rows {s:'a', t:'a'} and {s:'a', t:'b'}:
$ne against the reference selects BOTH rows, including the one where s equals t;
$eq selects none.
Why it matters
A row-level using that compares two fields with != is a read restriction. On driver-mongodb it widens to every row. The SQL family compiles $field to a column-to-column comparison (#5222).
Seam: spec:FieldReferenceSchema{ $field } → runtime: driver-mongodb translateFilter (no lowering).
Dedupe words: mongodb field reference $field not lowered · rls field-to-field ne mongodb every row · translateFilter $field
Triage: lands in the MongoDB driver's filter translation ⇒ domain:engine, security, bug, priority:p2, pm:queue; rationale: a { $field } cross-field reference is passed through as a literal sub-document, so an RLS using such as record.s != record.t matches every row on MongoDB — a read restriction that silently widens; the driver is under the #5499 investment freeze (no MongoDB customer), and the maintainer ruled a narrow security exception: refuse the reference (fail closed), do not implement it.
Deferred (serial) behind PR #19947 — stays pm:queue, not dispatched this round
domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T12:59Z. Thread read through 5807932277 (triage, ruling B).
Why not now. Draft PR #19947 (#19886 stage 2c, domain:spec seat 5, cross-lane declared in its claim 5806396564) adds a shape-walk refusal to packages/drivers/driver-mongodb/src/mongodb-filter.ts (+43: $ne with an array comparand refused with INVALID_FILTER / 400, at any depth, $not / $nor included). This card's fix lands in the same walk of the same file. Same region ⇒ hard serial across rounds; ⛔ not folded into a PR another seat owns.
Scope stays ruling B, verbatim on this card (5807932277): 「维护者答:「19949 B」。」 Refuse { $field } under every operator, fail closed on the RLS read path; ⛔ no $expr lowering.
On main at 2c1011b01b, $field has 0 hits in mongodb-filter.ts (control: translateFilter 5 hits in the same file); the 1 hit in packages/drivers/driver-mongodb/src/ is prose in mongodb-aggregation.ts.
Wakes when: PR #19947 merges or closes. This seat's hot-file serial queue (seat post #6367) holds the entry.
a new test file of its own in packages/drivers/driver-mongodb (test side only);
.changeset/19949-*.md.
Stop on breach and explain in the report. ⛔ No $field lowering (no $expr column-to-column), which ruling B excludes. ⛔ No other driver-mongodb defect: those stay closed under #5499. ⛔ Not packages/spec, not compileCelToFilter, not the RLS compiler.
Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; security, so the default tier)
Clause-②: no (narrowing)
Thread-read: 5814595680
Serial constraints cleared: at 2026-09-27T05:44Z, the region predecessor PR #19947 (#19886 stage 2c, spec seat 5) has merged, and PR #19882 has merged. A census of the 9 open PRs finds none under packages/drivers/driver-mongodb/src. The seat runs batch:3 (the maintainer's ruling); in flight are PR #20174's review and this card.
os-dev-report
{
"issue": 19949,
"status": "done",
"branch": "claude/issue-19949-mongodb-field-ref-refused",
"pr": "#20182 (draft, opened by objectstack-fleet[bot] through relay run 36299771940; the body read back byte-identical, 10678 bytes; PR assignee set to the card assignee os-sales through relay run 36299812160, read back)",
"head_sha": "00612182e9",
"session": "session_01Bvd69VPa6puiNzzPUroDBx. This is the dispatching seat's session, because this run is its subagent (mode:subagent).",
"premise_still_valid": true,
"summary": "The premise holds at origin/main 3bd28e2. mongodb-filter.ts had no $field arm, and translateFilter emitted { s: { $ne: { $field: "t" } } } verbatim. Through mingo 7.2.4 that selected both card rows, and the full RLS read path (compileCelToFilter, RLSCompiler, SecurityPlugin, ObjectQL, MongoDBDriver) returned both rows from find, 2 from count, and the s == t row from findOne. One gate was added to classifyFilterKey, on the shape walk that PR #19947 extended, and placed before the other comparand gates. It refuses a { $field }-shaped object (any $field key, so a malformed reference too) in three positions: the whole constraint (the bare form, or a list holding one), any $-operator comparand (addDays included), and any member of a list comparand. The refusal is INVALID_FILTER / 400, and its message names "field-to-field comparison" and withholds the fields, the operator and the position. At head the RLS read is refused (find, findOne and count throw 400, and the collection is asked 0 times), and every literal comparand translates to the same document as before. There is no $expr lowering, per ruling B. The pins are in a new test file of their own, and a minor BREAKING changeset declares Clause-②: no (narrowing).",
"tests": "All at HEAD 0061218 unless stated. (1) pnpm --filter @objectstack/driver-mongodb test: "Test Files 28 passed | 5 skipped (33); Tests 630 passed | 147 skipped (777)". The 5 skipped files are the opt-in live-mongod suites (OS_TEST_MONGODB_MEMORY_SERVER_ENABLED); a live mongod is NOT MEASURED because the binary cannot be fetched here. (2) vitest run src/mongodb-field-reference-refusal.test.ts: 39 passed. (3) pnpm --filter @objectstack/driver-mongodb typecheck: exit 0, "check:test-typecheck: OK ... 0 file(s) / 0 error(s)"; tsc -p tsconfig.test.json --listFiles counts 33 test files, the new one included. (4) Ablation, run after the fix was committed (a53249d), through scripts/ablation-replace.mjs in WRAP mode under the verify lock. It deleted the gate line: anchor x1 to x0, blob 7b33578be43b to 343b8c5aa323 ("ok mutation landed"). The result was "Tests 33 failed | 6 passed (39)": every refusal case red, and the 6 controls green. Restore: blob after restore 7b33578be43b equals the blob at HEAD, and git diff HEAD is empty ("ok restored"). No dist step was needed, because the suite imports ./mongodb-filter.js from src by relative path, and the ObjectQL dist was not mutated. (5) The CJS entry dist/index.js, rebuilt at HEAD, loads and refuses { s: [ref] } with INVALID_FILTER/400. (6) Narrowed eslint on the 2 changed .ts files: eslint --no-inline-config --format json reports 2 files, 0 errors, 0 warnings. Population: --print-config gives both files a rule set. Invariance: no parserOptions.project or projectService on either file, and the config itself states it never enables type-aware linting, so the diff cannot move another file's verdict. The full pnpm lint is left to CI. (7) The H1/H2 readings came from a scratch script (scratchpad measure.mjs) over the built dists, at base 3bd28e2 and at head 0061218.",
"mcp_calls": "0",
"api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft), run 36299771940; (2) label-write --assign os-sales, which is POST /repos//issues/20182/assignees, run 36299812160; (3) this os-dev-report comment, which is POST /repos//issues/19949/comments. git push is not a REST write. Reads, not counted: GET issues/19949, issues/19949/comments, pulls?head=..., pulls/20182, issues/comments/5806886759.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. An observation, recorded in the PR Acceptance notes: translateFilter lowers a non-boolean $exists comparand to { $eq: null }, because the arm tests value === true. Measured on translateFilter at head: "yes", 1, null and "false" all give $eq: null. Not measured through a public door, and not compared with the other drivers, so it is not a class a/b/c card. driver-mongodb stays under the #5499 freeze."
],
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, taken with no paths at HEAD 0061218, derived 59 commands (changed set: 3 paths vs merge base 3bd28e2). All 59 were run with exit codes recorded. dispatch-gates --ran: "59 derived famil(ies) accounted for — 57 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". 57 exited 0. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET, it needs the whole workspace built; narrowed to this package's CJS entry loading, see tests (5)); pnpm check:type-check-debt (exit 3, PREREQUISITE NOT MET, 12 ledgered-closure packages are unbuilt; this package has no DEBT entry and its own test-layer typecheck is green). Also run, all exit 0: node scripts/check-issue-citations.mjs --base 3bd28e2 (5 citations resolve), and the 5 roster gates that dispatch-gates flagged as sitting under this diff's directories (check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:object-def-param-keys, check:tenant-chokepoint). New against the PM list from 3bd28e2: check-adr-0087-registration (base and self-test), check-empty-changeset (base and self-test), check:engine-double-contract, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher, and release-rehearsal-clone --self-test. All were run. H4: check-changeset-no-major --event (a simulated pull_request payload carrying this PR's exact body) reports "LEVEL AXIS: this PR declares clause-② no (narrowing), and no package ... is graded patch", so minor is accepted. check-adr-0087-registration accepts "not-required (no-migration-prescription)". Neither should differ: there is no conversion target on this driver, so a registry entry would carry no prescription, and adding one would also edit packages/spec, which the claim excludes. CI convergence is not awaited.",
"line_budget": "n/a: no skills/** path in the diff.",
"deviations": [
"This session was given a copy of os-dev.md that is older than .claude/agents/os-dev.md at 3bd28e2. Where the two differ, I followed the repo copy: the PR assignee is set through label-write --issue PR --assign LOGIN, and findings carry a reach: field. AGENTS.md was first read from the shared checkout (at 2c1011b, which is stale). The worktree copy was then diffed against it, and its current PR-assignee clause was the one followed.",
"The gate covers more than the literal "operator value" wording. It refuses a reference as the whole constraint (the bare form and a list holding a reference, i.e. implicit equality) and as a list member ($in/$nin/$between/arrays). H1 named those positions, and the ruling says it must no longer be sent down as an ordinary value.",
"The gate runs first among the comparand gates. Four shapes that were already refused now get the cross-field wording, with the same INVALID_FILTER/400 envelope: bare { s: ref } (before: "Unsupported filter operator $field", naming the field), $icontains: ref, $null: ref, and $ne: [ref]. At base, this package's tests had 0 hits for $field, so no pin changed. The $ne array refusal from PR #19947 is unchanged for literal arrays.",
"The engine-level pin in the suite uses an ObjectQL middleware that AND-composes the compiled policy, standing in for SecurityPlugin, because plugin-security is not a dependency of driver-mongodb. The real SecurityPlugin path was measured outside the suite, in a scratch script."
],
"files_changed": [
"packages/drivers/driver-mongodb/src/mongodb-filter.ts (+87: gate in classifyFilterKey; isFieldReferenceShape, carriesFieldReference, holdsFieldReference, fieldReferenceUnsupportedError)",
"packages/drivers/driver-mongodb/src/mongodb-field-reference-refusal.test.ts (+303, new)",
".changeset/19949-mongodb-field-reference-refused.md (+36, new; driver-mongodb minor, BREAKING, Clause-②: no (narrowing), adr-0087 not-required (no-migration-prescription))"
],
"rows": {
"proxy": "mingo 7.2.4 (driver-memory's dependency) over rows r1 {s:a,t:a} and r2 {s:a,t:b}, ref = { $field: "t" }. A live mongod is NOT MEASURED.",
"base": "3bd28e2b2e",
"head": "00612182e9",
"cells": [
"s $ne ref: base selects r1,r2 | head 400",
"s $eq ref: base none | head 400",
"$gt / $gte / $lt / $lte ref: base none | head 400",
"$in [ref]: base none | head 400",
"$nin [ref]: base selects r1,r2 | head 400",
"$between [ref,z] / [a,ref]: base none | head 400",
"$and [s $ne ref]: base r1,r2 | head 400",
"$or [s $ne ref, s=zz]: base r1,r2 | head 400",
"$or [{}, s $ne ref]: base translated {} (r1,r2) | head 400 (the gate is on the walk)",
"$not {s $eq ref} (lowered to $nor): base r1,r2 | head 400",
"$not {s $ne ref}: base none | head 400",
"s $ne {$field:t, addDays:1}: base r1,r2 | head 400",
"s $eq {$field:t, addDays:{$field:n}}: base none | head 400",
"s $ne {$field:42} (malformed): base r1,r2 | head 400",
"$notContains ref: base r1,r2 | head 400",
"$contains / $startsWith / $endsWith ref: base none | head 400",
"$exists ref: base lowered to $eq null, none | head 400",
"bare {s: ref}: base 400 (unknown operator $field, names the field) | head 400 (cross-field wording)",
"$icontains ref / $null ref / $ne [ref]: base 400 (their own gates) | head 400 (cross-field wording)",
"$nor at node level: base 400 | head 400, unchanged",
"CONTROL s $ne a: none | none, same document",
"CONTROL s $eq a: r1,r2 | r1,r2, same document",
"CONTROL t $ne b: r1 | r1",
"CONTROL t $eq b: r2 | r2",
"CONTROL t $in [b] / $nin [b] / $between [a,a] / $not {t $eq b}: r2 / r1 / r1 / r1 | identical"
]
},
"rls_path": "compileCelToFilter("record.s != record.t") and ("s != t") both return { s: { $ne: { $field: "t" } } }. RLSCompiler.compileFilter with declared {id,s,t} keeps it. Full path: SecurityPlugin with a rowLevelSecurity policy (operation all), ObjectQL, and the real MongoDBDriver over a stub Db whose collection selects with mingo. The caller is a MEMBER holding the set. Base, using "s != t": find returns [r1,r2], count 2, findOne({id:r1}) returns r1, and the server received {"s":{"$ne":{"$field":"t"}}}. The read restriction was lost. Head: find, findOne and count each throw INVALID_FILTER/400, and the qa_pair collection is asked 0 times, so the read is refused: not allowed through, not swallowed, never unfiltered. Using "s == t" (base: 0 rows) is also refused at head. Controls, identical at base and head: using t == "a" gives [r1] / count 1, using t != "a" gives [r2], using s == "a" gives [r1,r2]. The engine's find path logs at warn and rethrows. Every driver door reads where through translateFilter: find, findOne, count, updateMany, deleteMany, aggregate (buildAggregationPipeline) and explain. The suite pins six doors plus the engine rethrow.",
"collateral": "Every literal comparand translates to the same document (the controls above and in the suite). The $ne array refusal from PR #19947 is unchanged for literal arrays: mongodb-ne-array-comparand-refusal.test.ts is green, and the new file carries a control. The #19757 equality-slot array pin still holds: { tags: ["a"] } and $eq ["a"] pass through. mongodb-equality-array-comparand-refusal.test.ts is green, and the gate fires only on a reference member. The whole driver-mongodb suite is green (630 passed, 147 skipped in the opt-in live-mongod files). check:driver-conformance, check:filter-alias-parity and check:where-matcher exit 0. No workspace consumer's tests use $field with this driver (grepped in runtime, service-datasource and cli). Exports and types are unchanged. Wording moved for 4 shapes that were already refused (see deviations).",
"cleanup": "After this post, the worktree node_modules is removed and git worktree remove runs without --force; the outcome is reported in the final hand-off. No dev server, background process or monitor was started."
}
domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T06:45Z. Reviewed on GitHub against references/review-checklist.md, not from the dev's os-dev-report.
Shape: the first line is Fixes #19949. The body and changeset declare Clause-②: no (narrowing): BREAKING, @objectstack/driver-mongodbminor, ADR-0087 disposition not-required (no-migration-prescription).
Inside the maintainer's ruling B (「19949 B」): refuse and fail closed. No $expr lowering, and packages/spec, compileCelToFilter and the RLS compiler are untouched.
Contract review of record: PASS at 00612182e9 (5853495299).
34 of 34 { $field } shapes are refused INVALID_FILTER / 400. That covers every operator, list members, the bare form, addDays and malformed references.
The 21 literal controls translate to byte-identical documents.
On the real RLS chain (compileCelToFilter → RLSCompiler → SecurityPlugin → ObjectQL → MongoDBDriver), a using of s != t returned every row at base. At head, find, findOne, count, aggregate, bulk update and bulk delete all refuse with 0 collection calls.
The message withholds field names.
The ablation reproduces the dev's count: 33 of 39 red.
A live mongod was NOT MEASURED; mingo 7.2.4 is the proxy.
CI at this head: 34 runs, 31 success, 3 skipped, 0 failed. All seven required contexts are success. git merge-tree against current main is clean.
Acceptance notes (not cards):
PR-body nit: "Three shapes were already refused" should say four. The rows table lists all four.
Changeset imprecision: "Before this change it returned every row" is true for !=-style policies only, while == returned none. The changeset's own line 26 states both directions, so it is not a contract falsity, and a new head is not worth it.
Its patch-id equals the reviewed diff 3bd28e2b2e..00612182e9 (PASS 5853495299): 3 files, +426/−0.
The changeset 19949-mongodb-field-reference-refused.md is present at that commit and absent at its parent.
This card: closed completed via Fixes #19949, per the maintainer's ruling B (「19949 B」). pm:dispatched is removed in the same act.
driver-mongodb's translateFilter refuses a { $field } reference in every position with INVALID_FILTER / 400, where before it sent the reference to MongoDB as a literal document.
A row-level using such as record.s != record.t now refuses the read, with 0 collection calls, instead of returning every row.
There is no $expr lowering, as ruled.
A live mongod was NOT MEASURED; mingo 7.2.4 is the proxy.
Activity
objectstack-fleet commentedon Sep 24, 2026
分诊首次定级:
priority:p2·security·bug·domain:engine·pm:queue—— 维护者裁定 B:#5499 冻结下的安全例外,只做最小的"拒绝"修法Path:
packages/drivers/driver-mongodb/src/mongodb-filter.ts→translateFilter/translateFieldOperatorsTriage: lands in the MongoDB driver's filter translation ⇒
domain:engine,security,bug,priority:p2,pm:queue; rationale: a{ $field }cross-field reference is passed through as a literal sub-document, so an RLSusingsuch asrecord.s != record.tmatches every row on MongoDB — a read restriction that silently widens; the driver is under the #5499 investment freeze (no MongoDB customer), and the maintainer ruled a narrow security exception: refuse the reference (fail closed), do not implement it.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T04:53Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论)。前提复核(
origin/main2dc5889013)packages/drivers/driver-mongodb/src/mongodb-filter.ts里$field出现 0 次。对照组正常:同一文件里translateFilter在:568;driver-sql 有 [spec] SqlDriver 将$field编译为列对列比较(cross-field comparison push-down) #5222 的字段对字段一致性用例,$field在cross-field-conformance-cases.ts里能搜到。compileCelToFilter把record.s != record.t降成{ s: { $ne: { $field: 't' } } },MongoDB 驱动原样下发,$ne选中所有行,$eq一行也不选(dev 用 mingo 7.2.4 代测;真实mongod未测)。维护者裁定(本席在对话里把两条规则的冲突报给了维护者)
要做的(⛔ 只做这些)
translateFilter遇到任何运算符的值是{ $field: … }时直接拒绝:抛一个清楚的错误,点名"MongoDB 驱动不支持字段对字段比较($field)",⛔ 不再当作普通值下发。{s:'a', t:'a'}、{s:'a', t:'b'},$ne和$eq配{ $field: 't' }都必须被拒绝;对照:普通字面值的$ne/$eq行为不变。⛔ 不做的
$field的真正下降(例如用$expr做列对列比较)。那是冻结明确排除的投入,等 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 解冻(有客户要 MongoDB)再做。为什么是 p2
读权限被放宽是安全缺陷,但只影响用 MongoDB 驱动的部署,而目前没有这样的客户(#5499)。修法很小,改动面只有一处。
Generated by Claude Code
objectstack-fleet commentedon Sep 24, 2026
Deferred (serial) behind PR #19947 — stays
pm:queue, not dispatched this rounddomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T12:59Z. Thread read through 5807932277 (triage, ruling B).Why not now. Draft PR #19947 (#19886 stage 2c,
domain:specseat 5, cross-lane declared in its claim 5806396564) adds a shape-walk refusal topackages/drivers/driver-mongodb/src/mongodb-filter.ts(+43:$newith an array comparand refused withINVALID_FILTER/ 400, at any depth,$not/$norincluded). This card's fix lands in the same walk of the same file. Same region ⇒ hard serial across rounds; ⛔ not folded into a PR another seat owns.Known pits, recorded for the taker:
main: the same walk, the sameINVALID_FILTER/ 400 envelope and its withheld-diagnostic posture, so the driver gains one refusal vocabulary, not two.packages/drivers/driver-mongodb/src/mongodb-equality-array-comparand-refusal.test.ts, which pinstranslateFilterpassing an equality-slot array through. That file is fenced; this card's pins go in a file of their own.{ $field }under every operator, fail closed on the RLS read path; ⛔ no$exprlowering.mainat2c1011b01b,$fieldhas 0 hits inmongodb-filter.ts(control:translateFilter5 hits in the same file); the 1 hit inpackages/drivers/driver-mongodb/src/is prose inmongodb-aggregation.ts.Wakes when: PR #19947 merges or closes. This seat's hot-file serial queue (seat post #6367) holds the entry.
Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 22
Session:
session_01Bvd69VPa6puiNzzPUroDBxAccount:
os-sales(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-19949-mongodb-field-ref-refusedWorktree:
objectstack-issue-19949Domain:
domain:engineSeat:
domain:engine#1File surface:
packages/drivers/driver-mongodb/src/mongodb-filter.ts:translateFilter/translateFieldOperatorsonly. A{ $field: … }comparand under ANY operator is refused with the sameINVALID_FILTER/ 400 envelope, and the same withheld-diagnostic posture, as PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947's shape-walk refusal in the same walk. This is the maintainer's ruling B (triage 5807932277): 「19949 B」;packages/drivers/driver-mongodb(test side only);.changeset/19949-*.md.Stop on breach and explain in the report. ⛔ No
$fieldlowering (no$exprcolumn-to-column), which ruling B excludes. ⛔ No other driver-mongodb defect: those stay closed under #5499. ⛔ Notpackages/spec, notcompileCelToFilter, not the RLS compiler.Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; security, so the default tier)Clause-②: no (narrowing)
Thread-read: 5814595680
Serial constraints cleared: at 2026-09-27T05:44Z, the region predecessor PR #19947 (#19886 stage 2c, spec seat 5) has merged, and PR #19882 has merged. A census of the 9 open PRs finds none under
packages/drivers/driver-mongodb/src. The seat runsbatch:3(the maintainer's ruling); in flight are PR #20174's review and this card.objectstack-fleet commentedon Sep 27, 2026
os-dev-report$field key, so a malformed reference too) in three positions: the whole constraint (the bare form, or a list holding one), any $ -operator comparand (addDays included), and any member of a list comparand. The refusal is INVALID_FILTER / 400, and its message names "field-to-field comparison" and withholds the fields, the operator and the position. At head the RLS read is refused (find, findOne and count throw 400, and the collection is asked 0 times), and every literal comparand translates to the same document as before. There is no $expr lowering, per ruling B. The pins are in a new test file of their own, and a minor BREAKING changeset declares Clause-②: no (narrowing).",
{
"issue": 19949,
"status": "done",
"branch": "claude/issue-19949-mongodb-field-ref-refused",
"pr": "#20182 (draft, opened by objectstack-fleet[bot] through relay run 36299771940; the body read back byte-identical, 10678 bytes; PR assignee set to the card assignee os-sales through relay run 36299812160, read back)",
"head_sha": "00612182e9",
"session": "session_01Bvd69VPa6puiNzzPUroDBx. This is the dispatching seat's session, because this run is its subagent (mode:subagent).",
"premise_still_valid": true,
"summary": "The premise holds at origin/main 3bd28e2. mongodb-filter.ts had no $field arm, and translateFilter emitted { s: { $ne: { $field: "t" } } } verbatim. Through mingo 7.2.4 that selected both card rows, and the full RLS read path (compileCelToFilter, RLSCompiler, SecurityPlugin, ObjectQL, MongoDBDriver) returned both rows from find, 2 from count, and the s == t row from findOne. One gate was added to classifyFilterKey, on the shape walk that PR #19947 extended, and placed before the other comparand gates. It refuses a { $field }-shaped object (any
"tests": "All at HEAD 0061218 unless stated. (1) pnpm --filter @objectstack/driver-mongodb test: "Test Files 28 passed | 5 skipped (33); Tests 630 passed | 147 skipped (777)". The 5 skipped files are the opt-in live-mongod suites (OS_TEST_MONGODB_MEMORY_SERVER_ENABLED); a live mongod is NOT MEASURED because the binary cannot be fetched here. (2) vitest run src/mongodb-field-reference-refusal.test.ts: 39 passed. (3) pnpm --filter @objectstack/driver-mongodb typecheck: exit 0, "check:test-typecheck: OK ... 0 file(s) / 0 error(s)"; tsc -p tsconfig.test.json --listFiles counts 33 test files, the new one included. (4) Ablation, run after the fix was committed (a53249d), through scripts/ablation-replace.mjs in WRAP mode under the verify lock. It deleted the gate line: anchor x1 to x0, blob 7b33578be43b to 343b8c5aa323 ("ok mutation landed"). The result was "Tests 33 failed | 6 passed (39)": every refusal case red, and the 6 controls green. Restore: blob after restore 7b33578be43b equals the blob at HEAD, and git diff HEAD is empty ("ok restored"). No dist step was needed, because the suite imports ./mongodb-filter.js from src by relative path, and the ObjectQL dist was not mutated. (5) The CJS entry dist/index.js, rebuilt at HEAD, loads and refuses { s: [ref] } with INVALID_FILTER/400. (6) Narrowed eslint on the 2 changed .ts files: eslint --no-inline-config --format json reports 2 files, 0 errors, 0 warnings. Population: --print-config gives both files a rule set. Invariance: no parserOptions.project or projectService on either file, and the config itself states it never enables type-aware linting, so the diff cannot move another file's verdict. The full pnpm lint is left to CI. (7) The H1/H2 readings came from a scratch script (scratchpad measure.mjs) over the built dists, at base 3bd28e2 and at head 0061218.",
"mcp_calls": "0",
"api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft), run 36299771940; (2) label-write --assign os-sales, which is POST /repos//issues/20182/assignees, run 36299812160; (3) this os-dev-report comment, which is POST /repos//issues/19949/comments. git push is not a REST write. Reads, not counted: GET issues/19949, issues/19949/comments, pulls?head=..., pulls/20182, issues/comments/5806886759.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. An observation, recorded in the PR Acceptance notes: translateFilter lowers a non-boolean $exists comparand to { $eq: null }, because the arm tests value === true. Measured on translateFilter at head: "yes", 1, null and "false" all give $eq: null. Not measured through a public door, and not compared with the other drivers, so it is not a class a/b/c card. driver-mongodb stays under the #5499 freeze."
],
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack, taken with no paths at HEAD 0061218, derived 59 commands (changed set: 3 paths vs merge base 3bd28e2). All 59 were run with exit codes recorded. dispatch-gates --ran: "59 derived famil(ies) accounted for — 57 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)". 57 exited 0. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET, it needs the whole workspace built; narrowed to this package's CJS entry loading, see tests (5)); pnpm check:type-check-debt (exit 3, PREREQUISITE NOT MET, 12 ledgered-closure packages are unbuilt; this package has no DEBT entry and its own test-layer typecheck is green). Also run, all exit 0: node scripts/check-issue-citations.mjs --base 3bd28e2 (5 citations resolve), and the 5 roster gates that dispatch-gates flagged as sitting under this diff's directories (check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:object-def-param-keys, check:tenant-chokepoint). New against the PM list from 3bd28e2: check-adr-0087-registration (base and self-test), check-empty-changeset (base and self-test), check:engine-double-contract, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher, and release-rehearsal-clone --self-test. All were run. H4: check-changeset-no-major --event (a simulated pull_request payload carrying this PR's exact body) reports "LEVEL AXIS: this PR declares clause-② no (narrowing), and no package ... is graded patch", so minor is accepted. check-adr-0087-registration accepts "not-required (no-migration-prescription)". Neither should differ: there is no conversion target on this driver, so a registry entry would carry no prescription, and adding one would also edit packages/spec, which the claim excludes. CI convergence is not awaited.",
"line_budget": "n/a: no skills/** path in the diff.",
"deviations": [
"This session was given a copy of os-dev.md that is older than .claude/agents/os-dev.md at 3bd28e2. Where the two differ, I followed the repo copy: the PR assignee is set through label-write --issue PR --assign LOGIN, and findings carry a reach: field. AGENTS.md was first read from the shared checkout (at 2c1011b, which is stale). The worktree copy was then diffed against it, and its current PR-assignee clause was the one followed.",
"The gate covers more than the literal "operator value" wording. It refuses a reference as the whole constraint (the bare form and a list holding a reference, i.e. implicit equality) and as a list member ($in/$nin/$between/arrays). H1 named those positions, and the ruling says it must no longer be sent down as an ordinary value.",
"The gate runs first among the comparand gates. Four shapes that were already refused now get the cross-field wording, with the same INVALID_FILTER/400 envelope: bare { s: ref } (before: "Unsupported filter operator $field", naming the field), $icontains: ref, $null: ref, and $ne: [ref]. At base, this package's tests had 0 hits for $field, so no pin changed. The $ne array refusal from PR #19947 is unchanged for literal arrays.",
"The engine-level pin in the suite uses an ObjectQL middleware that AND-composes the compiled policy, standing in for SecurityPlugin, because plugin-security is not a dependency of driver-mongodb. The real SecurityPlugin path was measured outside the suite, in a scratch script."
],
"files_changed": [
"packages/drivers/driver-mongodb/src/mongodb-filter.ts (+87: gate in classifyFilterKey; isFieldReferenceShape, carriesFieldReference, holdsFieldReference, fieldReferenceUnsupportedError)",
"packages/drivers/driver-mongodb/src/mongodb-field-reference-refusal.test.ts (+303, new)",
".changeset/19949-mongodb-field-reference-refused.md (+36, new; driver-mongodb minor, BREAKING, Clause-②: no (narrowing), adr-0087 not-required (no-migration-prescription))"
],
"rows": {
"proxy": "mingo 7.2.4 (driver-memory's dependency) over rows r1 {s:a,t:a} and r2 {s:a,t:b}, ref = { $field: "t" }. A live mongod is NOT MEASURED.",
"base": "3bd28e2b2e",
"head": "00612182e9",
"cells": [
"s $ne ref: base selects r1,r2 | head 400",
"s $eq ref: base none | head 400",
"$gt / $gte / $lt / $lte ref: base none | head 400",
"$in [ref]: base none | head 400",
"$nin [ref]: base selects r1,r2 | head 400",
"$between [ref,z] / [a,ref]: base none | head 400",
"$and [s $ne ref]: base r1,r2 | head 400",
"$or [s $ne ref, s=zz]: base r1,r2 | head 400",
"$or [{}, s $ne ref]: base translated {} (r1,r2) | head 400 (the gate is on the walk)",
"$not {s $eq ref} (lowered to $nor): base r1,r2 | head 400",
"$not {s $ne ref}: base none | head 400",
"s $ne {$field:t, addDays:1}: base r1,r2 | head 400",
"s $eq {$field:t, addDays:{$field:n}}: base none | head 400",
"s $ne {$field:42} (malformed): base r1,r2 | head 400",
"$notContains ref: base r1,r2 | head 400",
"$contains / $startsWith / $endsWith ref: base none | head 400",
"$exists ref: base lowered to $eq null, none | head 400",
"bare {s: ref}: base 400 (unknown operator $field, names the field) | head 400 (cross-field wording)",
"$icontains ref / $null ref / $ne [ref]: base 400 (their own gates) | head 400 (cross-field wording)",
"$nor at node level: base 400 | head 400, unchanged",
"CONTROL s $ne a: none | none, same document",
"CONTROL s $eq a: r1,r2 | r1,r2, same document",
"CONTROL t $ne b: r1 | r1",
"CONTROL t $eq b: r2 | r2",
"CONTROL t $in [b] / $nin [b] / $between [a,a] / $not {t $eq b}: r2 / r1 / r1 / r1 | identical"
]
},
"rls_path": "compileCelToFilter("record.s != record.t") and ("s != t") both return { s: { $ne: { $field: "t" } } }. RLSCompiler.compileFilter with declared {id,s,t} keeps it. Full path: SecurityPlugin with a rowLevelSecurity policy (operation all), ObjectQL, and the real MongoDBDriver over a stub Db whose collection selects with mingo. The caller is a MEMBER holding the set. Base, using "s != t": find returns [r1,r2], count 2, findOne({id:r1}) returns r1, and the server received {"s":{"$ne":{"$field":"t"}}}. The read restriction was lost. Head: find, findOne and count each throw INVALID_FILTER/400, and the qa_pair collection is asked 0 times, so the read is refused: not allowed through, not swallowed, never unfiltered. Using "s == t" (base: 0 rows) is also refused at head. Controls, identical at base and head: using t == "a" gives [r1] / count 1, using t != "a" gives [r2], using s == "a" gives [r1,r2]. The engine's find path logs at warn and rethrows. Every driver door reads where through translateFilter: find, findOne, count, updateMany, deleteMany, aggregate (buildAggregationPipeline) and explain. The suite pins six doors plus the engine rethrow.",
"collateral": "Every literal comparand translates to the same document (the controls above and in the suite). The $ne array refusal from PR #19947 is unchanged for literal arrays: mongodb-ne-array-comparand-refusal.test.ts is green, and the new file carries a control. The #19757 equality-slot array pin still holds: { tags: ["a"] } and $eq ["a"] pass through. mongodb-equality-array-comparand-refusal.test.ts is green, and the gate fires only on a reference member. The whole driver-mongodb suite is green (630 passed, 147 skipped in the opt-in live-mongod files). check:driver-conformance, check:filter-alias-parity and check:where-matcher exit 0. No workspace consumer's tests use $field with this driver (grepped in runtime, service-datasource and cli). Exports and types are unchanged. Wording moved for 4 shapes that were already refused (see deviations).",
"cleanup": "After this post, the worktree node_modules is removed and git worktree remove runs without --force; the outcome is reported in the final hand-off. No dev server, background process or monitor was started."
}
objectstack-fleet commentedon Sep 27, 2026
ACCEPT: PR #20182 at
00612182e9(#19949)domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T06:45Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev'sos-dev-report.Fixes #19949. The body and changeset declareClause-②: no (narrowing): BREAKING,@objectstack/driver-mongodbminor, ADR-0087 dispositionnot-required (no-migration-prescription).mongodb-filter.ts: one gate on thetranslateFiltershape walk from PR fix(formula, driver-mongodb): refuse == / != against a list literal at the CEL lowering and $ne arrays at the mongodb face #19947.$exprlowering, andpackages/spec,compileCelToFilterand the RLS compiler are untouched.00612182e9(5853495299).{ $field }shapes are refusedINVALID_FILTER/ 400. That covers every operator, list members, the bare form,addDaysand malformed references.compileCelToFilter→RLSCompiler→SecurityPlugin→ObjectQL→MongoDBDriver), ausingofs != treturned every row at base. At head,find,findOne,count,aggregate, bulk update and bulk delete all refuse with 0 collection calls.mongodwas NOT MEASURED; mingo 7.2.4 is the proxy.success, 3skipped, 0 failed. All seven required contexts aresuccess.git merge-treeagainst currentmainis clean.!=-style policies only, while==returned none. The changeset's own line 26 states both directions, so it is not a contract falsity, and a new head is not worth it.carrier:none today: the generatedcontent/docs/references/data/filter.mdx$eq/$nerows, and [finding] a{ $field }reference as a$betweenendpoint is refused by the schema door and accepted by the runtime door — the #19071 shape, one endpoint spelling over, already ruled on the schema side #19377's two-bound prescription, read as if a{ $field }comparand compiles on MongoDB. It is now an explicit 400. Correcting them is apackages/specdescribe()edit.Landing:
readyplus auto-merge through the queue now.objectstack-fleet commentedon Sep 27, 2026
Landed: PR #20182, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T07:05Z.98f722a742. It has one parent (9e7824a445) and is an ancestor oforigin/mainafter a fresh fetch.3bd28e2b2e..00612182e9(PASS 5853495299): 3 files, +426/−0.19949-mongodb-field-reference-refused.mdis present at that commit and absent at its parent.completedviaFixes #19949, per the maintainer's ruling B (「19949 B」).pm:dispatchedis removed in the same act.driver-mongodb'stranslateFilterrefuses a{ $field }reference in every position withINVALID_FILTER/ 400, where before it sent the reference to MongoDB as a literal document.usingsuch asrecord.s != record.tnow refuses the read, with 0 collection calls, instead of returning every row.$exprlowering, as ruled.mongodwas NOT MEASURED; mingo 7.2.4 is the proxy.carrier:none): the generatedcontent/docs/references/data/filter.mdx$eq/$nerows, and [finding] a{ $field }reference as a$betweenendpoint is refused by the schema door and accepted by the runtime door — the #19071 shape, one endpoint spelling over, already ruled on the schema side #19377's two-bound prescription, read as if a{ $field }comparand compiles on MongoDB. On this driver it is now an explicit 400.fix(formula, driver-mongodb): refuse == / != against a list literal a…
fix(driver-mongodb)!: refuse a { $field } cross-field reference inste…
fix(security)!: the RLS write check refuses a field-to-field comparis…