feat(automation,spec): 流程执行器 parse() 自己的 config,未声明键在注册时报错 (#4277) - #4332
Merged
Conversation
…d config keys reject at registration (#4277) Half (a): the 12 contract-carrying builtins (CRUD quartet, screen, map, notify, http, loop/parallel/try_catch) now parse node.config against their Zod contracts before executing (builtin/parse-config.ts). A type or missing-required violation refuses the node as a guard — not routable via fault edges. Templates stay legal: string slots parse the raw config; http parses post-interpolation, the shape its executor reads. A legacy flat-graph loop (no config.body) stays exempt. LoopConfigSchema.collection widened to string|array — the executor always accepted inline arrays (map.collection already declared the union), so string-only under-declared. Half (b): registerFlow now REJECTS config keys the node type's descriptor configSchema does not declare (tightening the #4059 warning), naming the path, the declared key set, a did-you-mean, and per-key tombstones (the UNKNOWN_KEY_GUIDANCE pattern). assignment stays exempt wholesale (its top-level keys are author variable names); schemaless types and keyValue maps are unchanged. Example metadata fixed at the producer (PD #12): two showcase notify nodes missing the required recipients/title (both already failed at run time), and app-todo's dead getAll/message/buttons keys plus bare-string select options. Closes #4277 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CW8ZP3zUuC7ovSxqnN5o77
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
|
This PR is very large. Consider breaking it into smaller PRs for easier review. |
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…4277) The docs-drift advisory on #4332 flagged flows.mdx, and it was right: the node-property table and the strict-shells callout both described config as an open record with no enforcement, which #4277 changed — undeclared keys reject at registerFlow() and the contract-carrying builtins parse their config at execute time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CW8ZP3zUuC7ovSxqnN5o77
os-zhuang
marked this pull request as ready for review
July 31, 2026 06:40
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.
Closes #4277(#4045 步骤 3b —— 唯一被有意推迟的一项)。两半都装上了:
(a) 执行器
parse()自己的 config —— 执行时强制12 个带 Zod 契约的内置节点(CRUD 四件、
screen、map、notify、http、loop/parallel/try_catch)现在在执行前把node.config过一遍各自的契约(新增service-automation/builtin/parse-config.ts,结构化鉴权safeParse—— service-automation 仍不直接依赖 zod,与 ledger 测试同一边界)。要点:errorClass: 'guard',fault 边不可路由)。config 是元数据,重跑不会变 —— 这正是refuseNode文档里"缺必填 config 键"的范例场景。guard-refusal-inventory的 5 个必填键条目改为钉住契约拒绝的片段,分类不变。http外都 parse 原始存储的 config(契约的类型槽都是 string / unknown,{token}原样通过);http因为执行器整体插值后才读,所以 parse 插值后的形状 —— 整串单 token 插值保留类型,timeoutMs: '{t}'在契约看到之前已解析成数字(有测试钉住)。loop(无config.body)先于 ADR-0031 构造存在,不 parse(注释 + 测试钉住)。LoopConfigSchema.collection从 string-only 放宽为string | array联合 —— 执行器一直接受内联数组(与map.collection共享解析逻辑,后者早已声明联合),string-only 是"照执行器写契约"时的漏报。run-history的 big_loop 测试(内联数组)暴露了这一点。retry块但缺retryDelayMs时,基础延迟从执行器历史上的 0 变为契约文档化的 1000ms(changeset 有迁移说明)。decision/script/wait/subflow/connector_action保持无契约不 parse(wait/connector_action的契约在waitEventConfig/connectorConfig兄弟块上,议题约束已核对);assignment不可 parse,整体豁免。(b) 未声明键 warn → error —— 注册时强制
registerFlow的 #4059 警告收紧为 hard throw:一次性列出所有违例(路径、声明键集、did-you-mean),并按object.zod.ts的UNKNOWN_KEY_GUIDANCE模式加了 per-node-type 墓碑映射(FLOW_NODE_UNKNOWN_KEY_GUIDANCE),先立三条有据可查的:screen.visibleIf→visibleWhen(#3528)、create_record/update_record.fieldValues→fields(#2419/cloud#688)。豁免与 warn 时代一致:assignment整体豁免、无 configSchema 类型天然跳过、keyValue map 处止步。所有registerFlow调用点(boot pull / kernel:ready / metadata:reloaded)已有 per-flow try/catch,坏 flow 是响亮跳过,不炸内核。例子元数据修复(PD #12,在生产端修)
对三个示例 app 的全部 63 个 flow 跑了注册 + 执行两层审计,查出并修正:
showcase_inquiry_purge.report与showcase_task_due_reminder.remind_owner两个 notify 节点缺必填的recipients/title—— 注意这两处今天运行期本来就必失败(执行器的 execute-time guard 早已要求),即 [P2] Provide a declarative time-relative trigger (avoid fragile date-equality on record-change) #1874 的提醒 demo 从未真正投递过;现在补上({record.assignee}/ admin)。get_record.getAll死键 ×2(换成声明机制limit: 200,顺带让"取全部"意图第一次真实生效 —— 之前实际是findOne单条)、screen.message/buttons死键(换成description+waitForInput: true,按钮本就无任何读者)、selectoptions裸字符串改为契约的{ value, label }对。验证
pnpm test132/132 任务全绿check:generated全绿(仅check:docs曾陈旧,已--fix重新生成 4 个 MDX);api-surface / authorable-surface 无漂移config-parse.test.ts(11 例:guard 不可路由、模板保留、http 插值后 parse、loop 双路径、enum/类型/必填拒绝);config-unknown-keys.test.ts重写为钉住拒绝行为.changeset/flow-executors-parse-config.md(spec minor + service-automation minor,含 FROM → TO 迁移说明)objectui
无需改动:设计器表单读的是 descriptor 上手写的 configSchema 字面量(未变),
json-schema-to-fields的 keyValue 判定路径也未触及。该仓库不开 PR。风险
议题已言明的既定风险:今天能注册/加载的存量 metadata,若带未声明键会在下次注册时被拒(错误信息自带处方),若声明键类型不符会在节点执行时被 guard 拒绝。示例仓内的全部此类存量已在本 PR 修净;错误文案对两类都给出改法(改名/删键,或在 descriptor configSchema 上声明)。
🤖 Generated with Claude Code
https://claude.ai/code/session_01CW8ZP3zUuC7ovSxqnN5o77
Generated by Claude Code