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

vercelBot commented Aug 2, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 2, 2026 2:13pm

Request Review

@github-actions

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-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling labels Aug 2, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

影响面实测(#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-todoobjectstack.config.ts(完整 stack)00
app-crmobjectstack.config.ts(完整 stack)00
app-showcase各元数据 barrel(见下)90

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:84MarkDoneAction 示例type: 'script'
protocol/objectui/actions.mdx:64,73greet_user / stamp_now YAML 示例type: script
data-modeling/formulas.mdx:339notify_on_escalationhook 的 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 queueAug 2, 2026
Merged via the queue into main with commit 430dcc2Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/mteststooling

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

@os-zhuang@claude