Skip to content

[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

Description

@os-zhuang

This is "A" of the maintainer ruling of 2026-08-12 on #7929 (comment 5266067250, verbatim 「接受你的全部建议。」 — adopting "B now, A next"). B landed in #8198 (merged a5dcb74, closing #7929 and #7988). This card is the sanctioned follow-up, not a new proposal.

⛔ Filed unassigned — claim it before writing code, per the repo's claim discipline.

Why A exists

B stopped driver-sql's cross-field INVALID_FILTER refusal from echoing its operands, for every caller, because the driver genuinely cannot tell an administrator's predicate from an author's. Measured on both merge boundaries: the read scope arrives as a bare FilterCondition in where, DriverQuery is Omit<QueryAST, 'object'> with no provenance slot, and the only thing that crosses (context) says who is asking, never which subtree they did not write.

The accepted cost, stated plainly in B's changeset: an author debugging their own cross-field filter now gets the redacted message too. Their diagnostic is relocated to the server log, not destroyed. A is what gives it back — behind a real mark rather than a guess.

What A has to build

  1. A provenance mark declared in packages/spec. Declared, not conventional — the whole point is that the driver may trust it because the contract defines it.
  2. Set at BOTH merge boundaries, or the fix is half-live:
    • plugin-security's CRUD injection — the ast.where = ast.where ? { $and: [ast.where, scoped] } : scoped merge around security-plugin.ts:2198;
    • service-analytics' read-scope merge (ObjectQLStrategy.withReadScope).
  3. driver-sql consumes it: a marked (policy-authored) subtree keeps B's redaction; an unmarked, author-written filter gets its full diagnostic back — both columns, the operator, the list index, the boundary reason.

⚠️ The invariant that must not regress — fail closed

An unmarked or ambiguous predicate must default to WITHHELD. The mark is permission to reveal, never a requirement to prove secrecy. A driver-side guess at provenance is precisely the shape triage rejected on #7929, and an inverted default would silently restore the original disclosure — including the sentence naming which column is the object's tenant-isolation column. Any design where "the mark is missing" lands on the disclosing branch is wrong, however convenient.

⚠️ One pin A is expected to REWRITE — it is not a regression

packages/runtime/src/cross-field-refusal-operand-withhold.test.ts contains a byte-equality assertion: an author-written $field filter and a policy-injected one must produce identical messages. That pin is the strongest available statement of "the driver cannot tell them apart" — which is exactly the fact A is chartered to change.

Whoever implements A must rewrite that assertion deliberately, and say so in the changeset. ⛔ Do not treat it as a failing test to make green by weakening it, and ⛔ do not delete it — replace it with its successor: marked and unmarked now differ, and the unmarked-by-default case still withholds. Left unexplained, this pin reads like a bug to the next agent who runs the suite.

Scope notes

Done means

An author's own cross-field filter names its columns again; a policy-injected one does not; an unmarked one does not; and all three are pinned at the two real merge boundaries with a real driver, not a mock.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions