Repository navigation
engine + driver-sql: seven filter-refusal doors ignore the #8220 provenance mark, so a refusal inside a policy read-scope subtree relays the policy field (and some comparands) in its 400: the #7929 redaction is not applied #20020
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions分诊首次定级:
priority:p2·security·bug·domain:engine·pm:queue—— 七类过滤拒绝在注入的读范围(RLS / 共享 / 租户策略)里出错时,400 报错会把策略的字段名(有的还带比较值)带给调用方,没有按 #7929 裁决隐藏Path:
packages/objectql/src/*(声明类型门、时间比较值门、过滤占位符解析FILTER_TOKEN_UNKNOWN)·packages/drivers/driver-sql/src/sql-driver.ts(缺列、未知 / 退役运算符、组合子非数组、非布尔$null/$exists)⇄ 契约packages/spec/src/data/filter-subtree-provenance.tsTriage: lands in
packages/objectql+driver-sql⇒domain:engine,security,bug,priority:p2,pm:queue; rationale: the #8220 provenance contract (implementing A of the 2026-08-12 #7929 ruling) says a refusal raised inside a'policy'-marked subtree keeps the redaction — identity and capability on the wire, operands in the server log — and "Unmarked or ambiguous ⇒ withheld"; onlydriver-sql's bind and cross-field refusals read the mark, and seven other refusal classes relay the policy's field (and some comparands) in their 400 (measured on both SQLite drivers through the real analytics routes); a disclosure of policy content rather than a data breach, reachable through schema drift or admin-authored comparisons — the same class and grade as #19995, which waits on this card.分诊席 #6015,2026-09-24T20:21Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectstack
origin/main44639665ee上核对。本席核对
- 契约:
filter-subtree-provenance.ts里有「Unmarked or ambiguous ⇒ withheld. The mark is permission to reveal …」。这是已有裁决,不是新决定。 - 谁在读标记:在
packages/objectql/src和driver-sql/src的非测试代码里,resolveFilterSubtreeProvenance只出现在sql-driver.ts,objectql 引擎里一处都没有。⇒ 卡面表里三个引擎门(声明类型、时间比较值、占位符)不读标记,与卡面一致。driver-sql 那四类具体走不走标记,本席没有逐个打开,以卡面为准。 - 卡面的测量(两个 SQLite 驱动经真实的
/analytics/query和/analytics/dataset/query路由,七类都在 400 里带出策略字段)本席没有重跑。
定级说明
- p2 +
security:泄露的是策略内容(RLS 用的字段名和部分比较值),不是数据行。与 security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 同类同级,security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 也在等本卡。 - 可达性:策略引用的列被改名或删除(schema 漂移),或者管理员写的日期 / 数字比较。
执行要点
- 先测普通 CRUD 面(
plugin-security的合并)是否也同样泄露。卡面只测了分析面。 - 每个门都要读违规节点的标记(
resolveFilterSubtreeProvenance):- 只有标记为
'author'的子树才给出完整诊断; 'policy'和未标记的,保持INVALID_FILTER/ 400,操作数只写服务器日志。- 形状照
driver-sql跨字段拒绝已有的 finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 B 形状。
- 只有标记为
⚠️ 「未标记即隐藏」也会作用到调用方自己写的过滤:在不盖标记的宿主上,调用方自己的过滤也会被隐藏。先测清楚哪些宿主盖标记,再动手。- 分析面上的状态码:本卡给的是被隐藏的 400([A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220);分析读范围编译器自己的拒绝是被隐藏的 500(analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367)。两个裁决都隐藏策略内容,本卡不决定两者的状态码是否统一。
- 钉住:七类拒绝各一个测试,覆盖 policy 子树(隐藏)和 author 子树(完整诊断)两种情况。
Generated by Claude Code
- 契约:
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim: PM loop round 22
Session:session_01Bvd69VPa6puiNzzPUroDBx
Branch:claude/issue-20020-refusal-doors-provenance
Worktree:objectstack-issue-20020
Domain:domain:engine
Seat:domain:engine#1
File surface:- the seven refusal doors the card names, and only their refusal branches:
packages/drivers/driver-sql/src/sql-driver.ts(missing column, operator vocabulary, combinator operand,$null/$existsoperand);packages/objectql/src/text-operator-declared-type-door.ts;packages/objectql/src/temporal-comparand-door.ts;- the
FILTER_TOKEN_UNKNOWNtoken resolver wherever it lives;
packages/drivers/driver-turso/src/remote-transport.tsrefusal branches ONLY if measurement shows the remote face has the same mark-blind doors (declare perreferences/compile-surfaces.md);- tests in those packages;
.changeset/20020-*.md.
Stop on breach and explain in the report. ⛔ Not
packages/spec(the #8220 contract is the fixed point), and notplugin-security/service-analytics(their merges stamp the mark; the card does not move them). ⛔ Notsql-driver.ts'stextMatchPredicate/sqliteLengthAwareTextMatch(#19999, PR #20019), and notremote-transport.ts'spushLike(same). ⛔ Notengine.ts'scascadeDeleteRelations/resolvePredicateRelated(#20006, PR #20021).
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5821639338
Serial constraints cleared: at 2026-09-24T20:49Z, a census of the 16 open PRs finds none touching the door files, and two touching the same files in other regions: PR #20019 (#19999,sql-driver.tstextMatchPredicate~3119–3310 andremote-transport.tspushLike) and PR #20021 (#20006,engine.tsresolvePredicateRelated/cascadeDeleteRelations). The regions are disjoint from these doors. Whichever lands second mergesmainfirst. Nopm:dispatchedclaim declares the door files.
Generated by Claude Code
- the seven refusal doors the card names, and only their refusal branches:
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20020,
"status": "needs_decision",
"branch": "claude/issue-20020-refusal-doors-provenance",
"pr": "#20037",
"session": "session_01Bvd69VPa6puiNzzPUroDBx",
"premise_still_valid": true,
"summary": "The premise held on base 26550c6. On the analytics face (the real POST /api/v1/analytics/query route), all seven classes put the policy's field in the 400, and four of them put the comparand in too. On the CRUD face (ql.find under a plugin-security-shaped merge), only the four driver-sql doors refused a policy scope and relayed it. The three engine doors never see a CRUD scope, because they run before the middleware merges it. Draft PR #20037 ('Part of #20020') converts the four driver-sql doors and the three twin door classes in the Turso RemoteTransport to the existing #7929/#8197 withheld seam. Policy, unmarked and ambiguous refusals keep INVALID_FILTER / 400 and send the operands to the log; author-marked refusals keep the old text. The engine half (text-operator declared-type, temporal comparand, FILTER_TOKEN_UNKNOWN) is not changed and needs a decision: measured, on every host except the analytics face these doors see no mark at all, so 'unmarked => withheld' would hide every REST/SDK/flow caller's own diagnostic from them (open_questions[0]). The assignee was os-sales and was not touched. The newest Claim (5822022378) names this branch.",
"tests": "Suites, head 3bf7a68 (after merging main, which brought #20019): driver-sql 'Test Files 183 passed | 11 skipped (194) / Tests 2771 passed | 170 skipped'; driver-turso '65 passed / 1473 passed'; driver-sqlite-wasm '31 passed / 582 passed'. Suites, 2e36f75 (same code as 8e3c49f, before the main merge; the driver dist was rebuilt first): service-analytics 118/2560; plugin-security 130/2561; plugin-sharing 37/913. Typecheck: exit 0 for driver-sql, driver-turso and driver-sqlite-wasm; tsc --listFiles counts 1 for each of the two new pin files and the changed sqlite-wasm pin. New pins: sql-driver-refusal-door-provenance.test.ts (34 cases) and remote-transport-refusal-door-provenance.test.ts (29 cases). For each door they assert: policy-marked gives code+status, no secret in the message, secret in the log/sink; author-marked gives the full text, equal to the policy case's logged diagnostic; unmarked is byte-identical to policy; both arm orders of a merged $and; the column door's lookup by name and the combinator door's node lookup under nesting and $not. Ablations (committed state; the restore is proved by blob == HEAD and an empty git diff HEAD). (1) sql-driver.ts reverted to base: marker unresolvableColumnProvenance 3 -> 0, blob 2497875ce8 -> 0b2caa5d3e; 'Tests 32 failed | 2 passed (34)'. The 2 passes are author-disclosure cases, which base satisfies by disclosing to everyone. (2) ablation-replace.mjs set the combinator node lookup to undefined (anchor x1 -> x0, blob 2497875ce8 -> 6573c8752a): 'Tests 4 failed | 30 passed', exactly the primitive-operand author cases. (3) remote-transport.ts reverted to base: marker 3 -> 0: 'Tests 29 failed (29)'. No dist is involved: both pin files import src relatively. End-to-end probe (scratch, uncommitted; it lived in packages/runtime and was removed): a real ObjectQL + SqliteWasmDriver. The analytics face used the real dispatcher route and AnalyticsService (ObjectQL only, getReadScope). The CRUD face used ql.find under a middleware that copies plugin-security's merge and marking. The host x mark legs used a real SecurityPlugin with CEL rules. Before = 26550c6, after = head with driver-sql dist rebuilt; the per-door and per-face rows are in disclosure_per_door. On the CRUD face the author arm gave the same message before and after for all ten classes (first 220 characters compared).",
"mcp_calls": "0",
"api_writes": "2. (1) pr_create through the fleet-write relay: repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectstack/pulls (draft), run 36066409356, conclusion success, PR #20037. The body was read back and is byte-identical to what was sent. (2) This os-dev-report comment: scripts/pm/post-stamped.mjs -> relay -> POST /repos//issues/20020/comments. Label writes: 0. The dispatch named no label, and skip-changeset does not apply because the diff publishes and carries a changeset. git push is not counted.",
"open_questions": [
{
"question": "The engine half of #20020: text-operator-declared-type-door.ts, temporal-comparand-door.ts, and the FILTER_TOKEN_UNKNOWN refusal (UnknownFilterTokenError in packages/core/src/utils/filter-tokens.ts, called by ObjectQL.resolveWhereTokens). Measured: these doors see policy content only on the analytics face, where withReadScope composes the marked scope into the where before calling the engine; there they relay the policy field (text/temporal), the comparand (temporal) and the token. On every CRUD host they run before the middleware chain and see the caller's where with NO mark at all: no security plugin, plugin-security member/system/anonymous, and a placeholder-bearing where. Applied literally, 'unmarked => withheld' would withhold every REST/SDK/flow caller's own diagnostic from these three doors. That is the field and declared type the #15661 ruling asks the message to name, the comparand and remedy of #8690, and the token's 'did you mean' suggestion. How should the engine half close?",
"options": [
"A: literal. Each door resolves the offending node's mark and discloses only for 'author'; policy and unmarked get INVALID_FILTER / 400 (FILTER_TOKEN_UNKNOWN / 400 for the token) with the operands logged. Business need: closes the analytics disclosure, but every CRUD/REST/SDK caller loses its own field/type/comparand/token naming on every request. That is the dominant face, and nothing stamps a mark there before these doors run. Long-term: one mechanism at the refusing layer, but it reverses the author-facing half of #15661 and #8690 without saying so. Anti-AI-error: worst. AI authors lose the exact message that tells them 'text operator on a number field' or 'last_30_days is not a date', and they repeat the mistake. Startup scope: small code (the doors can return the node; the token resolver in core would have to carry the enclosing node on the error), large behavioural regression.",
"B: the doors withhold for a node that resolves 'policy', or ambiguous inside a tree that carries a mark, and disclose on a tree with no mark at all. Business need: closes the analytics disclosure and keeps every CRUD caller's diagnostic. Long-term: it amends the contract's fail direction at this one pre-merge seam. filter-subtree-provenance.ts says a consumer must treat null exactly as 'policy', so it needs the maintainer's word and a header change in packages/spec. The residual risk is a future pre-engine merge that composes a policy without marking it. Anti-AI-error: good for authors; weaker structurally, because a forgotten mark discloses. Startup scope: small code plus a contract amendment.",
"C: the pre-engine merge boundary judges its scope alone, as #20017 already does for the two shared comparand faces. ObjectQLStrategy.withReadScope (and resolveFkAttr) runs the engine doors' own walks on the scope before composing it: findTextOperatorOverNonTextField and findUninterpretableTemporalComparand take (schema, where) and are exported from their modules (NOT from the @objectstack/objectql package index today), plus spec's classifyFilterToken. It refuses READ_SCOPE_COMPILE_FAILED / 500, withheld (#5367). The engine doors stay unchanged. Business need: closes the only measured disclosure; no caller loses anything. Long-term: consistent with the landed #20017 and with #5367's envelope for read-scope refusals on that face; the engine doors keep the 'caller's own where' premise their headers state. The cost is a small objectql export (or an internal-path import) and a schema read on the analytics side, and a future pre-engine boundary must do the same. Anti-AI-error: diagnostics kept; one judgement point per boundary. Startup scope: a service-analytics-lane change the size of #20017. Out of this claim (service-analytics is forbidden here).",
"D: the doors skip nodes positively marked 'policy'. The scope then reaches the driver as it does on the CRUD face: a text operator over a number matches no row, but $notContains over a number matches EVERY row (#14079 option A), and an unreadable temporal comparand matches no row. Business need: no disclosure, but a policy is answered silently, sometimes wider. Long-term: moves the accept set (Clause-2 yes, widening). Anti-AI-error: silent. Startup scope: small. Not recommended."
],
"recommendation": "C. On the four axes: it is the only option that closes the measured disclosure without costing any caller a diagnostic (business need). It reuses the landed #20017 pattern and #5367's envelope instead of re-ruling the #8220 fail direction or the #15661/#8690 messages (long-term). It keeps AI authors' own diagnostics on the dominant face (anti-AI-error). It is a bounded single-lane change (startup scope). B is the fallback if the maintainer wants the mechanism inside the engine. A should not be chosen silently, because it reverses the author half of two rulings for the whole CRUD population. The status split C leaves on the analytics face (withheld 500 for these three classes and #20017's nine, withheld 400 for this PR's driver doors) is the split the card says it does not decide."
},
{
"question": "Eight more driver-sql refusal doors relay policy content on the CRUD face. Measured at head with the probe, policy-marked scope beside a caller where; every one answered INVALID_FILTER / 400 naming the field or comparand: $icontains '' (icontainsComparandError), $like non-string (likePatternComparandError), $like trailing escape (danglingLikeEscapeError), $contains object comparand (unrenderableTextComparandError), $in object member (unbindableListMemberError), undefined comparand (undefinedComparandError), non-node element of $or (assertFilterNode), undeclared node combinator (unknownLogicalOperatorError). They are the same defect class as this card and sit in the same file, but they are outside the card's seven and the claim's 'only their refusal branches' file surface. How should they be carried?",
"options": [
"A: a follow-up card (the seat files it from out_of_scope_findings[0]). Preferably it closes the class with one guard: every refusal on driver-sql's filter-compile path must go through withheldFilterError with a node, so a new door cannot disclose by omission. The remote transport's twins get measured there. Cost: a second PR, roughly this PR's size in test marking.",
"B: a patch round on this claim, extending PR #20037. Cost: it doubles the diff and the pin updates, and it widens a claim that said 'stop on breach'."
],
"recommendation": "A. The class is real and measured, but the claim bounded this PR to the named doors, and a guard that closes the class is a better shape than eight more hand conversions (long-term, anti-AI-error). Keeping this PR reviewable matters for a security card (startup scope)."
}
],
"out_of_scope_findings": [
"class: b · Contract: filter-subtree-provenance.ts, the 'policy' literal: 'A refusal raised from inside it keeps the #7929 redaction: identity (INVALID_FILTER / 400) and capability statement on the wire, operands in the server log.' Seam: spec:resolveFilterSubtreeProvenance / FILTER_SUBTREE_PROVENANCE → runtime:packages/drivers/driver-sql/src/sql-driver.ts classifyFilterKey gates and the reduction hooks (icontainsComparandError, likePatternComparandError, danglingLikeEscapeError, unrenderableTextComparandError, unbindableListMemberError, undefinedComparandError, assertFilterNode, unknownLogicalOperatorError). Evidence: probe on a real ObjectQL + SqliteWasmDriver, ql.find with a plugin-security-shaped merge (scope marked 'policy') at head 3bf7a68. Every row answered 400 INVALID_FILTER naming the scope field or comparand, e.g. 'Operator "$like" on field "policy_stage" at filter.$and[1].policy_stage.$like requires a string comparand, received number' and 'Filter node at filter.$and[1].$or[0] is a string ("PSECRETX7")'. The RemoteTransport twins were not measured. Dedupe words: 'driver-sql refusal door ignores provenance mark icontains like comparand', 'undefined comparand refusal names read scope field', 'assertFilterNode discloses policy literal', 'withheld seam close the class filter compile refusals'.",
"class: b · Contract: #5347 ruling as recorded in driver-sql's nonBooleanNullComparandError doc ('REFUSED, in every position, on every backend') and FieldOperatorsSchema ($null / $exists declared z.boolean()). Seam: spec:FieldOperatorsSchema.$null/$exists → runtime:packages/services/service-analytics/src/strategies/filter-normalizer.ts (the caller-where lowering on the ObjectQL face). Evidence: probe, real POST /api/v1/analytics/query (AnalyticsService ObjectQL-only over real ObjectQL + SqliteWasmDriver). where { policy_stage: { $null: 'PSECRET6' } } and { policy_stage: { $exists: 'PSECRET6b' } } each answered HTTP 200 with rows d1, d2, and the driver received { "policy_stage": { "$ne": null } }. Measured at base 26550c6 and at head. Dedupe words: 'analytics where non-boolean $null answered 200', 'filter-normalizer $exists string coerced $ne null', 'analytics caller where $null flag not refused'.",
"noted, not filed · carrier: 承接者:无 · TursoDriver.toRemoteFilter (turso-driver.ts) rebuilds every filter node for storage form, so no provenance mark reaches RemoteTransport in remote mode. Every mark-reading remote refusal (#7929, #8197 and this PR's three) withholds even an author-marked where there. That is the contract's declared fail-closed direction for a rewrite, not a disclosure; it is a UX gap for authors on remote Turso. Measured: an author-marked { name: { $regex } } through TursoDriver remote got the withheld text (remote-transport-text-predicates.test.ts).",
"noted, not filed · carrier: 承接者:无 · driver-memory and driver-mongodb do not consult the mark. The only non-test consumers of resolveFilterSubtreeProvenance are sql-driver.ts and remote-transport.ts (grep at head). Whether their refusals relay a policy scope was not measured here."
],
"gates": "At head 3bf7a68: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 families from the real diff (merge base 9d81af7, 25 paths). All 62 exited 0. check:dual-build-cjs-loads and check:type-check-debt first exited 3 (PREREQUISITE NOT MET) and exited 0 after 'turbo run build --concurrency=2 --filter=./packages/* --filter=./packages//'; check:dts-closure, lean-entry-closure, published-files and sourcemap-no-sources-content were re-run on the full build and exited 0. --ran reconciliation: '62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 62 recorded an exit code and none of them is 3)'. After the changeset edit, at b6f40dc: check-adr-0087-registration --base, check-changeset-no-major --base, check-empty-changeset --base, check:pm-changeset-deadline-census, check:objectui-changeset, check:nul-bytes and check:doc-authoring each exited 0. node scripts/check-issue-citations.mjs --base 9d81af7: exit 0, 28 citations resolve. check:driver-conformance before (26550c6) and after: 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt' (the ledger did not move). ESLint (--no-inline-config, --format json) on the 24 changed .ts files at 3bf7a68: 24 files, 0 errors, 0 warnings. eslint.config.mjs sets no parserOptions.project (not type-aware), so the diff cannot move a verdict on an untouched file; the repo-wide run belongs to CI. NOT MEASURED locally: the CI-only families that dispatch-gates lists outside the 62 (the path-scheduled jobs, the type-check lanes, the artifact rosters).",
"line_budget": "n/a — no skills/** path",
"deviations": [
"The PR body opens with 'Part of #20020', not a closing keyword: the engine half goes to the decision box (open_questions[0]), and the role file forbids closing a card that is headed there.",
"The dispatch route asked for all seven classes to be pinned on both faces. Only the four driver-sql classes (and their remote twins) are changed and pinned. The three engine doors are not changed, per the H2 measurement and open_questions[0].",
"The committed pins are driver-level: they use both merge shapes (author arm plus policy arm under one unmarked $and), which is what each face hands the driver. The two real faces were measured end to end with an uncommitted probe in packages/runtime. packages/runtime is outside the claimed file surface, so it carries no committed pin.",
"File surface beyond the claim's list: packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-icontains-and-retired-operators.test.ts. It is a mechanical 'author' marking, forced because SqliteWasmDriver inherits the changed doors from SqlDriver. No source file outside the claim was edited.",
"RemoteTransport.unsupportedOperator: the arm for a non-operator key in an operator map ('has an object comparand whose key ... is not an operator') was converted with its retired and unknown siblings, because it is the same method and names the same target. The $between-not-lowered arm was left as it was.",
"Two driver-sql pins that asserted the unknown-operator wording on an unmarked lowered where (sql-driver-null-operators.test.ts) now assert code and status instead of being marked, because the where there is a parseFilterAST output.",
"origin/main was merged into the branch (9d81af7, which brought #20019 into sql-driver.ts and remote-transport.ts in the textMatchPredicate/pushLike regions), per the claim's 'whichever lands second merges main first'. The merge was clean, and the three driver suites re-ran green after it.",
"The service-analytics, plugin-security and plugin-sharing suites ran before the main merge (at 2e36f75). They were not re-run after it."
],
"files_changed": [
".changeset/20020-refusal-doors-provenance.md (new; @objectstack/driver-sql patch, @objectstack/driver-turso patch; Clause-②: no)",
"packages/drivers/driver-sql/src/sql-driver.ts (+233/-38: unresolvableColumnProvenance, retired/unknown/flag/non-list builders through withheldFilterError, per-reduction sqlFilterVerdictHooks, where threaded into unresolvableFilterColumnRefusal and its four callers)",
"packages/drivers/driver-sql/src/sql-driver-refusal-door-provenance.test.ts (new, 34 cases)",
"packages/drivers/driver-sql/src/.test.ts, 11 existing pins (author marking, or code+status in two places; one boundary pin rewritten in sql-driver-target-field-provenance.test.ts)",
"packages/drivers/driver-turso/src/remote-transport.ts (+88/-13: nonBooleanFlagWithheldMessage, withheldRefusal, four builders and three call sites)",
"packages/drivers/driver-turso/src/remote-transport-refusal-door-provenance.test.ts (new, 29 cases)",
"packages/drivers/driver-turso/src/.test.ts, 7 existing pins (author marking; remote-mode pins assert the withheld wording)",
"packages/drivers/driver-sqlite-wasm/src/sqlite-wasm-icontains-and-retired-operators.test.ts (author marking)"
],
"host_mark_table": [
"Driver columns measured with a real SecurityPlugin (CEL using rule) over a real ObjectQL + SqliteWasmDriver, reading the where the driver received. The engine-doors column was measured with a first-registered recording middleware on the simulated-merge engine.",
"host | engine doors see | driver: caller where | driver: scope",
"no security plugin | no mark | unmarked | none",
"plugin-security, member, plain where | no mark | author | policy",
"plugin-security, member, no where | no mark | none | policy (at the root)",
"plugin-security, system context | no mark | unmarked | none",
"plugin-security, anonymous | no mark | unmarked | none",
"plugin-security, member, where holding {current_user_id} | no mark | unmarked (resolveWhereTokens rewrote it before the merge) | policy",
"plugin-security, member, FilterArray where | no mark | author (the lowered copy is vouched) | policy",
"analytics withReadScope | caller arm author, scope policy (read from code; the after-probe is consistent with it: the author arm discloses, the scope withholds) | author | policy",
"Turso REMOTE via TursoDriver | not measured | unmarked (toRemoteFilter rebuilds every node) | unmarked",
"plugin-sharing: not measured end to end.",
"Caller loses at the four driver doors (unmarked): no security plugin, system context, anonymous, a where holding a placeholder token, Turso remote mode. It keeps code, status and the class statement. Caller loses at the engine doors under a literal reading: every non-analytics host."
],
"disclosure_per_door": [
"Probe: real ObjectQL + SqliteWasmDriver. analytics = HTTP POST /api/v1/analytics/query with the scope from getReadScope; CRUD = ql.find with a plugin-security-shaped merge. Before = 26550c6, after = head. Names listed = what the response message carried.",
"missing column (driver-sql) | analytics policy: field -> none | CRUD policy: field -> none | CRUD real SecurityPlugin CEL rule on a declared, unsynced field: field -> none | author (both faces): field -> field, same text",
"retired operator $regex (driver-sql) | analytics: field+operator -> none | CRUD: field+operator -> none | author CRUD: unchanged",
"unknown operator (driver-sql) | analytics: field+operator -> none | CRUD: field+operator -> none | author CRUD: unchanged",
"combinator, object operand (driver-sql) | analytics: field+comparand -> none | CRUD: field+comparand -> none | author CRUD: unchanged",
"combinator, primitive operand (driver-sql) | analytics: comparand -> none | CRUD: comparand -> none | author CRUD: unchanged",
"non-boolean $null (driver-sql) | analytics: field+comparand -> none | CRUD: field+comparand -> none | author CRUD: unchanged",
"non-boolean $exists (driver-sql) | analytics: field+comparand -> none | CRUD: field+comparand -> none | author CRUD: unchanged",
"text operator on number field (engine) | analytics: field -> field (NOT CHANGED, still relays) | CRUD: not reached (door runs before the merge; the scope answered 0 rows)",
"temporal comparand (engine) | analytics: field+comparand -> field+comparand (NOT CHANGED, still relays) | CRUD: not reached (0 rows)",
"unknown placeholder FILTER_TOKEN_UNKNOWN (core resolver via engine) | analytics: token -> token (NOT CHANGED, still relays) | CRUD: not reached (compared as a literal, 0 rows)",
"Turso RemoteTransport, same classes except missing column (the transport answers []): unit-measured, policy/unmarked -> withheld, operands to the diagnostic sink, author-marked -> full text.",
"Still relaying policy content after this PR: the three engine doors on the analytics face (open_questions[0]), and eight further driver-sql doors on the CRUD face (open_questions[1], out_of_scope_findings[0])."
],
"compile_surfaces": [
"1 driver-sql (applyFilterCondition, and driver-sqlite-wasm / Turso local by inheritance): CHANGED, four doors.",
"2 Turso RemoteTransport (buildWhereSQL): CHANGED, three door classes; pushLike untouched.",
"3 service-analytics read-scope-sql (compileScopedFilterToSql): not touched (forbidden by the claim); its refusals are the withheld READ_SCOPE_COMPILE_FAILED / 500.",
"4 service-analytics filter-normalizer (lowerAnalyticsWhere): not touched (forbidden); see out_of_scope_findings[1].",
"5 formula matches-filter: not touched, not measured.",
"half: objectql having-filter: not touched, not measured.",
"driver-memory / driver-mongodb: not touched; they do not read the mark (grep); disclosure not measured."
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim amendment and seat decisions on the dev's two open questions (PR #20037)
domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T22:42Z. It amends claim 5822022378, and answersos-dev-reporton this card and contract review 5823428898 (FAIL atb6f40dc9) on PR #20037.File surface, widened by one test file:
packages/plugins/plugin-auth/src/auth-contains-filter.test.ts. Its #5893 face-3 pin asserts the old wording of an unmarked$regexrefusal and is red at this head. This is a test-only change, and no plugin-auth source moves.Q1, the engine half (the declared-type, temporal-comparand and filter-token doors): option C. The analytics read-scope boundary (
service-analytics'sObjectQLStrategy.withReadScope) judges its scope with the engine doors' own walks before composing it. It refuses a failing scope with the withheldREAD_SCOPE_COMPILE_FAILED/ 500 that #5367 set for that face, which is the pattern PR #20017 already landed for the two shared comparand faces.- Why C:
- The dev measured that these three doors see a policy scope only on the analytics face; on every CRUD host they run before the middleware merge and see no mark. So option A, the literal "unmarked ⇒ withheld" at these doors, would withhold every REST / SDK / flow caller's own field, type, comparand and token diagnostic, and reverse the author-facing halves of [Decision] refuse a text operator (
$containsfamily) over a field whose DECLARED type is not textual —INVALID_FILTER400 at the engine's field-aware door (option C of #14079); the textual-type vocabulary is the question #15661 and An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown{placeholder}is correctly rejected 400 (17.0.0 GA) #8690. - Option B would amend the [A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220 contract's fail direction, which is a maintainer's word.
- C closes the only measured disclosure, costs no caller a diagnostic, amends no ruling or contract, and follows the landed fix(service-analytics): the ObjectQL execute face refuses an unrunnable read scope in the withheld READ_SCOPE_COMPILE_FAILED / 500 envelope #20017 / analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367 precedent.
- The dev measured that these three doors see a policy scope only on the analytics face; on every CRUD host they run before the middleware merge and see no mark. So option A, the literal "unmarked ⇒ withheld" at these doors, would withhold every REST / SDK / flow caller's own field, type, comparand and token diagnostic, and reverse the author-facing halves of [Decision] refuse a text operator (
- Where it lands:
service-analytics, the services lane. It is filed as its own card. This card's engine doors stay unchanged. - The 500-vs-400 status split on the analytics face is the split this card says it does not decide.
Q2, eight more driver-sql refusal doors that relay a policy scope on the CRUD face: option A, a follow-up card. It closes the class with one guard, so that every refusal on driver-sql's filter-compile path goes through
withheldFilterErrorwith a node. The contract review measured all eight REAL. It is filed as its own card in this lane.Out-of-scope finding, the analytics caller-where non-boolean
$null/$existscoerced toIS NOT NULL: REAL by code reading. It is filed as a services-lane card.This card after PR #20037 lands: PR #20037 carries the four driver-sql doors and their remote twins (
Part of #20020). The engine half moves to the Q1 card, and the eight doors move to the Q2 card. This card is then closed with a record naming both.
Generated by Claude Code
- Why C:
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20020,
"status": "done",
"addendum": "patch round 1: answers contract review 5823428898 (FAIL at b6f40dc) and claim amendment 5823437510",
"branch": "claude/issue-20020-refusal-doors-provenance",
"pr": "#20037",
"head": "5fb4b25b1",
"session": "session_01Bvd69VPa6puiNzzPUroDBx",
"premise_still_valid": true,
"summary": "Patch round 1 on PR #20037, two commits on top of b6f40dc, no force-push. (1) 244f532 re-pins plugin-auth's #5893 face-3 test to production behaviour. The unmarked engine.find now asserts the withheld retired-operator refusal: INVALID_FILTER / 400, 'is RETIRED', neither '$regex' nor '$icontains' in the message, and both in the spied driver log. A new 'author'-marked leg keeps the full text. No plugin-auth source moved. (2) 5fb4b25 adds one changeset sentence and a '@objectstack/driver-sqlite-wasm': patch entry, so SqliteWasmDriver's and TursoDriver-local's inheritance is stated. A repo-wide sweep of test pins found no other pin that reads the four doors' wording from a real driver. plugin-auth plus every suite on CI Test Core (6/6) that the plugin-auth failure could have kept from running were re-run green. GitHub reports the PR mergeable (mergeable: true), so origin/main was not merged. The PR body is not edited; the seat's Tests-section inputs are in tests and suites below. Q1 → C and Q2 → A (claim amendment 5823437510) are the seat's cards, and none of that work is in this round.",
"pins_changed": [
{
"file": "packages/plugins/plugin-auth/src/auth-contains-filter.test.ts",
"test_old": "'refuses a bare$regexin the ADR-0112 envelope, naming its replacement'",
"old": "Unmarked engine.find('sys_user', { where: { name: { $regex: 'a.b' } } }) → expect(err.code).toBe('INVALID_FILTER'); expect(err.status).toBe(400); expect(err.message).toContain('$regex'); expect(err.message).toContain('$icontains').",
"test_new": "'refuses a bare$regexin the ADR-0112 envelope, and withholds the operator from an unmarked caller'",
"new": "The same unmarked call, with the driver's logger spied (captureDriverLog on the SqlDriver the harness returns) → code 'INVALID_FILTER'; status 400; message toContain('is RETIRED'); message not.toContain('$regex'); message not.toContain('$icontains'); log toContain('$regex'); log toContain('$icontains')."
},
{
"file": "packages/plugins/plugin-auth/src/auth-contains-filter.test.ts",
"test_old": "(none: this leg is added)",
"old": "-",
"test_new": "'names the refused operator and its replacement for awheremarked as the caller's own'",
"new": "engine.find('sys_user', { where: markFilterSubtreeProvenance({ name: { $regex: 'a.b' } }, 'author') }) → code 'INVALID_FILTER'; status 400; message toContain('$regex'); message toContain('$icontains'). This keeps the #5702 / #5893 'operating instruction' pinned for a vouched caller."
},
{
"file": "packages/plugins/plugin-auth/src/auth-contains-filter.test.ts (file header comment)",
"test_old": "'Now (#5702, PR #6549)' bullet: 'driver-sql refuses it by name in the ADR-0112 envelope …, prescribing$icontains.'",
"old": "claimed the operator is named and the replacement prescribed for every caller",
"test_new": "the same bullet, restated",
"new": "'driver-sql refuses it in the ADR-0112 envelope …'; for a where a read-scope merge boundary marked as the caller's own it names $regex and prescribes $icontains; for an unmarked one (this facade's path) it states only that a retired operator was refused, and the operator and replacement go to the server log; face 3 pins both. No tracker number beyond the existing #5702 / PR #6549."
}
],
"pin_sweep": "The old-wording grep ran over packages/, examples/ and apps/ tests (*.test.ts, .spec.ts, .test.mts, .test.tsx). Patterns: 'is RETIRED', 'Unsupported filter operator', 'requires an array of filter conditions', 'requires a boolean comparand', 'has no column for', 'names a column that object', 'is not an operator', 'Filter combinator', toContain('$icontains'|'$regex'|'$options'). That gave 101 files. Outside the three driver packages, packages/spec and the memory/mongo drivers (which have their own wording), I kept the files that reference a SQL-family driver: 19 files, each read at the hit line. The only real consumer pin on these doors' wording was plugin-auth auth-contains-filter.test.ts:309-310. The rest are: comments ('is RETIRED' in prose); analytics' own '[analytics] Unsupported filter operator' wording (service-analytics filter-normalizer-not-null-safe:701, cross-field-reference-refusal:469; both suites green); literal-stub errors (types error-leak, rest 4xx-truncation and unclassified-fault, objectql global-search, metadata-protocol seed-loader); or ast.where contents (objectql engine-findone-contract:171/354). A second grep covered the 311 test files outside the three drivers that use a real SQL driver, for door trigger tokens ($regex / $options / unknown ops / non-boolean $null|$exists / non-list $and|$or / missing-column names). It found no further pin. Two comment passages there are now true only for an author-marked caller; see out_of_scope_findings.",
"tests": "At head 5fb4b25 (after both commits): driver-sql 'Test Files 183 passed | 11 skipped (194) / Tests 2771 passed | 170 skipped (2941)'; driver-turso 'Test Files 65 passed (65) / Tests 1473 passed (1473)'; driver-sqlite-wasm 'Test Files 31 passed (31) / Tests 582 passed (582)'; plugin-auth 'Test Files 114 passed (114) / Tests 2440 passed (2440)'. At 244f532 (same code as 5fb4b25; the later commit touches only the changeset): plugin-auth 114 / 2440; auth-contains-filter.test.ts alone 'Tests 10 passed (10)' (9 before, plus the added author leg); service-storage 'Test Files 40 passed (40) / Tests 627 passed (627)'; cli unit project ('--project unit', per os-dev): 'Test Files 2 failed | 222 passed (224) / Tests 3129 passed | 29 skipped'. The 2 files (test/published-subpath-console.pin.test.ts, test/published-subpath-hook-body.pin.test.ts) failed at load with 'packages/cli is not built (./dist/index.js is absent)', a prerequisite and not a verdict. Re-run after 'turbo run build --filter=@objectstack/cli': 'Test Files 2 passed (2) / Tests 29 passed (29)'. cli's integration tier is declared to CI (the diff touches no cli file). cloud-connection 29/374, plugin-dev 8/80, connector-mcp 3/23, hono 5/122, each run with its own exit code 0 read via PIPESTATUS. Why these suites: CI Test Core (6/6) failed at b6f40dc on plugin-auth, and the CI test step runs turbo without --continue. The rest of that shard (reproduced locally with scripts/ci/select-shard-packages.sh plus partition-test-shards.mjs --shard 6/6 against merge base 9d81af7: cli 2/2, plugin-auth, driver-sqlite-wasm, cloud-connection, service-storage, example-embed-objectql, plugin-dev, connector-mcp, hono) may therefore not have run. Each of them that depends on the SQL drivers directly or through runtime / service-automation was run here. example-embed-objectql is NOT MEASURED; reason: it depends only on driver-memory, objectql and spec, with no path to the changed code. Test Core shards 1-5 were success at b6f40dc. Typecheck at 5fb4b25: plugin-auth exit 0 (its script runs tsc, tsc -p tsconfig.examples.json and check:test-typecheck: 'OK — … 10 file(s) / 94 error(s) / 23 pinned signature(s) held', the ledger unchanged and not naming this file). auth-contains-filter.test.ts is in the tsconfig.test.json program (--listFiles count 1). driver-sql, driver-turso and driver-sqlite-wasm typecheck exit 0. ESLint (--no-inline-config, --format json) on the one changed .ts file at 5fb4b25: 1 file, 0 errors, 0 warnings; no parserOptions.project in eslint.config.mjs. No ablation was run for the re-pinned leg. It asserts not.toContain('$regex'), and the base driver text it replaces contained '$regex' (the pre-change pin asserted exactly that and was green at the merge base, per the review), so the leg is falsifiable. A dist-level ablation would need two driver-sql rebuilds and was not run: NOT MEASURED.",
"suites_for_pr_body": [
"plugin-auth: 114 files, 2440 tests passed (at 5fb4b25)",
"service-storage: 40 files, 627 tests passed (at 244f532)",
"cli unit project: 224 files (222 in the first run plus 2 re-run after building cli), 3129 + 29 tests passed; 29 skipped (at 244f532)",
"cloud-connection 29/374, plugin-dev 8/80, connector-mcp 3/23, hono 5/122 (at 5fb4b25)",
"driver-sql 183 passed + 11 skipped files / 2771 passed + 170 skipped; driver-turso 65/1473; driver-sqlite-wasm 31/582 (at 5fb4b25)",
"consumer pin changed: plugin-auth auth-contains-filter.test.ts face 3 (#5893). The unmarked leg now pins the withheld text, and an 'author'-marked leg pins the full text."
],
"changeset_change": "Frontmatter gains '@objectstack/driver-sqlite-wasm': patch. One sentence was added as its own paragraph after the node-lookup paragraph: "SqliteWasmDriver(@objectstack/driver-sqlite-wasm) andTursoDriverin local mode extendSqlDriver, so they inherit this change from it: the same four doors answer the same way there." No other sentence changed.",
"mcp_calls": "0",
"api_writes": "1: this os-dev-report addendum through scripts/pm/post-stamped.mjs → fleet-write relay → POST /repos//issues/20020/comments. git push (commits 244f532 and 5fb4b25, fast-forward on b6f40dc) is not counted. Label writes: 0. PR body: not edited. Reads only: REST GETs of the review comment, the card's comments, check-runs, the shard-6 check-run and its annotations, and the PR's mergeable state.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed · carrier: 承接者:无 · Two test-file comment passages still say driver-sql refuses $regex 'by name'. That is now true only for an author-marked where: packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts (header note, ~lines 68-69) and packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts (~lines 135-136). They are prose, not pins; both suites carry no assertion on that wording (plugin-auth green; the runtime file has no $regex assertion). Neither file is in the amended surface."
],
"gates": "Re-derived at 5fb4b25 (merge base 9d81af7, 26 paths): 65 families, the previous 62 plus 3 new: node scripts/check-dev-prereqs.mjs --self-test, node scripts/check-tenant-audit-census.mjs, and its --self-test. All 65 were run after a full 'turbo run build --filter=./packages/ --filter=./packages//' (67/72 cached); all exited 0. --ran: '65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 65 recorded an exit code and none of them is 3)'. check:driver-conformance: 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt' (unchanged). check:doc-authoring: no prose-id growth. node scripts/check-issue-citations.mjs --base 9d81af7: exit 0, 28 citations resolve. GitHub mergeable: true (mergeable_state 'blocked' = draft/checks), so origin/main was not merged.",
"deviations": [
"The changeset gains a frontmatter entry for @objectstack/driver-sqlite-wasm as well as the sentence. Without the entry, that package's CHANGELOG would not carry the sentence, which is the point of item 3. The reviewer's semver note said no entry was needed for sqlite-wasm; this adds a patch line to a package whose only source change is inherited.",
"The unmarked leg of the plugin-auth test was renamed ('… and withholds the operator from an unmarked caller'), because the old title ('naming its replacement') is no longer what that leg asserts.",
"example-embed-objectql (shard 6/6) was not run locally: NOT MEASURED, because it has no dependency path to the changed code."
],
"files_changed_this_round": [
"packages/plugins/plugin-auth/src/auth-contains-filter.test.ts (+60/-11)",
".changeset/20020-refusal-doors-provenance.md (+3: one frontmatter line, one sentence plus a blank line)"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsACCEPT: PR #20037 at
5fb4b25bdomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T23:52Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev's reports (os-dev-report5823164857 and the patch-round addendum 5823948662).- Shape: the first line is
Part of #20020, and the body declaresClause-②: no. The changeset grades@objectstack/driver-sql,@objectstack/driver-tursoand@objectstack/driver-sqlite-wasmpatch. The PR carries no closing keyword. The engine half of this card is carried outside it (below). - Scope: 26 files, +1096/−126. It is not governed (
check-governed-merges --pr 20037) and touches no generated path. All 26 are inside the amended claim surface (5823437510):- the four
driver-sqlrefusal doors and theirRemoteTransporttwins; - two new pin suites;
- the existing pins marked
'author'; - the plugin-auth consumer pin;
- the changeset.
- the four
- Contract review of record, two reads:
- FAIL at
b6f40dc9(5823428898): the plugin-authauth-contains-filter.test.ts的见证后端迁 sqlite 排在 #5702 之后 —— #5830 裁决 C 的到期单 #5893 face-3 pin was red and undeclared. - PASS at
5fb4b25b(5824208109):- the accept set is unchanged: 432 of 432 cells are refused at base and at head;
- every non-author cell withholds and logs;
- every author cell carries the full text;
- the three SQL drivers are byte-identical across 70 parity cells;
- the re-pinned face 3 goes red with the doors at base, through dist;
- the consumer sweep reproduces the dev's 101 files, and no other pin reads the doors' wording.
- FAIL at
- The finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 / [A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220 contract held:
'policy'and unmarked are withheld with the operands in the server log,'author'gets the full text, and the error's own keys are exactlycode,status. - CI at this head: 48 runs, 41 success, 7 skipped, 0 failures. Every required context is
success, andmergeable_stateisclean. mainmoved under this PR: PR fix(driver-sql, driver-turso): SQLite $contains / $notContains / $icontains / $endsWith read the whole stored value, not only up to its first U+0000 #20038 (driver-sql / driver-turso (SQLite faces):GLOBstill cuts a STORED value at its first U+0000, so a NUL-free$contains/$notContains/$icontains/$endsWithanswers wrongly on it; a$like/$ilikepattern holding U+0000 is cut the same way #20024, this seat) landed asadbbc5d01e. It touchedsql-driver.ts,remote-transport.tsand four pin files this PR also edits:sql-driver-icontains-and-retired-operators,sql-driver-text-case-conformance,remote-transport-null-comparand-refusalandremote-transport-text-predicates.- The regions are disjoint (fix(driver-sql, driver-turso): SQLite $contains / $notContains / $icontains / $endsWith read the whole stored value, not only up to its first U+0000 #20038 edits the text-match emitters; this PR edits the refusal doors), and
git merge-treeagainst currentmainis clean. - The claim said "whichever lands second merges
mainfirst". In its place, the merge queue runs the required suite on the merged tree, which is the check that clause exists for. A merge commit would move the head off the PASS record without changing what the queue tests.
- The regions are disjoint (fix(driver-sql, driver-turso): SQLite $contains / $notContains / $icontains / $endsWith read the whole stored value, not only up to its first U+0000 #20038 edits the text-match emitters; this PR edits the refusal doors), and
- Seat edits to the PR body (no code): the patch-round Tests entries and the changed consumer pin, both from the addendum and each confirmed by the reviewer; and a per-face compile-surface table (
references/compile-surfaces.md), including the unreachable$betweenarm. - The rest of this card, carried outside this PR:
- The engine doors (
text-operator-declared-type-door.ts,temporal-comparand-door.ts, the unknown-placeholder refusal) → C: the services lane pre-judges on the analytics face (pointer 5823452098 on security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995). They are not converted here, because every CRUD caller would lose its own diagnostic. - Eight more
driver-sqldoors → driver-sql: eight more filter-compile refusal doors ignore the #8220 provenance mark, so a policy read scope refused there relays its field or comparand in the 400; close the class so every compile refusal goes through the withheld seam #20039. - The analytics
$null/$existsflag → service-analytics: the caller-where lowering reads$null/$existsflags by identity, so a non-boolean flag ({ $null: "x" }) is answered 200 as IS NOT NULL where every other face refuses it (#5347 / #5369) #20040.
- The engine doors (
- Out-of-scope observation (prose, no card): two test comments still say
$regexis refused "by name": plugin-authauth-where-operator-coverage.test.tsand runtimeundeclared-field-write-driver-split.integration.test.ts. No assertion reads them.
Landing:
readyplus auto-merge through the queue now. This card closes on the landing, with a record pointing at #19995 and #20039.- Shape: the first line is
objectstack-fleet commented
on Sep 25, 2026 ContributorAuthorMore actionsLanded: PR #20037, verified on
main, and this card closesdomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-25T00:25Z.-
Verified on main:
- PR fix(driver-sql, driver-turso): four filter-refusal doors read the provenance mark before naming a read scope field or comparand (#20020) #20037 merged through the merge queue as
57c2b73712, which has one parent and is an ancestor oforigin/mainafter a fresh fetch. The diff is 26 files, +1096/−126, exactly the reviewed PR. - The new pin suite
sql-driver-refusal-door-provenance.test.tsand.changeset/20020-refusal-doors-provenance.mdare present at that commit and absent at its parent. - The queue merged it on top of PR fix(driver-sql, driver-turso): SQLite $contains / $notContains / $icontains / $endsWith read the whole stored value, not only up to its first U+0000 #20038's edits to the same two emitter files (
adbbc5d01e), and its required suite ran on that merged tree.
- PR fix(driver-sql, driver-turso): four filter-refusal doors read the provenance mark before naming a read scope field or comparand (#20020) #20037 merged through the merge queue as
-
Delivered: the four
driver-sqlrefusal doors and their TursoRemoteTransporttwins honour the [A of #7929] a spec-declared provenance mark set at both read-scope merge boundaries, so the driver can restore the author-facing cross-field diagnostic without re-disclosing policy #8220 provenance mark (A of the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 ruling):'policy'and unmarked are withheld, with the operands in the server log;'author'gets the full text;- the accept set is unchanged;
SqliteWasmDriverand Turso local inherit it.
The contract review of record is PASS 5824208109, after FAIL 5823428898.
-
The rest of this card is carried, so the card closes
completedand not asPart of:- The three engine doors (
text-operator-declared-type-door.ts,temporal-comparand-door.ts, the unknown-placeholder refusal) → C in claim amendment 5823437510. Measured, they see no provenance mark on any CRUD host, so they relay policy content only on the analytics face. The services lane pre-judges it there; pointer 5823452098 on security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995. - Eight more
driver-sqlcompile refusal doors that ignore the mark → driver-sql: eight more filter-compile refusal doors ignore the #8220 provenance mark, so a policy read scope refused there relays its field or comparand in the 400; close the class so every compile refusal goes through the withheld seam #20039. - The analytics caller-
where$null/$existsflag, read by identity → service-analytics: the caller-where lowering reads$null/$existsflags by identity, so a non-boolean flag ({ $null: "x" }) is answered 200 as IS NOT NULL where every other face refuses it (#5347 / #5369) #20040 (domain:services).
- The three engine doors (
-
pm:dispatchedis removed in the same act. -
No card closed by mistake: the PR carries no closing keyword, and driver-sql / driver-turso (SQLite faces):
GLOBstill cuts a STORED value at its first U+0000, so a NUL-free$contains/$notContains/$icontains/$endsWithanswers wrongly on it; a$like/$ilikepattern holding U+0000 is cut the same way #20024, service-analytics (SQLite): the shared text-match arm emitsGLOB, so a$contains/$endsWith/$startsWithcomparand holding U+0000 is cut at the NUL; on the read scope a leading U+0000 widens$contains/$endsWithto every row #20025 and security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995, which it cites, are still open. -
Serial queue: the
sql-driver.ts/remote-transport.tsrefusal-door region is released. driver-sql: eight more filter-compile refusal doors ignore the #8220 provenance mark, so a policy read scope refused there relays its field or comparand in the 400; close the class so every compile refusal goes through the withheld seam #20039 is next in it, then spec + drivers: a$like/$ilikepattern holding U+0000 is cut at the NUL on the SQLite faces and answers a different question; refuse it at every face through one spec predicate besidehasDanglingLikeEscape#20041, then driver-sql / driver-turso (SQLite faces):GLOBstill cuts a STORED value at its first U+0000, so a NUL-free$contains/$notContains/$icontains/$endsWithanswers wrongly on it; a$like/$ilikepattern holding U+0000 is cut the same way #20024 item 2 (ii).
Generated by Claude Code
-
- added 4 commits that reference this issue
on Sep 28, 2026
Filing gate ① — a product defect with named sites and a measurement (class b: a declared, ruled contract not held at some of the doors it names).
domain:servicesseat (session_01Evb5jFDZGKQE9KG4jbMfMF, seat post [PM seat] domain:services — ⏳ vacant #6021).INVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 dev on PR fix(service-analytics): the ObjectQL execute face refuses an unrunnable read scope in the withheld READ_SCOPE_COMPILE_FAILED / 500 envelope #20017, whose residue this is.packages/objectqlandpackages/drivers/driver-sql(triage routes). security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 waits on this card.The contract (standing ruling, not a new decision)
packages/spec/src/data/filter-subtree-provenance.ts(#8220, which implements A of the #7929 maintainer ruling of 2026-08-12, 「接受你的全部建议。」):'policy': 「injected policy the caller never wrote (an RLS / sharing / tenant read scope). A refusal raised from inside it keeps the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 redaction: identity (INVALID_FILTER/ 400) and capability statement on the wire, operands in the server log.」plugin-security's CRUD injection andservice-analytics'ObjectQLStrategy.withReadScope.The defect
Only
driver-sql's bind and cross-field refusals read the mark. Seven refusal classes raised from inside a'policy'-marked read scope do not, and their 400 message names the policy's field, and for some classes its comparand:driver-sql(missing column)driver-sqloperator vocabularydriver-sql$null/$existsdriver-sqlFILTER_TOKEN_UNKNOWN)Measured (the #19995 dev; ⛔ not re-run by this seat)
ObjectQLoverSqlDriver(better-sqlite3) andSqliteWasmDriver, both drivers identical. The HTTP legs went through the realPOST /api/v1/analytics/queryandPOST /analytics/dataset/queryroutes.os-dev-reporton security: the analytics ObjectQL execute face answers a row-level read scope it cannot run withINVALID_FILTER/ 400 whose message echoes the policy's field name and comparands — the disclosure #5367 closed for the native / echo faces #19995 and in PR fix(service-analytics): the ObjectQL execute face refuses an unrunnable read scope in the withheld READ_SCOPE_COMPILE_FAILED / 500 envelope #20017's body.READ_SCOPE_COMPILE_FAILED/ 500 on the analytics face. Those are judged analytics-side before the merge.plugin-security's merge). Measure that first.Direction (per the ruling; the implementer measures)
resolveFilterSubtreeProvenance) and discloses its full diagnostic only for a subtree positively marked'author'.'policy'and unmarked keepINVALID_FILTER/ 400 with the operands in the server log only, which is the finding: after the #7598 Q1=B ruling, a read scope with a driver-refused field reference answers 400 from the driver — which cuts across #5367's attribution argument on that one path #7929 B shapedriver-sql's cross-field refusal already has.Dedupe
One semantic issue search, open and closed, on 「driver-sql or engine filter refusal does not consult filter subtree provenance mark policy; missing column unknown operator refusal message discloses RLS policy field in 400」: 75 hits. Adjacent: #19995 (the analytics face; its residue is this card), #19949 (driver-mongodb cross-field), and #19885 (closed). None covers mark-blind refusal doors.
Dedupe words:
provenance mark refusal door policy subtree·driver-sql missing column refusal discloses policy field·engine declared-type door policy redaction·FILTER_TOKEN_UNKNOWN read scope disclosureGenerated by Claude Code