@amalgm/automations 0.1.1 → 0.2.0
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 +14 -0
- package/PURPOSE.md +15 -3
- package/README.md +61 -100
- package/dist/host/auth.d.ts +11 -0
- package/dist/host/auth.js +89 -0
- package/dist/host/config.d.ts +8 -0
- package/dist/host/config.js +29 -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 +47 -0
- package/dist/host/server.d.ts +11 -0
- package/dist/host/server.js +53 -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 +17 -1
- package/dist/src/crud/automations.d.ts +3 -0
- package/dist/src/crud/automations.js +55 -0
- package/dist/src/crud/context.d.ts +13 -0
- package/dist/src/crud/context.js +34 -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/events-http.d.ts +19 -0
- package/dist/src/events-http.js +107 -0
- package/dist/src/executor.d.ts +16 -0
- package/dist/src/executor.js +76 -0
- package/dist/src/index.d.ts +9 -2
- package/dist/src/index.js +6 -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 +2 -2
- package/dist/src/schema.d.ts +8 -0
- package/dist/src/schema.js +2 -0
- 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 +13 -4
- package/skills/automations/SKILL.md +52 -0
- package/supabase/migrations/20260829010000_bounded_schedules_and_machine_claims.sql +311 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Automations } from './automations.js';
|
|
2
|
+
import type { AutomationTarget } from './types.js';
|
|
3
|
+
export interface AutomationEventReceipt {
|
|
4
|
+
receivedAt: string;
|
|
5
|
+
targetId: string;
|
|
6
|
+
source: string;
|
|
7
|
+
event: string;
|
|
8
|
+
runIds: string[];
|
|
9
|
+
status: 'pending' | 'unmatched';
|
|
10
|
+
}
|
|
11
|
+
export interface AutomationEventsApiConfig {
|
|
12
|
+
delivery: Pick<Automations, 'receiveEvent'>;
|
|
13
|
+
target(request: Request): Promise<AutomationTarget>;
|
|
14
|
+
now?: () => Date;
|
|
15
|
+
maxBodyBytes?: number;
|
|
16
|
+
recentLimit?: number;
|
|
17
|
+
}
|
|
18
|
+
export type AutomationEventsApi = (request: Request) => Promise<Response>;
|
|
19
|
+
export declare function createAutomationEventsApi(config: AutomationEventsApiConfig): AutomationEventsApi;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Automations, EventRejectedError } from './automations.js';
|
|
2
|
+
import { eventReference } from './webhook.js';
|
|
3
|
+
// Engine reference: amalgm-engine/runtime/scripts/amalgm-mcp/events/ingress.js.
|
|
4
|
+
// The 2 MiB transport bound and safe recent-events door are preserved. The
|
|
5
|
+
// authority changes deliberately: success now means matching runs are durable
|
|
6
|
+
// in Supabase, never that an envelope was written to a local SQLite inbox.
|
|
7
|
+
const DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_RECENT_LIMIT = 200;
|
|
9
|
+
export function createAutomationEventsApi(config) {
|
|
10
|
+
const recent = [];
|
|
11
|
+
const now = config.now ?? (() => new Date());
|
|
12
|
+
const maxBodyBytes = positiveInteger(config.maxBodyBytes, DEFAULT_MAX_BODY_BYTES);
|
|
13
|
+
const recentLimit = positiveInteger(config.recentLimit, DEFAULT_RECENT_LIMIT);
|
|
14
|
+
return async (request) => {
|
|
15
|
+
const pathname = new URL(request.url).pathname;
|
|
16
|
+
if (pathname !== '/events')
|
|
17
|
+
return json(404, { error: 'not found' });
|
|
18
|
+
if (request.method === 'GET')
|
|
19
|
+
return json(200, { events: recent.slice(-50) });
|
|
20
|
+
if (request.method !== 'POST')
|
|
21
|
+
return json(405, { error: 'method not allowed' });
|
|
22
|
+
try {
|
|
23
|
+
const body = await readLimitedBody(request, maxBodyBytes);
|
|
24
|
+
const payload = parsePayload(body);
|
|
25
|
+
const headers = Object.fromEntries(request.headers.entries());
|
|
26
|
+
const target = await config.target(request);
|
|
27
|
+
const receivedAt = now();
|
|
28
|
+
const runs = await config.delivery.receiveEvent({
|
|
29
|
+
target,
|
|
30
|
+
headers,
|
|
31
|
+
body,
|
|
32
|
+
payload,
|
|
33
|
+
now: receivedAt,
|
|
34
|
+
});
|
|
35
|
+
const firstInput = runs[0]?.input;
|
|
36
|
+
const reference = firstInput?.kind === 'event'
|
|
37
|
+
? { source: firstInput.source, event: firstInput.event }
|
|
38
|
+
: eventReference(headers, payload);
|
|
39
|
+
const receipt = {
|
|
40
|
+
receivedAt: receivedAt.toISOString(),
|
|
41
|
+
targetId: target.targetId,
|
|
42
|
+
source: reference.source,
|
|
43
|
+
event: reference.event,
|
|
44
|
+
runIds: runs.map(({ id }) => id),
|
|
45
|
+
status: runs.length ? 'pending' : 'unmatched',
|
|
46
|
+
};
|
|
47
|
+
recent.push(receipt);
|
|
48
|
+
if (recent.length > recentLimit)
|
|
49
|
+
recent.splice(0, recent.length - recentLimit);
|
|
50
|
+
return json(202, { ok: true, accepted: true, ...receipt });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof EventRejectedError)
|
|
54
|
+
return json(401, { error: error.message });
|
|
55
|
+
if (error instanceof EventHttpError)
|
|
56
|
+
return json(error.status, { error: error.message });
|
|
57
|
+
return json(500, { error: error instanceof Error ? error.message : String(error) });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
class EventHttpError extends Error {
|
|
62
|
+
status;
|
|
63
|
+
constructor(status, message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.status = status;
|
|
66
|
+
this.name = 'EventHttpError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function readLimitedBody(request, limit) {
|
|
70
|
+
if (!request.body)
|
|
71
|
+
return Buffer.alloc(0);
|
|
72
|
+
const reader = request.body.getReader();
|
|
73
|
+
const chunks = [];
|
|
74
|
+
let size = 0;
|
|
75
|
+
while (true) {
|
|
76
|
+
const { done, value } = await reader.read();
|
|
77
|
+
if (done)
|
|
78
|
+
break;
|
|
79
|
+
const chunk = Buffer.from(value);
|
|
80
|
+
size += chunk.length;
|
|
81
|
+
if (size > limit) {
|
|
82
|
+
await reader.cancel();
|
|
83
|
+
throw new EventHttpError(413, `Event body exceeds ${limit} bytes`);
|
|
84
|
+
}
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
}
|
|
87
|
+
return Buffer.concat(chunks);
|
|
88
|
+
}
|
|
89
|
+
function parsePayload(body) {
|
|
90
|
+
if (body.length === 0)
|
|
91
|
+
return {};
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(body.toString('utf8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw new EventHttpError(400, 'Event body must be valid JSON');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function positiveInteger(value, fallback) {
|
|
100
|
+
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
101
|
+
}
|
|
102
|
+
function json(status, body) {
|
|
103
|
+
return new Response(JSON.stringify(body), {
|
|
104
|
+
status,
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -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,14 +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
|
+
export { createAutomationEventsApi, type AutomationEventReceipt, type AutomationEventsApi, type AutomationEventsApiConfig, } from './events-http.js';
|
|
10
11
|
export { nextCronAt, validateCron } from './schedule.js';
|
|
11
12
|
export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
|
|
12
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';
|
|
13
20
|
export type { SupabaseRpcClient as SupabaseStoreRpcClient } from './supabase-store.js';
|
|
14
21
|
export type { AutomationLog, AutomationRun as AutomationRunRecord, AutomationStore, AutomationTarget, AutomationTransport, DueCronTrigger, RunInput, RunUpdate, StoredEventTrigger, } from './types.js';
|
package/dist/src/index.js
CHANGED
|
@@ -13,6 +13,12 @@ export { runAutomationCli } from './cli.js';
|
|
|
13
13
|
export { createAutomationMcpServer } from './mcp.js';
|
|
14
14
|
export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
|
|
15
15
|
export { Automations, EventRejectedError } from './automations.js';
|
|
16
|
+
export { createAutomationEventsApi, } from './events-http.js';
|
|
16
17
|
export { nextCronAt, validateCron } from './schedule.js';
|
|
17
18
|
export { eventReference, eventReferences, matchesEvent, normalizeHeaders, verifyEventSecret, } from './webhook.js';
|
|
18
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
|
@@ -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 }) => {
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -78,37 +78,45 @@ export declare const createScheduleTriggerSchema: z.ZodObject<{
|
|
|
78
78
|
cron: z.ZodEffects<z.ZodString, string, string>;
|
|
79
79
|
timezone: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
80
80
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
maxOccurrences: z.ZodOptional<z.ZodNumber>;
|
|
81
82
|
}, "strict", z.ZodTypeAny, {
|
|
82
83
|
id?: string | undefined;
|
|
83
84
|
cron: string;
|
|
84
85
|
timezone?: string | undefined;
|
|
85
86
|
enabled?: boolean | undefined;
|
|
87
|
+
maxOccurrences?: number | undefined;
|
|
86
88
|
}, {
|
|
87
89
|
id?: string | undefined;
|
|
88
90
|
cron: string;
|
|
89
91
|
timezone?: string | undefined;
|
|
90
92
|
enabled?: boolean | undefined;
|
|
93
|
+
maxOccurrences?: number | undefined;
|
|
91
94
|
}>;
|
|
92
95
|
export declare const updateScheduleTriggerSchema: z.ZodEffects<z.ZodObject<{
|
|
93
96
|
cron: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
94
97
|
timezone: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
95
98
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
99
|
+
maxOccurrences: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
96
100
|
}, "strict", z.ZodTypeAny, {
|
|
97
101
|
cron?: string | undefined;
|
|
98
102
|
timezone?: string | undefined;
|
|
99
103
|
enabled?: boolean | undefined;
|
|
104
|
+
maxOccurrences?: number | null | undefined;
|
|
100
105
|
}, {
|
|
101
106
|
cron?: string | undefined;
|
|
102
107
|
timezone?: string | undefined;
|
|
103
108
|
enabled?: boolean | undefined;
|
|
109
|
+
maxOccurrences?: number | null | undefined;
|
|
104
110
|
}>, {
|
|
105
111
|
cron?: string | undefined;
|
|
106
112
|
timezone?: string | undefined;
|
|
107
113
|
enabled?: boolean | undefined;
|
|
114
|
+
maxOccurrences?: number | null | undefined;
|
|
108
115
|
}, {
|
|
109
116
|
cron?: string | undefined;
|
|
110
117
|
timezone?: string | undefined;
|
|
111
118
|
enabled?: boolean | undefined;
|
|
119
|
+
maxOccurrences?: number | null | undefined;
|
|
112
120
|
}>;
|
|
113
121
|
export declare const createWebhookTriggerSchema: z.ZodObject<{
|
|
114
122
|
id: z.ZodOptional<z.ZodString>;
|
package/dist/src/schema.js
CHANGED
|
@@ -41,11 +41,13 @@ export const createScheduleTriggerSchema = z.object({
|
|
|
41
41
|
cron: text(200, 'Schedule cron'),
|
|
42
42
|
timezone: text(200, 'Schedule timezone').optional(),
|
|
43
43
|
enabled: z.boolean().optional(),
|
|
44
|
+
maxOccurrences: z.number().int().min(1).max(100_000).optional(),
|
|
44
45
|
}).strict();
|
|
45
46
|
export const updateScheduleTriggerSchema = z.object({
|
|
46
47
|
cron: text(200, 'Schedule cron').optional(),
|
|
47
48
|
timezone: text(200, 'Schedule timezone').optional(),
|
|
48
49
|
enabled: z.boolean().optional(),
|
|
50
|
+
maxOccurrences: z.number().int().min(1).max(100_000).nullable().optional(),
|
|
49
51
|
}).strict().refine((patch) => Object.keys(patch).length > 0, 'Schedule trigger update must include a change');
|
|
50
52
|
export const createWebhookTriggerSchema = z.object({
|
|
51
53
|
id: identifierSchema.optional(),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AutomationCrudRepository } from '../crud.js';
|
|
2
|
+
import { type SupabaseRpcClient } from './rpc.js';
|
|
3
|
+
type Operations = Pick<AutomationCrudRepository, 'createAutomation' | 'listAutomations' | 'getAutomation' | 'updateAutomation' | 'deleteAutomation'>;
|
|
4
|
+
export declare function automationRepository(client: SupabaseRpcClient): Operations;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { automation, paged } from './mappers.js';
|
|
2
|
+
import { call } from './rpc.js';
|
|
3
|
+
export function automationRepository(client) {
|
|
4
|
+
return {
|
|
5
|
+
createAutomation: async (userId, input) => automation(await call(client, 'create_amalgm_automation', {
|
|
6
|
+
p_user_id: userId,
|
|
7
|
+
p_automation: input,
|
|
8
|
+
})),
|
|
9
|
+
listAutomations: async (userId, query) => paged(await call(client, 'list_amalgm_automations', {
|
|
10
|
+
p_user_id: userId,
|
|
11
|
+
p_target_id: query.targetId || null,
|
|
12
|
+
p_enabled: query.enabled ?? null,
|
|
13
|
+
p_limit: query.limit,
|
|
14
|
+
p_offset: query.offset,
|
|
15
|
+
}), automation, query),
|
|
16
|
+
getAutomation: async (userId, automationId) => {
|
|
17
|
+
const row = await call(client, 'get_amalgm_automation', {
|
|
18
|
+
p_user_id: userId,
|
|
19
|
+
p_automation_id: automationId,
|
|
20
|
+
});
|
|
21
|
+
return row ? automation(row) : null;
|
|
22
|
+
},
|
|
23
|
+
updateAutomation: async (userId, automationId, patch) => {
|
|
24
|
+
const row = await call(client, 'update_amalgm_automation', {
|
|
25
|
+
p_user_id: userId,
|
|
26
|
+
p_automation_id: automationId,
|
|
27
|
+
p_patch: patch,
|
|
28
|
+
});
|
|
29
|
+
return row ? automation(row) : null;
|
|
30
|
+
},
|
|
31
|
+
deleteAutomation: (userId, automationId) => call(client, 'delete_amalgm_automation', {
|
|
32
|
+
p_user_id: userId,
|
|
33
|
+
p_automation_id: automationId,
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Automation, AutomationRun, Page, PageQuery, ScheduleTrigger, WebhookTrigger, Workflow } from '../contract.js';
|
|
2
|
+
import type { AutomationRow, PageResult, RunRow, TriggerRow, WorkflowRow } from './rows.js';
|
|
3
|
+
export declare function automation(row: AutomationRow): Automation;
|
|
4
|
+
export declare function schedule(row: TriggerRow): ScheduleTrigger;
|
|
5
|
+
export declare function webhook(row: TriggerRow): WebhookTrigger;
|
|
6
|
+
export declare function workflow(row: WorkflowRow): Workflow;
|
|
7
|
+
export declare function run(row: RunRow): AutomationRun;
|
|
8
|
+
export declare function paged<T, U>(result: PageResult<T>, mapper: (row: T) => U, query: Required<PageQuery>): Page<U>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function automation(row) {
|
|
2
|
+
return {
|
|
3
|
+
id: row.id,
|
|
4
|
+
targetId: row.target_id,
|
|
5
|
+
...(row.name ? { name: row.name } : {}),
|
|
6
|
+
...(row.description ? { description: row.description } : {}),
|
|
7
|
+
enabled: row.enabled,
|
|
8
|
+
createdAt: timestamp(row.created_at),
|
|
9
|
+
updatedAt: timestamp(row.updated_at),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function schedule(row) {
|
|
13
|
+
return {
|
|
14
|
+
id: row.id,
|
|
15
|
+
automationId: row.automation_id,
|
|
16
|
+
kind: 'schedule',
|
|
17
|
+
enabled: row.enabled,
|
|
18
|
+
cron: row.cron,
|
|
19
|
+
timezone: row.timezone,
|
|
20
|
+
nextRunAt: timestamp(row.next_run_at),
|
|
21
|
+
...(row.max_occurrences === null ? {} : { maxOccurrences: row.max_occurrences }),
|
|
22
|
+
...(row.remaining_occurrences === null ? {} : { remainingOccurrences: row.remaining_occurrences }),
|
|
23
|
+
createdAt: timestamp(row.created_at),
|
|
24
|
+
updatedAt: timestamp(row.updated_at),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function webhook(row) {
|
|
28
|
+
return {
|
|
29
|
+
id: row.id,
|
|
30
|
+
automationId: row.automation_id,
|
|
31
|
+
kind: 'webhook',
|
|
32
|
+
enabled: row.enabled,
|
|
33
|
+
source: row.source,
|
|
34
|
+
event: row.event,
|
|
35
|
+
secretConfigured: row.secret_configured,
|
|
36
|
+
createdAt: timestamp(row.created_at),
|
|
37
|
+
updatedAt: timestamp(row.updated_at),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function workflow(row) {
|
|
41
|
+
return {
|
|
42
|
+
id: row.id,
|
|
43
|
+
automationId: row.automation_id,
|
|
44
|
+
...(row.name ? { name: row.name } : {}),
|
|
45
|
+
script: row.script,
|
|
46
|
+
...(row.compiled !== null ? { compiled: row.compiled } : {}),
|
|
47
|
+
...(row.allowlist !== null ? { allowlist: row.allowlist } : {}),
|
|
48
|
+
...(row.limits !== null ? { limits: row.limits } : {}),
|
|
49
|
+
createdAt: timestamp(row.created_at),
|
|
50
|
+
updatedAt: timestamp(row.updated_at),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function run(row) {
|
|
54
|
+
return {
|
|
55
|
+
id: row.id,
|
|
56
|
+
automationId: row.automation_id,
|
|
57
|
+
triggerId: row.trigger_id,
|
|
58
|
+
workflowId: row.workflow_id,
|
|
59
|
+
targetId: row.target_id,
|
|
60
|
+
status: row.status,
|
|
61
|
+
input: row.input,
|
|
62
|
+
createdAt: timestamp(row.created_at),
|
|
63
|
+
...(row.sent_at ? { sentAt: timestamp(row.sent_at) } : {}),
|
|
64
|
+
...(row.started_at ? { startedAt: timestamp(row.started_at) } : {}),
|
|
65
|
+
...(row.finished_at ? { finishedAt: timestamp(row.finished_at) } : {}),
|
|
66
|
+
...(row.output !== null ? { output: row.output } : {}),
|
|
67
|
+
...(row.error !== null ? { error: row.error } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function paged(result, mapper, query) {
|
|
71
|
+
return {
|
|
72
|
+
items: result.items.map(mapper),
|
|
73
|
+
total: result.total,
|
|
74
|
+
limit: query.limit,
|
|
75
|
+
offset: query.offset,
|
|
76
|
+
hasMore: query.offset + result.items.length < result.total,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function timestamp(value) {
|
|
80
|
+
return new Date(value).toISOString();
|
|
81
|
+
}
|