Skip to content

fix(objectql): the read-only strip stops calling the addressed row's own id a forged caller write (#8141) - #8216

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-8141-readonly-strip-address-warn
Aug 12, 2026
Merged

fix(objectql): the read-only strip stops calling the addressed row's own id a forged caller write (#8141)#8216
os-zhuang merged 2 commits into
mainfrom
claude/issue-8141-readonly-strip-address-warn

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8141

The read-only strip logged, on every single-record update of every platform object, that the addressed row's own primary key was a caller forgery:

Field 'id' on 'sys_user_preference' is read-only: the caller-supplied value was
DROPPED and the update is being COMMITTED WITHOUT IT — ...

All three claims are false for that write. The REST ingress folds the path id into the body (#6479) before the engine's caller snapshot, so the value was never caller-supplied; nothing the caller wanted was dropped; nothing it asked for was left out of the commit. #8093 fixed this in the report channel (droppedFields / onFieldsDropped) and deliberately left the strip and its log alone. This is the log half.

The change

stripReadonlyFields gains options.addressKey — the key that carries the write's address. That key is still stripped; it just no longer logs. The by-id update branch passes it from the same idAddressesThisRow predicate #8093 already wired to the report channel, so the two channels cannot disagree about what an address is (one predicate, two consumers — no second derivation).

Ruled scope, honoured: option A only. Not "stop stripping id" (a same-value primary-key write is a no-op on SQL but an outright rejection on stores with immutable primary keys); not option B (removing the address before the read-only pass changes what the driver receives for objects whose id is not readonly — #6435's explicitly separate decision).

The acceptance pair, predicted then measured

Reverse verification: drop the addressKey argument at the engine.ts call site, leaving everything else — including #8093's report exclusion — in place.

predictedobserved
card's repro (by-id, body carries no id)RED, gains the id lineRED, gained exactly readonlyStripWarning('id', 'pref')
canonical update(obj, { id, ...fields })RED, same lineRED, same line
address + a really-forged readonly fieldRED, two warns instead of oneRED, two warns
ruled-non-id data.id (primary_key strip)GREENGREEN
MULTI branchGREENGREEN
object whose id is not readonlyGREENGREEN
isSystem callerGREENGREEN
#8093's whole report-channel suiteGREENGREEN

3 failed / 6 passed in the new file, 0 failed everywhere else — the prediction held exactly. The direction did not invert the way #8093's strict rows did: this option feeds no counter and no derived refusal. strictReadonlyWrites is keyed on reportDroppedFields, which already excluded the address, so the loud half cannot move when only the log line does.

With the fix in place, the tripwire is asserted positively: a forged readonly field still warns, compared with toBe against the exported readonlyStripWarning (so a reworded line fails) and with the emitted level asserted (so a downgrade fails).

The message wording and its remedy

Checked as asked, and not changed — the acceptance bar pins it unchanged for the cases that still print, and it holds up:

  • "caller-supplied" and "DROPPED" are true for every case that still reaches the line (the supplied-snapshot own-property plus Object.is guards make that exact);
  • the { context: { isSystem: true } } remedy becomes correct by construction once the line prints only for callers that really did write a read-only column — the card's complaint was that following it for the address write would exempt that caller from the strip entirely, and the address write no longer prints.

Two remaining wording defects were found, measured, and filed rather than folded in, because fixing either changes the text this PR's acceptance bar pins as unchanged:

Verification

  • packages/objectql full suite: 193 files / 3415 tests passed (with the fix; the one earlier failure was this PR's own draft expectation, corrected to the measured preserveAudit behaviour).
  • pnpm --filter @objectstack/objectql typecheck — exit 0.
  • Gates derived with node scripts/pm/dispatch-gates.mjs over the changed paths, all exit 0: check:adr-anchors, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:engine-double-contract, check:error-code-casing, check:objectui-changeset, check:stack-collection-maps, check:nul-bytes, check:query-options-erasure, check:type-check-coverage, check-changeset-no-major.mjs, check-engine-split-ratio.mjs. No baseline or ratchet was raised.
  • check-objectui-pin-fresh.mjs exits 1 at this base and is not caused by this branch: it compares .objectui-sha against objectui main, a file this PR does not touch (last bumped by chore: bump the console pin to objectui 6d77acfe3125 (123 commits, not 405) #8137).

Changeset: patch for @objectstack/objectql — observable log behaviour changes, so this is not a skip-changeset PR.

Cross-seat: none. packages/spec/src/contracts/data-engine.ts was not touched; #8093's WriteObservabilityOptions invariant still reads true, because this change moves no report, no refusal and no payload.

Generated by Claude Code


Generated by Claude Code

…own id a forged caller write (#8141)
WIP scaffold — implementation in, tests and changeset to follow.
`stripReadonlyFields` gains an `addressKey` option: the named key is still
stripped, it just no longer logs. The by-id update branch passes the SAME
`idAddressesThisRow` predicate #8093 wired to the report channel, so log and
report cannot disagree about what an address is. Every other call site passes
nothing and is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014C8pAprWdmtecFsEprZax4
… must not silence (#8141)
Engine-level acceptance pair on a real ObjectQL + recording driver/logger:
the card's measured repro (by-id PATCH, body carrying no `id`) emits no WARN
and the driver's SET clause is unchanged; a really-forged read-only field in
the same write still WARNs byte-identically against the exported message, at
`warn`. Plus the multi branch, the ruled-non-id `primary_key` diagnostic, a
non-readonly `id`, and the `isSystem` exemption, all unmoved.
Helper-level cases cover `addressKey` opt-in-ness (no option ⇒ the line still
prints, which is what keeps the other two call sites unchanged), composition
with `preserveAudit`, and the runtime-owned message.
Changeset: patch, this changes observable log behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014C8pAprWdmtecFsEprZax4
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 8:51pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx(via packages/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31643011741 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (3/3) — 失败步骤: Boot example apps and exercise real user flows

    �[41m�[1m FAIL �[22m�[49m �[30m�[46m isolated �[49m�[39m test/federated-phantom-share-grant.dogfood.test.ts�[2m > �[22m[#8119] federated phantom anchor: single-record gates + share posture�[2m > �[22m
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 6 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The read-only strip still logs a WARN calling the addressed row's own id a forged caller write, on every single-record update of a platform object

2 participants

@os-zhuang@claude