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-zhuang os-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';
await ctx.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/runtime 995 passed(sandbox 89)
@objectstack/lint 634 passed
@objectstack/spec 7161 passed
@objectstack/cli 628 passed
spec check:generated 8/8 up to date
typecheck + eslint clean

🤖 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

vercel Bot commented Jul 31, 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 Jul 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-actions Bot 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-zhuang and 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-zhuang os-zhuang changed the title feat(runtime,lint,spec): action body 写 ctx.record 从静默丢弃变为两端可见 (#4345) feat(runtime): action body 被丢弃的 ctx.record 写在调用时可见 (#4345) Jul 31, 2026
@github-actions github-actions Bot added size/l and removed size/xl labels Jul 31, 2026
@os-zhuang
os-zhuang merged commit a4e2684 into main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant