@amalgm/automations 0.1.2 → 0.2.1
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/AXIOMS.md +27 -11
- package/PURPOSE.md +13 -3
- package/README.md +60 -113
- package/dist/host/auth.d.ts +11 -0
- package/dist/host/auth.js +89 -0
- package/dist/host/config.d.ts +9 -0
- package/dist/host/config.js +30 -0
- package/dist/host/index.d.ts +3 -0
- package/dist/host/index.js +3 -0
- package/dist/host/main.d.ts +1 -0
- package/dist/host/main.js +48 -0
- package/dist/host/server.d.ts +12 -0
- package/dist/host/server.js +54 -0
- package/dist/src/automations.js +4 -1
- package/dist/src/client.d.ts +3 -1
- package/dist/src/client.js +6 -4
- package/dist/src/contract.d.ts +19 -2
- package/dist/src/crud/automations.d.ts +3 -0
- package/dist/src/crud/automations.js +55 -0
- package/dist/src/crud/context.d.ts +14 -0
- package/dist/src/crud/context.js +43 -0
- package/dist/src/crud/repository.d.ts +35 -0
- package/dist/src/crud/repository.js +1 -0
- package/dist/src/crud/runs.d.ts +3 -0
- package/dist/src/crud/runs.js +19 -0
- package/dist/src/crud/triggers.d.ts +3 -0
- package/dist/src/crud/triggers.js +118 -0
- package/dist/src/crud/workflow.d.ts +3 -0
- package/dist/src/crud/workflow.js +42 -0
- package/dist/src/crud.d.ts +3 -33
- package/dist/src/crud.js +10 -237
- package/dist/src/executor.d.ts +16 -0
- package/dist/src/executor.js +76 -0
- package/dist/src/index.d.ts +8 -2
- package/dist/src/index.js +5 -0
- package/dist/src/machine-client.d.ts +7 -0
- package/dist/src/machine-client.js +30 -0
- package/dist/src/machine-http.d.ts +5 -0
- package/dist/src/machine-http.js +40 -0
- package/dist/src/machine.d.ts +41 -0
- package/dist/src/machine.js +43 -0
- package/dist/src/mcp.js +3 -3
- package/dist/src/schema.d.ts +50 -42
- package/dist/src/schema.js +3 -1
- package/dist/src/supabase-crud/automations.d.ts +5 -0
- package/dist/src/supabase-crud/automations.js +36 -0
- package/dist/src/supabase-crud/mappers.d.ts +8 -0
- package/dist/src/supabase-crud/mappers.js +81 -0
- package/dist/src/supabase-crud/rows.d.ts +56 -0
- package/dist/src/supabase-crud/rows.js +1 -0
- package/dist/src/supabase-crud/rpc.d.ts +10 -0
- package/dist/src/supabase-crud/rpc.js +19 -0
- package/dist/src/supabase-crud/triggers.d.ts +5 -0
- package/dist/src/supabase-crud/triggers.js +46 -0
- package/dist/src/supabase-crud/workflow-runs.d.ts +5 -0
- package/dist/src/supabase-crud/workflow-runs.js +41 -0
- package/dist/src/supabase-crud.d.ts +5 -41
- package/dist/src/supabase-crud.js +4 -267
- package/dist/src/supabase-machine.d.ts +16 -0
- package/dist/src/supabase-machine.js +63 -0
- package/dist/src/supabase-store.js +1 -0
- package/dist/src/types.d.ts +1 -0
- package/package.json +12 -3
- package/skills/automations/SKILL.md +52 -0
- package/supabase/migrations/20260829010000_bounded_schedules_and_machine_claims.sql +311 -0
package/dist/src/crud.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { automationOperations } from './crud/automations.js';
|
|
2
|
+
import { createContext } from './crud/context.js';
|
|
3
|
+
import { runOperations } from './crud/runs.js';
|
|
4
|
+
import { triggerOperations } from './crud/triggers.js';
|
|
5
|
+
import { workflowOperations } from './crud/workflow.js';
|
|
5
6
|
export class AutomationCrudService {
|
|
6
7
|
repository;
|
|
7
8
|
clock;
|
|
@@ -10,240 +11,12 @@ export class AutomationCrudService {
|
|
|
10
11
|
this.clock = clock;
|
|
11
12
|
}
|
|
12
13
|
for(principal) {
|
|
13
|
-
|
|
14
|
-
const read = () => assertScope(principal, 'automations:read');
|
|
15
|
-
const write = () => assertScope(principal, 'automations:write');
|
|
16
|
-
const runsRead = () => assertScope(principal, 'runs:read');
|
|
17
|
-
const exists = async (automationId) => {
|
|
18
|
-
const automation = await this.repository.getAutomation(principal.userId, id(automationId, 'Automation id'));
|
|
19
|
-
if (!automation)
|
|
20
|
-
throw new NotFoundError('Automation');
|
|
21
|
-
};
|
|
14
|
+
const context = createContext(this.repository, principal, this.clock);
|
|
22
15
|
return {
|
|
23
|
-
automations:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const targetId = parsed.targetId;
|
|
28
|
-
assertTarget(principal, targetId);
|
|
29
|
-
if (parsed.id && await this.repository.getAutomation(principal.userId, id(parsed.id, 'Automation id'))) {
|
|
30
|
-
throw new ConflictError('Automation');
|
|
31
|
-
}
|
|
32
|
-
return this.repository.createAutomation(principal.userId, {
|
|
33
|
-
id: parsed.id ? id(parsed.id, 'Automation id') : newId('automation'),
|
|
34
|
-
targetId,
|
|
35
|
-
name: parsed.name || '',
|
|
36
|
-
description: parsed.description || '',
|
|
37
|
-
enabled: parsed.enabled !== false,
|
|
38
|
-
});
|
|
39
|
-
},
|
|
40
|
-
list: async (query = {}) => {
|
|
41
|
-
read();
|
|
42
|
-
const parsed = parseListAutomations(query);
|
|
43
|
-
if (parsed.targetId)
|
|
44
|
-
assertTarget(principal, parsed.targetId);
|
|
45
|
-
return this.repository.listAutomations(principal.userId, {
|
|
46
|
-
...page(parsed),
|
|
47
|
-
...(parsed.targetId ? { targetId: id(parsed.targetId, 'targetId') } : {}),
|
|
48
|
-
...(parsed.enabled === undefined ? {} : { enabled: parsed.enabled }),
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
get: async (automationId) => {
|
|
52
|
-
read();
|
|
53
|
-
return this.repository.getAutomation(principal.userId, id(automationId, 'Automation id'));
|
|
54
|
-
},
|
|
55
|
-
update: async (automationId, patch) => {
|
|
56
|
-
write();
|
|
57
|
-
const parsed = parseUpdateAutomation(patch);
|
|
58
|
-
if (parsed.targetId !== undefined) {
|
|
59
|
-
assertTarget(principal, parsed.targetId);
|
|
60
|
-
}
|
|
61
|
-
const updated = await this.repository.updateAutomation(principal.userId, id(automationId, 'Automation id'), parsed);
|
|
62
|
-
if (!updated)
|
|
63
|
-
throw new NotFoundError('Automation');
|
|
64
|
-
return updated;
|
|
65
|
-
},
|
|
66
|
-
delete: async (automationId) => {
|
|
67
|
-
write();
|
|
68
|
-
const deleted = await this.repository.deleteAutomation(principal.userId, id(automationId, 'Automation id'));
|
|
69
|
-
if (!deleted)
|
|
70
|
-
throw new NotFoundError('Automation');
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
triggers: {
|
|
74
|
-
list: async (automationId) => {
|
|
75
|
-
read();
|
|
76
|
-
await exists(automationId);
|
|
77
|
-
return this.repository.listTriggers(principal.userId, automationId);
|
|
78
|
-
},
|
|
79
|
-
schedule: {
|
|
80
|
-
create: async (automationId, input) => {
|
|
81
|
-
write();
|
|
82
|
-
await exists(automationId);
|
|
83
|
-
const parsed = parseCreateScheduleTrigger(input);
|
|
84
|
-
const timezone = parsed.timezone || 'UTC';
|
|
85
|
-
schedule(parsed.cron, timezone);
|
|
86
|
-
if (parsed.id && (await this.repository.listTriggers(principal.userId, automationId))
|
|
87
|
-
.some((trigger) => trigger.id === parsed.id))
|
|
88
|
-
throw new ConflictError('Trigger');
|
|
89
|
-
return this.repository.createScheduleTrigger(principal.userId, automationId, {
|
|
90
|
-
id: parsed.id ? id(parsed.id, 'Schedule trigger id') : newId('schedule'),
|
|
91
|
-
cron: parsed.cron,
|
|
92
|
-
timezone,
|
|
93
|
-
enabled: parsed.enabled !== false,
|
|
94
|
-
nextRunAt: nextCronAt(parsed.cron, timezone, this.clock()),
|
|
95
|
-
});
|
|
96
|
-
},
|
|
97
|
-
list: async (automationId, query = {}) => {
|
|
98
|
-
read();
|
|
99
|
-
await exists(automationId);
|
|
100
|
-
return this.repository.listScheduleTriggers(principal.userId, automationId, page(parsePageQuery(query)));
|
|
101
|
-
},
|
|
102
|
-
get: async (automationId, triggerId) => {
|
|
103
|
-
read();
|
|
104
|
-
await exists(automationId);
|
|
105
|
-
return this.repository.getScheduleTrigger(principal.userId, automationId, id(triggerId, 'Schedule trigger id'));
|
|
106
|
-
},
|
|
107
|
-
update: async (automationId, triggerId, patch) => {
|
|
108
|
-
write();
|
|
109
|
-
await exists(automationId);
|
|
110
|
-
const parsed = parseUpdateScheduleTrigger(patch);
|
|
111
|
-
let storedPatch = parsed;
|
|
112
|
-
if (parsed.cron !== undefined || parsed.timezone !== undefined) {
|
|
113
|
-
const existing = await this.repository.getScheduleTrigger(principal.userId, automationId, triggerId);
|
|
114
|
-
if (!existing)
|
|
115
|
-
throw new NotFoundError('Schedule trigger');
|
|
116
|
-
const cron = parsed.cron ?? existing.cron;
|
|
117
|
-
const timezone = parsed.timezone ?? existing.timezone;
|
|
118
|
-
schedule(cron, timezone);
|
|
119
|
-
storedPatch = { ...parsed, nextRunAt: nextCronAt(cron, timezone, this.clock()) };
|
|
120
|
-
}
|
|
121
|
-
const updated = await this.repository.updateScheduleTrigger(principal.userId, automationId, id(triggerId, 'Schedule trigger id'), storedPatch);
|
|
122
|
-
if (!updated)
|
|
123
|
-
throw new NotFoundError('Schedule trigger');
|
|
124
|
-
return updated;
|
|
125
|
-
},
|
|
126
|
-
delete: async (automationId, triggerId) => {
|
|
127
|
-
write();
|
|
128
|
-
await exists(automationId);
|
|
129
|
-
const deleted = await this.repository.deleteScheduleTrigger(principal.userId, automationId, id(triggerId, 'Schedule trigger id'));
|
|
130
|
-
if (!deleted)
|
|
131
|
-
throw new NotFoundError('Schedule trigger');
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
webhook: {
|
|
135
|
-
create: async (automationId, input) => {
|
|
136
|
-
write();
|
|
137
|
-
await exists(automationId);
|
|
138
|
-
const parsed = parseCreateWebhookTrigger(input);
|
|
139
|
-
if (parsed.id && (await this.repository.listTriggers(principal.userId, automationId))
|
|
140
|
-
.some((trigger) => trigger.id === parsed.id))
|
|
141
|
-
throw new ConflictError('Trigger');
|
|
142
|
-
return this.repository.createWebhookTrigger(principal.userId, automationId, {
|
|
143
|
-
id: parsed.id ? id(parsed.id, 'Webhook trigger id') : newId('webhook'),
|
|
144
|
-
source: parsed.source || '*',
|
|
145
|
-
event: parsed.event || '*',
|
|
146
|
-
secret: parsed.secret,
|
|
147
|
-
enabled: parsed.enabled !== false,
|
|
148
|
-
});
|
|
149
|
-
},
|
|
150
|
-
list: async (automationId, query = {}) => {
|
|
151
|
-
read();
|
|
152
|
-
await exists(automationId);
|
|
153
|
-
return this.repository.listWebhookTriggers(principal.userId, automationId, page(parsePageQuery(query)));
|
|
154
|
-
},
|
|
155
|
-
get: async (automationId, triggerId) => {
|
|
156
|
-
read();
|
|
157
|
-
await exists(automationId);
|
|
158
|
-
return this.repository.getWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'));
|
|
159
|
-
},
|
|
160
|
-
update: async (automationId, triggerId, patch) => {
|
|
161
|
-
write();
|
|
162
|
-
await exists(automationId);
|
|
163
|
-
const parsed = parseUpdateWebhookTrigger(patch);
|
|
164
|
-
const updated = await this.repository.updateWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'), parsed);
|
|
165
|
-
if (!updated)
|
|
166
|
-
throw new NotFoundError('Webhook trigger');
|
|
167
|
-
return updated;
|
|
168
|
-
},
|
|
169
|
-
delete: async (automationId, triggerId) => {
|
|
170
|
-
write();
|
|
171
|
-
await exists(automationId);
|
|
172
|
-
const deleted = await this.repository.deleteWebhookTrigger(principal.userId, automationId, id(triggerId, 'Webhook trigger id'));
|
|
173
|
-
if (!deleted)
|
|
174
|
-
throw new NotFoundError('Webhook trigger');
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
workflow: {
|
|
179
|
-
create: async (automationId, input) => {
|
|
180
|
-
write();
|
|
181
|
-
await exists(automationId);
|
|
182
|
-
if (await this.repository.getWorkflow(principal.userId, automationId)) {
|
|
183
|
-
throw new ConflictError('Workflow');
|
|
184
|
-
}
|
|
185
|
-
const parsed = parseCreateWorkflow(input);
|
|
186
|
-
return this.repository.createWorkflow(principal.userId, automationId, {
|
|
187
|
-
id: parsed.id ? id(parsed.id, 'Workflow id') : newId('workflow'),
|
|
188
|
-
name: parsed.name || '',
|
|
189
|
-
script: parsed.script,
|
|
190
|
-
compiled: parsed.compiled ?? null,
|
|
191
|
-
allowlist: parsed.allowlist ?? null,
|
|
192
|
-
limits: parsed.limits ?? null,
|
|
193
|
-
});
|
|
194
|
-
},
|
|
195
|
-
get: async (automationId) => {
|
|
196
|
-
read();
|
|
197
|
-
await exists(automationId);
|
|
198
|
-
return this.repository.getWorkflow(principal.userId, automationId);
|
|
199
|
-
},
|
|
200
|
-
update: async (automationId, patch) => {
|
|
201
|
-
write();
|
|
202
|
-
await exists(automationId);
|
|
203
|
-
const parsed = parseUpdateWorkflow(patch);
|
|
204
|
-
const updated = await this.repository.updateWorkflow(principal.userId, automationId, parsed);
|
|
205
|
-
if (!updated)
|
|
206
|
-
throw new NotFoundError('Workflow');
|
|
207
|
-
return updated;
|
|
208
|
-
},
|
|
209
|
-
delete: async (automationId) => {
|
|
210
|
-
write();
|
|
211
|
-
await exists(automationId);
|
|
212
|
-
if (!await this.repository.deleteWorkflow(principal.userId, automationId)) {
|
|
213
|
-
throw new NotFoundError('Workflow');
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
runs: {
|
|
218
|
-
list: async (automationId, query = {}) => {
|
|
219
|
-
runsRead();
|
|
220
|
-
const parsed = parseListRuns(query);
|
|
221
|
-
return this.repository.listRuns(principal.userId, id(automationId, 'Automation id'), {
|
|
222
|
-
...page(parsed),
|
|
223
|
-
...(parsed.status ? { status: parsed.status } : {}),
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
get: async (automationId, runId) => {
|
|
227
|
-
runsRead();
|
|
228
|
-
return this.repository.getRun(principal.userId, id(automationId, 'Automation id'), id(runId, 'Run id'));
|
|
229
|
-
},
|
|
230
|
-
},
|
|
16
|
+
automations: automationOperations(context),
|
|
17
|
+
triggers: triggerOperations(context),
|
|
18
|
+
workflow: workflowOperations(context),
|
|
19
|
+
runs: runOperations(context),
|
|
231
20
|
};
|
|
232
21
|
}
|
|
233
22
|
}
|
|
234
|
-
function assertPrincipal(principal) {
|
|
235
|
-
requiredText(principal.userId, 'Authenticated user id');
|
|
236
|
-
if (!Array.isArray(principal.scopes))
|
|
237
|
-
throw new ForbiddenError();
|
|
238
|
-
principal.targetIds?.forEach((targetId) => id(targetId, 'Authorized target id'));
|
|
239
|
-
}
|
|
240
|
-
function assertScope(principal, scope) {
|
|
241
|
-
if (!principal.scopes.includes('*') && !principal.scopes.includes(scope)) {
|
|
242
|
-
throw new ForbiddenError(`This credential requires ${scope}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
function assertTarget(principal, targetId) {
|
|
246
|
-
if (principal.targetIds && !principal.targetIds.includes(targetId)) {
|
|
247
|
-
throw new ForbiddenError('This credential cannot access that target');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AutomationPlan, Json } from './contract.js';
|
|
2
|
+
import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
|
|
3
|
+
export interface AutomationActionPort {
|
|
4
|
+
call(input: Readonly<{
|
|
5
|
+
actionId: string;
|
|
6
|
+
payload: Json;
|
|
7
|
+
idempotencyKey: string;
|
|
8
|
+
}>): Promise<Json>;
|
|
9
|
+
}
|
|
10
|
+
export declare class AutomationRunExecutor {
|
|
11
|
+
private readonly runs;
|
|
12
|
+
private readonly actions;
|
|
13
|
+
constructor(runs: MachineRuns, actions: AutomationActionPort);
|
|
14
|
+
execute(run: ClaimedAutomationRun): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function automationPlan(snapshot: Json): AutomationPlan;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class AutomationRunExecutor {
|
|
2
|
+
runs;
|
|
3
|
+
actions;
|
|
4
|
+
constructor(runs, actions) {
|
|
5
|
+
this.runs = runs;
|
|
6
|
+
this.actions = actions;
|
|
7
|
+
}
|
|
8
|
+
async execute(run) {
|
|
9
|
+
await this.runs.update(run.id, { leaseToken: run.leaseToken, status: 'running' });
|
|
10
|
+
try {
|
|
11
|
+
const plan = automationPlan(run.automation);
|
|
12
|
+
const steps = [];
|
|
13
|
+
for (const step of plan.steps) {
|
|
14
|
+
const output = await this.actions.call({
|
|
15
|
+
actionId: step.actionId,
|
|
16
|
+
payload: step.input,
|
|
17
|
+
idempotencyKey: `${run.id}:${step.id}`,
|
|
18
|
+
});
|
|
19
|
+
steps.push({ id: step.id, output });
|
|
20
|
+
}
|
|
21
|
+
await this.runs.update(run.id, {
|
|
22
|
+
leaseToken: run.leaseToken,
|
|
23
|
+
status: 'completed',
|
|
24
|
+
output: { steps },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
await this.runs.update(run.id, {
|
|
29
|
+
leaseToken: run.leaseToken,
|
|
30
|
+
status: 'failed',
|
|
31
|
+
error: safeError(error),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function automationPlan(snapshot) {
|
|
37
|
+
const root = record(snapshot, 'Automation snapshot');
|
|
38
|
+
const workflow = record(root.workflow, 'Workflow snapshot');
|
|
39
|
+
const compiled = record(workflow.compiled, 'Compiled workflow');
|
|
40
|
+
if (compiled.version !== 1 || !Array.isArray(compiled.steps) || compiled.steps.length > 100) {
|
|
41
|
+
throw new Error('Compiled workflow must be a version 1 plan with at most 100 steps');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
steps: compiled.steps.map((value, index) => step(value, index)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function step(value, index) {
|
|
49
|
+
const item = record(value, `Workflow step ${index + 1}`);
|
|
50
|
+
if (typeof item.id !== 'string' || !item.id.trim())
|
|
51
|
+
throw new Error(`Workflow step ${index + 1} needs an id`);
|
|
52
|
+
if (typeof item.actionId !== 'string' || !item.actionId.includes('.')) {
|
|
53
|
+
throw new Error(`Workflow step ${index + 1} needs a qualified actionId`);
|
|
54
|
+
}
|
|
55
|
+
assertJson(item.input);
|
|
56
|
+
return { id: item.id, actionId: item.actionId, input: item.input };
|
|
57
|
+
}
|
|
58
|
+
function record(value, name) {
|
|
59
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
60
|
+
throw new Error(`${name} is missing`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function assertJson(value) {
|
|
64
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
65
|
+
return;
|
|
66
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
67
|
+
return;
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
return void value.forEach(assertJson);
|
|
70
|
+
if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
71
|
+
return void Object.values(value).forEach(assertJson);
|
|
72
|
+
}
|
|
73
|
+
throw new Error('Workflow step input must be JSON');
|
|
74
|
+
}
|
|
75
|
+
const safeError = (error) => (error instanceof Error ? error.message : String(error))
|
|
76
|
+
.replace(/[\r\n]+/g, ' ').slice(0, 1_000);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
export { createAutomationClient, type AutomationAuthorization } from './client.js';
|
|
1
|
+
export { createAutomationClient, type AutomationAuthorization, type AutomationHeaders } from './client.js';
|
|
2
2
|
export { AutomationCrudService, type AutomationCrudRepository } from './crud.js';
|
|
3
3
|
export { AutomationError, ConflictError, ForbiddenError, NotFoundError, ValidationError, type AutomationErrorCode, } from './errors.js';
|
|
4
4
|
export { createAutomationApi, type AuthenticateAutomationRequest, type AutomationApi } from './http.js';
|
|
5
5
|
export { runAutomationCli } from './cli.js';
|
|
6
6
|
export { createAutomationMcpServer } from './mcp.js';
|
|
7
7
|
export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
|
|
8
|
-
export type { Automation, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationRun, AutomationScope, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Json, ListAutomations, ListRuns, Page, PageQuery, RunStatus, ScheduleTrigger, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow, } from './contract.js';
|
|
8
|
+
export type { Automation, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationPlan, AutomationRun, AutomationScope, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Json, ListAutomations, ListRuns, Page, PageQuery, RunStatus, ScheduleTrigger, Trigger, ToolActionStep, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow, } from './contract.js';
|
|
9
9
|
export { Automations, EventRejectedError } from './automations.js';
|
|
10
10
|
export { createAutomationEventsApi, type AutomationEventReceipt, type AutomationEventsApi, type AutomationEventsApiConfig, } from './events-http.js';
|
|
11
11
|
export { nextCronAt, validateCron } from './schedule.js';
|
|
12
12
|
export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
|
|
13
13
|
export { SupabaseStore } from './supabase-store.js';
|
|
14
|
+
export { SupabaseMachineRunRepository, type MachineRpcClient } from './supabase-machine.js';
|
|
15
|
+
export { createMachineRuns } from './machine.js';
|
|
16
|
+
export type { AutomationMachinePrincipal, ClaimedAutomationRun, MachineRunRepository, MachineRuns, MachineRunUpdate, } from './machine.js';
|
|
17
|
+
export { createMachineRunsApi } from './machine-http.js';
|
|
18
|
+
export { createMachineRunsClient, type AutomationRequestHeaders } from './machine-client.js';
|
|
19
|
+
export { AutomationRunExecutor, automationPlan, type AutomationActionPort } from './executor.js';
|
|
14
20
|
export type { SupabaseRpcClient as SupabaseStoreRpcClient } from './supabase-store.js';
|
|
15
21
|
export type { AutomationLog, AutomationRun as AutomationRunRecord, AutomationStore, AutomationTarget, AutomationTransport, DueCronTrigger, RunInput, RunUpdate, StoredEventTrigger, } from './types.js';
|
package/dist/src/index.js
CHANGED
|
@@ -17,3 +17,8 @@ export { createAutomationEventsApi, } from './events-http.js';
|
|
|
17
17
|
export { nextCronAt, validateCron } from './schedule.js';
|
|
18
18
|
export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
|
|
19
19
|
export { SupabaseStore } from './supabase-store.js';
|
|
20
|
+
export { SupabaseMachineRunRepository } from './supabase-machine.js';
|
|
21
|
+
export { createMachineRuns } from './machine.js';
|
|
22
|
+
export { createMachineRunsApi } from './machine-http.js';
|
|
23
|
+
export { createMachineRunsClient } from './machine-client.js';
|
|
24
|
+
export { AutomationRunExecutor, automationPlan } from './executor.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MachineRuns } from './machine.js';
|
|
2
|
+
export type AutomationRequestHeaders = (method: string, url: string) => Promise<Readonly<Record<string, string>>> | Readonly<Record<string, string>>;
|
|
3
|
+
export declare function createMachineRunsClient(options: {
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly headers: AutomationRequestHeaders;
|
|
6
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
7
|
+
}): MachineRuns;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AutomationError } from './errors.js';
|
|
2
|
+
export function createMachineRunsClient(options) {
|
|
3
|
+
const baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
4
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
5
|
+
const request = async (path, method, body) => {
|
|
6
|
+
const url = `${baseUrl}${path}`;
|
|
7
|
+
const response = await fetch(url, {
|
|
8
|
+
method,
|
|
9
|
+
headers: {
|
|
10
|
+
accept: 'application/json',
|
|
11
|
+
'content-type': 'application/json',
|
|
12
|
+
...await options.headers(method, url),
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify(body),
|
|
15
|
+
});
|
|
16
|
+
const payload = await response.json().catch(() => ({}));
|
|
17
|
+
if (!response.ok)
|
|
18
|
+
throw new AutomationError(response.status === 403 ? 'forbidden' : response.status >= 500 ? 'internal' : 'validation', payload.error ?? `Automations API returned ${response.status}`);
|
|
19
|
+
return payload;
|
|
20
|
+
};
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
async claim(input = {}) {
|
|
23
|
+
const result = await request('/v1/machine/runs/claim', 'POST', input);
|
|
24
|
+
return result.runs;
|
|
25
|
+
},
|
|
26
|
+
update(runId, update) {
|
|
27
|
+
return request(`/v1/machine/runs/${encodeURIComponent(runId)}`, 'PATCH', update);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AutomationMachinePrincipal, MachineRuns } from './machine.js';
|
|
2
|
+
export declare function createMachineRunsApi(options: {
|
|
3
|
+
readonly authenticate: (request: Request) => Promise<AutomationMachinePrincipal>;
|
|
4
|
+
readonly runsFor: (principal: AutomationMachinePrincipal) => MachineRuns;
|
|
5
|
+
}): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AutomationError, ValidationError } from './errors.js';
|
|
2
|
+
export function createMachineRunsApi(options) {
|
|
3
|
+
return async (request) => {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
const parts = url.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
7
|
+
if (parts[0] !== 'v1' || parts[1] !== 'machine' || parts[2] !== 'runs') {
|
|
8
|
+
return json(404, { error: 'Not found' });
|
|
9
|
+
}
|
|
10
|
+
const runs = options.runsFor(await options.authenticate(request));
|
|
11
|
+
if (parts[3] === 'claim' && parts.length === 4 && request.method === 'POST') {
|
|
12
|
+
return json(200, { runs: await runs.claim(await body(request)) });
|
|
13
|
+
}
|
|
14
|
+
if (parts[3] && parts.length === 4 && request.method === 'PATCH') {
|
|
15
|
+
return json(200, await runs.update(parts[3], await body(request)));
|
|
16
|
+
}
|
|
17
|
+
return json(405, { error: 'Method not allowed' });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof AutomationError)
|
|
21
|
+
return json(error.code === 'validation' ? 400 : 403, {
|
|
22
|
+
error: error.message, code: error.code,
|
|
23
|
+
});
|
|
24
|
+
const code = error instanceof Error && 'code' in error ? String(error.code) : 'internal';
|
|
25
|
+
return json(code.startsWith('dpop_') || code.includes('access') ? 401 : 500, {
|
|
26
|
+
error: code === 'internal' ? 'Automations service failed' : 'Authorization denied', code,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function body(request) {
|
|
32
|
+
const value = await request.json().catch(() => null);
|
|
33
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
throw new ValidationError('Request body must be a JSON object');
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function json(status, value) {
|
|
39
|
+
return Response.json(value, { status, headers: { 'cache-control': 'no-store' } });
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AutomationScope, Json, RunStatus } from './contract.js';
|
|
2
|
+
import type { AutomationRun } from './types.js';
|
|
3
|
+
export interface AutomationMachinePrincipal {
|
|
4
|
+
readonly userId: string;
|
|
5
|
+
readonly computerId: string;
|
|
6
|
+
readonly scopes: readonly AutomationScope[];
|
|
7
|
+
}
|
|
8
|
+
export interface ClaimedAutomationRun extends AutomationRun {
|
|
9
|
+
readonly leaseToken: string;
|
|
10
|
+
readonly leaseExpiresAt: string;
|
|
11
|
+
readonly attempts: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MachineRunUpdate {
|
|
14
|
+
readonly leaseToken: string;
|
|
15
|
+
readonly status: Extract<RunStatus, 'running' | 'completed' | 'failed'>;
|
|
16
|
+
readonly output?: Json;
|
|
17
|
+
readonly error?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface MachineRuns {
|
|
20
|
+
claim(input?: {
|
|
21
|
+
readonly limit?: number;
|
|
22
|
+
readonly leaseSeconds?: number;
|
|
23
|
+
}): Promise<readonly ClaimedAutomationRun[]>;
|
|
24
|
+
update(runId: string, update: MachineRunUpdate): Promise<ClaimedAutomationRun | null>;
|
|
25
|
+
}
|
|
26
|
+
export interface MachineRunRepository {
|
|
27
|
+
claim(input: Readonly<{
|
|
28
|
+
userId: string;
|
|
29
|
+
targetId: string;
|
|
30
|
+
limit: number;
|
|
31
|
+
leaseSeconds: number;
|
|
32
|
+
}>): Promise<readonly ClaimedAutomationRun[]>;
|
|
33
|
+
update(input: Readonly<{
|
|
34
|
+
userId: string;
|
|
35
|
+
targetId: string;
|
|
36
|
+
runId: string;
|
|
37
|
+
update: MachineRunUpdate;
|
|
38
|
+
now: Date;
|
|
39
|
+
}>): Promise<ClaimedAutomationRun | null>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createMachineRuns(repository: MachineRunRepository, principal: AutomationMachinePrincipal, now?: () => Date): MachineRuns;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ForbiddenError, ValidationError } from './errors.js';
|
|
2
|
+
export function createMachineRuns(repository, principal, now = () => new Date()) {
|
|
3
|
+
requireScope(principal, 'runs:execute');
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
async claim(input = {}) {
|
|
6
|
+
const limit = input.limit ?? 5;
|
|
7
|
+
const leaseSeconds = input.leaseSeconds ?? 300;
|
|
8
|
+
integer(limit, 1, 20, 'limit');
|
|
9
|
+
integer(leaseSeconds, 30, 900, 'leaseSeconds');
|
|
10
|
+
return repository.claim({
|
|
11
|
+
userId: principal.userId,
|
|
12
|
+
targetId: principal.computerId,
|
|
13
|
+
limit,
|
|
14
|
+
leaseSeconds,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
async update(runId, update) {
|
|
18
|
+
if (!runId.trim() || !update.leaseToken.trim()) {
|
|
19
|
+
throw new ValidationError('Run id and lease token are required');
|
|
20
|
+
}
|
|
21
|
+
if (!['running', 'completed', 'failed'].includes(update.status)) {
|
|
22
|
+
throw new ValidationError('Run status is invalid');
|
|
23
|
+
}
|
|
24
|
+
return repository.update({
|
|
25
|
+
userId: principal.userId,
|
|
26
|
+
targetId: principal.computerId,
|
|
27
|
+
runId,
|
|
28
|
+
update,
|
|
29
|
+
now: now(),
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function requireScope(principal, scope) {
|
|
35
|
+
if (!principal.scopes.includes('*') && !principal.scopes.includes(scope)) {
|
|
36
|
+
throw new ForbiddenError(`Missing scope: ${scope}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function integer(value, minimum, maximum, name) {
|
|
40
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
41
|
+
throw new ValidationError(`${name} must be an integer from ${minimum} to ${maximum}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/src/mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ const triggerIdInput = { ...automationIdInput, trigger_id: identifierSchema };
|
|
|
7
7
|
const runIdInput = { ...automationIdInput, run_id: identifierSchema };
|
|
8
8
|
export function createAutomationMcpServer(sdk) {
|
|
9
9
|
const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
|
|
10
|
-
register(server, 'amalgm_automations_create', 'Create automation', 'Create an automation configuration without triggers or workflow.', { input: createAutomationSchema }, write(false), ({ input }) => sdk.automations.create(input));
|
|
10
|
+
register(server, 'amalgm_automations_create', 'Create automation', 'Create an automation configuration without triggers or workflow. In an authenticated machine session, omit input.targetId so the service uses that machine; never guess a target id.', { input: createAutomationSchema }, write(false), ({ input }) => sdk.automations.create(input));
|
|
11
11
|
register(server, 'amalgm_automations_list', 'List automations', 'List the caller\'s automations with optional target and enabled filters.', { query: listAutomationsSchema.optional() }, read(), ({ query }) => sdk.automations.list(query));
|
|
12
12
|
register(server, 'amalgm_automations_get', 'Get automation', 'Get one automation by id.', automationIdInput, read(), ({ automation_id }) => sdk.automations.get(automation_id));
|
|
13
13
|
register(server, 'amalgm_automations_update', 'Update automation', 'Update automation metadata, target, or enabled state.', { automation_id: identifierSchema, patch: updateAutomationSchema }, write(true), ({ automation_id, patch }) => sdk.automations.update(automation_id, patch));
|
|
@@ -16,7 +16,7 @@ export function createAutomationMcpServer(sdk) {
|
|
|
16
16
|
return { deleted: automation_id };
|
|
17
17
|
});
|
|
18
18
|
register(server, 'amalgm_automation_triggers_list', 'List automation triggers', 'List all schedule and webhook triggers belonging to one automation.', automationIdInput, read(), ({ automation_id }) => sdk.triggers.list(automation_id));
|
|
19
|
-
register(server, 'amalgm_schedule_triggers_create', 'Create schedule trigger', 'Create a cron schedule trigger for one automation.', { automation_id: identifierSchema, input: createScheduleTriggerSchema }, write(false), ({ automation_id, input }) => sdk.triggers.schedule.create(automation_id, input));
|
|
19
|
+
register(server, 'amalgm_schedule_triggers_create', 'Create schedule trigger', 'Create a cron schedule trigger for one automation. Set maxOccurrences for a bounded request such as “every minute for ten minutes”; the service disables it after exactly that many admitted runs.', { automation_id: identifierSchema, input: createScheduleTriggerSchema }, write(false), ({ automation_id, input }) => sdk.triggers.schedule.create(automation_id, input));
|
|
20
20
|
register(server, 'amalgm_schedule_triggers_list', 'List schedule triggers', 'List schedule triggers for one automation.', { automation_id: identifierSchema, query: pageQuerySchema.optional() }, read(), ({ automation_id, query }) => sdk.triggers.schedule.list(automation_id, query));
|
|
21
21
|
register(server, 'amalgm_schedule_triggers_get', 'Get schedule trigger', 'Get one schedule trigger by automation and trigger id.', triggerIdInput, read(), ({ automation_id, trigger_id }) => sdk.triggers.schedule.get(automation_id, trigger_id));
|
|
22
22
|
register(server, 'amalgm_schedule_triggers_update', 'Update schedule trigger', 'Update a cron schedule trigger without changing its type.', { automation_id: identifierSchema, trigger_id: identifierSchema, patch: updateScheduleTriggerSchema }, write(true), ({ automation_id, trigger_id, patch }) => sdk.triggers.schedule.update(automation_id, trigger_id, patch));
|
|
@@ -32,7 +32,7 @@ export function createAutomationMcpServer(sdk) {
|
|
|
32
32
|
await sdk.triggers.webhook.delete(automation_id, trigger_id);
|
|
33
33
|
return { deleted: trigger_id };
|
|
34
34
|
});
|
|
35
|
-
register(server, 'amalgm_workflow_create', 'Create automation workflow', 'Create the one workflow
|
|
35
|
+
register(server, 'amalgm_workflow_create', 'Create automation workflow', 'Create the one workflow owned by an automation. For tool execution, compiled must be {version:1,steps:[{id,actionId,input}]}; for a reminder use actionId “channels.notify_user” and input {message,title?}.', { automation_id: identifierSchema, input: createWorkflowSchema }, write(false), ({ automation_id, input }) => sdk.workflow.create(automation_id, input));
|
|
36
36
|
register(server, 'amalgm_workflow_get', 'Get automation workflow', 'Get the workflow script owned by an automation.', automationIdInput, read(), ({ automation_id }) => sdk.workflow.get(automation_id));
|
|
37
37
|
register(server, 'amalgm_workflow_update', 'Update automation workflow', 'Update the workflow script or its configuration.', { automation_id: identifierSchema, patch: updateWorkflowSchema }, write(true), ({ automation_id, patch }) => sdk.workflow.update(automation_id, patch));
|
|
38
38
|
register(server, 'amalgm_workflow_delete', 'Delete automation workflow', 'Delete the workflow script. The automation and its triggers remain.', automationIdInput, destructive(), async ({ automation_id }) => {
|