amalgm 0.1.86 → 0.1.87
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/package.json +1 -1
- package/runtime/lib/harnesses.js +15 -2
- package/runtime/scripts/amalgm-mcp/automations/rest.js +41 -3
- package/runtime/scripts/amalgm-mcp/automations/runner.js +111 -15
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +116 -7
- package/runtime/scripts/amalgm-mcp/automations/store.js +508 -66
- package/runtime/scripts/amalgm-mcp/automations/tools.js +55 -10
- package/runtime/scripts/amalgm-mcp/events/ingress.js +142 -29
- package/runtime/scripts/amalgm-mcp/events/matcher.js +9 -4
- package/runtime/scripts/amalgm-mcp/server/routes/automations.js +4 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +12 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +2 -2
- package/runtime/scripts/amalgm-mcp/tests/automations-reliability.test.js +547 -0
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +3 -3
- package/runtime/scripts/chat-core/contract.js +10 -0
- package/runtime/scripts/chat-server/model-catalog.js +2 -1
|
@@ -91,6 +91,7 @@ function summarizeAutomation(automation) {
|
|
|
91
91
|
workflowIds: automation.workflowIds,
|
|
92
92
|
triggers: automation.triggers.map(summarizeTrigger),
|
|
93
93
|
workflow: summarizeWorkflow(automation.workflow || automation.workflows?.[0]),
|
|
94
|
+
workflows: (automation.workflows || []).map(summarizeWorkflow),
|
|
94
95
|
updatedAt: automation.updatedAt,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
@@ -214,7 +215,7 @@ module.exports = [
|
|
|
214
215
|
{
|
|
215
216
|
name: 'automations_create',
|
|
216
217
|
description:
|
|
217
|
-
'Create a local Automation. An automation is one or more triggers plus one workflow. It is stored on the user computer in the Local Live SQLite store and runs locally. Scheduled/event triggers do not wake an agent unless
|
|
218
|
+
'Create a local Automation. An automation is one or more triggers plus one or more workflows; every trigger firing runs each enabled workflow and records one run per workflow. It is stored on the user computer in the Local Live SQLite store and runs locally. Scheduled/event triggers do not wake an agent unless a workflow explicitly calls an agent/tool.',
|
|
218
219
|
inputSchema: {
|
|
219
220
|
type: 'object',
|
|
220
221
|
properties: {
|
|
@@ -252,6 +253,27 @@ module.exports = [
|
|
|
252
253
|
},
|
|
253
254
|
workflowText: { type: 'string', description: WORKFLOW_DESCRIPTION },
|
|
254
255
|
workflow: { type: 'string', description: 'Alias for workflowText.' },
|
|
256
|
+
workflows: {
|
|
257
|
+
type: 'array',
|
|
258
|
+
description: 'Multiple workflows for this automation. Each item is a workflow script string or `{ name, workflowText, enabled, allowlist, limits }`. Every trigger firing runs each enabled workflow in order and records one run per workflow. Use this OR workflowText, not both.',
|
|
259
|
+
items: {
|
|
260
|
+
anyOf: [
|
|
261
|
+
{ type: 'string', description: WORKFLOW_DESCRIPTION },
|
|
262
|
+
{
|
|
263
|
+
type: 'object',
|
|
264
|
+
properties: {
|
|
265
|
+
id: { type: 'string', description: 'Optional stable workflow id.' },
|
|
266
|
+
name: { type: 'string' },
|
|
267
|
+
description: { type: 'string' },
|
|
268
|
+
enabled: { type: 'boolean' },
|
|
269
|
+
workflowText: { type: 'string', description: WORKFLOW_DESCRIPTION },
|
|
270
|
+
allowlist: { type: 'object' },
|
|
271
|
+
limits: { type: 'object' },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
255
277
|
allowlist: { type: 'object', description: 'Workflow permissions: localCompute, network, secrets, actions.' },
|
|
256
278
|
limits: { type: 'object', description: 'Workflow limits: maxConcurrentRuns, queueLimit, cellTimeoutMs, codeTimeoutMs.' },
|
|
257
279
|
verbose: { type: 'boolean', description: 'Set false for a concise response with ids, status, triggers, webhook URL/secret, and workflow summary. Defaults to true.' },
|
|
@@ -260,13 +282,14 @@ module.exports = [
|
|
|
260
282
|
},
|
|
261
283
|
async handler(input = {}) {
|
|
262
284
|
try {
|
|
263
|
-
const workflowText = input.workflowText || input.workflow;
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
const workflowText = input.workflowText || (typeof input.workflow === 'string' ? input.workflow : undefined);
|
|
286
|
+
const hasWorkflowsArray = Array.isArray(input.workflows) && input.workflows.length > 0;
|
|
287
|
+
if (!hasWorkflowsArray && (typeof workflowText !== 'string' || !workflowText.trim())) {
|
|
288
|
+
return errorResult('workflowText, workflow, or workflows is required.');
|
|
266
289
|
}
|
|
267
290
|
const automation = createAutomation({
|
|
268
291
|
...input,
|
|
269
|
-
workflowText,
|
|
292
|
+
...(workflowText ? { workflowText } : {}),
|
|
270
293
|
}, { source: 'automations_create' });
|
|
271
294
|
return textResult(`Automation created.\n\n${JSON.stringify(automationResponse(automation, {
|
|
272
295
|
verbose: input.verbose !== false,
|
|
@@ -333,7 +356,7 @@ module.exports = [
|
|
|
333
356
|
{
|
|
334
357
|
name: 'automations_update',
|
|
335
358
|
description:
|
|
336
|
-
'Update a local automation. You can edit metadata, enabled state, replace triggers,
|
|
359
|
+
'Update a local automation. You can edit metadata, enabled state, replace triggers, update the primary workflow source/allowlist/limits, or replace the full workflows array. Existing triggers and workflows are matched by id (then fingerprint/position) so webhook secrets, schedule clocks, and run-history linkage are preserved. The next trigger firing uses the updated workflows.',
|
|
337
360
|
inputSchema: {
|
|
338
361
|
type: 'object',
|
|
339
362
|
properties: {
|
|
@@ -343,8 +366,29 @@ module.exports = [
|
|
|
343
366
|
enabled: { type: 'boolean' },
|
|
344
367
|
projectPath: { type: 'string' },
|
|
345
368
|
triggers: { type: 'array', description: TRIGGER_DESCRIPTION },
|
|
346
|
-
workflowText: { type: 'string', description: WORKFLOW_DESCRIPTION },
|
|
369
|
+
workflowText: { type: 'string', description: `${WORKFLOW_DESCRIPTION}\n\nApplies to the primary (first) workflow.` },
|
|
347
370
|
workflow: { type: 'string', description: 'Alias for workflowText.' },
|
|
371
|
+
workflows: {
|
|
372
|
+
type: 'array',
|
|
373
|
+
description: 'Replace the full ordered workflows array. Items are workflow script strings or `{ id, name, workflowText, enabled, allowlist, limits }`; include ids from automations_get to update workflows in place rather than recreating them.',
|
|
374
|
+
items: {
|
|
375
|
+
anyOf: [
|
|
376
|
+
{ type: 'string' },
|
|
377
|
+
{
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
id: { type: 'string' },
|
|
381
|
+
name: { type: 'string' },
|
|
382
|
+
description: { type: 'string' },
|
|
383
|
+
enabled: { type: 'boolean' },
|
|
384
|
+
workflowText: { type: 'string' },
|
|
385
|
+
allowlist: { type: 'object' },
|
|
386
|
+
limits: { type: 'object' },
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
348
392
|
allowlist: { type: 'object' },
|
|
349
393
|
limits: { type: 'object' },
|
|
350
394
|
verbose: { type: 'boolean', description: 'Set false for a concise response. Defaults to true.' },
|
|
@@ -397,7 +441,7 @@ module.exports = [
|
|
|
397
441
|
},
|
|
398
442
|
async handler({ automation_id, trigger_id, payload, verbose } = {}) {
|
|
399
443
|
try {
|
|
400
|
-
const
|
|
444
|
+
const runs = await executeAutomationById(automation_id, {
|
|
401
445
|
id: `manual:${Date.now()}`,
|
|
402
446
|
source: 'amalgm.manual',
|
|
403
447
|
event: 'run_now',
|
|
@@ -405,8 +449,9 @@ module.exports = [
|
|
|
405
449
|
headers: {},
|
|
406
450
|
receivedAt: new Date().toISOString(),
|
|
407
451
|
}, { triggerId: trigger_id });
|
|
408
|
-
|
|
409
|
-
|
|
452
|
+
const payloadOut = verbose === false ? runs.map(summarizeRun) : runs;
|
|
453
|
+
return textResult(`Automation ran ${runs.length} workflow${runs.length === 1 ? '' : 's'}.\n\n${JSON.stringify(
|
|
454
|
+
runs.length === 1 ? payloadOut[0] : payloadOut,
|
|
410
455
|
null,
|
|
411
456
|
2,
|
|
412
457
|
)}`);
|
|
@@ -10,13 +10,24 @@ const {
|
|
|
10
10
|
extractSignature,
|
|
11
11
|
matchAllBySignature,
|
|
12
12
|
matchBySignature,
|
|
13
|
+
matchesEventRef,
|
|
13
14
|
pickSourceLabel,
|
|
14
15
|
collectPassthroughHeaders,
|
|
15
16
|
} = require('./matcher');
|
|
16
|
-
const { listEventTriggersForIngress, markTriggerFired } = require('../automations/store');
|
|
17
|
+
const { listEventTriggersForIngress, markTriggerFired, recordAutomationRun } = require('../automations/store');
|
|
17
18
|
const { executeAutomationForTrigger } = require('../automations/runner');
|
|
18
19
|
const ring = require('./ring-buffer');
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize a sender-declared source/event ref part.
|
|
23
|
+
*/
|
|
24
|
+
function cleanRefPart(value) {
|
|
25
|
+
if (typeof value !== 'string') return '';
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (!trimmed || trimmed.length > 128) return '';
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Read the raw request body as a string.
|
|
22
33
|
*/
|
|
@@ -30,6 +41,42 @@ async function readRawBody(req) {
|
|
|
30
41
|
});
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
function dayStamp(date = new Date()) {
|
|
45
|
+
return date.toISOString().slice(0, 10).replace(/-/g, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Record an authenticated-but-unmatched delivery as a small terminal
|
|
50
|
+
* 'unmatched' run on each authenticated automation. The run id is
|
|
51
|
+
* deterministic per automation + ref + day, so repeated deliveries of the
|
|
52
|
+
* same unmatched ref refresh ONE row (its events array logs each receipt)
|
|
53
|
+
* instead of spamming run history and the live event stream — a sender
|
|
54
|
+
* holding one secret cannot flood the store with junk refs.
|
|
55
|
+
*/
|
|
56
|
+
function recordUnmatchedEventRuns(authenticated, eventSource, eventName, timestamp) {
|
|
57
|
+
const refSlug = `${eventSource}.${eventName}`.toLowerCase().replace(/[^a-z0-9._-]+/g, '_').slice(0, 80);
|
|
58
|
+
const seenAutomations = new Set();
|
|
59
|
+
for (const trigger of authenticated) {
|
|
60
|
+
if (!trigger.automationId || seenAutomations.has(trigger.automationId)) continue;
|
|
61
|
+
seenAutomations.add(trigger.automationId);
|
|
62
|
+
try {
|
|
63
|
+
recordAutomationRun(trigger.automationId, {
|
|
64
|
+
runId: `unmatched-${trigger.automationId}-${refSlug}-${dayStamp()}`,
|
|
65
|
+
automationId: trigger.automationId,
|
|
66
|
+
triggerId: trigger.id,
|
|
67
|
+
triggerName: trigger.name,
|
|
68
|
+
status: 'unmatched',
|
|
69
|
+
startedAt: timestamp,
|
|
70
|
+
finishedAt: timestamp,
|
|
71
|
+
output: { received: { source: eventSource, event: eventName, receivedAt: timestamp } },
|
|
72
|
+
error: `Received ${eventSource}.${eventName} — authenticated, but no trigger selector matched (trigger "${trigger.name}" wants ${trigger.source || '*'}.${trigger.event || '*'}).`,
|
|
73
|
+
}, { source: 'automation_runs:event-unmatched' });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.warn('[AmalgmMCP:Events] Failed to record unmatched event run:', error.message || error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
33
80
|
/**
|
|
34
81
|
* Handle a POST /events request. Assumes CORS + method checking has already
|
|
35
82
|
* happened. Writes the response via `sendJson`.
|
|
@@ -47,12 +94,49 @@ async function handleEventsPost(req, sendJson) {
|
|
|
47
94
|
|
|
48
95
|
const triggers = listEventTriggersForIngress();
|
|
49
96
|
const githubEvent = req.headers['x-github-event'];
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
|
|
98
|
+
let parsedBody;
|
|
99
|
+
try {
|
|
100
|
+
parsedBody = JSON.parse(rawBody);
|
|
101
|
+
} catch {
|
|
102
|
+
parsedBody = rawBody;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Vendor webhooks (GitHub/Stripe/Linear/GitLab) get their ref from vendor
|
|
106
|
+
// headers. Generic senders carry no vendor headers, so let them declare
|
|
107
|
+
// their own ref via x-amalgm-source/x-amalgm-event headers or body
|
|
108
|
+
// source/event fields — otherwise custom refs like "audit.ping" are
|
|
109
|
+
// permanently unmatchable. This is safe to read before matching: a sender
|
|
110
|
+
// still only matches triggers whose per-trigger secret verifies its proof
|
|
111
|
+
// (HMAC binds the body bytes; token compare authenticates the sender).
|
|
112
|
+
const vendorSource = pickSourceLabel(req.headers);
|
|
113
|
+
const vendorEvent =
|
|
52
114
|
githubEvent ||
|
|
53
115
|
req.headers['x-linear-event'] ||
|
|
54
116
|
req.headers['x-gitlab-event'] ||
|
|
55
|
-
|
|
117
|
+
null;
|
|
118
|
+
let eventSource = vendorSource;
|
|
119
|
+
let eventName = vendorEvent || 'webhook';
|
|
120
|
+
let fallbackRef = null;
|
|
121
|
+
if (vendorSource === 'external' && !vendorEvent) {
|
|
122
|
+
const headerSource = cleanRefPart(req.headers['x-amalgm-source']);
|
|
123
|
+
const headerEvent = cleanRefPart(req.headers['x-amalgm-event']);
|
|
124
|
+
const bodySource = cleanRefPart(typeof parsedBody === 'object' ? parsedBody?.source : null);
|
|
125
|
+
const bodyEvent = cleanRefPart(typeof parsedBody === 'object' ? parsedBody?.event : null);
|
|
126
|
+
if (headerSource || headerEvent) {
|
|
127
|
+
// Explicit x-amalgm-* headers are an unambiguous declaration.
|
|
128
|
+
eventSource = headerSource || 'external';
|
|
129
|
+
eventName = headerEvent || 'webhook';
|
|
130
|
+
} else if (bodySource || bodyEvent) {
|
|
131
|
+
// Body source/event are often incidental keys in third-party payloads
|
|
132
|
+
// (e.g. {"event": "user.signup"}). Use them as the primary ref, but
|
|
133
|
+
// keep the legacy external.webhook ref as a matching fallback so
|
|
134
|
+
// pre-existing generic triggers pinned to it keep firing.
|
|
135
|
+
eventSource = bodySource || 'external';
|
|
136
|
+
eventName = bodyEvent || 'webhook';
|
|
137
|
+
fallbackRef = { source: 'external', event: 'webhook' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
56
140
|
|
|
57
141
|
// GitHub webhook handshake: acknowledge verified ping events immediately.
|
|
58
142
|
if (githubEvent === 'ping') {
|
|
@@ -65,29 +149,27 @@ async function handleEventsPost(req, sendJson) {
|
|
|
65
149
|
return sendJson(200, { ok: true, message: 'pong' });
|
|
66
150
|
}
|
|
67
151
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.warn(`[AmalgmMCP:Events] No trigger matched ${eventSource}.${eventName}`);
|
|
76
|
-
return sendJson(401, { error: 'Invalid webhook signature or source/event — no matching trigger' });
|
|
152
|
+
// Authenticate first, then filter by source/event, so a bad secret (401)
|
|
153
|
+
// is distinguishable from a valid-but-unmatched ref (202). Vendors treat
|
|
154
|
+
// 401 as delivery failure and may auto-disable the webhook.
|
|
155
|
+
const authenticated = matchAllBySignature(triggers, signature, rawBody);
|
|
156
|
+
if (!authenticated?.length) {
|
|
157
|
+
console.warn('[AmalgmMCP:Events] Webhook signature did not verify against any trigger');
|
|
158
|
+
return sendJson(401, { error: 'Invalid webhook signature' });
|
|
77
159
|
}
|
|
78
160
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
161
|
+
let matchedTriggers = authenticated.filter((trigger) => (
|
|
162
|
+
matchesEventRef(trigger, { source: eventSource, event: eventName })
|
|
163
|
+
));
|
|
164
|
+
if (!matchedTriggers.length && fallbackRef) {
|
|
165
|
+
const fallbackMatches = authenticated.filter((trigger) => matchesEventRef(trigger, fallbackRef));
|
|
166
|
+
if (fallbackMatches.length > 0) {
|
|
167
|
+
eventSource = fallbackRef.source;
|
|
168
|
+
eventName = fallbackRef.event;
|
|
169
|
+
matchedTriggers = fallbackMatches;
|
|
170
|
+
}
|
|
86
171
|
}
|
|
87
|
-
|
|
88
|
-
console.log(
|
|
89
|
-
`[AmalgmMCP:Events] Matched ${matchedTriggers.length} trigger(s) for ${eventSource}.${eventName}`,
|
|
90
|
-
);
|
|
172
|
+
const eventHeaders = collectPassthroughHeaders(req.headers);
|
|
91
173
|
|
|
92
174
|
const eventObj = {
|
|
93
175
|
source: eventSource,
|
|
@@ -96,17 +178,48 @@ async function handleEventsPost(req, sendJson) {
|
|
|
96
178
|
headers: eventHeaders,
|
|
97
179
|
timestamp: new Date().toISOString(),
|
|
98
180
|
};
|
|
181
|
+
|
|
182
|
+
if (!matchedTriggers.length) {
|
|
183
|
+
ring.push({ ...eventObj, disposition: 'unmatched' });
|
|
184
|
+
console.warn(`[AmalgmMCP:Events] Authenticated event ${eventSource}.${eventName} matched no trigger selector`);
|
|
185
|
+
// Receiving an authenticated event counts as a run: surface it in the
|
|
186
|
+
// automation's run history instead of a separate event log.
|
|
187
|
+
recordUnmatchedEventRuns(authenticated, eventSource, eventName, eventObj.timestamp);
|
|
188
|
+
return sendJson(202, {
|
|
189
|
+
ok: true,
|
|
190
|
+
triggered: false,
|
|
191
|
+
reason: `No enabled trigger matches ${eventSource}.${eventName}`,
|
|
192
|
+
authenticatedTriggerIds: authenticated.map((trigger) => trigger.id),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(
|
|
197
|
+
`[AmalgmMCP:Events] Matched ${matchedTriggers.length} trigger(s) for ${eventSource}.${eventName}`,
|
|
198
|
+
);
|
|
99
199
|
ring.push(eventObj);
|
|
100
200
|
|
|
101
201
|
for (const matchedTrigger of matchedTriggers) {
|
|
102
|
-
|
|
202
|
+
// Isolate per trigger: a synchronous throw (missing workflow, IR drift)
|
|
203
|
+
// must not skip the remaining matched triggers or 5xx the sender.
|
|
204
|
+
try {
|
|
205
|
+
executeAutomationForTrigger(matchedTrigger.id, eventObj).catch((err) => {
|
|
206
|
+
console.error(
|
|
207
|
+
`[AmalgmMCP:Events] Automation workflow failed for trigger "${matchedTrigger.name}":`,
|
|
208
|
+
err.message,
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
103
212
|
console.error(
|
|
104
|
-
`[AmalgmMCP:Events] Automation
|
|
105
|
-
err.message,
|
|
213
|
+
`[AmalgmMCP:Events] Automation could not start for trigger "${matchedTrigger.name}":`,
|
|
214
|
+
err.message || err,
|
|
106
215
|
);
|
|
107
|
-
}
|
|
216
|
+
}
|
|
108
217
|
|
|
109
|
-
|
|
218
|
+
try {
|
|
219
|
+
markTriggerFired(matchedTrigger.id, { source: 'events:ingress' });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error(`[AmalgmMCP:Events] Failed to mark trigger fired "${matchedTrigger.name}":`, err.message || err);
|
|
222
|
+
}
|
|
110
223
|
}
|
|
111
224
|
|
|
112
225
|
return sendJson(200, {
|
|
@@ -43,10 +43,10 @@ function extractSignature(headers) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* Compute the expected
|
|
46
|
+
* Compute the expected `<algorithm>=<hex>` HMAC over `rawBody` using `secret`.
|
|
47
47
|
*/
|
|
48
|
-
function computeSignature(secret, rawBody) {
|
|
49
|
-
return
|
|
48
|
+
function computeSignature(secret, rawBody, algorithm = 'sha256') {
|
|
49
|
+
return `${algorithm}=` + crypto.createHmac(algorithm, secret).update(rawBody).digest('hex');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -74,7 +74,12 @@ function verifiesSignature(trigger, signature, rawBody) {
|
|
|
74
74
|
return timingSafeEqual(signature.value, trigger.secret);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
// Legacy x-hub-signature carries `sha1=<hex>` — match the algorithm the
|
|
78
|
+
// sender used instead of always computing sha256 (which can never verify).
|
|
79
|
+
const algorithm = typeof signature.value === 'string' && signature.value.startsWith('sha1=')
|
|
80
|
+
? 'sha1'
|
|
81
|
+
: 'sha256';
|
|
82
|
+
const expected = computeSignature(trigger.secret, rawBody, algorithm);
|
|
78
83
|
return timingSafeEqual(signature.value, expected);
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -27,6 +27,10 @@ async function handleAutomationRoutes(ctx) {
|
|
|
27
27
|
await automationsRest.handleRunNow(await ctx.readJsonBody(), ctx.sendJson);
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
|
+
if (ctx.pathname === '/automations/runs' && ctx.method === 'POST') {
|
|
31
|
+
await automationsRest.handleRuns(await ctx.readJsonBody(), ctx.sendJson);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
30
34
|
if (ctx.pathname === '/automations/validate' && ctx.method === 'POST') {
|
|
31
35
|
await automationsRest.handleValidate(await ctx.readJsonBody(), ctx.sendJson);
|
|
32
36
|
return true;
|
|
@@ -130,6 +130,18 @@ function migrate(database = openLocalDb()) {
|
|
|
130
130
|
CREATE INDEX IF NOT EXISTS automation_triggers_due_idx
|
|
131
131
|
ON automation_triggers(kind, enabled, next_run_at);
|
|
132
132
|
|
|
133
|
+
CREATE TABLE IF NOT EXISTS automation_workflow_links (
|
|
134
|
+
automation_id TEXT NOT NULL,
|
|
135
|
+
workflow_id TEXT NOT NULL,
|
|
136
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
137
|
+
PRIMARY KEY (automation_id, workflow_id),
|
|
138
|
+
FOREIGN KEY(automation_id) REFERENCES automation_definitions(id) ON DELETE CASCADE,
|
|
139
|
+
FOREIGN KEY(workflow_id) REFERENCES automation_workflows(id) ON DELETE CASCADE
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
CREATE INDEX IF NOT EXISTS automation_workflow_links_workflow_idx
|
|
143
|
+
ON automation_workflow_links(workflow_id);
|
|
144
|
+
|
|
133
145
|
CREATE TABLE IF NOT EXISTS automation_runs (
|
|
134
146
|
id TEXT PRIMARY KEY,
|
|
135
147
|
automation_id TEXT NOT NULL,
|
|
@@ -19,9 +19,9 @@ function readResource(resource, cache) {
|
|
|
19
19
|
cache.triggers ||= require('../automations/store').listTriggers();
|
|
20
20
|
return cache.triggers;
|
|
21
21
|
case 'automation_runs':
|
|
22
|
-
return require('../automations/store').listAutomationRuns({ limit:
|
|
22
|
+
return require('../automations/store').listAutomationRuns({ limit: 300 });
|
|
23
23
|
case 'workflow_cell_runs':
|
|
24
|
-
return require('../automations/store').listWorkflowCellRuns({ limit:
|
|
24
|
+
return require('../automations/store').listWorkflowCellRuns({ limit: 300 });
|
|
25
25
|
case 'tasks':
|
|
26
26
|
return require('../tasks/store').loadTasks().tasks;
|
|
27
27
|
case 'task_runs':
|