Skip to content

fix(objectql): readonly 剥离改为「说清后果 + 给出出路」,并把写路径语义写进文档 (#4903) - #5123

Merged
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4903-readonly-strip-signal
Aug 4, 2026
Merged

fix(objectql): readonly 剥离改为「说清后果 + 给出出路」,并把写路径语义写进文档 (#4903)#5123
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4903-readonly-strip-signal

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4903

先说结论:这一轮落了什么、什么必须留给维护者

方向(issue 三选一) 本轮 说明
① 失败可见 —— dropped-fields 清单 早已存在(#3407) options.onFieldsDropped 每次剥离回调一次 { object, fields, reason },readonly / readonly_when 两种 reason 都发,单条 / 多行两条路径都发。issue 里「拿不到任何机器可读信号」这半句是陈述过期:信号在,只是没写进文档、没人指得到。本 PR 补了指路(日志 + 文档 + pin 测试)。
① 失败可见 —— strict 开关(写入直接报错) 未落,需维护者决策 需要新增一个写选项键,而两个可能的落点都在 packages/spec/**(本轮硬约束零改动)。见下方「需要维护者拍板」。
② 插件默认可信 ⛔ 不做 PM 已裁定:安全姿态放宽,只有维护者能授权。本 PR 不动任何默认值。
③ 文档写清楚 ✅ 已落 content/docs/protocol/objectql/security.mdx §3。
(追加)剥离日志的级别与措辞 ✅ 已落 见下。
(追加)Hook / 插件不对称 只加 pin 测试,不改行为 该不该对称是 open question,本 PR 把现状和机制钉住,让下一个人是「有意改」而不是「顺手改坏」。

改了什么

1. 剥离日志:从「我做了什么」改成「你付出了什么 + 怎么解决」

原来只有:

WARN Field 'work_duration' is read-only — ignoring incoming change (#2948)

这句话说的是引擎的动作,没说调用方的代价,也没说出路 —— 于是下游看到的现象是「同一个字段 REST 写得进、cron 写不进」,读起来像 cron 坏了,而不是值被剥离了(os-project-titanwind-ehr#750)。现在这条 WARN 会指名对象与字段、明说 update 已经在没有这个字段的情况下提交了,并同时给出两条出路:可信服务端代码传 { context: { isSystem: true } };想要程序可读的信号就传 options.onFieldsDropped

级别保持 warn,这是刻意的,理由写进了代码注释:这个接缝分不出「恶意客户端伪造 created_by」和「可信 cron 写系统结算值」。ExecutionContext 里没有任何 origin / channel / transport 标记,isSystem 是唯一的信任位,而它恰恰就是豁免条件;「没有 context」也不能当作服务端代码的证据(插件完全可以代表某个用户写,匿名 REST 写同样没有 principal)。所以升到 error 等于把错误日志变成任何客户端都能按需灌满的通道,降到 debug 等于把静默丢弃再藏回去。一个级别,选定并写明理由;要改这一点,得先给 ExecutionContext 一个真正的来源标记,而不是从「没有 principal」去猜。

行为零变化:剥什么、留什么、onFieldsDropped 报什么,全部同前。

2. 文档(content/docs/protocol/objectql/security.mdx §3 新增小节)

写清了 issue 要的四件事:

  • 剥离条件:readonly: true 是 schema 级锁,权限集授不动;执行方式是剥离而非拒绝,所以写入成功(REST 200),该列保持原值;
  • suppliedKeys 口径:候选键在引擎入口快照,只有调用方送来的键才是候选;
  • Hook 豁免及其原因(已核实机制,不是照抄 issue 措辞):快照发生在中间件与 beforeUpdate hook 之前,所以 hook 新增的键根本不在快照里 —— 这正是 updated_by / updated_at 明明是 readonly 却能落库的原因。并写明这条是按键不按值:hook 能补写一个只读字段,但救不回调用方自己送来的那个键;
  • 插件写入的 isSystem 约定:ctx.getService('data') 拿到的就是 REST 用的同一个引擎、执行上下文为空,所以服务端插件默认不可信,可信代码必须显式声明 —— 附带说明 isSystem 同时绕过权限检查,所以它的语义是「平台代码以平台身份写」,不是「这是插件写的」;
  • 以及 onFieldsDropped 的用法、reason 取值,和它不跨 RPC / Virtual Data Engine 边界这一限制。

3. Pin 测试 packages/objectql/src/engine-readonly-strip-signal.test.ts(10 例)

  • 复现现场:无 context 的插件式写入,其余字段落库、work_duration 恒为 null,且调用不抛;传 { context: { isSystem: true } } 后同一次写入完整落库;
  • onFieldsDropped正是 issue 描述的调用形态(进程内引擎、无 context)下确实回调,单条与 bulk 两条路径都钉;
  • 日志契约:断言「后果 + 两条出路 + 对象名字段名」都在,以及只发一条 warn、不发 error;
  • 不对称 pin(PM 裁定 ⑤):hook 补写的只读字段落库、同名字段由调用方送来则被剥离 —— 并附一例把机制钉死(hook 改值救不回快照里的键)。注释里明说这是钉住现状、不是背书

需要维护者拍板:strict(写入直接拒绝)模式该落在哪里

这是本轮唯一没做的实质项 —— 不是漏掉,是做不了而不该猜。strict 需要一个新的写选项键,而它的两个候选落点都在 packages/spec/**(本轮零改动约束):

  • EngineUpdateOptionsSchema(packages/spec/src/data/data-engine.zod.ts)—— 可序列化选项;
  • WriteObservabilityOptions(packages/spec/src/contracts/data-engine.ts)—— onFieldsDropped 就住在这里,因为函数无法进 JSON Schema。

三个选项、两条固定轴(项目长期健壮性 / 让 AI 写的元数据应用难以出错)的分析写在 issue #4903 的报告里,推荐 B(在 WriteObservabilityOptions 旁加 strictReadonlyWrites?: boolean,默认 false,per-call 显式选择,违例抛带 code 的错)。请维护者确认后再开一轮。

验证

pnpm --filter @objectstack/objectql typecheck   # tsc --noEmit,干净
pnpm --filter @objectstack/objectql test        # Test Files 115 passed | Tests 1820 passed
pnpm lint                                        # eslint . --no-inline-config,干净
pnpm check:doc-authoring / check:docs-audit-scope / check:nul-bytes /
  check:role-word / check:release-notes / check:durability-log-level /
  check:engine-double-contract                   # 全绿

已合入当日 origin/main 并重跑上述 objectql 闸门(合入的提交不触及 packages/objectql / packages/spec)。

约束遵守


🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX


Generated by Claude Code

claude added 2 commits August 4, 2026 05:24
…ence and remedy (#4903)

A `readonly: true` column written by server-side code — a cron/background job
reaching the engine via `ctx.getService('data')` — was dropped while the call
reported success, leaving one log line that said what the engine did but not
what it cost the caller or how to fix it.

The strip now names the object and field, states that the update was COMMITTED
WITHOUT the field, and carries both remedies: `{ context: { isSystem: true } }`
for genuinely trusted server code, and `options.onFieldsDropped` (#3407) for a
machine-readable signal. Level stays `warn` — this seam cannot tell a forged
client body from trusted server code (`ExecutionContext` has no origin marker;
`isSystem` is the only trust bit and it is the exemption), so `error` would be
client-triggerable log spam and `debug` would restore the silent drop.

Behaviour unchanged. Adds a pin suite for the hook-backfill asymmetry and its
mechanism (`suppliedKeys` is snapshotted at engine entry, before hooks run), and
documents the semantics on the security protocol page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 5:27am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

13 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/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/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)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

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.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[objectql] readonly 字段:服务端插件 data.update 直写被静默剥离(200但值不变),与 beforeUpdate Hook 补写可落库不对称

2 participants