amalgm 0.1.51 → 0.1.53
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 +547 -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-binaries.js +34 -9
- 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,613 @@
|
|
|
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 { compileWorkflowText } = require('../workflows/compiler');
|
|
10
|
+
const { callToolboxMcpTool, toolboxMcpToolName } = require('../toolbox/runner');
|
|
11
|
+
const { buildWorkflowInput } = require('./context');
|
|
12
|
+
const { resolveAmalgmCoreTool, resolveToolboxAction, suggestWorkflowToolActions } = require('./tool-actions');
|
|
13
|
+
const {
|
|
14
|
+
getAutomation,
|
|
15
|
+
getAutomationByTriggerId,
|
|
16
|
+
recordAutomationRun,
|
|
17
|
+
recordWorkflowCellRun,
|
|
18
|
+
} = require('./store');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CELL_TIMEOUT_MS = 120_000;
|
|
21
|
+
const DEFAULT_CODE_TIMEOUT_MS = 15_000;
|
|
22
|
+
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
23
|
+
const runQueues = new Map();
|
|
24
|
+
|
|
25
|
+
class WorkflowStop extends Error {
|
|
26
|
+
constructor(message = 'Workflow stopped') {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'WorkflowStop';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isObject(value) {
|
|
33
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isFunctionRef(value) {
|
|
37
|
+
return isObject(value) && typeof value.$fn === 'string';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function durationSince(startedAt) {
|
|
41
|
+
return Date.now() - new Date(startedAt).getTime();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function positiveInteger(value, fallback = null) {
|
|
45
|
+
const number = Number(value);
|
|
46
|
+
if (!Number.isFinite(number)) return fallback;
|
|
47
|
+
return Math.max(1, Math.floor(number));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function queueLimitValue(value) {
|
|
51
|
+
const number = Number(value);
|
|
52
|
+
if (!Number.isFinite(number)) return 0;
|
|
53
|
+
return Math.max(0, Math.floor(number));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
57
|
+
let timer = null;
|
|
58
|
+
return Promise.race([
|
|
59
|
+
promise,
|
|
60
|
+
new Promise((_, reject) => {
|
|
61
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
62
|
+
}),
|
|
63
|
+
]).finally(() => {
|
|
64
|
+
if (timer) clearTimeout(timer);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function publicTrigger(trigger) {
|
|
69
|
+
return {
|
|
70
|
+
id: trigger.id,
|
|
71
|
+
automationId: trigger.automationId,
|
|
72
|
+
name: trigger.name,
|
|
73
|
+
kind: trigger.kind,
|
|
74
|
+
source: trigger.source,
|
|
75
|
+
event: trigger.event,
|
|
76
|
+
schedule: trigger.schedule,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function publicWorkflow(workflow) {
|
|
81
|
+
return {
|
|
82
|
+
id: workflow.id,
|
|
83
|
+
name: workflow.name,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function publicAutomation(automation) {
|
|
88
|
+
return {
|
|
89
|
+
id: automation.id,
|
|
90
|
+
name: automation.name,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildSecrets(allowlist = {}) {
|
|
95
|
+
const names = Array.isArray(allowlist.secrets) ? allowlist.secrets : [];
|
|
96
|
+
const secrets = {};
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const key = String(name || '').trim();
|
|
99
|
+
if (key && process.env[key] != null) secrets[key] = process.env[key];
|
|
100
|
+
}
|
|
101
|
+
return secrets;
|
|
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-automation-cell',
|
|
120
|
+
codeGeneration: { strings: false, wasm: false },
|
|
121
|
+
});
|
|
122
|
+
const script = new vm.Script(`__result = (${fnSource})(__input)`, {
|
|
123
|
+
filename: 'automation-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: 'automation-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 workflowIrForAutomation(automation) {
|
|
326
|
+
const workflow = automation.workflow || automation.workflows?.[0];
|
|
327
|
+
if (!workflow) throw new Error('Automation has no workflow.');
|
|
328
|
+
if (workflow.workflowIr && workflow.workflowIr.version === 1) return workflow.workflowIr;
|
|
329
|
+
if (workflow.workflowText || workflow.workflow) return compileWorkflowText(workflow.workflowText || workflow.workflow);
|
|
330
|
+
throw new Error('Automation workflow has no script.');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function executeWorkflowCell(cell, ctx, allowlist, limits) {
|
|
334
|
+
const timeoutMs = Number(cell.timeoutMs || limits.cellTimeoutMs) || DEFAULT_CELL_TIMEOUT_MS;
|
|
335
|
+
const stop = () => {
|
|
336
|
+
throw new WorkflowStop();
|
|
337
|
+
};
|
|
338
|
+
const input = buildWorkflowInput(ctx, stop);
|
|
339
|
+
|
|
340
|
+
if (cell.if !== undefined) {
|
|
341
|
+
const condition = await resolveDynamic(cell.if, input, { timeoutMs });
|
|
342
|
+
if (!condition) return { skipped: true, output: null };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (cell.kind === 'code') {
|
|
346
|
+
const output = await runFunctionSource(cell.code, input, {
|
|
347
|
+
timeoutMs: Number(cell.timeoutMs || limits.codeTimeoutMs) || DEFAULT_CODE_TIMEOUT_MS,
|
|
348
|
+
});
|
|
349
|
+
return { output };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (cell.kind === 'cli') {
|
|
353
|
+
const config = await resolveDynamic(cell.config || {}, input, { timeoutMs });
|
|
354
|
+
const output = await runShellCommand(config, allowlist);
|
|
355
|
+
return { output };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (cell.kind === 'tool') {
|
|
359
|
+
const args = await resolveDynamic(cell.args || {}, input, { timeoutMs });
|
|
360
|
+
const output = await runToolCell(cell, args, allowlist);
|
|
361
|
+
return { output };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw new Error(`Unsupported workflow cell kind "${cell.kind}".`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runAutomationNow(automation, trigger, eventObj = {}) {
|
|
368
|
+
if (!automation) throw new Error('Automation is required.');
|
|
369
|
+
if (automation.enabled === false) throw new Error(`Automation "${automation.name}" is disabled.`);
|
|
370
|
+
const workflow = automation.workflow || automation.workflows?.[0];
|
|
371
|
+
if (!workflow) throw new Error(`Automation "${automation.name}" has no workflow.`);
|
|
372
|
+
if (workflow.enabled === false) throw new Error(`Workflow "${workflow.name}" is disabled.`);
|
|
373
|
+
|
|
374
|
+
const runId = crypto.randomUUID();
|
|
375
|
+
const startedAt = new Date().toISOString();
|
|
376
|
+
const resolvedTrigger = trigger || automation.triggers?.[0] || {
|
|
377
|
+
id: null,
|
|
378
|
+
name: 'Manual run',
|
|
379
|
+
kind: 'manual',
|
|
380
|
+
automationId: automation.id,
|
|
381
|
+
};
|
|
382
|
+
const ir = workflowIrForAutomation(automation);
|
|
383
|
+
const allowlist = {
|
|
384
|
+
...(ir.allowlist || {}),
|
|
385
|
+
...(workflow.allowlist || {}),
|
|
386
|
+
};
|
|
387
|
+
const limits = {
|
|
388
|
+
...(ir.limits || {}),
|
|
389
|
+
...(workflow.limits || {}),
|
|
390
|
+
};
|
|
391
|
+
const ctx = {
|
|
392
|
+
automation: publicAutomation(automation),
|
|
393
|
+
event: eventObj,
|
|
394
|
+
trigger: publicTrigger(resolvedTrigger),
|
|
395
|
+
workflow: publicWorkflow(workflow),
|
|
396
|
+
previous: null,
|
|
397
|
+
outputs: {},
|
|
398
|
+
secrets: buildSecrets(allowlist),
|
|
399
|
+
};
|
|
400
|
+
const projectPath = workflow.projectPath || automation.projectPath || resolvedTrigger.projectPath || null;
|
|
401
|
+
|
|
402
|
+
let run = recordAutomationRun(automation.id, {
|
|
403
|
+
runId,
|
|
404
|
+
automationId: automation.id,
|
|
405
|
+
automationName: automation.name,
|
|
406
|
+
triggerId: resolvedTrigger.id,
|
|
407
|
+
triggerName: resolvedTrigger.name,
|
|
408
|
+
workflowId: workflow.id,
|
|
409
|
+
workflowName: workflow.name,
|
|
410
|
+
startedAt,
|
|
411
|
+
status: 'running',
|
|
412
|
+
projectPath,
|
|
413
|
+
workflow: {
|
|
414
|
+
id: workflow.id,
|
|
415
|
+
name: workflow.name,
|
|
416
|
+
version: ir.version,
|
|
417
|
+
cells: ir.cells.map((cell) => ({ name: cell.name, kind: cell.kind })),
|
|
418
|
+
},
|
|
419
|
+
}, { source: 'automation_runs:workflow:start' });
|
|
420
|
+
|
|
421
|
+
let status = 'completed';
|
|
422
|
+
try {
|
|
423
|
+
for (const cell of ir.cells) {
|
|
424
|
+
const cellStartedAt = new Date().toISOString();
|
|
425
|
+
recordAutomationRun(automation.id, {
|
|
426
|
+
runId,
|
|
427
|
+
automationId: automation.id,
|
|
428
|
+
triggerId: resolvedTrigger.id,
|
|
429
|
+
workflowId: workflow.id,
|
|
430
|
+
status: 'running',
|
|
431
|
+
cell: { name: cell.name, kind: cell.kind, status: 'running', startedAt: cellStartedAt },
|
|
432
|
+
}, { source: 'automation_runs:workflow:cell-start' });
|
|
433
|
+
recordWorkflowCellRun(run, {
|
|
434
|
+
name: cell.name,
|
|
435
|
+
kind: cell.kind,
|
|
436
|
+
status: 'running',
|
|
437
|
+
startedAt: cellStartedAt,
|
|
438
|
+
}, { source: 'workflow_cell_runs:start' });
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const result = await executeWorkflowCell(cell, ctx, allowlist, limits);
|
|
442
|
+
const cellStatus = result.skipped ? 'skipped' : 'completed';
|
|
443
|
+
if (!result.skipped) {
|
|
444
|
+
ctx.previous = result.output;
|
|
445
|
+
ctx.outputs[cell.name] = result.output;
|
|
446
|
+
}
|
|
447
|
+
const finishedAt = new Date().toISOString();
|
|
448
|
+
const cellResult = {
|
|
449
|
+
name: cell.name,
|
|
450
|
+
kind: cell.kind,
|
|
451
|
+
status: cellStatus,
|
|
452
|
+
startedAt: cellStartedAt,
|
|
453
|
+
finishedAt,
|
|
454
|
+
durationMs: durationSince(cellStartedAt),
|
|
455
|
+
output: result.output,
|
|
456
|
+
};
|
|
457
|
+
recordAutomationRun(automation.id, {
|
|
458
|
+
runId,
|
|
459
|
+
automationId: automation.id,
|
|
460
|
+
triggerId: resolvedTrigger.id,
|
|
461
|
+
workflowId: workflow.id,
|
|
462
|
+
status: 'running',
|
|
463
|
+
cell: cellResult,
|
|
464
|
+
}, { source: 'automation_runs:workflow:cell-complete' });
|
|
465
|
+
recordWorkflowCellRun(run, cellResult, { source: 'workflow_cell_runs:complete' });
|
|
466
|
+
} catch (error) {
|
|
467
|
+
const stopped = error instanceof WorkflowStop || error?.name === 'WorkflowStop';
|
|
468
|
+
const cellStatus = stopped ? 'stopped' : 'failed';
|
|
469
|
+
if (stopped) status = 'stopped';
|
|
470
|
+
const finishedAt = new Date().toISOString();
|
|
471
|
+
const cellResult = {
|
|
472
|
+
name: cell.name,
|
|
473
|
+
kind: cell.kind,
|
|
474
|
+
status: cellStatus,
|
|
475
|
+
startedAt: cellStartedAt,
|
|
476
|
+
finishedAt,
|
|
477
|
+
durationMs: durationSince(cellStartedAt),
|
|
478
|
+
error: stopped ? null : (error.message || String(error)),
|
|
479
|
+
};
|
|
480
|
+
recordAutomationRun(automation.id, {
|
|
481
|
+
runId,
|
|
482
|
+
automationId: automation.id,
|
|
483
|
+
triggerId: resolvedTrigger.id,
|
|
484
|
+
workflowId: workflow.id,
|
|
485
|
+
status: stopped ? status : 'failed',
|
|
486
|
+
cell: cellResult,
|
|
487
|
+
}, { source: stopped ? 'automation_runs:workflow:cell-stopped' : 'automation_runs:workflow:cell-failed' });
|
|
488
|
+
recordWorkflowCellRun(run, cellResult, { source: stopped ? 'workflow_cell_runs:stopped' : 'workflow_cell_runs:failed' });
|
|
489
|
+
if (stopped) break;
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
run = recordAutomationRun(automation.id, {
|
|
495
|
+
runId,
|
|
496
|
+
automationId: automation.id,
|
|
497
|
+
automationName: automation.name,
|
|
498
|
+
triggerId: resolvedTrigger.id,
|
|
499
|
+
triggerName: resolvedTrigger.name,
|
|
500
|
+
workflowId: workflow.id,
|
|
501
|
+
workflowName: workflow.name,
|
|
502
|
+
finishedAt: new Date().toISOString(),
|
|
503
|
+
status,
|
|
504
|
+
durationMs: durationSince(startedAt),
|
|
505
|
+
output: ctx.outputs,
|
|
506
|
+
}, { source: `automation_runs:workflow:${status}` });
|
|
507
|
+
return run;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
recordAutomationRun(automation.id, {
|
|
510
|
+
runId,
|
|
511
|
+
automationId: automation.id,
|
|
512
|
+
automationName: automation.name,
|
|
513
|
+
triggerId: resolvedTrigger.id,
|
|
514
|
+
triggerName: resolvedTrigger.name,
|
|
515
|
+
workflowId: workflow.id,
|
|
516
|
+
workflowName: workflow.name,
|
|
517
|
+
finishedAt: new Date().toISOString(),
|
|
518
|
+
status: 'failed',
|
|
519
|
+
error: error.message || String(error),
|
|
520
|
+
durationMs: durationSince(startedAt),
|
|
521
|
+
}, { source: 'automation_runs:workflow:failed' });
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function runQueuedAutomation(key, state, item) {
|
|
527
|
+
state.active += 1;
|
|
528
|
+
runAutomationNow(item.automation, item.trigger, item.eventObj)
|
|
529
|
+
.then(item.resolve, item.reject)
|
|
530
|
+
.finally(() => {
|
|
531
|
+
state.active = Math.max(0, state.active - 1);
|
|
532
|
+
const next = state.queue.shift();
|
|
533
|
+
if (next) {
|
|
534
|
+
runQueuedAutomation(key, state, next);
|
|
535
|
+
} else if (state.active === 0) {
|
|
536
|
+
runQueues.delete(key);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function executeAutomation(automation, trigger, eventObj = {}) {
|
|
542
|
+
const ir = workflowIrForAutomation(automation);
|
|
543
|
+
const workflow = automation.workflow || automation.workflows?.[0];
|
|
544
|
+
const limits = {
|
|
545
|
+
...(ir.limits || {}),
|
|
546
|
+
...(workflow?.limits || {}),
|
|
547
|
+
};
|
|
548
|
+
const maxConcurrentRuns = positiveInteger(limits.maxConcurrentRuns);
|
|
549
|
+
if (!maxConcurrentRuns) return runAutomationNow(automation, trigger, eventObj);
|
|
550
|
+
|
|
551
|
+
const key = automation.id;
|
|
552
|
+
let state = runQueues.get(key);
|
|
553
|
+
if (!state) {
|
|
554
|
+
state = { active: 0, queue: [] };
|
|
555
|
+
runQueues.set(key, state);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return new Promise((resolve, reject) => {
|
|
559
|
+
const item = { automation, trigger, eventObj, resolve, reject };
|
|
560
|
+
if (state.active < maxConcurrentRuns) {
|
|
561
|
+
runQueuedAutomation(key, state, item);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const queueLimit = queueLimitValue(limits.queueLimit);
|
|
566
|
+
if (state.queue.length >= queueLimit) {
|
|
567
|
+
const timestamp = new Date().toISOString();
|
|
568
|
+
const error = new Error(`Automation queue is full for "${automation.name || automation.id}" (${queueLimit} queued).`);
|
|
569
|
+
recordAutomationRun(automation.id, {
|
|
570
|
+
runId: crypto.randomUUID(),
|
|
571
|
+
automationId: automation.id,
|
|
572
|
+
automationName: automation.name,
|
|
573
|
+
triggerId: trigger?.id || null,
|
|
574
|
+
triggerName: trigger?.name || null,
|
|
575
|
+
workflowId: workflow?.id || null,
|
|
576
|
+
workflowName: workflow?.name || null,
|
|
577
|
+
startedAt: timestamp,
|
|
578
|
+
finishedAt: timestamp,
|
|
579
|
+
status: 'rejected',
|
|
580
|
+
projectPath: workflow?.projectPath || automation.projectPath || null,
|
|
581
|
+
error: error.message,
|
|
582
|
+
}, { source: 'automation_runs:workflow:queue-full' });
|
|
583
|
+
reject(error);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
state.queue.push(item);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function executeAutomationById(automationId, eventObj = {}, opts = {}) {
|
|
592
|
+
const automation = getAutomation(automationId);
|
|
593
|
+
if (!automation) return Promise.reject(new Error(`Automation not found: ${automationId}`));
|
|
594
|
+
const trigger = opts.triggerId
|
|
595
|
+
? automation.triggers.find((item) => item.id === opts.triggerId)
|
|
596
|
+
: automation.triggers[0];
|
|
597
|
+
return executeAutomation(automation, trigger, eventObj);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function executeAutomationForTrigger(triggerId, eventObj = {}) {
|
|
601
|
+
const automation = getAutomationByTriggerId(triggerId);
|
|
602
|
+
if (!automation) return Promise.reject(new Error(`Trigger not found: ${triggerId}`));
|
|
603
|
+
const trigger = automation.triggers.find((item) => item.id === triggerId);
|
|
604
|
+
if (!trigger) return Promise.reject(new Error(`Trigger not found: ${triggerId}`));
|
|
605
|
+
return executeAutomation(automation, trigger, eventObj);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = {
|
|
609
|
+
executeAutomation,
|
|
610
|
+
executeAutomationById,
|
|
611
|
+
executeAutomationForTrigger,
|
|
612
|
+
runFunctionSource,
|
|
613
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automation scheduler.
|
|
5
|
+
*
|
|
6
|
+
* Scheduled delivery is now just one trigger kind. The scheduler claims due
|
|
7
|
+
* scheduled triggers from the unified automation store and runs their
|
|
8
|
+
* workflows; it does not wake an agent unless the workflow explicitly calls
|
|
9
|
+
* an agent/tool cell.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SCHEDULER_INTERVAL_MS } = require('../config');
|
|
13
|
+
const { claimDueScheduledTriggers } = require('./store');
|
|
14
|
+
const { executeAutomation } = require('./runner');
|
|
15
|
+
|
|
16
|
+
let schedulerTimer = null;
|
|
17
|
+
let isPolling = false;
|
|
18
|
+
|
|
19
|
+
const schedulerStatus = {
|
|
20
|
+
startedAt: null,
|
|
21
|
+
lastTickAt: null,
|
|
22
|
+
lastError: null,
|
|
23
|
+
lastClaimed: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function startAutomationScheduler() {
|
|
27
|
+
console.log(`[AmalgmMCP:Automations] Starting scheduler (interval: ${SCHEDULER_INTERVAL_MS}ms)`);
|
|
28
|
+
if (schedulerTimer) clearInterval(schedulerTimer);
|
|
29
|
+
schedulerStatus.startedAt = new Date().toISOString();
|
|
30
|
+
pollAutomationScheduler();
|
|
31
|
+
schedulerTimer = setInterval(pollAutomationScheduler, SCHEDULER_INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pollAutomationScheduler() {
|
|
35
|
+
if (isPolling) return;
|
|
36
|
+
isPolling = true;
|
|
37
|
+
try {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
schedulerStatus.lastTickAt = now.toISOString();
|
|
40
|
+
const claims = claimDueScheduledTriggers(now, { source: 'automation-scheduler' });
|
|
41
|
+
schedulerStatus.lastClaimed = claims.length;
|
|
42
|
+
schedulerStatus.lastError = null;
|
|
43
|
+
|
|
44
|
+
for (const claim of claims) {
|
|
45
|
+
if (!claim?.automation || !claim?.trigger) continue;
|
|
46
|
+
executeAutomation(claim.automation, claim.trigger, {
|
|
47
|
+
id: claim.scheduledFor || now.toISOString(),
|
|
48
|
+
source: 'amalgm.scheduler',
|
|
49
|
+
event: 'scheduled',
|
|
50
|
+
payload: {
|
|
51
|
+
scheduledFor: claim.scheduledFor,
|
|
52
|
+
triggerId: claim.trigger.id,
|
|
53
|
+
automationId: claim.automation.id,
|
|
54
|
+
},
|
|
55
|
+
headers: {},
|
|
56
|
+
receivedAt: now.toISOString(),
|
|
57
|
+
}).catch((error) => {
|
|
58
|
+
console.error(`[AmalgmMCP:Automations] Automation ${claim.automation.id} failed:`, error.message);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
schedulerStatus.lastError = error.message || String(error);
|
|
63
|
+
console.error('[AmalgmMCP:Automations] Scheduler poll error:', schedulerStatus.lastError);
|
|
64
|
+
} finally {
|
|
65
|
+
isPolling = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getAutomationSchedulerStatus() {
|
|
70
|
+
return {
|
|
71
|
+
intervalMs: SCHEDULER_INTERVAL_MS,
|
|
72
|
+
running: Boolean(schedulerTimer),
|
|
73
|
+
polling: isPolling,
|
|
74
|
+
...schedulerStatus,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stopAutomationScheduler() {
|
|
79
|
+
if (schedulerTimer) {
|
|
80
|
+
clearInterval(schedulerTimer);
|
|
81
|
+
schedulerTimer = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
getAutomationSchedulerStatus,
|
|
87
|
+
pollAutomationScheduler,
|
|
88
|
+
startAutomationScheduler,
|
|
89
|
+
stopAutomationScheduler,
|
|
90
|
+
};
|