Repository navigation
[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
Activity
Triage: half-annotated shape completed ⇒
pm:queue, type Feature (maintainer-sanctioned A of the 2026-08-12 #7929 ruling — no new decision needed; the ruling and its scope are already on this card).Lane anchoring — one label, and it is
domain:spec: the deliverable's contract half is a provenance mark declared inpackages/spec, and any card touchingpackages/specroutes to the spec seat as sole owner. Removed the second lane label (domain:drivers) — one card, one lane; the driver/boundary consumers stay in card scope, not in the label.Cross-domain single-PR exception (designated here): the card is correctly argued as unsplittable — the mark set at only one merge boundary is half-live. The
domain:specseat (#6017) takes it under the exception path: the claim comment must declare the full file surface (packages/specmark +plugin-security/src/security-plugin.ts~2198 merge +service-analyticsObjectQLStrategy.withReadScope+drivers/driver-sqlanddriver-tursoRemoteTransport+packages/runtime/src/cross-field-refusal-operand-withhold.test.tsrewrite) and run the targeted in-flight check against the identity / services / drivers lanes' claimed surfaces before dispatch.Model (not discretionary): expands the contract's public face ⇒
claude-fable-5per the 2026-08-12 tiering clause. Size: L.Related state set this round: #8197 graded as evidence for this card's scope and parked
pm:blockedon it — on landing, its unlock re-measure is part of proving A closed the class. The card's own fail-closed invariant (unmarked ⇒ withheld) and the #7865/#8116 prior-art check are dev must-verifies; the byte-equality pin rewrite must be explained in the changeset, per the card.
Generated by Claude Code
Claim: PM loop round 2 (maintainer 2026-08-13: 「任务很多,并发加到3」)
Session:session_01Euoy6wyfzgiWtgCg4s6JK2
Branch:claude/issue-8220-read-scope-provenance-mark
Worktree:objectstack-issue-8220
Domain:domain:spec(cross-domain single-PR exception, designated by triage 2026-08-12 — full file surface declared per that path)
File surface (FULL declaration, exception-path requirement):packages/spec(the provenance mark declaration — beside/related to the filter contract, NOT the #8342 injected-column module, which is prior art for style only),packages/plugins/plugin-security/src/security-plugin.ts(~:2198 CRUD-injection merge),packages/services/service-analytics(ObjectQLStrategy.withReadScope),packages/drivers/driver-sql,packages/drivers/driver-turso(RemoteTransport),packages/runtime/src/cross-field-refusal-operand-withhold.test.ts(deliberate pin REWRITE, explained in changeset), pins at both merge boundaries; changeset (stop on breach; explain in the report)
Container & model:L,mode:cloud,model: claude-fable-5(mandatory clause — expands the contract's public face; per the triage designation "Model (not discretionary)")
Serial constraints cleared — targeted in-flight check run across the three affected lanes (exception-path requirement), read from each claim's declared surface:- identity:
sys_audit_log.actiondeclaresrestorewith no writer anywhere —actionFor()structurally cannot return it, while a comment and a shipped list view both assert it is covered #8315 in flight →plugin-audit(sys_audit_log.action) — disjoint fromplugin-security. - services: [services half of #7990] Close the datasource/connector credential write/read paths: scrub
getDatasource().config, fix the false "credential-stripped" claim, and write the stored-cleartext-rows migration story #8081 in flight →service-datasourcecredential paths; [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 →plugin-webhooksheaders — both disjoint fromservice-analytics. - drivers: no in-flight claim (lane list empty at check time).
- this lane:
SETTINGS_CRYPTO_UNAVAILABLEhas no wire spelling — the fail-closed settings refusal answers a generic 500 a client cannot branch on #8273 → error-code-ledger + service-settings; [finding]options.upsertis accepted byengine.update()'s option surface and never read — a declared-but-unenforced key (ADR-0049) #8057 → objectql engine + kernel schema; feat(lint): ADR-0091 seed pair crosses the runtime publish gate (#8307) #8390 (queued) → lint — all disjoint. - Provenance-concept serialization satisfied: the hold "not before finding: author-time expression validation resolves injected anchors on external objects but cannot warn they are unprovisioned — the #7865 provenance marker is unreachable from @objectstack/lint #8116 lands" released at PR feat(spec): sink the #7865 injected-column provenance derivation into @objectstack/spec/data and warn at lint time #8342's merge (
2d8dba312); its landedinjected-system-column-provenance.tsis the prior-art read the card's scope notes require — a SECOND parallel provenance concept must not be invented where one mechanism can serve, and the dev is instructed to read it first and justify the separate mechanism (filter-subtree marks vs column provenance are different questions; the justification burden is on the design). - driver-sql: the #7929 withhold covers the cross-field family only — every other INVALID_FILTER refusal still names the target field, which is admin-authored on a read-scope predicate #8197 graded as evidence for this card and
pm:blockedon it (triage 2026-08-12) — unlock re-measure at landing is part of acceptance.
Generated by Claude Code
- identity:
- added a commit that references this issue
on Aug 13, 2026 Collection note (hot-handover provision per #8428; written by the dispatching seat
session_01Euoy6wyfzgiWtgCg4s6JK2so ANY successor can collect this card without that session):- Dev: cloud session
session_01AkiE189Csm7aBT2qCVSx3X(started 10:59Z), branchclaude/issue-8220-read-scope-provenance-mark(last pushf4210c6~12:04Z). Deliverable contract: draft PR titled for this card +<!-- os-dev-report -->comment here. - Review criteria (from the dispatch, in claim comment above): ① fail-closed invariant — unmarked/ambiguous ⇒ WITHHELD, pinned; any design where "mark missing" discloses is a REWORK; ② the byte-equality pin
cross-field-refusal-operand-withhold.test.tsREWRITTEN (not deleted/weakened) with the changeset explaining it; ③ mark declared inpackages/spec, set at BOTH merge boundaries (plugin-security ~:2198, service-analyticswithReadScope), consumed by driver-sql + tursoRemoteTransport; ④ prior-art justification vsinjected-system-column-provenance.ts(two sentences in PR body); ⑤ file surface = the six-package set in the claim; ⑥ REST 5xx-withhold (analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367/fix(service-analytics,rest): analytics dimension 的源字段闸门 —— 不存在的 dimension 答 400 INVALID_FIELD,dataset 500 不再回显 SQL (#5520) #5667) untouched; ⑦ done-means: author's filter names columns again / policy-injected doesn't / unmarked doesn't, pinned with a REAL driver. - Landing steps: gates green on the PR head (job conclusions, not aggregate) → ready + auto-merge SQUASH (the API may echo "merge" — the queue squashes;
pull_request.enqueuedevent is the proof) → MERGED: verify squash commit onorigin/main, clear this card'spm:dispatched, run driver-sql: the #7929 withhold covers the cross-field family only — every other INVALID_FILTER refusal still names the target field, which is admin-authored on a read-scope predicate #8197's unlock re-measure note (it ispm:blockedon this card), and update seat post [PM seat] domain:spec — 🟢 os-tesla · session_01VZqqwTj2wsihZEbfT6yyYN #6017. - Liveness: if the cloud session goes silent ≥2h with no push, probe it (
SendMessageto the session ID /get_session); the direct-acceptance fallback (PR green + probe-dead + no report ⇒ review from the PR itself) applies as usual.
Generated by Claude Code
- Dev: cloud session
{ "issue": 8220, "status": "review_ready", "branch": "claude/issue-8220-read-scope-provenance-mark", "pr": null, "premise_still_valid": true, "summary": "A of the #7929 ruling is implemented end-to-end on the six-package surface the claim declared. DECLARED: packages/spec/src/data/filter-subtree-provenance.ts — FilterSubtreeProvenance ('author'|'policy'), markFilterSubtreeProvenance (non-enumerable Symbol.for key, first-mark-wins, silent no-op on frozen input), filterSubtreeProvenanceOf, and resolveFilterSubtreeProvenance(root, node) (positional, innermost-mark-wins, identity-located; unreachable/conflicting -> null). The mark crosses the driver boundary ON the where tree; DriverQuery grows no slot and its TSDoc declares the crossing and the fail direction. SET at both boundaries: plugin-security's RLS injection marks every injected scope (RLS/CBP/delegator/deny-sentinel) 'policy' and the caller's predicate 'author' only under the identity vouch opCtx.ast.where === opCtx.options.where (a sibling-rewritten tree is vouched for nobody); service-analytics' ObjectQLStrategy.withReadScope marks scope 'policy' and the strategy-built user filter 'author' (resolveFkAttr's scope arm 'policy', its internal idFilter deliberately unmarked). CONSUMED: driver-sql resolves withheld refusals against the query's own where root at its two existing seams (applyFilters catch for eager refusals; withWithheldFilterLog, now threaded with the root, for knex's lazy group callbacks) — 'author' gets a fresh INVALID_FILTER/400 carrying the full diagnostic (both columns, operator, list index, boundary reason); 'policy'/unmarked/unreachable/ambiguous keep #8198's redaction + server-log relocation. driver-turso's RemoteTransport applies the same resolution at buildWhereSQL entry under the same Symbol.for keys; driver-sqlite-wasm inherits and is pinned by the runtime suite. FAIL-CLOSED INVARIANT pinned at every layer incl. byte-equality unmarked==policy and a JSON-round-trip-drops-author-mark pin. PRIOR ART: #8116's injected-system-column-provenance was read first — it is COLUMN provenance (a derivation over metadata documents at rest: is storage behind an injected column), this is FILTER-SUBTREE provenance (an attestation about one query value in flight, knowable only at the merge moment); folding them would give the column verdict a mutable runtime carrier it must not have and the filter mark a document derivation it cannot have — separate mechanism justified in the module headers. TWO B-era pins rewritten deliberately, both explained in the changeset: (1) packages/runtime/src/cross-field-refusal-operand-withhold.test.ts byte-equality pin -> successor three-way split (policy withholds; vouched author names columns on BOTH real faces — CRUD and POST /analytics/query; unmarked withholds byte-identical to policy); (2) service-analytics cross-field-engine-fallback.test.ts pinned B's blanket redaction on the caller's OWN where — those callers are now the vouched author, so the routed cases assert the corpus diagnosticIncludes fragments back on the wire, with the policy-scope case gaining explicit non-disclosure assertions as the fail-closed pair. Changeset: minor spec + patch x4 consumers, non-breaking, adr-0087 gate green with no disposition needed.", "tests": "All green. New: spec filter-subtree-provenance.test.ts (15), driver-sql sql-driver-cross-field-provenance.test.ts (11, incl. the lazy group-callback disclosure seam), driver-turso remote-transport-cross-field-provenance.test.ts (7), plugin-security read-scope-provenance-mark.test.ts (4), service-analytics read-scope-provenance-mark.test.ts (5). Rewritten: runtime cross-field-refusal-operand-withhold.test.ts (11), service-analytics cross-field-engine-fallback.test.ts (93). Full suites: spec 10346, plugin-security 1036, driver-sql 1431(+52 skipped pre-existing), objectql 3496 (read-only dependency check), driver-sqlite-wasm 393, driver-turso 973, service-analytics 1675, runtime 2309. pnpm typecheck 127/127 (one transient SIGKILL-137 on plugin-hono-server at full concurrency, clean at --concurrency=4 and standalone). spec check:generated all 13 artifacts current (api-surface + export-origins regenerated for the new exports). dispatch-gates set run green: adr-anchors, changeset-gate-self-tests, adr-0087-registration, cross-package-test-inputs, doc-formula-expressions, docs-audit-scope, driver-conformance, durability-log-level, i18n, merge-driver, release-body, spec-parsed-alias, test-source-alias, type-source-resolution, changeset-fixed, dev-prereqs, query-options-erasure, type-check-coverage, nul-bytes, startup-registry-verdict. ESLint clean on every changed file.", "open_questions": [ "BLOCKER (environment, not code): this dev session's egress proxy refuses ALL api.github.com calls with 'GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App' (403 on read and write; no gh CLI in the image). Git push works, so the branch is the recovery index (tip f4210c60b061dfd78537a8f36c87e6f016256024), but I cannot open the draft PR or post this report as the #8220 comment myself. The PM is asked to open the draft PR from claude/issue-8220-read-scope-provenance-mark against main (first line 'Fixes #8220') and post this report verbatim.", "The author-restored tenant-arm message states which column is the tenant-isolation column TO THE VOUCHED AUTHOR (who typed that column name into their own filter). The card's 'Done means' grants the author 'the boundary reason', and the reason sentence names the column's role; if the maintainer wants the role sentence withheld even from a vouched author, it is a one-line change in uncompilableFieldReferenceError's authorText." ], "out_of_scope_findings": [ "#8197 unlock (pm:blocked on this card): NOT absorbed. Its class — refusals OUTSIDE the cross-field family naming the target field — is behaviourally unchanged by A. What the re-measure should now show: read-scope-injected predicates arrive at the driver marked 'policy' from both boundaries, so those refusal families (e.g. INVALID_FIELD on an undeclared target, the $between arity refusal) have a real, spec-declared mark available to consume; closing #8197 becomes 'consume resolveFilterSubtreeProvenance in those builders' rather than 'invent provenance'. The blanket alternative (withhold the target field from every author) is now strictly dominated.", "plugin-sharing's read-path merge (sharing-plugin.ts composeAnd sites, ~:871/:896) is a THIRD read-scope merge boundary outside this card's file surface. Its injected filters arrive unmarked, which withholds — fail-closed, correct — but an author's filter on an object with active sharing rules loses the identity vouch at the security boundary (ast.where already rewritten) and stays redacted. Marking at plugin-sharing (policy on its filters; it could also preserve the vouch) would restore the author diagnostic in that composition; needs its own card in the sharing lane.", "The $not rewrite path (nullSafeNegationOperand) creates new nodes the resolver cannot find under the root, so an author's cross-field refusal nested under $not stays redacted (fail-closed; walk-raised refusals on the ORIGINAL nodes are unaffected). Documented at the site; a card only if a real author hits it.", "engine.resolveWhereTokens rewrites the tree when context tokens resolve, which breaks the identity vouch and (correctly) withholds; an author using {current_user_id} tokens in a cross-field filter keeps the redacted message. Noted here so the symptom has a written cause." ] }(Relayed verbatim by the dispatching PM
session_01Euoy6wyfzgiWtgCg4s6JK2— the dev's cloud session has no GitHub API egress; provenance and the environment blocker are recorded in the report's own open_questions. Draft PR opened by the PM: #8429.)
Generated by Claude Code
ACCEPT — reviewed by the dispatching seat (session
session_01Euoy6wyfzgiWtgCg4s6JK2) against PR #8429's diff (17 files), not the report's self-description.Verified against the collection-note criteria: ① fail-closed invariant pinned at every layer (incl. the byte-equality unmarked==policy pin and the JSON-round-trip-drops-the-mark pin — the two ways the default could silently invert); ② the B-era byte-equality pin is REWRITTEN (+118/−27, three-way successor), not deleted or weakened, and the service-analytics engine-fallback pins flip WITH explicit non-disclosure assertions as the fail-closed pair; ③ mark declared in
packages/spec(new module +DriverQueryTSDoc declaring the crossing), set at BOTH boundaries (security-plugin +33 with the identity-vouch condition — a sibling-rewritten tree vouched for nobody is exactly the right paranoia; objectql-strategy +20), consumed by driver-sql at its two real seams (+187, incl. the lazy knex group-callback seam) and turso's RemoteTransport, wasm inheriting; ④ the #8116 prior-art separation is argued in both directions (column provenance must not gain a mutable runtime carrier; filter-subtree provenance cannot have a document derivation) — accepted; ⑤ file surface exact to the exception-path declaration; ⑥ REST 5xx-withhold untouched; ⑦ done-means pinned with a real driver.For the maintainer, non-blocking: the report's open question 2 — the vouched author's restored tenant-arm message names the column's tenant-isolation ROLE (the card's "Done means" grants the author the boundary reason, so this is within the ruling; the author typed that column name themselves). If you want the role sentence withheld even from a vouched author, it is a one-line
authorTextchange — say so and it rides the next spec-lane card.Out-of-scope findings: the dev's environment had no GitHub API (see the report), so the PM verified and filed on its behalf — the plugin-sharing third merge boundary is filed as a card (see cross-reference below); the
$not-rewrite andresolveWhereTokensredaction notes stay documented-at-site per the dev's own grading (fail-closed, no author has hit them). #8197's unlock re-measure note is recorded in the report — it unblocks at this card's landing with a spec-declared mark to consume.Landing: draft until gate jobs conclude on the PR head; flip + auto-merge (squash) on the timed check; regen relay slot is free.
Generated by Claude Code
3 remaining items
Correction to a load-bearing sentence in this card's body — measured on #8197, recorded here because this is where the next reader will find it
This card is closed and its work landed. One sentence in its body has since been measured false, and it is exactly the sentence a future author would rely on:
#8197 is probably evidence for this card's scope, not independent work. […] A's mark closes that class without the blanket cost of withholding the target field from every author.
That was written before A was implemented, and it assumes marked/unmarked maps onto policy/author. Measured on
origin/mainc8806ae0aagainst a realSqlDriver, running each predicate four ways (unmarked / policy-marked / author-marked / author-arm inside a merged$and):- The mark structurally reaches the non-cross-field
INVALID_FILTERsites — this card threadsrootintoapplyFilterCondition, andresolveWithheldFilterRefusalresolves against it. - It is not consulted there: those builders never pass through
withheldFilterError, so the resolver short-circuits on a null diagnostic and returns the disclosing error untouched. - The author-vouch surface is two call sites. So "unmarked" is not a synonym for "policy-authored" — it holds a large population of genuine authors, including every direct/driver-level path and every object with active sharing rules, where
plugin-sharingrewritesast.whereand the security identity vouch then vouches nothing (plugin-sharing's read-path merge is a THIRD read-scope boundary the #8220 provenance mark does not cover — sharing-composed queries withhold the author diagnostic (fail-closed, but restorable) #8430).
⇒ The blanket author cost this card believed its mark avoided is still present — relocated, not removed. Consuming the mark at those sites with the fail-closed default intact does not merely withhold from policy; it strips the target-field name from every unmarked subtree.
Nothing here impugns what this card built: A's mark works as specified at the boundaries that set it, and the fail-closed invariant is right. What is wrong is only the scope claim about #8197 — which is why #8197 is now
needs-user-decisionrather than a mechanical follow-up, with the disclosure trade-off escalated to the maintainer.Recorded rather than left standing, because a confident sentence in a merged card's body is indistinguishable from an established fact to whoever reads it next.
Generated by Claude Code
- The mark structurally reaches the non-cross-field
- added a commit that references this issue
on Aug 16, 2026 - added 3 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 24, 2026 - added 3 commits that reference this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 1, 2026 - added 5 commits that reference this issue
on Oct 7, 2026
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 (mergeda5dcb74, 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-fieldINVALID_FILTERrefusal 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 bareFilterConditioninwhere,DriverQueryisOmit<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
packages/spec. Declared, not conventional — the whole point is that the driver may trust it because the contract defines it.plugin-security's CRUD injection — theast.where = ast.where ? { $and: [ast.where, scoped] } : scopedmerge aroundsecurity-plugin.ts:2198;service-analytics' read-scope merge (ObjectQLStrategy.withReadScope).driver-sqlconsumes 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 boundaryreason.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.
packages/runtime/src/cross-field-refusal-operand-withhold.test.tscontains a byte-equality assertion: an author-written$fieldfilter 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
applySystemFieldsinjects platform anchors intoexternalobjects the platform provisions no storage for — three consumers have now independently re-derived "that column is not really there" #7865 provenance marker" in the context of injected anchors on external objects. I have not confirmed whether that marker generalises to read-scope filter subtrees; it may be an unrelated mechanism. Read it first — a second, parallel provenance concept inpackages/specwould be worse than either alone.driver-tursocarries its own copy of the refusal (RemoteTransport.uncompilableComparand), fixed in fix(driver-sql,driver-turso): a cross-field$fieldrefusal stops naming the two columns it compared (#7929, #7988) #8198;driver-sqlite-wasminherits fromSqlDriver. A has the same three-driver surface.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.