fix(objectql): readonly 剥离改为「说清后果 + 给出出路」,并把写路径语义写进文档 (#4903) - #5123
Merged
Conversation
…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
…donly-strip-signal
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 4, 2026
[决策] readonly 剥离的 strict/reject 模式落在哪一层 —— B(WriteObservabilityOptions)推荐,A/C/D 各有代价(#4903 后续)
#5126
Open
xuyushun441-sys
marked this pull request as ready for review
August 4, 2026 06:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4903
先说结论:这一轮落了什么、什么必须留给维护者
options.onFieldsDropped每次剥离回调一次{ object, fields, reason },readonly/readonly_when两种 reason 都发,单条 / 多行两条路径都发。issue 里「拿不到任何机器可读信号」这半句是陈述过期:信号在,只是没写进文档、没人指得到。本 PR 补了指路(日志 + 文档 + pin 测试)。packages/spec/**(本轮硬约束零改动)。见下方「需要维护者拍板」。content/docs/protocol/objectql/security.mdx§3。改了什么
1. 剥离日志:从「我做了什么」改成「你付出了什么 + 怎么解决」
原来只有:
这句话说的是引擎的动作,没说调用方的代价,也没说出路 —— 于是下游看到的现象是「同一个字段 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),该列保持原值;beforeUpdatehook 之前,所以 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 例)work_duration恒为 null,且调用不抛;传{ context: { isSystem: true } }后同一次写入完整落库;onFieldsDropped在正是 issue 描述的调用形态(进程内引擎、无 context)下确实回调,单条与 bulk 两条路径都钉;warn、不发error;需要维护者拍板: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的错)。请维护者确认后再开一轮。验证
已合入当日
origin/main并重跑上述 objectql 闸门(合入的提交不触及packages/objectql/packages/spec)。约束遵守
packages/spec/**、生成物:零改动;packages/metadata-protocol/src/protocol.ts(data: updateMany runs hooks for a nonexistent id — the row fails INTERNAL_ERROR from a hook-condition abort instead of RECORD_NOT_FOUND #5088 借道占用):零改动;packages/services/service-analytics/**(service-analytics 的 executeRawSql 自动桥接丢弃 objectName —— dataset 原始 SQL 永远打在默认 datasource 上,凡被路由到非默认 datasource 的对象一律读成 0 #5033):零改动;content/docs/releases/:未触碰;packages/objectql/src/validation/rule-validator.ts+ 一个新测试文件 + 一页文档 + 一个 changeset。🤖 Generated with Claude Code
https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
Generated by Claude Code