amalgm 0.1.51 → 0.1.52
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/lib/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const vm = require('vm');
|
|
7
|
+
|
|
8
|
+
const { DEFAULT_CWD } = require('../config');
|
|
9
|
+
const { recordEventRun } = require('../events/store');
|
|
10
|
+
const { compileWorkflowText } = require('./compiler');
|
|
11
|
+
const { loadWorkflows } = require('./store');
|
|
12
|
+
const { callToolboxMcpTool, toolboxMcpToolName } = require('../toolbox/runner');
|
|
13
|
+
const { resolveAmalgmCoreTool, resolveToolboxAction, suggestWorkflowToolActions } = require('../automations/tool-actions');
|
|
14
|
+
|
|
15
|
+
const DEFAULT_CELL_TIMEOUT_MS = 120_000;
|
|
16
|
+
const DEFAULT_CODE_TIMEOUT_MS = 15_000;
|
|
17
|
+
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
18
|
+
const workflowRunQueues = new Map();
|
|
19
|
+
|
|
20
|
+
class WorkflowStop extends Error {
|
|
21
|
+
constructor(message = 'Workflow stopped') {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'WorkflowStop';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isObject(value) {
|
|
28
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isFunctionRef(value) {
|
|
32
|
+
return isObject(value) && typeof value.$fn === 'string';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function durationSince(startedAt) {
|
|
36
|
+
return Date.now() - new Date(startedAt).getTime();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function positiveInteger(value, fallback = null) {
|
|
40
|
+
const number = Number(value);
|
|
41
|
+
if (!Number.isFinite(number)) return fallback;
|
|
42
|
+
return Math.max(1, Math.floor(number));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function queueLimitValue(value) {
|
|
46
|
+
const number = Number(value);
|
|
47
|
+
if (!Number.isFinite(number)) return 0;
|
|
48
|
+
return Math.max(0, Math.floor(number));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
52
|
+
let timer = null;
|
|
53
|
+
return Promise.race([
|
|
54
|
+
promise,
|
|
55
|
+
new Promise((_, reject) => {
|
|
56
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
57
|
+
}),
|
|
58
|
+
]).finally(() => {
|
|
59
|
+
if (timer) clearTimeout(timer);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function publicTrigger(trigger) {
|
|
64
|
+
return {
|
|
65
|
+
id: trigger.id,
|
|
66
|
+
name: trigger.name,
|
|
67
|
+
source: trigger.source,
|
|
68
|
+
event: trigger.event,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function publicWorkflow(workflow) {
|
|
73
|
+
return {
|
|
74
|
+
id: workflow.id,
|
|
75
|
+
name: workflow.name,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildSecrets(allowlist = {}) {
|
|
80
|
+
const names = Array.isArray(allowlist.secrets) ? allowlist.secrets : [];
|
|
81
|
+
const secrets = {};
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const key = String(name || '').trim();
|
|
84
|
+
if (key && process.env[key] != null) secrets[key] = process.env[key];
|
|
85
|
+
}
|
|
86
|
+
return secrets;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildInput(ctx, stop) {
|
|
90
|
+
return {
|
|
91
|
+
event: ctx.event,
|
|
92
|
+
payload: ctx.event?.payload,
|
|
93
|
+
headers: ctx.event?.headers || {},
|
|
94
|
+
trigger: ctx.trigger,
|
|
95
|
+
workflow: ctx.workflow,
|
|
96
|
+
previous: ctx.previous,
|
|
97
|
+
outputs: ctx.outputs,
|
|
98
|
+
secrets: ctx.secrets,
|
|
99
|
+
stop,
|
|
100
|
+
...ctx.outputs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runFunctionSource(fnSource, input, options = {}) {
|
|
105
|
+
const timeoutMs = Number(options.timeoutMs) || DEFAULT_CODE_TIMEOUT_MS;
|
|
106
|
+
const sandbox = {
|
|
107
|
+
__input: input,
|
|
108
|
+
__result: undefined,
|
|
109
|
+
console,
|
|
110
|
+
fetch: globalThis.fetch,
|
|
111
|
+
URL,
|
|
112
|
+
URLSearchParams,
|
|
113
|
+
TextDecoder,
|
|
114
|
+
TextEncoder,
|
|
115
|
+
setTimeout,
|
|
116
|
+
clearTimeout,
|
|
117
|
+
};
|
|
118
|
+
const context = vm.createContext(sandbox, {
|
|
119
|
+
name: 'amalgm-workflow-cell',
|
|
120
|
+
codeGeneration: { strings: false, wasm: false },
|
|
121
|
+
});
|
|
122
|
+
const script = new vm.Script(`__result = (${fnSource})(__input)`, {
|
|
123
|
+
filename: 'workflow-cell.js',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const result = script.runInContext(context, { timeout: Math.min(timeoutMs, 1000) });
|
|
128
|
+
if (result && typeof result.then === 'function') {
|
|
129
|
+
return await withTimeout(result, timeoutMs, 'Workflow function');
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof WorkflowStop || error?.name === 'WorkflowStop') throw error;
|
|
134
|
+
throw new Error(error?.message || String(error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function resolveDynamic(value, input, options = {}) {
|
|
139
|
+
if (isFunctionRef(value)) return runFunctionSource(value.$fn, input, options);
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
const next = [];
|
|
142
|
+
for (const item of value) next.push(await resolveDynamic(item, input, options));
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
if (isObject(value)) {
|
|
146
|
+
const next = {};
|
|
147
|
+
for (const [key, child] of Object.entries(value)) {
|
|
148
|
+
next[key] = await resolveDynamic(child, input, options);
|
|
149
|
+
}
|
|
150
|
+
return next;
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function appendBuffer(current, chunk) {
|
|
156
|
+
const next = Buffer.concat([current, Buffer.from(chunk)]);
|
|
157
|
+
return next.length > MAX_OUTPUT_BYTES ? next.subarray(next.length - MAX_OUTPUT_BYTES) : next;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function safeEnv(extraEnv, allowlist = {}) {
|
|
161
|
+
const env = {};
|
|
162
|
+
for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'TMPDIR']) {
|
|
163
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
164
|
+
}
|
|
165
|
+
for (const key of Array.isArray(allowlist.secrets) ? allowlist.secrets : []) {
|
|
166
|
+
if (process.env[key] != null) env[key] = process.env[key];
|
|
167
|
+
}
|
|
168
|
+
if (isObject(extraEnv)) {
|
|
169
|
+
for (const [key, value] of Object.entries(extraEnv)) {
|
|
170
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && value != null) env[key] = String(value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return env;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function runShellCommand(config, allowlist) {
|
|
177
|
+
const command = String(config.command || config.run || '').trim();
|
|
178
|
+
if (!command) throw new Error('CLI cell needs a command.');
|
|
179
|
+
if (allowlist.localCompute !== true) {
|
|
180
|
+
throw new Error('CLI cells require allowlist.localCompute=true.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const timeoutMs = Number(config.timeoutMs || config.timeout_ms) || DEFAULT_CELL_TIMEOUT_MS;
|
|
184
|
+
const cwd = config.cwd || DEFAULT_CWD;
|
|
185
|
+
if (!fs.existsSync(cwd)) fs.mkdirSync(cwd, { recursive: true });
|
|
186
|
+
const child = spawn(command, {
|
|
187
|
+
cwd,
|
|
188
|
+
env: safeEnv(config.env, allowlist),
|
|
189
|
+
shell: true,
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (config.stdin != null) child.stdin.end(String(config.stdin));
|
|
195
|
+
else child.stdin.end();
|
|
196
|
+
|
|
197
|
+
let stdout = Buffer.alloc(0);
|
|
198
|
+
let stderr = Buffer.alloc(0);
|
|
199
|
+
let timedOut = false;
|
|
200
|
+
const result = await new Promise((resolve, reject) => {
|
|
201
|
+
const timer = setTimeout(() => {
|
|
202
|
+
timedOut = true;
|
|
203
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
204
|
+
setTimeout(() => {
|
|
205
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
206
|
+
}, 1500).unref?.();
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
|
|
209
|
+
child.stdout.on('data', (chunk) => { stdout = appendBuffer(stdout, chunk); });
|
|
210
|
+
child.stderr.on('data', (chunk) => { stderr = appendBuffer(stderr, chunk); });
|
|
211
|
+
child.on('error', (error) => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
reject(error);
|
|
214
|
+
});
|
|
215
|
+
child.on('close', (code, signal) => {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
resolve({
|
|
218
|
+
exitCode: code,
|
|
219
|
+
signal,
|
|
220
|
+
timedOut,
|
|
221
|
+
stdout: stdout.toString('utf8'),
|
|
222
|
+
stderr: stderr.toString('utf8'),
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
228
|
+
const status = result.timedOut ? 'timed out' : `failed with exit code ${result.exitCode ?? result.signal ?? 'unknown'}`;
|
|
229
|
+
throw new Error([
|
|
230
|
+
`Command ${status}: ${command}`,
|
|
231
|
+
result.stdout ? `stdout:\n${result.stdout.trim()}` : null,
|
|
232
|
+
result.stderr ? `stderr:\n${result.stderr.trim()}` : null,
|
|
233
|
+
].filter(Boolean).join('\n\n'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
command,
|
|
238
|
+
cwd,
|
|
239
|
+
exitCode: result.exitCode,
|
|
240
|
+
stdout: result.stdout,
|
|
241
|
+
stderr: result.stderr,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function runHttpTool(args) {
|
|
246
|
+
const method = String(args.method || 'GET').toUpperCase();
|
|
247
|
+
if (!args.url) throw new Error('http.fetch needs a url.');
|
|
248
|
+
const response = await fetch(args.url, {
|
|
249
|
+
method,
|
|
250
|
+
headers: args.headers || {},
|
|
251
|
+
body: ['GET', 'HEAD'].includes(method) ? undefined : (
|
|
252
|
+
typeof args.body === 'string' ? args.body : JSON.stringify(args.body ?? {})
|
|
253
|
+
),
|
|
254
|
+
});
|
|
255
|
+
const text = await response.text();
|
|
256
|
+
let body = text;
|
|
257
|
+
try {
|
|
258
|
+
body = JSON.parse(text);
|
|
259
|
+
} catch {
|
|
260
|
+
// Keep raw text.
|
|
261
|
+
}
|
|
262
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}: ${text.slice(0, 12000)}`);
|
|
263
|
+
return {
|
|
264
|
+
status: response.status,
|
|
265
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
266
|
+
body,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function mcpResultToOutput(result, actionKey) {
|
|
271
|
+
if (!result) throw new Error(`Tool action returned no result: ${actionKey}.`);
|
|
272
|
+
if (result.isError) {
|
|
273
|
+
const text = result.content?.map((part) => part.text).filter(Boolean).join('\n') || `${actionKey} failed.`;
|
|
274
|
+
throw new Error(text);
|
|
275
|
+
}
|
|
276
|
+
return result.structuredContent ?? {
|
|
277
|
+
text: result.content?.map((part) => part.text).filter(Boolean).join('\n') || '',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runToolCell(cell, args, allowlist) {
|
|
282
|
+
const actionKey = `${cell.toolId}.${cell.actionName}`;
|
|
283
|
+
const allowedActions = Array.isArray(allowlist.actions) ? allowlist.actions : [];
|
|
284
|
+
if (allowedActions.length > 0 && !allowedActions.includes(actionKey)) {
|
|
285
|
+
throw new Error(`Workflow is not allowed to call ${actionKey}.`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (cell.toolId === 'http' && cell.actionName === 'fetch') {
|
|
289
|
+
if (allowlist.network !== true) throw new Error('http.fetch requires allowlist.network=true.');
|
|
290
|
+
return runHttpTool(args);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (cell.toolId === 'amalgm') {
|
|
294
|
+
const coreTool = resolveAmalgmCoreTool(cell.actionName);
|
|
295
|
+
if (!coreTool?.handler) throw new Error(`Amalgm action not found: ${cell.actionName}.`);
|
|
296
|
+
const result = await coreTool.handler(args, {
|
|
297
|
+
workflow: true,
|
|
298
|
+
sessionMetadata: {
|
|
299
|
+
origin: 'event-workflow',
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
return mcpResultToOutput(result, actionKey);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const resolved = resolveToolboxAction(cell.toolId, cell.actionName);
|
|
306
|
+
if (!resolved) {
|
|
307
|
+
const suggestions = suggestWorkflowToolActions(cell.toolId, cell.actionName);
|
|
308
|
+
throw new Error([
|
|
309
|
+
`Tool action not found: ${actionKey}.`,
|
|
310
|
+
suggestions.length > 0 ? `Did you mean: ${suggestions.join(', ')}?` : 'Use automations_list_actions to inspect callable workflow actions.',
|
|
311
|
+
].join(' '));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = await callToolboxMcpTool(toolboxMcpToolName(resolved.action), args, {
|
|
315
|
+
sessionMetadata: {
|
|
316
|
+
tools: {
|
|
317
|
+
mode: 'selected',
|
|
318
|
+
selectedToolIds: [resolved.tool.id, resolved.action.id],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
return mcpResultToOutput(result, actionKey);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function workflowIrForTarget(target) {
|
|
326
|
+
if (target.workflowIr && target.workflowIr.version === 1) return target.workflowIr;
|
|
327
|
+
if (target.workflowText || target.workflow) return compileWorkflowText(target.workflowText || target.workflow);
|
|
328
|
+
throw new Error('Workflow has no script.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function workflowTargetsForTrigger(trigger) {
|
|
332
|
+
const workflowIds = Array.isArray(trigger.workflowIds)
|
|
333
|
+
? trigger.workflowIds.map((id) => String(id || '').trim()).filter(Boolean)
|
|
334
|
+
: [];
|
|
335
|
+
if (workflowIds.length === 0) return [trigger];
|
|
336
|
+
|
|
337
|
+
const workflowsById = new Map(loadWorkflows().workflows.map((workflow) => [workflow.id, workflow]));
|
|
338
|
+
return workflowIds.map((workflowId) => {
|
|
339
|
+
const workflow = workflowsById.get(workflowId);
|
|
340
|
+
if (!workflow) throw new Error(`Linked workflow not found: ${workflowId}`);
|
|
341
|
+
return workflow;
|
|
342
|
+
}).filter((workflow) => workflow.enabled !== false);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function executeWorkflowCell(cell, ctx, allowlist, limits) {
|
|
346
|
+
const timeoutMs = Number(cell.timeoutMs || limits.cellTimeoutMs) || DEFAULT_CELL_TIMEOUT_MS;
|
|
347
|
+
const stop = () => {
|
|
348
|
+
throw new WorkflowStop();
|
|
349
|
+
};
|
|
350
|
+
const input = buildInput(ctx, stop);
|
|
351
|
+
|
|
352
|
+
if (cell.if !== undefined) {
|
|
353
|
+
const condition = await resolveDynamic(cell.if, input, { timeoutMs });
|
|
354
|
+
if (!condition) return { skipped: true, output: null };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (cell.kind === 'code') {
|
|
358
|
+
const output = await runFunctionSource(cell.code, input, {
|
|
359
|
+
timeoutMs: Number(cell.timeoutMs || limits.codeTimeoutMs) || DEFAULT_CODE_TIMEOUT_MS,
|
|
360
|
+
});
|
|
361
|
+
return { output };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (cell.kind === 'cli') {
|
|
365
|
+
const config = await resolveDynamic(cell.config || {}, input, { timeoutMs });
|
|
366
|
+
const output = await runShellCommand(config, allowlist);
|
|
367
|
+
return { output };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (cell.kind === 'tool') {
|
|
371
|
+
const args = await resolveDynamic(cell.args || {}, input, { timeoutMs });
|
|
372
|
+
const output = await runToolCell(cell, args, allowlist);
|
|
373
|
+
return { output };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
throw new Error(`Unsupported workflow cell kind "${cell.kind}".`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function runWorkflowTargetNow(target, eventObj, opts = {}) {
|
|
380
|
+
const runId = crypto.randomUUID();
|
|
381
|
+
const startedAt = new Date().toISOString();
|
|
382
|
+
const trigger = opts.trigger || target;
|
|
383
|
+
const triggerId = opts.triggerId || trigger.id;
|
|
384
|
+
const ir = workflowIrForTarget(target);
|
|
385
|
+
const allowlist = {
|
|
386
|
+
...(ir.allowlist || {}),
|
|
387
|
+
...(target.allowlist || {}),
|
|
388
|
+
...(trigger.allowlist || {}),
|
|
389
|
+
};
|
|
390
|
+
const limits = {
|
|
391
|
+
...(ir.limits || {}),
|
|
392
|
+
...(target.limits || {}),
|
|
393
|
+
...(trigger.limits || {}),
|
|
394
|
+
};
|
|
395
|
+
const ctx = {
|
|
396
|
+
event: eventObj,
|
|
397
|
+
trigger: publicTrigger(trigger),
|
|
398
|
+
workflow: publicWorkflow(target),
|
|
399
|
+
previous: null,
|
|
400
|
+
outputs: {},
|
|
401
|
+
secrets: buildSecrets(allowlist),
|
|
402
|
+
};
|
|
403
|
+
const projectPath = target.projectPath || trigger.projectPath || null;
|
|
404
|
+
|
|
405
|
+
recordEventRun(triggerId, {
|
|
406
|
+
runId,
|
|
407
|
+
triggerName: trigger.name,
|
|
408
|
+
workflowId: target.id,
|
|
409
|
+
workflowName: target.name,
|
|
410
|
+
startedAt,
|
|
411
|
+
status: 'running',
|
|
412
|
+
projectPath,
|
|
413
|
+
workflow: {
|
|
414
|
+
id: target.id,
|
|
415
|
+
name: target.name,
|
|
416
|
+
version: ir.version,
|
|
417
|
+
cells: ir.cells.map((cell) => ({ name: cell.name, kind: cell.kind })),
|
|
418
|
+
},
|
|
419
|
+
}, { source: 'event_runs:workflow:start' });
|
|
420
|
+
|
|
421
|
+
let status = 'completed';
|
|
422
|
+
try {
|
|
423
|
+
for (const cell of ir.cells) {
|
|
424
|
+
const cellStartedAt = new Date().toISOString();
|
|
425
|
+
recordEventRun(triggerId, {
|
|
426
|
+
runId,
|
|
427
|
+
status: 'running',
|
|
428
|
+
cell: { name: cell.name, kind: cell.kind, status: 'running', startedAt: cellStartedAt },
|
|
429
|
+
}, { source: 'event_runs:workflow:cell-start' });
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const result = await executeWorkflowCell(cell, ctx, allowlist, limits);
|
|
433
|
+
const cellStatus = result.skipped ? 'skipped' : 'completed';
|
|
434
|
+
if (!result.skipped) {
|
|
435
|
+
ctx.previous = result.output;
|
|
436
|
+
ctx.outputs[cell.name] = result.output;
|
|
437
|
+
}
|
|
438
|
+
recordEventRun(triggerId, {
|
|
439
|
+
runId,
|
|
440
|
+
status: 'running',
|
|
441
|
+
cell: {
|
|
442
|
+
name: cell.name,
|
|
443
|
+
kind: cell.kind,
|
|
444
|
+
status: cellStatus,
|
|
445
|
+
startedAt: cellStartedAt,
|
|
446
|
+
finishedAt: new Date().toISOString(),
|
|
447
|
+
durationMs: durationSince(cellStartedAt),
|
|
448
|
+
output: result.output,
|
|
449
|
+
},
|
|
450
|
+
}, { source: 'event_runs:workflow:cell-complete' });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (error instanceof WorkflowStop || error?.name === 'WorkflowStop') {
|
|
453
|
+
status = 'stopped';
|
|
454
|
+
recordEventRun(triggerId, {
|
|
455
|
+
runId,
|
|
456
|
+
status,
|
|
457
|
+
cell: {
|
|
458
|
+
name: cell.name,
|
|
459
|
+
kind: cell.kind,
|
|
460
|
+
status: 'stopped',
|
|
461
|
+
startedAt: cellStartedAt,
|
|
462
|
+
finishedAt: new Date().toISOString(),
|
|
463
|
+
durationMs: durationSince(cellStartedAt),
|
|
464
|
+
},
|
|
465
|
+
}, { source: 'event_runs:workflow:cell-stopped' });
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
recordEventRun(triggerId, {
|
|
469
|
+
runId,
|
|
470
|
+
status: 'failed',
|
|
471
|
+
cell: {
|
|
472
|
+
name: cell.name,
|
|
473
|
+
kind: cell.kind,
|
|
474
|
+
status: 'failed',
|
|
475
|
+
startedAt: cellStartedAt,
|
|
476
|
+
finishedAt: new Date().toISOString(),
|
|
477
|
+
durationMs: durationSince(cellStartedAt),
|
|
478
|
+
error: error.message || String(error),
|
|
479
|
+
},
|
|
480
|
+
}, { source: 'event_runs:workflow:cell-failed' });
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
recordEventRun(triggerId, {
|
|
486
|
+
runId,
|
|
487
|
+
triggerName: trigger.name,
|
|
488
|
+
workflowId: target.id,
|
|
489
|
+
workflowName: target.name,
|
|
490
|
+
finishedAt: new Date().toISOString(),
|
|
491
|
+
status,
|
|
492
|
+
durationMs: durationSince(startedAt),
|
|
493
|
+
output: ctx.outputs,
|
|
494
|
+
}, { source: `event_runs:workflow:${status}` });
|
|
495
|
+
} catch (error) {
|
|
496
|
+
status = 'failed';
|
|
497
|
+
recordEventRun(triggerId, {
|
|
498
|
+
runId,
|
|
499
|
+
triggerName: trigger.name,
|
|
500
|
+
workflowId: target.id,
|
|
501
|
+
workflowName: target.name,
|
|
502
|
+
finishedAt: new Date().toISOString(),
|
|
503
|
+
status,
|
|
504
|
+
error: error.message || String(error),
|
|
505
|
+
durationMs: durationSince(startedAt),
|
|
506
|
+
}, { source: 'event_runs:workflow:failed' });
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function runQueuedWorkflow(key, state, item) {
|
|
512
|
+
state.active += 1;
|
|
513
|
+
runWorkflowTargetNow(item.target, item.eventObj, item.opts)
|
|
514
|
+
.then(item.resolve, item.reject)
|
|
515
|
+
.finally(() => {
|
|
516
|
+
state.active = Math.max(0, state.active - 1);
|
|
517
|
+
const next = state.queue.shift();
|
|
518
|
+
if (next) {
|
|
519
|
+
runQueuedWorkflow(key, state, next);
|
|
520
|
+
} else if (state.active === 0) {
|
|
521
|
+
workflowRunQueues.delete(key);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function executeWorkflowTarget(target, eventObj, opts = {}) {
|
|
527
|
+
const trigger = opts.trigger || target;
|
|
528
|
+
const ir = workflowIrForTarget(target);
|
|
529
|
+
const limits = {
|
|
530
|
+
...(ir.limits || {}),
|
|
531
|
+
...(target.limits || {}),
|
|
532
|
+
...(trigger.limits || {}),
|
|
533
|
+
};
|
|
534
|
+
const maxConcurrentRuns = positiveInteger(limits.maxConcurrentRuns);
|
|
535
|
+
if (!maxConcurrentRuns) return runWorkflowTargetNow(target, eventObj, opts);
|
|
536
|
+
|
|
537
|
+
const triggerId = opts.triggerId || trigger.id;
|
|
538
|
+
const key = target.id || triggerId || 'event-workflow';
|
|
539
|
+
let state = workflowRunQueues.get(key);
|
|
540
|
+
if (!state) {
|
|
541
|
+
state = { active: 0, queue: [] };
|
|
542
|
+
workflowRunQueues.set(key, state);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return new Promise((resolve, reject) => {
|
|
546
|
+
const item = { target, eventObj, opts, resolve, reject };
|
|
547
|
+
if (state.active < maxConcurrentRuns) {
|
|
548
|
+
runQueuedWorkflow(key, state, item);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const queueLimit = queueLimitValue(limits.queueLimit);
|
|
553
|
+
if (state.queue.length >= queueLimit) {
|
|
554
|
+
const timestamp = new Date().toISOString();
|
|
555
|
+
const error = new Error(`Workflow queue is full for "${target.name || target.id || triggerId}" (${queueLimit} queued).`);
|
|
556
|
+
recordEventRun(triggerId, {
|
|
557
|
+
runId: crypto.randomUUID(),
|
|
558
|
+
triggerName: trigger.name,
|
|
559
|
+
workflowId: target.id,
|
|
560
|
+
workflowName: target.name,
|
|
561
|
+
startedAt: timestamp,
|
|
562
|
+
finishedAt: timestamp,
|
|
563
|
+
status: 'rejected',
|
|
564
|
+
projectPath: target.projectPath || trigger.projectPath || null,
|
|
565
|
+
error: error.message,
|
|
566
|
+
}, { source: 'event_runs:workflow:queue-full' });
|
|
567
|
+
reject(error);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
state.queue.push(item);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function executeEventWorkflow(trigger, eventObj, opts = {}) {
|
|
576
|
+
try {
|
|
577
|
+
const targets = workflowTargetsForTrigger(trigger);
|
|
578
|
+
if (targets.length === 0) return Promise.resolve([]);
|
|
579
|
+
return Promise.all(targets.map((target) => executeWorkflowTarget(target, eventObj, {
|
|
580
|
+
...opts,
|
|
581
|
+
trigger,
|
|
582
|
+
triggerId: opts.triggerId || trigger.id,
|
|
583
|
+
})));
|
|
584
|
+
} catch (error) {
|
|
585
|
+
return Promise.reject(error);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
executeEventWorkflow,
|
|
591
|
+
executeWorkflowTarget,
|
|
592
|
+
runFunctionSource,
|
|
593
|
+
};
|