Repository navigation
fix(objectql): extend the credential-column guard from aggregates to filter and sort keys - #27477
Conversation
…filter and sort keys Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
…system; ledger the guard's reference read Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
…s at the REST data door Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
…ding double's find Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
…ial-field-predicates
📓 Docs Drift CheckThis PR changes 1 package(s): 48 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b0a62a912c32f9e97e676d040a907842b4db55aa && git checkout b0a62a912c32f9e97e676d040a907842b4db55aa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 5eb62f096eec61828542a2bba3bfa57628843f61 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 5eb62f096eec61828542a2bba3bfa57628843f61
node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc
|
…iter census Claude-Session: https://claude.ai/code/session_017xRBcSwDXZrZg7nGjP6P3U Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Face: narrowing. The diff touches no Security-hardening ruling: this record names positions and refusals; it carries no reproduction. ① Derived judgmentsThe accept set the diff changes, read off the code:
② Semver levelDeclared: What is right and stays: ③ Boundary flags
④ Design judgmentOn the narrowing face, against the published spec on Must-fix
Should-fix
Implemented-by: VERDICT: FAIL The refusal is complete across every position the PR claims, the system-context exemption is unreachable from the wire, and the presence check is safe to keep; the FAIL is the declaration alone (must-fix 1). A head that carries the corrected declaration and nothing else owes a delta review by this reviewer, not a full round. |
The changeset now declares `Clause-②: no (narrowing)` with a BREAKING banner, a FROM / TO that names the import upsert match consequence, and one ADR-0087 not-required (no-migration-prescription) marker. The two declared-type door tests' comments now state the real order: the door runs first inside lowerWhereFilterArray and the guard answers the rows it passes. No behaviour change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk {
"issue": null,
"status": "done",
"branch": "claude/harden-credential-field-predicates",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27477",
"head": "5eb62f096eec61828542a2bba3bfa57628843f61",
"session": "session_017xRBcVy4VXmkQjn2MGeaWk (subagent: the parent's id)",
"premise_still_valid": true,
"summary": "Declaration-only rework of the contract review (comment 6495005521) on one new commit 5eb62f0 (base 472e55c, no merge of origin/main needed, no force-push, PR left draft). Changeset .changeset/credential-column-filter-sort-guard.md now declares `Clause-②: no (narrowing)` + `Metadata-protocol: none` at `patch`, carries exactly one ADR-0087 marker `not-required (no-migration-prescription)` (no metadata key, spelling, export or stored shape moves; what narrows is a runtime read door), and a `**BREAKING** (an accept-set narrowing)` banner with FROM / TO / Fix modelled on the #22646 entry; FROM / TO names the import consequence (an `upsert` / `update` import whose `matchFields` names a credential column now meets the refusal in `findExisting`'s lookup and that row is reported failed, not written). Should-fix 3: the comment in engine-number-comparand-declared-type-door.test.ts and engine-text-operator-declared-type-door.test.ts now says the door runs first inside lowerWhereFilterArray and the guard answers the rows it passes; no code change. PR BODY NOT WRITTEN BY ME: the os-dev contract forbids a dev PATCH of the PR body (dev writes it once, at pr_create), which conflicts with the dispatch asking for a PR-body change; per that contract the seat writes it. The complete replacement body (lines 1-3 `Clause-②: no (narrowing)` / `Metadata-protocol: none` / `Skills-docs: follow-up owed — skills/objectstack-data/rules/security.md`, a BREAKING sentence, the import-match bullet, the corrected fixture-triage sentence, the 维护者速读 risk line and `需要你做的` updated, plus a declaration-round Verification block) is at /tmp/claude-0/-home-user-objectstack/bdda939c-7424-5594-af38-0e6fe5d7eeed/scratchpad/pr-27477/prbody.new.md. It omits the old session-URL footer; add the footer per platform-readings, and read the body back after writing it. Skills-docs: I picked `follow-up owed`. The skill teaches `type: 'secret'` and says nothing about querying it, so an AI author following it can still write a refused filter.",
"tests": "At head 5eb62f0: `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-number-comparand-declared-type-door.test.ts src/engine-text-operator-declared-type-door.test.ts src/credential-evaluation-guard.test.ts` under the lock: VERDICT command-exit 0, Test Files 3 passed (3), Tests 88 passed (88). `pnpm --filter @objectstack/objectql typecheck`: VERDICT command-exit 0; check:test-typecheck OK, 39 files / 233 errors / 64 pinned signatures held. Built `@objectstack/objectql^...` first (VERDICT 0). No ablation: no tested behaviour changed, only a changeset and two test comments.",
"gates": "Declaration gates: `pnpm check:adr-0087-registration` exit 0 (self-test 705 assertions; `1 declared-breaking changeset(s)` `[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`). `node scripts/check-changeset-no-major.mjs` exit 0 with no event (level axis NOT APPLICABLE locally). With `--event` built from the draft replacement body, exit 0: `LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol none ... a non-protocol change at patch`. With the current live body, exit 0 and arm `none declared`. Derived list (`dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, tree 5eb62f0, 12 commits behind origin/main, stale only in scripts/pm/check-owed-reviews.mjs and record-recognisers.mjs): 96 commands, all run with exit codes captured before any pipe. 94 gave exit 0. Three of those first exited 3 on a missing prerequisite and gave exit 0 on re-run: check-plugin-teardown-shape --self-test after the pinned fixture commit was present, check:lean-entry-closure after the objectql dist was built, and spec check:skill-examples after the client-react closure was built. 2 NOT MEASURED locally (exit 3, they need a whole-repo build): check:dual-build-cjs-loads and check:type-check-debt. `--ran` exit 0: 96 derived, 94 run, 2 NOT-MEASURED, 0 UNRUN. CI check-runs at 5eb62f0, all completed: Lint & Repo Gates success (it carries the 2 NOT MEASURED), TypeScript Type Check success, Test Core success (8/8 shards), Build Core success, Governed Surface Queue Guard success, Check Changeset success, Type Check UI success, Test UI success, Build & E2E UI success, Live E2E UI success, Lint UI skipped. Those check-runs ran against the CURRENT PR body (`Clause-②: no`). Once the seat writes the new body, Check Changeset / no-major re-read the declaration from the edit event.",
"dogfood_battery": "5eb62f0 · owed by the `(narrowing)` arm · closure `@objectstack/dogfood^...` rebuilt, packages/runtime/.objectstack cleared · lock VERDICT command-exit 0 (held 1618s) · Test Files 348 passed, 3 skipped (351) · Tests 2808 passed, 20 skipped (2828) · no failures. The first attempt's build hit lock exit 99, and its test ran on an unbuilt closure (348 files failed). That run was NOT MEASURED and is not counted. It was re-run from the same slot.",
"mcp_calls": "0",
"api_writes": "1 — POST repos/objectstack-ai/objectstack/issues/27477/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). git push of 5eb62f0 is not a REST write. No PR body, label or ready-state write.",
"deviations": [
"The harness attribution asked for a `Co-Authored-By` trailer that names the model. The commit carries AGENTS.md's model-free pair (`Co-authored-by: Claude` + `Claude-Session`) instead: the pre-push hook refuses a model identifier, and AGENTS.md takes precedence.",
"Dispatch vs os-dev contract: the PR-body change was asked of me, but the contract says a dev never PATCHes a PR body. I prepared the replacement body for the seat to write and did not write it myself.",
"I waited once (no kill) on a PID found with pgrep -f, for my own client-react build after the harness backgrounded it. Discipline is to wait only on a recorded PID."
],
"open_questions": [],
"out_of_scope_findings": [
"class: c · reach: named producer: an object's authored `searchableFields` (stored object metadata) listing a `secret` / `password` field. The engine now silently withholds it from the search scan set, while the #22646 precedent refuses an explicit `$searchFields` naming an `internal` column loudly. Evidence: contract review ③-c and should-fix 1. Wanted fix: refuse such an entry at the spec door, and extend the data door's explicit-`searchFields` refusal to credential types. It changes what an author can write, so it gets its own card, not a rider here. Dedupe words: searchableFields credential secret password, searchFields refusal credential, silently inert searchableFields",
"carrier: seat (Skills-docs follow-up owed) · skills/objectstack-data/rules/security.md: one sentence saying a credential column is never a filter or sort key, only a has-a-value check. The seat files it and rewrites the `Skills-docs:` line to `#N` before ACCEPT.",
"carrier: seat (PR title) · noted, not filed. The PR title lacks the `!` that the BREAKING declaration implies (suggest `fix(objectql)!: extend the credential-column guard from aggregates to filter and sort keys`). The seat edits titles."
]
} |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Delta review by the same reviewer of the full record at head 472e55c (comment 6495005521, FAIL on the declaration alone). The delta is one commit, 5eb62f0, declaration-only: the changeset body and two test-file comments. No source, test logic, docs page or census pin changed; every ①, ③ and ④ judgment of the full record stands unchanged and is not re-stated. The ADR-0100 hardening ruling holds: positions and refusals only, no reproduction. ① Derived judgments
② Semver levelDeclared now, in the changeset body and the PR body alike: ③ Boundary flags
④ Design judgmentUnchanged from the full record: the narrowing takes no declared capability away at the consumer and makes ADR-0100's read-mask promise real at every position that evaluates the stored value, the same disposition #22646 settled for Must-fix: none. Should-fix: none beyond what the seat already carries (the Implemented-by: VERDICT: PASS |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。增量契约复核 PASS(6497520141),dogfood 全套 348 个文件、0 失败;Skills-docs 后续已立卡 #27660。 席位核对
Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: #27660
Hardening, ADR-0100 family. No issue is linked: this change was dispatched directly, so there is no closing keyword. BREAKING (an accept-set narrowing), shipped as
patch: this PR narrows a query shape that was accepted before, and no metadata an author writes changes. The changeset carries the FROM / TO and one ADR-0087not-required (no-migration-prescription)marker. It needs one contract-review-tier review before it enters the queue.维护者速读
password/secret)做分组和 min/max 聚合,现在也拒绝把它们当过滤条件或排序键,统一返回400 INVALID_FIELD,driver 不会被调用。isSystem)不受影响,服务端自己的凭据查找照常工作,登录已在 dogfood 中验证。按凭据列过滤或排序的非系统调用会改为收到 400;以凭据列作matchFields的 upsert / update 导入,其匹配查找同样被拒,相关行报失败、不写入。回滚只需 revert 本 PR。packages/objectql/src/credential-evaluation-guard.ts(风险在isCredentialPresenceCondition与系统上下文豁免那一行),packages/objectql/src/engine.ts(六个入口的接入点),packages/qa/dogfood/test/encrypted-field-behavior.dogfood.test.ts。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/credential-evaluation-guard.test.ts,预期 30/30 通过。What changes
rejectCredentialEvaluation(new,packages/objectql/src/credential-evaluation-guard.ts) reads the same exemption-free collector asrejectCredentialAggregation,collectCredentialFields. It answers with the same ADR-0112 envelope:INVALID_FIELD/ 400, withfield,fields,objectandparam. It runs onfind,findOne,count,aggregate, and predicateupdate/delete. It runs right after thewherearray-form lowering and before the search expansion, token resolution, the middleware chain or any driver call.where(every logical branch), a{ $field }cross-field comparand,orderBy, eachaggregations[i].filter, and eachexpandentry's ownwhere/orderBy, at every depth, against the target object. A nested-relation condition is judged by the related object's own read, which passes through the same guard.null,$null,$exists,$empty,$eq: null,$ne: null, or$notover one of those. It reveals nothing the masked row does not already show. If keeping this check is a product question, this is the safe default. Every other operator is refused.upsertorupdatemode whosematchFieldsnames a credential column looks rows up through the same engine read with the import's caller context, so that lookup is now refused too: each row that reaches it is reported failed with the refusal and is not written.internal: truecolumns. This covers a credential column an object lists insearchableFields.internal: truecolumns: the filter / sort refusal for these stays where it was decided, at the generic data door (assertNoInternalFieldEvaluated). Some of their server-side consumers do not run as system. The new dogfood case pins that door for one such column, beside the password case.content/docs/data-modeling/field-types.mdxstates the refusal.content/docs/permissions/system-context.mdxgains row 18c and a regenerated register for the guard's oneisSystemread.Tests
packages/objectql/src/credential-evaluation-guard.test.ts: a real engine over a recording driver. It covers 13 refusedwhereshapes,orderByonpasswordand onsecret,findOne/count/aggregate/ per-aggregationfilter/update/delete, anexpandentry and a nested-relation condition. Every refusal assertscode,status,httpStatus,fieldandparam, and that the driver was never called. The test also covers eight has-a-value shapes, a system context, non-credential fields and the search scan set.packages/qa/dogfood/test/encrypted-field-behavior.dogfood.test.ts: on the booted showcase REST door, the cases arePOST /data/showcase_field_zoo/querywith equality, range,$startsWith,$contains, asecret-column predicate andorderBy, plusGET /data/showcase_field_zoo?filter=…. Each answers 400INVALID_FIELD, and the response never contains the stored value.$null: falseandnullare served, and so is a control onname. On a scratch object,orderByandwhereon apasswordcolumn and on aninternal: truecolumn are refused. The seeded admin's sign-in in eachbeforeAllcovers the login path.engine-text-operator-declared-type-door.test.tsandengine-number-comparand-declared-type-door.test.ts, thepassword/secretcells now run as a system caller. Those doors run first, insidelowerWhereFilterArray; the credential guard runs after them and refuses a non-system caller on the rows they pass. The doors' verdicts do not depend on the caller.federated-injected-column-readers.test.tsgains the guard'sreferenceTargetOf()row.Acceptance notes
records-forms.encrypted-field-behavior: every acceptance clause and negative still holds, and none is contradicted. The new filter / sort refusal sits beside acceptance[4]. Adding a clause for it is checklist-authoring work and is not done here.isMaskedColumnKeyinui/packages/components/src/renderers/complex/data-table.tsx), so no UI path offers the refused action.Verification
Declaration round, at head 5eb62f0
Clause-②: no (narrowing), the BREAKING banner with FROM / TO naming the import match consequence, one ADR-0087not-required (no-migration-prescription)marker) and the comment in the two declared-type door tests. No behaviour change.pnpm check:adr-0087-registration: exit 0.1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.[BREAKING+clause-②-narrowing] not-required (no-migration-prescription).node scripts/check-changeset-no-major.mjs --eventwith this body: exit 0.LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol none ... a non-protocol change at patch.pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2over the two door tests andcredential-evaluation-guard.test.ts: Test Files 3 passed (3), Tests 88 passed (88).pnpm --filter @objectstack/objectql typecheck: exit 0,check:test-typecheck: OK, 39 files / 233 errors / 64 pinned signatures held.pnpm --filter @objectstack/dogfood test, closure rebuilt): Test Files 348 passed, 3 skipped (351); Tests 2808 passed, 20 skipped (2828).dispatch-gates.mjs --commands): 96 derived, 94 exit 0, 2 NOT MEASURED locally (check:dual-build-cjs-loadsandcheck:type-check-debtneed a whole-repo build).--ran: 96 accounted for. Both are inLint & Repo Gates, which is green on this head.All readings below were taken at head
feb0bdeb57, afterorigin/mainwas merged in, the dogfood dependency closure was rebuilt andpackages/runtime/.objectstackwas cleared.Unit and REST door
pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2: Test Files 485 passed (485), Tests 9123 passed (9123).pnpm --filter @objectstack/objectql typecheck: exit 0.check:test-typecheck: OK, with 39 files / 233 errors / 64 pinned signatures held, unchanged.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/encrypted-field-behavior.dogfood.test.ts: Tests 6 passed (6).pnpm --filter @objectstack/dogfood typecheck: exit 0.Ablation (one time each, with restore proven)
node scripts/ablation-replace.mjs: the guard's entry line was made to always return. The mutation landed (anchor 1 → 0, blob3d38a49b3d6a→316fd9e3b630).credential-evaluation-guard.test.tsthen showed 18 failed / 12 passed of 30. All 18 refusal cases went red. The 12 has-a-value, system, control and search cases stayed green, as they should. Restore: blob equal to HEAD,git diff HEADempty.@objectstack/objectqlrebuild.ablation-dist-preflightfound the marker in 4 built files. The dogfood file showed 2 failed / 4 passed. Both new cases failed on the first refused request, which answered 200 with rows. Restore: rebuilt,--absentcheck clean, and the whole tree clean against HEAD.Local gate subset
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 94 commands atfeb0bdeb57. All 94 were run, each exit code captured before any pipe: 94 × exit 0.--ranreconciliation: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN.check:objectql-double-limitflagged that the recording double read rows before applying the bound.check:query-options-erasurereported the test surface grew 236 → 241. Both were fixed (bound applied after the filter, options typed), and both are exit 0 above.The required CI families (
Lint & Repo Gates,TypeScript Type Check,Test Core,Build Coreand the rest) run on this PR's check-runs. They are not re-run here.Body refreshed by the PM seat 2026-10-29T16:41Z: Skills-docs follow-up filed as #27660.
🤖 Generated with Claude Code
https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk