Skip to content

Commit 5c5fd94

Browse files
os-zhuangclaude
andcommitted
fix(automation): recognize inline config.script so it isn't flagged as an empty no-op (#1870)
CI Build Core caught that the build-time check (and the runtime executor) treated a `script` node carrying inline JS in `config.script` as target-less, breaking example-crm/showcase builds. Inline `config.script` is a distinct, recognized form — the built-in runtime has no server-side JS sandbox so it does NOT execute it (a separate gap, out of #1870's callable-resolution scope). Now: - build validation accepts `function` | `actionType` | non-empty `script`; - the runtime executor warns loudly that inline scripts aren't executed (and points to `defineStack({ functions })`) instead of failing the step or silently succeeding. Verified example-crm, app-showcase, app-todo all build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2ca73ac commit 5c5fd94

5 files changed

Lines changed: 33 additions & 3 deletions

File tree

packages/cli/src/utils/validate-expressions.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ describe('validateStackExpressions (ADR-0032 build-time)', () => {
9292
{ id: 'start', type: 'start', config: {} },
9393
{ id: 'mail', type: 'script', config: { actionType: 'email' } },
9494
{ id: 'triage', type: 'script', config: { function: 'helpdesk.aiTriageStub' } },
95+
{ id: 'inline', type: 'script', config: { script: 'variables.x = 1;' } },
9596
],
9697
edges: [],
9798
}],

packages/cli/src/utils/validate-expressions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ export function validateStackExpressions(stack: AnyRec): ExprIssue[] {
8686
if (node.type === 'script') {
8787
const fn = typeof cfg.function === 'string' ? cfg.function.trim() : '';
8888
const action = typeof cfg.actionType === 'string' ? cfg.actionType.trim() : '';
89-
if (!fn && !action) {
89+
// Inline `config.script` (a JS body) is also a declared form — the
90+
// built-in runtime doesn't execute it (warned at run time), but the node
91+
// is not the empty no-op this check targets, so don't flag it.
92+
const inline = typeof cfg.script === 'string' ? cfg.script.trim() : '';
93+
if (!fn && !action && !inline) {
9094
issues.push({
9195
where: `flow '${flowName}' · node '${node.id}' (script) callable`,
9296
message:

packages/services/service-automation/src/builtin/screen-nodes.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ describe('script node (#1870 — callable resolution)', () => {
8686
expect(result.error).toMatch(/no function named|not a built-in/i);
8787
});
8888

89+
it('recognizes inline config.script as a no-op (not a loud failure) — built-in runtime has no JS sandbox', async () => {
90+
engine.registerFlow('script_flow', scriptFlow({ script: 'variables.x = 1;', outputVariables: ['x'] }));
91+
const result = await engine.execute('script_flow', {} as any);
92+
// Recognized form: succeeds (doesn't fail loud), but is documented as not executed.
93+
expect(result.success).toBe(true);
94+
});
95+
8996
it('FAILS LOUDLY when the script node declares no target at all (actionType: undefined repro)', async () => {
9097
engine.registerFlow('script_flow', scriptFlow({ actionType: undefined }));
9198
const result = await engine.execute('script_flow', {} as any);

packages/services/service-automation/src/builtin/screen-nodes.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ export function registerScreenNodes(engine: AutomationEngine, ctx: PluginContext
102102
};
103103
}
104104

105+
// Inline `config.script` (a JS source body) is a distinct, recognized
106+
// form — but the built-in runtime has no server-side JS sandbox, so it
107+
// does not execute it. Warn loudly (not a silent success) and steer the
108+
// author to the supported path — a registered function — rather than
109+
// failing the flow. Executing inline scripts is a separate capability,
110+
// out of #1870's callable-resolution scope.
111+
const inlineScript = typeof cfg.script === 'string' && cfg.script.trim() ? cfg.script : undefined;
112+
if (!fnName && inlineScript) {
113+
ctx.logger.warn(
114+
`[Script] node '${node.id}': inline \`config.script\` is not executed by the built-in runtime ` +
115+
`(no server-side JS sandbox) — this node is a no-op. To run server logic, move it into a ` +
116+
`registered function and call it via \`config.function\` + \`defineStack({ functions })\`.`,
117+
);
118+
return { success: true, output: { script: 'not-executed' } };
119+
}
120+
105121
// Otherwise the node names a function to invoke. `function` is canonical;
106122
// a bare `actionType` that matched no built-in is accepted as a shorthand
107123
// function name (so templates that point a node straight at e.g.

packages/spec/src/automation/flow.zod.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ export const FlowNodeAction = z.enum([
3434
'notify', // Outbound notification (ADR-0012) — dispatched via the messaging service
3535
'script', // Custom action: a built-in side-effect (`config.actionType: 'email'|'slack'`)
3636
// or a registered function (`config.function: 'name'` + `config.inputs`),
37-
// resolved from `defineStack({ functions })`. An empty script node is
38-
// rejected at build/registration and fails loudly at run time (#1870).
37+
// resolved from `defineStack({ functions })`. (Inline `config.script` JS is
38+
// recognized but NOT executed by the built-in runtime — no server-side
39+
// sandbox.) A script node naming none of these is flagged at build and
40+
// fails loudly at run time (#1870).
3941
'screen', // Screen / User-Input Element
4042
'wait', // Delay/Sleep
4143
'subflow', // Call another flow

0 commit comments

Comments
 (0)