Skip to content

fix(runtime,spec,lint): bind action.body only for type 'script' (#4352) - #4654

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-4352-action-body-type-gate
Aug 2, 2026
Merged

fix(runtime,spec,lint): bind action.body only for type 'script' (#4352)#4654
os-zhuang merged 3 commits into
mainfrom
claude/issue-4352-action-body-type-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4352

背景

ActionSchema.bodydescribe() 一直写着 "Only used when type is script",JSDoc 说得更明确("Only meaningful when type === 'script'")。但运行时从来没读过 typeactionBodyRunnerFactory 只要 body 解析成功就绑定 handler,collectBundleActions 也照单全收。于是一个 type: 'url' 的 action 带着遗留的 body,照样被注册进 action registry、照样在沙箱里执行。

这是 Prime Directive #10「declared ≠ enforced」最难受的一种形状:作者把 typescript 改成 url,合理地认为 body 已经死了,但它没有——仍然可以通过 ql.object(o).execute(name) 触达(ObjectQL proxy 直接调 executeAction,自己不做 type 分支),并且仍然被 ADR-0110 D5 治理清单算作一个活的 handler。

改动

按 issue 里 maintainer 裁定的方向,两端一起收口:

  1. 运行时(packages/runtime/src/sandbox/body-runner.ts——actionBodyRunnerFactory 只在 type === 'script' 时绑定 handler,其余类型返回 undefinedlogger.warn 说明原因(静默拒绝只是把「看不见」换个地方,不算修好)。

    门放在唯一的绑定点,不放在 collector:collectBundleActions 刻意保持 type-blind(治理面需要看到所有声明的 action,不管绑没绑),而且另一条绑定路径 engine.setDefaultActionRunner(Studio 里写的 action 元数据)根本不走这个 collector——在 collector 上再抄一份规则,等于一半重复、另一半漏网。

    type 省略时按 'script' 处理。这不是宽容 fallback,而是 schema 自己的默认值(ActionType.default('script')):collector 走的是原始 bundle 对象,strict: falsedefineStack 和 legacy manifest.actions[] 从来没经过 ActionSchema,所以省略的 type 必须仍然等于 spec 说的那个意思。只有显式声明了别的 type 的 action,行为才发生变化。

  2. Spec(packages/spec/src/ui/action.zod.ts——发布门本身的拒绝规则(type !== 'script' && body)在 fix(spec,objectql,metadata-protocol): a user field carries its target in the TYPE — bare {type:'user'} is not targetless #4438 已经落地,本 PR 不重复实现,而是补上接线的 pin:发布门并不直接 import ActionSchema,它通过 getMetadataTypeSchema('action') 按元数据类型解析(metadata-protocol 的存盘校验和 metadata-diagnostics 都走这条路),对象内联 action 则由 ObjectSchema.actions 判定。这两处注册只要被重新指向,洞就会悄悄重开,而上面所有 schema 测试仍然全绿。新增测试正是钉住这一点。JSDoc 同步改写为「两端强制」而不再只是一句描述。

  3. Lint(packages/lint/src/validate-action-body-writes.ts——feat(lint,spec): L2 action body 写不存在字段从盲区变为作者时 lint 告警 (#4271) #4344 当时刻意让这条规则 type-blind,理由是「运行时不看 type,所以检查真正执行的东西比检查 schema 声称的东西更有意义」,并且那段注释自己就预告了会被改("定了之后 lint 那边要跟着调")。现在裁定下来了:执行集合和声明集合重新合一,非 script 的 body provably 不会跑,再对它的写操作提建议就是噪音——指向的是 write,真正的缺陷是 type,而发布门已经用自己的措辞点名了。规则改回按 type 过滤,陈旧的 rationale 注释一并重写。

测试

  • packages/runtime/src/action-body-type-gate.test.ts(新增)——钉住 AppPlugin 真正执行的组合collectBundleActionsactionBodyRunnerFactoryif (!handler) continueregisterAction。注册决定是在这个循环里做出的,factory 返回 undefined 只有在循环尊重它时才算数。同时断言 collector 自身的输出,保证「复刻循环」不会和真实实现悄悄脱节。
  • packages/runtime/src/sandbox/body-runner.test.ts —— 隔离地钉 factory:url / modal / flow / api / form 五种类型各自不绑定且必须发出 warn;显式 script 和省略 type 均照常绑定;非 script 且没有 body 的 action 不产生任何 warn(没有矛盾就不该有噪音)。
  • packages/spec/src/ui/action.test.ts —— 发布门解析链的 pin(见上)。
  • packages/lint/src/validate-action-body-writes.test.ts —— 把 feat(lint,spec): L2 action body 写不存在字段从盲区变为作者时 lint 告警 (#4271) #4344 那条 provisional 断言反过来,并补上「省略 type」「显式 script」两种仍然要检查的情形。

(本 PR 为 draft:验证与影响面盘点的完整证据随后补充到评论区。)

🤖 Generated with Claude Code

https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5


Generated by Claude Code

`ActionSchema.body` has always said "Only used when type is `script`", but
the runtime read `body` alone: `actionBodyRunnerFactory` bound a handler the
moment the body parsed, so a `type: 'url'` action carrying a leftover body
was registered and executed. Declared != enforced, in its nastiest shape —
an author flips `type` away from `script`, reasonably concludes the body is
dead, and it keeps running.

- runtime: `actionBodyRunnerFactory` refuses to bind unless the type is
  `script` (omitted `type` = the spec's own `ActionType.default('script')`),
  and logs the refusal with the schema's prescription rather than dropping
  it silently. The gate lives at the single bind point, not the collector —
  `collectBundleActions` stays type-blind so governance surfaces still see
  every declared action, and the second binder
  (`engine.setDefaultActionRunner`) never walks the collector at all.
- spec: pins that the publish gate RESOLVES to the rejecting schema —
  `getMetadataTypeSchema('action')` and `ObjectSchema.actions` — so a
  re-point of either registration cannot silently reopen the hole.
- lint: `validate-action-body-writes` filters by `type` again (#4344's
  provisional type-blindness is over) and its stale rationale is rewritten.

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

vercel Bot commented Aug 2, 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 2, 2026 2:13pm

Request Review

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @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/lint, @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 @objectstack/lint, 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/runtime, @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.

@github-actions github-actions Bot added the size/m label Aug 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

影响面实测(#3746 先例)—— 三个示例 app + 全部 content/docs

裁决要求「实测三个示例 app + 全部 docs,预期零命中;若有命中在 PR 里逐个列出」。结果:零命中。 下面是实际找到的东西,不是「grep 了一下没看见」。

方法

示例 app 用的是结构化扫描,不是 greptsx 直接 import 每个 app 的真实 stack 对象,深度遍历,报出每一个带 { language, source } 形状 body 的节点连同它同级声明的 type。判定口径就是本 PR 改变行为的那一组:

body 存在  且  typeof type === 'string'  且  type !== 'script'

type 省略不算命中——那等于 ActionType.default('script'),行为不变。)

grep 在这里会骗人:content/docs 里 37 行 body: 绝大多数是 fetch 的请求体、邮件/通知正文、flow step 的 body 区域,以及一个body 的字段your-first-project.mdxField.textarea({ label: 'Body' }))。所以 docs 逐条看了,app 走结构遍历。

示例 app

App 扫描入口 带 body 的节点 非 script 命中
app-todo objectstack.config.ts(完整 stack) 0 0
app-crm objectstack.config.ts(完整 stack) 0 0
app-showcase 各元数据 barrel(见下) 9 0

app-showcase 的完整 config 需要 @objectstack/cloud-connection 等连接器插件构建产物,所以按 barrel 逐个扫,覆盖 action 能被声明的每个位置:

  • src/ui/actions/index.ts5 个 body,全部 type: 'script'showcase_action_param_galleryshowcase_archive_taskshowcase_mark_doneshowcase_portfolio_snapshotshowcase_submit_signoff
  • src/data/hooks/index.ts4 个 bodyshowcase_audit_task_completionshowcase_normalize_task_titleshowcase_stamp_inquiry_defaultsshowcase_warn_over_budget这些是 hook 不是 action,没有 type 字段,本 PR 的门完全不碰它们(走的是 hookBodyRunnerFactory)。
  • src/data/objects/index.tssrc/ui/pages/index.tssrc/ui/views/index.tssrc/ui/apps/index.ts — 0 个 body(即没有内联在对象/页面上的 action body)。

交叉验证:app-showcase/src/ui/actions/index.ts 里那些 script 的 action(type: 'url' L67、'flow' L84、'modal' L96/L175、'api' L108/L145、'form' L159)文本上确认均不带 bodyapp-crm 唯一的 action crm_convert_leadtype: 'flow' + target,无 body。

content/docs

37 行 body: + 5 行 "body",逐条归类后 action 相关的只有三处,全部合规:

位置 内容 判定
ui/actions.mdx:84 MarkDoneAction 示例 type: 'script'
protocol/objectui/actions.mdx:64,73 greet_user / stamp_now YAML 示例 type: script
data-modeling/formulas.mdx:339 notify_on_escalation hook 的 body,无 type

其余为 fetch(..., { body })、通知/邮件正文、flow step 的 body 区域、hook-bodies.mdx 的 hook body、以及名为 body 的字段。

值得单独指出:content/docs/protocol/objectui/actions.mdx 已经把这条规则写成了「parse-time error」——

body is only meaningful when type is script, and declaring one on any other type is a parse-time error — those types dispatch on target, so the body would never be invoked.

也就是说文档描述的一直是本 PR(连同 #4438)实现的那个世界,本 PR 不需要改任何一页 docs。

结论

没有任何 app 或 doc 依赖「非 script 也跑 body」。 裁决里那句「若出现真实依赖的命中就停手按 needs_decision 回报」的前提没有出现,按裁决继续实施。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 14:32
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 430dcc2 Aug 2, 2026
22 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4352-action-body-type-gate branch August 2, 2026 14:43
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 protocol:ui size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

action.body 的 type 门是 declared ≠ enforced —— spec 说只在 type:'script' 生效,runtime 有 body 就绑

2 participants