Skip to content

feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345) - #4357

Merged
os-zhuang merged 5 commits into
mainfrom
claude/action-body-ctx-record-dropped-75463d
Jul 31, 2026
Merged

feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345)#4357
os-zhuang merged 5 commits into
mainfrom
claude/action-body-ctx-record-dropped-75463d

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes#4345运行时一半。

范围已收敛。 本 PR 开着的时候,#4362 合并并关掉了 #4345 的作者时一半,
而且它的规则在一处比本分支原来的正确。本分支因此删掉了自己的 lint 规则,
只留 #4362 明确留作 open question、且 lint 够不着的那一层。原描述的 lint 部分
已作废,以下是重写的。

为什么本分支原来的 lint 规则被删掉

#4362 要求写是可证明的死写(ctx.record 不逃逸)。因为这是活的、确实落库的合法写法:

ctx.record.stage='won';awaitctx.api.object('crm_deal').update(ctx.record);// 落库了

本分支原来的规则会把它报成"被丢弃",并断言 the stored record is unchanged ——
那是一句错话,不只是噪音。实测确认后整条删除,lint 包与 hook-bodies.mdx 整体
采用 main 的版本。

同一个缺陷也在本分支的运行时层,一并修了(见下)。

留下的是什么:调用时报告

lint 只看字面模式、且只看经过 CLI 的 metadata。运行时能覆盖它够不着的三类:

  • computed key 与别名 —— ctx.record[k] = vconst r = ctx.record; r.x = 1,lint 刻意跳过而不猜;
  • 整体替换 —— ctx.record = {…};
  • lint 永远看不到的 body —— Studio / API 写进来的 metadata 不经过 os validate

沙箱在快照上装 set/deleteProperty/defineProperty proxy,放在 accessor 后面,
所以整体替换也抓得到、不会把记录器换掉。命中的 key 走 ScriptResult.droppedRecordWrites,
actionBodyRunnerFactory 报 warn,点名字段和 ctx.api.object(...).update(...) 的改法。
照常在 VM 内生效,拿快照当草稿纸的 body 读写仍自洽 —— 去掉的只是沉默。

逃逸检测(与 #4362 语义对齐)

写之后出现 ownKeys 即视为逃逸 —— 整体消费(交给 ctx.api、展开、返回、
JSON.stringify)会枚举键,而属性读不会。所以:

body 形状上报
ctx.record.stage='won'; return null;✅ 报
var id = ctx.recordId || (ctx.record && ctx.record.id); ctx.record.stage='won';✅ 报(showcase 真实惯用法,属性读不解救)
Object.keys(ctx.record) 发生在写之前✅ 报
ctx.record.stage='won'; await ctx.api.object('d').update(ctx.record);❌ 不报(活写)
拷出 / 整体 return / JSON.stringify❌ 不报

方向和 #4362 一致:歧义算活的。一句错的"被丢弃"是在对存储状态说假话,比漏报更糟。

hook 的 ctx 没有 record,不装 proxy、零成本。ctx.record 仍是只读;运行时到底该
refuse 还是 honour 仍未决 —— 只报告不预设任何一边的答案

净改动

8 个文件,只碰 runtime + 一段 ActionSchema.body JSDoc + actions.mdx
(#4362 没动这个文件)。

验证(合流后全部重跑)

结果
@objectstack/runtime995 passed(sandbox 89)
@objectstack/lint634 passed
@objectstack/spec7161 passed
@objectstack/cli628 passed
spec check:generated8/8 up to date
typecheck + eslintclean

🤖 Generated with Claude Code

…ted, not silently discarded (#4345)
`ctx.record` in an L2 action body is a pre-fetched snapshot that nothing writes
back, so every assignment to it was dropped — a correctly spelled, fully
declared field exactly like an unknown one — while the action returned success.
No diagnostic anywhere: the #4001 "silent no-op manufactures false completion"
shape, one layer below the unknown-column drop #4271 covers.
The snapshot stays read-only. An action's output is its return value and its
write channel is `ctx.api`; making the snapshot writable would raise questions
this bug does not answer (write back to what, under whose permissions, what
`requiresRecord: false` means). What was wrong was the silence.
Runtime: the sandbox installs a set/deleteProperty/defineProperty proxy over
the snapshot, behind an accessor so a wholesale `ctx.record = {…}` cannot swap
the recorder out, and reports the touched keys as
`ScriptResult.droppedRecordWrites`. `actionBodyRunnerFactory` warns naming the
discarded fields and the `ctx.api.object(...).update(...)` remedy. Writes still
work inside the VM, so a body using the snapshot as scratch keeps its reads
coherent — only the silence is removed. As a run-time trap it sees the computed
keys, `Object.assign` and aliases static analysis cannot, and it covers
metadata authored through Studio or the API, which no lint inspects. Hooks
carry no `record` and install no proxy.
Lint: new advisory rule `action-body-record-write-discarded`
(`validateActionRecordWrites`) in REFERENCE_INTEGRITY_RULES, so `os validate`,
`os lint` and `os compile` all report it. It never consults declared fields and
offers no did-you-mean — the field name is not the bug — and it dedupes
defineStack's merged action copies by value, not identity, so one authored
action reports once. Advisory because a body may use the snapshot as local
scratch, which no analysis short of data-flow tells from an intended persist.
Docs: `ScriptContext.record`, `ActionSchema.body`, `ScriptBodySchema`,
`content/docs/ui/actions.mdx` and `content/docs/automation/hook-bodies.mdx` now
state the read-only semantics and name the `ctx.input` analogy as the trap it
is.
Verified on the showcase app: zero findings before, exactly one after planting
the issue's repro — naming both declared fields and proposing the very
`ctx.api` call it replaced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 31, 2026 9:25am

Request Review

Both landed on the same surface while this branch was open, so the merge is a
reconciliation rather than a textual resolution:
- #4344 built the same `actions` + `objects[].actions` walk, with the same
value-dedupe of defineStack's merged copies (it found the double-report the
same way). Two walks over one surface would drift silently, so its
`collectActionBodies` is now exported (module-level, not via the barrel —
same posture as BODY_WRITE_SYSTEM_FIELDS), carries the bound object, and this
rule consumes it. The duplicate walk and its helpers are deleted.
- The two rules split one write surface and answer different questions, so the
suite, the barrel and the spec JSDoc now say which is which: `ctx.api` writes
→ does this field exist (did-you-mean); `ctx.record` writes → none of them
land, declared or not (no did-you-mean, because the name is not the bug).
- #4351/#4355 rewrote the hook-bodies write-set section this branch also
edited. Its write-shape table listed three shapes and said nothing about
`ctx.record`, which this branch now checks — added the row plus the note that
it is a different defect, and that the record rule's statically-opaque cases
are covered by the runtime recorder instead.
Verified after the merge: lint 642, spec 7160, runtime 989, cli 628, spec
check:generated 8/8, typecheck + eslint clean, showcase 0 findings clean and
exactly 1 with the repro planted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/runtime, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

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.

os-zhuangand others added 3 commits July 31, 2026 17:20
#4362(已合并)为 #4345 落了 lint 规则 `action-record-write-discarded`,而且
在一处比本分支原来的规则正确:它要求写是**可证明的死写**(`ctx.record` 不逃逸)。
因为这是活的、确实落库的合法写法:
ctx.record.stage = 'won';
await ctx.api.object('crm_deal').update(ctx.record);
本分支原来的 lint 规则会把它误报成"被丢弃",而且断言"stored record is
unchanged" —— 那是一句错话,不只是噪音。实测确认后:
- 删掉本分支的 `validate-action-record-writes.{ts,test.ts}` 及其全部接线,
lint 包与 hook-bodies.mdx 整体采用 main 的版本;
- **同一个缺陷也在本分支的运行时层**:它对上面那段一样报 dropped。补了逃逸
检测(写之后出现 `ownKeys` 即视为逃逸,proxy 上观察),语义与 #4362 对齐:
整体消费(交给 ctx.api、展开、返回、JSON.stringify)噤声,属性读不算解救,
所以 showcase 的 `ctx.recordId || (ctx.record && ctx.record.id)` 惯用法
照报。
保留的只剩 #4362 明确留作 open question、且 lint 够不着的那一层:运行时在
调用时报告被丢弃的写 —— 覆盖 computed key、别名、整体替换,以及 Studio / API
写入的 metadata(lint 永远看不到)。它只报告,不 refuse 也不 honour,不预设
那个 open question 的答案。
changeset 相应收窄为 @objectstack/runtime 单包。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuangos-zhuang changed the title feat(runtime,lint,spec): action body 写 ctx.record 从静默丢弃变为两端可见 (#4345)feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345)Jul 31, 2026
@os-zhuang
os-zhuang merged commit a4e2684 into mainJul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/action-body-ctx-record-dropped-75463d branch July 31, 2026 09:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:dataprotocol:uisize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

action body 里写 ctx.record 被静默丢弃 —— 已声明字段也一样(#4271 家族)

1 participant

@os-zhuang