@crewx/workflow 0.3.22-rc.2 → 0.3.22-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +255 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +72 -3
- package/dist/cli.js.map +1 -1
- package/dist/src/engine.d.ts +2 -0
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +49 -1
- package/dist/src/engine.js.map +1 -1
- package/dist/src/mermaid.d.ts.map +1 -1
- package/dist/src/mermaid.js +20 -4
- package/dist/src/mermaid.js.map +1 -1
- package/dist/src/run-manager.d.ts +9 -2
- package/dist/src/run-manager.d.ts.map +1 -1
- package/dist/src/run-manager.js +488 -24
- package/dist/src/run-manager.js.map +1 -1
- package/dist/src/types.d.ts +30 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/output-format.d.ts +12 -0
- package/dist/src/utils/output-format.d.ts.map +1 -1
- package/dist/src/utils/output-format.js +49 -0
- package/dist/src/utils/output-format.js.map +1 -1
- package/dist/src/utils/shell-security.d.ts +18 -0
- package/dist/src/utils/shell-security.d.ts.map +1 -0
- package/dist/src/utils/shell-security.js +138 -0
- package/dist/src/utils/shell-security.js.map +1 -0
- package/dist/workflow-schema.json +42 -1
- package/package.json +15 -3
- package/workflow-schema.json +42 -1
package/dist/src/run-manager.js
CHANGED
|
@@ -33,11 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.RunManager = void 0;
|
|
36
|
+
exports.RunManager = exports.SKILL_TASK_DEFAULT_TIMEOUT = void 0;
|
|
37
37
|
exports.parseSetArgs = parseSetArgs;
|
|
38
38
|
exports.readStdin = readStdin;
|
|
39
39
|
exports.interpolateTemplate = interpolateTemplate;
|
|
40
40
|
exports.safeEvaluate = safeEvaluate;
|
|
41
|
+
exports.safeEvaluateBranchCondition = safeEvaluateBranchCondition;
|
|
42
|
+
exports.validateSafeBranchCondition = validateSafeBranchCondition;
|
|
41
43
|
const fs = __importStar(require("fs"));
|
|
42
44
|
const path = __importStar(require("path"));
|
|
43
45
|
const yaml = __importStar(require("js-yaml"));
|
|
@@ -45,6 +47,112 @@ const cp = __importStar(require("child_process"));
|
|
|
45
47
|
const sdk_1 = require("@crewx/sdk");
|
|
46
48
|
const file_lock_1 = require("./file-lock");
|
|
47
49
|
const output_format_1 = require("./utils/output-format");
|
|
50
|
+
const shell_security_1 = require("./utils/shell-security");
|
|
51
|
+
exports.SKILL_TASK_DEFAULT_TIMEOUT = 600000;
|
|
52
|
+
const KNOWN_AUTO_NODE_TYPES = new Set([
|
|
53
|
+
'agent_task', 'skill_task', 'shell_task', 'branch', 'expression', 'join', 'end', 'error',
|
|
54
|
+
]);
|
|
55
|
+
const SELF_ADVANCING_NODE_TYPES = new Set(['branch', 'expression', 'end', 'error']);
|
|
56
|
+
function killProcessGroup(child) {
|
|
57
|
+
try {
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
if (child.pid) {
|
|
60
|
+
cp.spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (child.pid) {
|
|
64
|
+
try {
|
|
65
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
try {
|
|
69
|
+
child.kill('SIGKILL');
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
}
|
|
77
|
+
async function runProcess(command, argv, opts) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
child = cp.spawn(command, argv, {
|
|
82
|
+
cwd: opts.cwd,
|
|
83
|
+
env: opts.env,
|
|
84
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
|
+
shell: opts.shell ?? false,
|
|
86
|
+
detached: process.platform !== 'win32',
|
|
87
|
+
windowsHide: opts.windowsHide ?? true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
resolve({ status: null, stdout: '', stderr: '', timedOut: false, error: err });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
let settled = false;
|
|
97
|
+
const finish = (result) => {
|
|
98
|
+
if (settled)
|
|
99
|
+
return;
|
|
100
|
+
settled = true;
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
resolve(result);
|
|
103
|
+
};
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
killProcessGroup(child);
|
|
106
|
+
finish({ status: null, stdout, stderr, timedOut: true });
|
|
107
|
+
}, opts.timeout);
|
|
108
|
+
if (typeof timer.unref === 'function')
|
|
109
|
+
timer.unref();
|
|
110
|
+
child.stdout?.on('data', (d) => { stdout += d.toString(); });
|
|
111
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
112
|
+
child.on('error', (err) => finish({ status: null, stdout, stderr, timedOut: false, error: err }));
|
|
113
|
+
child.on('close', (code) => finish({ status: code, stdout, stderr, timedOut: false }));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function maskArg(arg) {
|
|
117
|
+
return arg.replace(/^(--?[\w-]*(?:token|secret|password|passwd|key|auth)[\w-]*)=(.+)$/i, '$1=***');
|
|
118
|
+
}
|
|
119
|
+
function summarizeNode(node) {
|
|
120
|
+
switch (node.type) {
|
|
121
|
+
case 'agent_task':
|
|
122
|
+
return `@${node.agent ?? '?'} (${node.mode === 'query' ? 'q' : 'x'})`;
|
|
123
|
+
case 'skill_task':
|
|
124
|
+
return `skill ${node.skill ?? '?'} ${(node.args ?? []).map(maskArg).join(' ')}`.trim();
|
|
125
|
+
case 'shell_task':
|
|
126
|
+
return (node.command ?? []).map(maskArg).join(' ');
|
|
127
|
+
case 'branch':
|
|
128
|
+
return `branch: ${node.condition ?? ''}`;
|
|
129
|
+
case 'expression':
|
|
130
|
+
return `set ${Object.keys(node.set ?? {}).join(', ')}`;
|
|
131
|
+
default:
|
|
132
|
+
return node.type;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function storeNodeOutput(nodeSpec, nodeId, stdout, stateUpdates) {
|
|
136
|
+
if (!nodeSpec.output)
|
|
137
|
+
return;
|
|
138
|
+
const trimmed = stdout.trim();
|
|
139
|
+
if (nodeSpec.output_format === 'json') {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse((0, output_format_1.extractJson)(trimmed));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
throw new Error(`Node "${nodeId}" output_format=json parse failed: ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
if (nodeSpec.output_schema) {
|
|
148
|
+
await (0, output_format_1.validateJsonSchema)(parsed, nodeSpec.output_schema);
|
|
149
|
+
}
|
|
150
|
+
stateUpdates[nodeSpec.output] = parsed;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
stateUpdates[nodeSpec.output] = trimmed;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
48
156
|
function parseValue(raw) {
|
|
49
157
|
try {
|
|
50
158
|
return JSON.parse(raw);
|
|
@@ -104,6 +212,7 @@ const FORBIDDEN_IDENTS = new Set([
|
|
|
104
212
|
'require', 'import', 'process', 'global', 'globalThis',
|
|
105
213
|
'Function', 'eval', '__proto__', 'constructor', 'prototype',
|
|
106
214
|
]);
|
|
215
|
+
const FORBIDDEN_STATE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
107
216
|
function tokenize(expr) {
|
|
108
217
|
const tokens = [];
|
|
109
218
|
let i = 0;
|
|
@@ -337,6 +446,8 @@ class ExprParser {
|
|
|
337
446
|
const val = this.state[key];
|
|
338
447
|
if (val === undefined || val === null)
|
|
339
448
|
return 0;
|
|
449
|
+
if (typeof val === 'boolean' || typeof val === 'number')
|
|
450
|
+
return val;
|
|
340
451
|
const strVal = String(val);
|
|
341
452
|
if (strVal === '')
|
|
342
453
|
return '';
|
|
@@ -359,11 +470,63 @@ function safeEvaluate(expr, state) {
|
|
|
359
470
|
const parser = new ExprParser(tokens, state);
|
|
360
471
|
return parser.parse();
|
|
361
472
|
}
|
|
473
|
+
function getStatePathValue(state, pathExpr) {
|
|
474
|
+
const parts = pathExpr.split('.');
|
|
475
|
+
let value = state;
|
|
476
|
+
for (const part of parts) {
|
|
477
|
+
if (FORBIDDEN_STATE_PATH_SEGMENTS.has(part)) {
|
|
478
|
+
throw new Error(`Forbidden state path segment in expression: "${part}"`);
|
|
479
|
+
}
|
|
480
|
+
if (!part || value === null || value === undefined || typeof value !== 'object') {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
value = value[part];
|
|
484
|
+
}
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
function unescapeQuotedLiteral(value) {
|
|
488
|
+
return value
|
|
489
|
+
.replace(/\\'/g, "'")
|
|
490
|
+
.replace(/\\"/g, '"')
|
|
491
|
+
.replace(/\\\\/g, '\\');
|
|
492
|
+
}
|
|
493
|
+
function normalizeBranchCondition(expr, state) {
|
|
494
|
+
const scope = {};
|
|
495
|
+
let counter = 0;
|
|
496
|
+
const bind = (value) => {
|
|
497
|
+
const key = `__state_${counter++}`;
|
|
498
|
+
scope[key] = value;
|
|
499
|
+
return key;
|
|
500
|
+
};
|
|
501
|
+
let normalized = expr
|
|
502
|
+
.replace(/!==/g, '!=')
|
|
503
|
+
.replace(/===/g, '==');
|
|
504
|
+
const replaceIncludes = (pattern) => {
|
|
505
|
+
normalized = normalized.replace(pattern, (_match, statePath, literal) => {
|
|
506
|
+
const value = getStatePathValue(state, statePath);
|
|
507
|
+
return bind(String(value ?? '').includes(unescapeQuotedLiteral(literal)));
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*'((?:\\.|[^'\\])*)'\s*\)/g);
|
|
511
|
+
replaceIncludes(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\.includes\(\s*"((?:\\.|[^"\\])*)"\s*\)/g);
|
|
512
|
+
normalized = normalized.replace(/\bstate\.([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)/g, (_match, statePath) => bind(getStatePathValue(state, statePath)));
|
|
513
|
+
normalized = normalized.replace(/\bNumber\(\s*([A-Za-z_]\w*)\s*\)/g, '$1');
|
|
514
|
+
normalized = normalized.replace(/\bparseInt\(\s*([A-Za-z_]\w*(?:\s*\|\|\s*(?:'[^']*'|"[^"]*"))?)\s*\)/g, '($1)');
|
|
515
|
+
return { expr: normalized, scope };
|
|
516
|
+
}
|
|
517
|
+
function safeEvaluateBranchCondition(expr, state) {
|
|
518
|
+
const normalized = normalizeBranchCondition(expr, state);
|
|
519
|
+
return safeEvaluate(normalized.expr, { ...state, ...normalized.scope });
|
|
520
|
+
}
|
|
521
|
+
function validateSafeBranchCondition(expr) {
|
|
522
|
+
safeEvaluateBranchCondition(expr, {});
|
|
523
|
+
}
|
|
362
524
|
class RunManager {
|
|
363
|
-
constructor(runsDir = '.crewx/workflow-runs') {
|
|
525
|
+
constructor(runsDir = '.crewx/workflow-runs', projectRoot) {
|
|
364
526
|
this.runsDir = runsDir;
|
|
365
|
-
const base = process.env.CREWX_WORKSPACE || process.cwd();
|
|
527
|
+
const base = projectRoot || process.env.CREWX_WORKSPACE || process.cwd();
|
|
366
528
|
this.resolvedDir = path.resolve(base, this.runsDir);
|
|
529
|
+
this.projectRoot = path.resolve(base);
|
|
367
530
|
}
|
|
368
531
|
ensureRunsDir() {
|
|
369
532
|
if (!fs.existsSync(this.resolvedDir)) {
|
|
@@ -521,7 +684,28 @@ class RunManager {
|
|
|
521
684
|
const nodesToComplete = [];
|
|
522
685
|
let currentNode = nodeId;
|
|
523
686
|
let status;
|
|
687
|
+
let failureMessage;
|
|
524
688
|
let nodeTaskId;
|
|
689
|
+
const trigger = opts?.trigger ?? 'manual';
|
|
690
|
+
const startedAt = new Date();
|
|
691
|
+
let nodeStdout = '';
|
|
692
|
+
let nodeStderr = '';
|
|
693
|
+
let nodeExitCode = null;
|
|
694
|
+
const buildAudit = (error) => {
|
|
695
|
+
const endedAt = new Date();
|
|
696
|
+
return {
|
|
697
|
+
exec_id: snapshot.id,
|
|
698
|
+
node_id: nodeId,
|
|
699
|
+
type: nodeSpec.type,
|
|
700
|
+
summary: summarizeNode(nodeSpec),
|
|
701
|
+
started_at: startedAt.toISOString(),
|
|
702
|
+
ended_at: endedAt.toISOString(),
|
|
703
|
+
duration_ms: endedAt.getTime() - startedAt.getTime(),
|
|
704
|
+
exit_code: nodeExitCode,
|
|
705
|
+
...(error ? { error } : {}),
|
|
706
|
+
trigger,
|
|
707
|
+
};
|
|
708
|
+
};
|
|
525
709
|
try {
|
|
526
710
|
switch (nodeSpec.type) {
|
|
527
711
|
case 'agent_task': {
|
|
@@ -531,9 +715,16 @@ class RunManager {
|
|
|
531
715
|
const input = nodeSpec.input
|
|
532
716
|
? interpolateTemplate(nodeSpec.input, { ...snapshot.state, state: snapshot.state })
|
|
533
717
|
: '';
|
|
718
|
+
const preamble = process.env.CREWX_WORKFLOW_PREAMBLE === 'off'
|
|
719
|
+
? ''
|
|
720
|
+
: (0, output_format_1.buildWorkflowPreamble)({
|
|
721
|
+
workflowId: snapshot.workflow_id,
|
|
722
|
+
nodeId,
|
|
723
|
+
execId: snapshot.id,
|
|
724
|
+
});
|
|
534
725
|
const finalInput = nodeSpec.output_format === 'json'
|
|
535
|
-
? input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
536
|
-
: input;
|
|
726
|
+
? preamble + input + (0, output_format_1.buildJsonOutputSuffix)(nodeSpec.output_schema, nodeSpec.output_strict)
|
|
727
|
+
: preamble + input;
|
|
537
728
|
const mode = nodeSpec.mode === 'query' ? 'q' : 'x';
|
|
538
729
|
const prompt = `@${nodeSpec.agent} ${finalInput}`;
|
|
539
730
|
if (opts?.dryRun) {
|
|
@@ -564,30 +755,38 @@ class RunManager {
|
|
|
564
755
|
packageName: 'crewx',
|
|
565
756
|
});
|
|
566
757
|
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, wfArgs);
|
|
567
|
-
const result =
|
|
568
|
-
encoding: 'utf-8',
|
|
569
|
-
timeout: nodeTimeout,
|
|
758
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
570
759
|
cwd: process.cwd(),
|
|
571
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
572
|
-
shell: invocation.shell ?? false,
|
|
573
|
-
windowsHide: invocation.windowsHide,
|
|
574
760
|
env: {
|
|
575
761
|
...process.env,
|
|
576
762
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
577
763
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
578
764
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
579
765
|
},
|
|
766
|
+
timeout: nodeTimeout,
|
|
767
|
+
shell: invocation.shell ?? false,
|
|
768
|
+
windowsHide: invocation.windowsHide,
|
|
580
769
|
});
|
|
770
|
+
nodeStdout = result.stdout;
|
|
771
|
+
nodeStderr = result.stderr;
|
|
772
|
+
nodeExitCode = result.status;
|
|
581
773
|
if (result.error) {
|
|
582
774
|
throw result.error;
|
|
583
775
|
}
|
|
584
|
-
|
|
776
|
+
if (result.timedOut) {
|
|
777
|
+
throw new Error(`Agent execution timed out after ${nodeTimeout}ms`);
|
|
778
|
+
}
|
|
779
|
+
const taskIdMatch = result.stderr.match(/crewx kill (tsk_\w+)/);
|
|
585
780
|
nodeTaskId = taskIdMatch?.[1];
|
|
586
781
|
if (result.status !== 0) {
|
|
587
|
-
const stderr = result.stderr
|
|
782
|
+
const stderr = result.stderr.trim();
|
|
588
783
|
throw new Error(`Agent execution failed (exit ${result.status}): ${stderr}`);
|
|
589
784
|
}
|
|
590
|
-
const output = result.stdout
|
|
785
|
+
const output = result.stdout.trim();
|
|
786
|
+
const signal = (0, output_format_1.detectWorkflowSignal)(output);
|
|
787
|
+
if (signal) {
|
|
788
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${signal.reason ?? '(no reason given)'}`);
|
|
789
|
+
}
|
|
591
790
|
if (nodeSpec.output) {
|
|
592
791
|
if (nodeSpec.output_format === 'json') {
|
|
593
792
|
const retryMax = nodeSpec.output_retry ?? 1;
|
|
@@ -607,26 +806,33 @@ class RunManager {
|
|
|
607
806
|
}
|
|
608
807
|
retryArgs.push('--metadata', JSON.stringify(wfMetadata));
|
|
609
808
|
const retryInvocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, retryArgs);
|
|
610
|
-
const retryResult =
|
|
611
|
-
encoding: 'utf-8',
|
|
612
|
-
timeout: nodeTimeout,
|
|
809
|
+
const retryResult = await runProcess(retryInvocation.command, retryInvocation.argv, {
|
|
613
810
|
cwd: process.cwd(),
|
|
614
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
615
|
-
shell: retryInvocation.shell ?? false,
|
|
616
|
-
windowsHide: retryInvocation.windowsHide,
|
|
617
811
|
env: {
|
|
618
812
|
...process.env,
|
|
619
813
|
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
620
814
|
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
621
815
|
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
622
816
|
},
|
|
817
|
+
timeout: nodeTimeout,
|
|
818
|
+
shell: retryInvocation.shell ?? false,
|
|
819
|
+
windowsHide: retryInvocation.windowsHide,
|
|
623
820
|
});
|
|
821
|
+
nodeStdout = retryResult.stdout;
|
|
822
|
+
nodeStderr = retryResult.stderr;
|
|
823
|
+
nodeExitCode = retryResult.status;
|
|
624
824
|
if (retryResult.error)
|
|
625
825
|
throw retryResult.error;
|
|
826
|
+
if (retryResult.timedOut)
|
|
827
|
+
throw new Error(`Agent retry timed out after ${nodeTimeout}ms`);
|
|
626
828
|
if (retryResult.status !== 0) {
|
|
627
|
-
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr
|
|
829
|
+
throw new Error(`Agent retry failed (exit ${retryResult.status}): ${retryResult.stderr.trim()}`);
|
|
830
|
+
}
|
|
831
|
+
attemptOutput = retryResult.stdout.trim();
|
|
832
|
+
const retrySignal = (0, output_format_1.detectWorkflowSignal)(attemptOutput);
|
|
833
|
+
if (retrySignal) {
|
|
834
|
+
throw new Error(`Node "${nodeId}" agent signaled failure: ${retrySignal.reason ?? '(no reason given)'}`);
|
|
628
835
|
}
|
|
629
|
-
attemptOutput = retryResult.stdout?.trim() ?? '';
|
|
630
836
|
}
|
|
631
837
|
try {
|
|
632
838
|
parsed = JSON.parse((0, output_format_1.extractJson)(attemptOutput));
|
|
@@ -652,12 +858,123 @@ class RunManager {
|
|
|
652
858
|
}
|
|
653
859
|
break;
|
|
654
860
|
}
|
|
861
|
+
case 'skill_task': {
|
|
862
|
+
if (!nodeSpec.skill) {
|
|
863
|
+
throw new Error(`Node "${nodeId}" is skill_task but has no skill defined`);
|
|
864
|
+
}
|
|
865
|
+
if (!shell_security_1.SKILL_NAME_RE.test(nodeSpec.skill)) {
|
|
866
|
+
throw new Error(`Node "${nodeId}" has invalid skill name "${nodeSpec.skill}" (must match ^[a-z0-9][a-z0-9-]*$)`);
|
|
867
|
+
}
|
|
868
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
869
|
+
const skillArgs = (nodeSpec.args ?? []).map((a) => interpolateTemplate(String(a), ctx));
|
|
870
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
871
|
+
if (!cwdCheck.ok)
|
|
872
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
873
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
874
|
+
if (opts?.dryRun) {
|
|
875
|
+
console.log(`[dry-run] Would execute: crewx skill ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
console.log(`Executing skill: ${nodeSpec.skill} ${skillArgs.join(' ')}`.trim());
|
|
879
|
+
const crewxCli = process.env.CREWX_CLI || 'npx crewx';
|
|
880
|
+
const cliParts = crewxCli.split(/\s+/);
|
|
881
|
+
const bin = cliParts[0];
|
|
882
|
+
const baseArgs = cliParts.slice(1);
|
|
883
|
+
const skillArgv = [...baseArgs, 'skill', nodeSpec.skill, ...skillArgs];
|
|
884
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
885
|
+
command: bin,
|
|
886
|
+
allowShellFallback: true,
|
|
887
|
+
packageName: 'crewx',
|
|
888
|
+
});
|
|
889
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, skillArgv);
|
|
890
|
+
const result = await runProcess(invocation.command, invocation.argv, {
|
|
891
|
+
cwd: cwdCheck.resolved,
|
|
892
|
+
env: {
|
|
893
|
+
...process.env,
|
|
894
|
+
CREWX_WORKFLOW_EXEC_ID: snapshot.id,
|
|
895
|
+
CREWX_WORKFLOW_NODE_ID: nodeId,
|
|
896
|
+
CREWX_WORKFLOW_ID: snapshot.workflow_id,
|
|
897
|
+
},
|
|
898
|
+
timeout,
|
|
899
|
+
shell: invocation.shell ?? false,
|
|
900
|
+
windowsHide: invocation.windowsHide,
|
|
901
|
+
});
|
|
902
|
+
nodeStdout = result.stdout;
|
|
903
|
+
nodeStderr = result.stderr;
|
|
904
|
+
nodeExitCode = result.status;
|
|
905
|
+
if (result.error)
|
|
906
|
+
throw result.error;
|
|
907
|
+
if (result.timedOut)
|
|
908
|
+
throw new Error(`skill_task "${nodeId}" timed out after ${timeout}ms`);
|
|
909
|
+
if (result.status !== 0) {
|
|
910
|
+
throw new Error(`skill_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
911
|
+
}
|
|
912
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
913
|
+
if (nodeSpec.output) {
|
|
914
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
915
|
+
}
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'shell_task': {
|
|
919
|
+
const shellEnvEnabled = process.env.CREWX_WORKFLOW_SHELL === '1';
|
|
920
|
+
const shellMetaAllowed = spec.metadata?.shell_task_allowed === true;
|
|
921
|
+
if (!shellEnvEnabled || !shellMetaAllowed) {
|
|
922
|
+
throw new Error('shell_task is disabled. Set CREWX_WORKFLOW_SHELL=1 and metadata.shell_task_allowed=true to enable.');
|
|
923
|
+
}
|
|
924
|
+
const cmdCheck = (0, shell_security_1.validateShellCommand)(nodeSpec.command);
|
|
925
|
+
if (!cmdCheck.ok)
|
|
926
|
+
throw new Error(`Node "${nodeId}": ${cmdCheck.error}`);
|
|
927
|
+
const envCheck = (0, shell_security_1.validateEnvKeys)(nodeSpec.env);
|
|
928
|
+
if (!envCheck.ok)
|
|
929
|
+
throw new Error(`Node "${nodeId}": ${envCheck.error}`);
|
|
930
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
931
|
+
const command = nodeSpec.command.map((c) => interpolateTemplate(c, ctx));
|
|
932
|
+
const postCheck = (0, shell_security_1.validateShellCommand)(command);
|
|
933
|
+
if (!postCheck.ok)
|
|
934
|
+
throw new Error(`Node "${nodeId}": ${postCheck.error}`);
|
|
935
|
+
const interpolatedEnv = nodeSpec.env
|
|
936
|
+
? Object.fromEntries(Object.entries(nodeSpec.env).map(([k, v]) => [k, interpolateTemplate(String(v), ctx)]))
|
|
937
|
+
: undefined;
|
|
938
|
+
const cwdCheck = (0, shell_security_1.normalizeCwd)(nodeSpec.cwd ? interpolateTemplate(nodeSpec.cwd, ctx) : undefined, this.projectRoot);
|
|
939
|
+
if (!cwdCheck.ok)
|
|
940
|
+
throw new Error(`Node "${nodeId}": ${cwdCheck.error}`);
|
|
941
|
+
const timeout = nodeSpec.timeout ?? exports.SKILL_TASK_DEFAULT_TIMEOUT;
|
|
942
|
+
if (opts?.dryRun) {
|
|
943
|
+
console.log(`[dry-run] Would execute: ${command.join(' ')}`);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
console.log(`Executing shell_task: ${command[0]} (${command.length - 1} args)`);
|
|
947
|
+
const childEnv = (0, shell_security_1.buildShellEnv)(interpolatedEnv, process.env);
|
|
948
|
+
childEnv.CREWX_WORKFLOW_EXEC_ID = snapshot.id;
|
|
949
|
+
childEnv.CREWX_WORKFLOW_NODE_ID = nodeId;
|
|
950
|
+
childEnv.CREWX_WORKFLOW_ID = snapshot.workflow_id;
|
|
951
|
+
const result = await runProcess(command[0], command.slice(1), {
|
|
952
|
+
cwd: cwdCheck.resolved,
|
|
953
|
+
env: childEnv,
|
|
954
|
+
timeout,
|
|
955
|
+
shell: false,
|
|
956
|
+
});
|
|
957
|
+
nodeStdout = result.stdout;
|
|
958
|
+
nodeStderr = result.stderr;
|
|
959
|
+
nodeExitCode = result.status;
|
|
960
|
+
if (result.error)
|
|
961
|
+
throw result.error;
|
|
962
|
+
if (result.timedOut)
|
|
963
|
+
throw new Error(`shell_task "${nodeId}" timed out after ${timeout}ms`);
|
|
964
|
+
if (result.status !== 0) {
|
|
965
|
+
throw new Error(`shell_task "${nodeId}" failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
966
|
+
}
|
|
967
|
+
await storeNodeOutput(nodeSpec, nodeId, result.stdout, stateUpdates);
|
|
968
|
+
if (nodeSpec.output) {
|
|
969
|
+
console.log(`Result stored in state.${nodeSpec.output}`);
|
|
970
|
+
}
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
655
973
|
case 'branch': {
|
|
656
974
|
if (!nodeSpec.condition) {
|
|
657
975
|
throw new Error(`Branch node "${nodeId}" has no condition`);
|
|
658
976
|
}
|
|
659
|
-
const
|
|
660
|
-
const condResult = String(evalFn(snapshot.state));
|
|
977
|
+
const condResult = String(safeEvaluateBranchCondition(String(nodeSpec.condition), snapshot.state));
|
|
661
978
|
const branches = nodeSpec.branches;
|
|
662
979
|
const targetNode = branches?.[condResult] ?? nodeSpec.default;
|
|
663
980
|
if (!targetNode) {
|
|
@@ -685,6 +1002,15 @@ class RunManager {
|
|
|
685
1002
|
console.log(`End "${nodeId}": workflow execution completed`);
|
|
686
1003
|
break;
|
|
687
1004
|
}
|
|
1005
|
+
case 'error': {
|
|
1006
|
+
status = 'failed';
|
|
1007
|
+
const ctx = { ...snapshot.state, state: snapshot.state };
|
|
1008
|
+
failureMessage = nodeSpec.message
|
|
1009
|
+
? interpolateTemplate(nodeSpec.message, ctx)
|
|
1010
|
+
: `Workflow failed at error node "${nodeId}"`;
|
|
1011
|
+
console.log(`Error "${nodeId}": workflow execution failed — ${failureMessage}`);
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
688
1014
|
case 'expression': {
|
|
689
1015
|
if (!nodeSpec.set || typeof nodeSpec.set !== 'object') {
|
|
690
1016
|
throw new Error(`Expression node "${nodeId}" requires "set" field`);
|
|
@@ -708,15 +1034,38 @@ class RunManager {
|
|
|
708
1034
|
default:
|
|
709
1035
|
console.log(`Unknown node type "${nodeSpec.type}" for node "${nodeId}"`);
|
|
710
1036
|
}
|
|
1037
|
+
if (nodeSpec.fail_when &&
|
|
1038
|
+
!opts?.dryRun &&
|
|
1039
|
+
(nodeSpec.type === 'agent_task' || nodeSpec.type === 'skill_task' || nodeSpec.type === 'shell_task')) {
|
|
1040
|
+
const judged = safeEvaluateBranchCondition(String(nodeSpec.fail_when), { ...snapshot.state, ...stateUpdates });
|
|
1041
|
+
if (judged === true || judged === 'true') {
|
|
1042
|
+
throw new Error(`Node "${nodeId}" judged failed by fail_when: ${nodeSpec.fail_when}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
711
1045
|
}
|
|
712
1046
|
catch (e) {
|
|
1047
|
+
const errMessage = e.message;
|
|
1048
|
+
const partial = [
|
|
1049
|
+
nodeStdout.trim() ? `stdout: ${nodeStdout.trim().slice(-500)}` : '',
|
|
1050
|
+
nodeStderr.trim() ? `stderr: ${nodeStderr.trim().slice(-500)}` : '',
|
|
1051
|
+
].filter(Boolean).join(' | ');
|
|
1052
|
+
const auditEntry = buildAudit(partial ? `${errMessage} (${partial})` : errMessage);
|
|
713
1053
|
await this.atomicUpdateAsync(execId, (run) => {
|
|
1054
|
+
if (run.status === 'cancelled')
|
|
1055
|
+
return;
|
|
1056
|
+
Object.assign(run.state, stateUpdates);
|
|
714
1057
|
run.current_node = nodeId;
|
|
715
1058
|
run.status = 'failed';
|
|
1059
|
+
run.error = errMessage;
|
|
1060
|
+
run.audit = run.audit ?? [];
|
|
1061
|
+
run.audit.push(auditEntry);
|
|
716
1062
|
});
|
|
717
1063
|
throw e;
|
|
718
1064
|
}
|
|
1065
|
+
const successAudit = buildAudit();
|
|
719
1066
|
return this.atomicUpdateAsync(execId, (run) => {
|
|
1067
|
+
if (run.status === 'cancelled')
|
|
1068
|
+
return;
|
|
720
1069
|
Object.assign(run.state, stateUpdates);
|
|
721
1070
|
for (const node of nodesToComplete) {
|
|
722
1071
|
if (!run.completed_nodes.includes(node)) {
|
|
@@ -729,10 +1078,125 @@ class RunManager {
|
|
|
729
1078
|
run.current_node = currentNode;
|
|
730
1079
|
if (status)
|
|
731
1080
|
run.status = status;
|
|
1081
|
+
if (failureMessage)
|
|
1082
|
+
run.error = failureMessage;
|
|
732
1083
|
if (nodeTaskId) {
|
|
733
1084
|
run.tasks = run.tasks ?? {};
|
|
734
1085
|
run.tasks[nodeId] = nodeTaskId;
|
|
735
1086
|
}
|
|
1087
|
+
run.audit = run.audit ?? [];
|
|
1088
|
+
run.audit.push(successAudit);
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
async runAuto(execId) {
|
|
1092
|
+
const initial = this.loadRun(execId);
|
|
1093
|
+
if (!initial)
|
|
1094
|
+
throw new Error(`Execution not found: ${execId}`);
|
|
1095
|
+
const doc = this.loadWorkflowYaml(initial.workflow_file);
|
|
1096
|
+
if (!doc)
|
|
1097
|
+
throw new Error(`Failed to load workflow file: ${initial.workflow_file}`);
|
|
1098
|
+
const spec = doc.workflows[initial.workflow_id];
|
|
1099
|
+
if (!spec) {
|
|
1100
|
+
throw new Error(`Workflow "${initial.workflow_id}" not found in ${initial.workflow_file}`);
|
|
1101
|
+
}
|
|
1102
|
+
const executed = [];
|
|
1103
|
+
if (initial.status === 'completed' || initial.status === 'failed' || initial.status === 'cancelled') {
|
|
1104
|
+
return {
|
|
1105
|
+
run: initial,
|
|
1106
|
+
outcome: initial.status === 'completed' ? 'completed' : initial.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1107
|
+
reason: `run already ${initial.status}`,
|
|
1108
|
+
executed,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const firstNode = Object.keys(spec.nodes ?? {})[0];
|
|
1112
|
+
const maxIterations = (typeof spec.max_iterations === 'number' && spec.max_iterations > 0)
|
|
1113
|
+
? spec.max_iterations
|
|
1114
|
+
: 30;
|
|
1115
|
+
const visitCounts = {};
|
|
1116
|
+
let iterations = 0;
|
|
1117
|
+
let run = initial;
|
|
1118
|
+
while (true) {
|
|
1119
|
+
run = this.loadRun(execId);
|
|
1120
|
+
if (run.status === 'completed')
|
|
1121
|
+
return { run, outcome: 'completed', executed };
|
|
1122
|
+
if (run.status === 'failed')
|
|
1123
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1124
|
+
if (run.status === 'cancelled')
|
|
1125
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1126
|
+
const nodeId = run.current_node || firstNode;
|
|
1127
|
+
if (!nodeId)
|
|
1128
|
+
return { run, outcome: 'failed', reason: 'workflow has no nodes', executed };
|
|
1129
|
+
const node = spec.nodes[nodeId];
|
|
1130
|
+
if (!node) {
|
|
1131
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1132
|
+
r.current_node = nodeId;
|
|
1133
|
+
r.status = 'failed';
|
|
1134
|
+
r.error = `node "${nodeId}" not found in workflow`;
|
|
1135
|
+
});
|
|
1136
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1137
|
+
}
|
|
1138
|
+
if (node.type === 'approval') {
|
|
1139
|
+
return { run, outcome: 'paused', reason: `approval node "${nodeId}" requires manual decision`, executed };
|
|
1140
|
+
}
|
|
1141
|
+
if (node.type === 'parallel') {
|
|
1142
|
+
return { run, outcome: 'paused', reason: `parallel auto-run not implemented (node "${nodeId}")`, executed };
|
|
1143
|
+
}
|
|
1144
|
+
if (!KNOWN_AUTO_NODE_TYPES.has(node.type)) {
|
|
1145
|
+
return { run, outcome: 'paused', reason: `unknown node type "${node.type}" (node "${nodeId}")`, executed };
|
|
1146
|
+
}
|
|
1147
|
+
visitCounts[nodeId] = (visitCounts[nodeId] ?? 0) + 1;
|
|
1148
|
+
iterations++;
|
|
1149
|
+
if (visitCounts[nodeId] > maxIterations || iterations > maxIterations) {
|
|
1150
|
+
run = await this.atomicUpdateAsync(execId, (r) => {
|
|
1151
|
+
r.status = 'failed';
|
|
1152
|
+
r.error = `auto runner exceeded max_iterations (${maxIterations}); possible loop at "${nodeId}"`;
|
|
1153
|
+
});
|
|
1154
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
run = await this.executeNode(execId, nodeId, { trigger: 'auto' });
|
|
1158
|
+
executed.push(nodeId);
|
|
1159
|
+
}
|
|
1160
|
+
catch (e) {
|
|
1161
|
+
run = this.loadRun(execId);
|
|
1162
|
+
return { run, outcome: 'failed', reason: e.message, executed };
|
|
1163
|
+
}
|
|
1164
|
+
if (run.status === 'completed')
|
|
1165
|
+
return { run, outcome: 'completed', executed };
|
|
1166
|
+
if (run.status === 'failed')
|
|
1167
|
+
return { run, outcome: 'failed', reason: run.error, executed };
|
|
1168
|
+
if (run.status === 'cancelled')
|
|
1169
|
+
return { run, outcome: 'cancelled', reason: run.error, executed };
|
|
1170
|
+
if (!SELF_ADVANCING_NODE_TYPES.has(node.type)) {
|
|
1171
|
+
const next = node.next;
|
|
1172
|
+
if (!next) {
|
|
1173
|
+
return { run, outcome: 'paused', reason: `node "${nodeId}" has no "next"; stopping`, executed };
|
|
1174
|
+
}
|
|
1175
|
+
run = this.moveNode(execId, next);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
cancelRun(execId, reason = 'Cancelled by user') {
|
|
1180
|
+
return this.atomicUpdate(execId, (run) => {
|
|
1181
|
+
if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const now = new Date();
|
|
1185
|
+
run.status = 'cancelled';
|
|
1186
|
+
run.error = reason;
|
|
1187
|
+
run.audit = run.audit ?? [];
|
|
1188
|
+
run.audit.push({
|
|
1189
|
+
exec_id: run.id,
|
|
1190
|
+
node_id: run.current_node || '',
|
|
1191
|
+
type: 'cancel',
|
|
1192
|
+
summary: reason,
|
|
1193
|
+
started_at: now.toISOString(),
|
|
1194
|
+
ended_at: now.toISOString(),
|
|
1195
|
+
duration_ms: 0,
|
|
1196
|
+
exit_code: null,
|
|
1197
|
+
error: reason,
|
|
1198
|
+
trigger: 'manual',
|
|
1199
|
+
});
|
|
736
1200
|
});
|
|
737
1201
|
}
|
|
738
1202
|
reset(execId) {
|