Uh oh!
There was an error while loading. Please reload this page.
fix(core): ActionRunner.execute 的 disabled 门改问「有没有条件」,空谓词不再拦掉执行 (#3848) - #3873
Conversation
执行门原来问 `action.disabled != null && action.disabled !== false`,然后把原始值
直接交给 `evaluateCondition` —— 而那个函数对「这里没有条件」只有一个默认答案:
返回 `true`(即 visible/enabled)。在 `disabled` 这个键上,`true` 的意思是「已禁用」。
于是任何空谓词都被判成禁用:handler 一次不跑,调用方收到一个元数据从未声明过的
`{ success: false, error: 'Action is disabled' }`。
实测(计数 handler,基线 origin/main @ f0a625a):
disabled '' | handler ran: false | Action is disabled
disabled ' ' | handler ran: false | Action is disabled
disabled {dialect:'cel',source:''} | handler ran: false | Action is disabled
disabled absent / false | handler ran: true | success
#3842 / #3849 修完渲染器两侧之后,这已经是活的用户可见行为:按钮能点了,点下去
收到 `Action is disabled` —— 渲染器与执行入口对同一个谓词值给出相反答案,正是
#3314 已经付过一次代价的形状。
新门先问「有没有条件可判」再求值:「没有条件」的判定取自 core 唯一的谓词归一器
(`toPredicateInput`,把 `''`/`null`/空 `source` envelope/非谓词值映射为
`undefined`),外加纯空白字符串 —— 归一器对它是包裹而非折叠,而 `evaluateCondition`
自己就把空白 `trimmed` 叫做「没有条件」(`evalRowPredicate` 用的也是这条 blank-source
规则)。
行为变更面单向且刻意窄:只有「没有东西可求值」的形状变化,且一律从「拦」变「放行」
—— `''`、纯空白、空 `source` envelope,以及非谓词垃圾值(`0`、`{}`,原先被强制成
「已禁用」)。`disabled: true`、取真表达式、取真 CEL envelope 仍拦;`disabled: false`
与未声明仍放行;任何表达式/envelope 的 verdict 一律不变。`catch { isDisabled = false }`
失败放行姿态原样保留。
交给求值器的值刻意仍是**原始值**,不是归一结果:对已经是 `${…}` 模板的字符串,
`toPredicateInput` 会二次包裹成 `'${${x}}'`,解析失败后原样退回,`Boolean(非空串)`
恒为 `true` —— 那样求值的模板谓词永远「已禁用」。该归一器缺陷另立 #3871(动作面
渲染器与 ActionEngine 都中招,而 SchemaRenderer 与 page:header 读原始值、钉的是正确
verdict),钉子旁留了 tripwire,#3871 修好那天它会红。因此仍有两行两侧不一致,各自
写明归属:空 envelope(渲染器半边归 #3850 的范围裁决)与 `${…}` 拼法(#3871)。
反向验证(方向先判后跑):还原旧门、求值不动 → 恰好五行「没有条件」钉子变红
(`''`、`' '`、空 envelope、`0`、`{}`)并各自点名形状,外加「空谓词根本不该到达
求值器」那条;`absent`/`false`/`true`/两个表达式行/两个非空 envelope 行全绿
(6 failed | 14 passed),与预判一致。
Co-authored-by: Claude <noreply@anthropic.com>The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
commented
Aug 8, 2026
✅ 验收(PM,session 实物核验:头 偏离裁定:派发词的优先形状(求值归一结果)被 dev 逐形状实测证伪 ——
转 ready 并挂 auto-merge。#3871(归一器二次包裹,A 修生产端→B spec 校验的裁决建议)与 #3872(condition 门真值判定过度放行)归分诊席定级;#3871 与 #3850/#3862 同族(空谓词/定义层三联),建议分诊席并案裁。#3850 落地后本门收敛共享定义(dev 荐 A)记入该单待办。 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
objectstack-ai#3872) (objectstack-ai#3916) `ActionRunner.execute` —— 引擎的公共执行入口,所有动作面共用 —— 用 `if (action.condition)` 把门开在**真值**上。真值回答不了门要问的那个问题 (「作者到底声明了条件吗」),而在这个键上它答错的方向是**过度放行**: `condition: false` 落在 `if (false)`,整段被跳过,`evaluateCondition` 根本没被 问过,动作照常执行。基线实测(`origin/main` @ 2937bcf,handler 自计数): condition: false | handler ran: true | {"success":true} condition: {cel,'false'} | handler ran: false | {"error":"Action condition not met"} 同一句话的两种拼法,结论相反 —— 布尔字面量执行了,同义 envelope 被拦。`false` 是元数据能写出的最明确的一句「永远不要执行这个」(把动作关掉的模板产出的正是 它),所以方向要紧:动作真的跑了,可能落库。这是 objectstack-ai#3492 家族的过度放行那一半, 下面一行的 `disabled` 门(objectstack-ai#3848 / PR objectstack-ai#3873)病在相反方向。 门现在先问「有没有声明 condition 门」再求值。「没有可求值的条件」取自 core 唯一 的谓词归一器 `toPredicateInput`(把 `''`、`null`、空 `source` envelope、非谓词值 一律映射为 `undefined`),外加它包裹而非折叠的纯空白字符串 —— 这就是 objectstack-ai#3850 对 「空谓词」范围的裁决,与 `disabled` 门已经在用的同一份。门问对了问题,verdict 就 不需要自己的布尔分支:`evaluateCondition` 对布尔实参原样返回。 ## 行为变更面:单向,且只有一行 只有一种形状换了结论 —— 已声明的布尔 `false`,从执行改为拒绝(`{ success: false, error: 'Action condition not met' }`,这个键本来就用的文案)。其余逐字节不变: `condition: true`、缺省、真表达式/真 envelope 照样执行;假表达式、假 CEL envelope、假 `${…}` 模板照样被拦;三种空谓词(`''`、纯空白、空 `source` envelope) 照样执行,只是理由从「`if ('')` 恰好为假」换成了「什么都没声明」;非谓词垃圾 (`0`、`{}`)照样执行 —— 不是谓词的值不该决定动作的命运,这正是本模块在 `disabled` 上已经确立的 fail-open 姿态。因此本改动只可能开始拒绝执行,绝不可能 开始放行,是 objectstack-ai#3848 修法的镜像。 `ActionDef.condition` 随之放宽为 `string | boolean`,与门现在兑现的面一致(也与 旁边的 `disabled` 一致)。这不是消费端的宽容别名:布尔本来就通过接口的索引签名 在运行时被接受,只是被忽略了。 交给求值器的值刻意保持**原样**而非先归一,理由与 objectstack-ai#3848 相同、符号相反: `toPredicateInput` 无条件包裹,已经是模板的 `'${x}'` 会变成 `'${${x}}'`,解析失败 后原样返回并被强制为恒 `true` —— 在 `disabled` 上这拦住一切,在 `condition` 上它 会**放行**一切。该归一器缺陷是 objectstack-ai#3871;新钉子旁的 tripwire 会在它被修好那天转红。 ## 反向验证(方向先写后跑) 把门还原成 `if (action.condition)`、求值不动:预判恰好三个测试转红,且都点名 `false` —— `condition: false` 那一行、布尔/envelope 等价性、以及「已声明的布尔确实 到达求值器」(真值门下它永远到不了)。实跑一致:`Tests 3 failed | 38 passed`, `disabled` 门的既有钉子全绿,纯表格断言与两条 tripwire 全绿。 Co-authored-by: Claude <noreply@anthropic.com>
Fixes#3848
基线
origin/main=f0a625aa7b00c93a48329e1c456b1aeacfceba43(即 #3849 / PR #3861 落地那一提交)。文件面仅packages/core。机理
ActionRunner.execute的执行门问的是action.disabled != null && action.disabled !== false,然后把原始值直接交给evaluateCondition—— 而那个函数对「这里没有条件」只有一个默认答案:返回true,含义是 visible/enabled。在disabled这个键上,true的意思恰好相反,是「已禁用」。于是任何空谓词都被判成禁用:handler 一次不跑,调用方收到一个元数据从未声明过的{ success: false, error: 'Action is disabled' }。#3842 / #3849 修完渲染器两侧之后,这已经是活的用户可见行为:按钮能点了,点下去收到
Action is disabled。同一个谓词值,渲染器与执行入口给出相反答案 —— #3314 已经付过一次代价的形状。逐形状新旧对照(计数 handler,同一张探针表跑基线与改后)
「拦」= handler 未被调用且返回
Action is disabled;「跑」= handler 被调用、success: true。disabled''' '(纯空白){ dialect: 'cel', source: '' }falsetrueuser.role == "admin"user.role == "guest"{ dialect: 'cel', source: 'true' }{ dialect: 'cel', source: 'false' }'${user.role === "admin"}'(模板取真)'${user.role === "guest"}'(模板取假)0(非谓词){}(非谓词)变化只有加粗那五行,方向一律是「拦 → 放行」。
修法
新门先问「有没有条件可判」,再求值。「没有条件」的判定取自 core 唯一的谓词归一器
toPredicateInput(把''/null/空sourceenvelope/非谓词值映射为undefined),外加纯空白字符串 —— 归一器对它是包裹而非折叠,而evaluateCondition自己就把空白trimmed叫做「没有条件」,evalRowPredicate(evaluator/listConditional.ts)用的也是这条 blank-source 规则。catch { isDisabled = false }失败放行姿态原样保留。为什么求值仍读原始值,而不是归一结果
派发时的优先形状是「先
toPredicateInput归一,再据归一结果求值」。逐形状实测发现这个复合对已经是${…}模板的字符串不成立:toPredicateInput假定入参是裸表达式、无条件包裹,'${x}'变成'${${x}}',单模板快路/^\$\{([^}]+)\}$/不再匹配,内层解析失败后 catch 里原样退回,Boolean(非空串)恒为true。也就是说那样求值的模板谓词永远「已禁用」,与表达式取值无关。实测:disabled: '${user.role === "guest"}'(admin context,取假)基线放行 → 归一求值后被拦。这是本 PR 唯一偏离派发形状的地方:归一只用于判门,verdict 仍读原始值,于是每个非空形状的 verdict 逐一不变,本改动只可能少拦、不可能多拦。归一器的二次包裹缺陷另立 #3871(动作面渲染器与
ActionEngine都以该复合书写,因此都中招;而SchemaRenderer与page:header读原始值、各自有钉子钉住正确 verdict)。钉子里留了指向 #3871 的 tripwire,该单修好那天它会红,提示可以让本门改读归一结果。仍然不一致的两行都写明归属,不在本单顺手修:空
sourceenvelope 的渲染器半边归 #3850(「空谓词」范围裁决,在队),${…}拼法归 #3871。⛔ 未碰components/visibility-gate.ts、未搬 helper 进 core、未碰packages/react(#3862)。钉子
packages/core/src/actions/__tests__/ActionRunner.disabledGate.test.ts(表驱动,形状抄 PR #3851 / #3861 的钉子风格):14 形状逐一断言 handler 跑没跑;一条断言空谓词根本不该到达求值器(门决定,不是 verdict 决定);一条断言「已声明 parity 的每一行,执行侧 verdict 等于渲染器侧 verdict」(渲染器列是从 #3848 正文表转录的证据 —— core 是渲染器包的被依赖方,不能反向 import,活的渲染器钉子在 #3842/#3861 的测试里),并把两行故意不声明 parity 的形状连同归属单号一起钉住;另有机理钉:evaluateCondition(undefined) === true这条反向默认值,以及 #3871 的 tripwire。反向验证(方向先判后跑,预判写在钉子头注释里)
还原旧门(
!= null && !== false)、求值不动 → 预判「恰好五行『没有条件』变红并各自点名形状,非空形状全绿」。实跑:与预判一致(第六条红的是那条「不该到达求值器」的钉子,同属这条门)。
验证
全仓扫过执行门的消费半径:
Action is disabled的断言只在packages/core;components/plugin-detail里的disabled: ''用例全是渲染器侧钉子,不经ActionRunner.execute,未受影响。顺手发现(已另立,未在本 PR 修)
${…}模板的字符串二次包裹,动作面的谓词一律判成 true(visible 永远显示 / disabled 永远置灰) #3871 ——toPredicateInput对已经是${…}模板的字符串二次包裹,动作面渲染器与ActionEngine因此把模板谓词一律判成 true(visible永远显示 /disabled永远置灰),而SchemaRenderer与page:header对同一拼法是对的(各有绿钉子)。condition门用真值判定(if (action.condition)),于是condition: false(最明确的「永不执行」)照样执行,实测 handler 跑了;而同义的{ dialect: 'cel', source: 'false' }被拦。components/action/**: 同一真值可见性门在动作面上未收口 ——action:group/action:menu成员动作声明visible: false照样渲染(#3758 的同族部分) #3812 的形状,方向是过度放行。Generated by Claude Code