@amalgm/automations 0.2.6 → 0.3.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 +54 -9
- package/PURPOSE.md +47 -11
- package/README.md +119 -12
- package/dist/src/cli/arguments.d.ts +20 -0
- package/dist/src/cli/arguments.js +49 -0
- package/dist/src/cli/input.d.ts +6 -0
- package/dist/src/cli/input.js +48 -0
- package/dist/src/cli/main.d.ts +7 -0
- package/dist/src/cli/main.js +39 -0
- package/dist/src/cli/run.d.ts +13 -0
- package/dist/src/cli/run.js +51 -0
- package/dist/src/cli/shell-runtime.d.ts +9 -0
- package/dist/src/cli/shell-runtime.js +131 -0
- package/dist/src/cli-main.js +2 -20
- package/dist/src/cli.d.ts +3 -10
- package/dist/src/cli.js +3 -186
- package/dist/src/command-result.d.ts +8 -0
- package/dist/src/command-result.js +13 -0
- package/dist/src/command-surface.d.ts +25 -0
- package/dist/src/command-surface.js +125 -0
- package/dist/src/contract.d.ts +1 -1
- package/dist/src/execution-contract.d.ts +28 -0
- package/dist/src/execution-contract.js +1 -0
- package/dist/src/execution-errors.d.ts +6 -0
- package/dist/src/execution-errors.js +48 -0
- package/dist/src/execution-limits.d.ts +6 -0
- package/dist/src/execution-limits.js +6 -0
- package/dist/src/executor.d.ts +3 -16
- package/dist/src/executor.js +31 -36
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +7 -2
- package/dist/src/input-template.d.ts +2 -2
- package/dist/src/input-template.js +4 -4
- package/dist/src/mcp-main.js +0 -0
- package/dist/src/mcp.js +23 -80
- package/dist/src/node-process-host.d.ts +13 -0
- package/dist/src/node-process-host.js +116 -0
- package/dist/src/node-process-runner.d.ts +23 -0
- package/dist/src/node-process-runner.js +140 -0
- package/dist/src/plan.js +125 -9
- package/dist/src/run-contract.d.ts +41 -1
- package/dist/src/run-journal.d.ts +2 -2
- package/dist/src/run-journal.js +56 -1
- package/dist/src/schema.d.ts +4 -4
- package/package.json +3 -3
- package/skills/automations/SKILL.md +69 -6
- package/supabase/migrations/20260904010000_typed_workflow_admission.sql +272 -0
package/dist/src/executor.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AutomationActionPort, AutomationRunExecutorOptions } from './execution-contract.js';
|
|
2
2
|
import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
|
|
3
3
|
export { automationPlan } from './plan.js';
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
actionId: string;
|
|
7
|
-
payload: Json;
|
|
8
|
-
idempotencyKey: string;
|
|
9
|
-
signal: AbortSignal;
|
|
10
|
-
}>): Promise<Json>;
|
|
11
|
-
}
|
|
12
|
-
export interface AutomationRunExecutorOptions {
|
|
13
|
-
readonly now?: () => Date;
|
|
14
|
-
readonly heartbeatMs?: number;
|
|
15
|
-
readonly maxAttempts?: number;
|
|
16
|
-
readonly retryDelayMs?: (attempt: number) => number;
|
|
17
|
-
}
|
|
4
|
+
export { transientFailure } from './execution-errors.js';
|
|
5
|
+
export type { AutomationActionPort, AutomationProcessExecution, AutomationProcessPort, AutomationRunExecutorOptions, } from './execution-contract.js';
|
|
18
6
|
export declare class AutomationRunExecutor {
|
|
19
7
|
#private;
|
|
20
8
|
private readonly runs;
|
|
@@ -22,4 +10,3 @@ export declare class AutomationRunExecutor {
|
|
|
22
10
|
constructor(runs: MachineRuns, actions: AutomationActionPort, options?: AutomationRunExecutorOptions);
|
|
23
11
|
execute(run: ClaimedAutomationRun): Promise<void>;
|
|
24
12
|
}
|
|
25
|
-
export declare function transientFailure(error: unknown): boolean;
|
package/dist/src/executor.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveInputTemplate } from './input-template.js';
|
|
2
2
|
import { automationPlan } from './plan.js';
|
|
3
|
+
import { errorOutput, safeError, transientFailure } from './execution-errors.js';
|
|
3
4
|
import { completed, emptyJournal, journalJson, putStep, runJournal, stepAttempt, } from './run-journal.js';
|
|
4
5
|
export { automationPlan } from './plan.js';
|
|
6
|
+
export { transientFailure } from './execution-errors.js';
|
|
5
7
|
export class AutomationRunExecutor {
|
|
6
8
|
runs;
|
|
7
9
|
actions;
|
|
@@ -9,6 +11,7 @@ export class AutomationRunExecutor {
|
|
|
9
11
|
#heartbeatMs;
|
|
10
12
|
#maxAttempts;
|
|
11
13
|
#retryDelayMs;
|
|
14
|
+
#processes;
|
|
12
15
|
constructor(runs, actions, options = {}) {
|
|
13
16
|
this.runs = runs;
|
|
14
17
|
this.actions = actions;
|
|
@@ -16,6 +19,7 @@ export class AutomationRunExecutor {
|
|
|
16
19
|
this.#heartbeatMs = integer(options.heartbeatMs ?? 20_000, 1, 'heartbeatMs');
|
|
17
20
|
this.#maxAttempts = integer(options.maxAttempts ?? 5, 1, 'maxAttempts');
|
|
18
21
|
this.#retryDelayMs = options.retryDelayMs ?? defaultRetryDelayMs;
|
|
22
|
+
this.#processes = options.processes;
|
|
19
23
|
}
|
|
20
24
|
async execute(run) {
|
|
21
25
|
const confirmed = await this.runs.update(run.id, {
|
|
@@ -60,6 +64,7 @@ export class AutomationRunExecutor {
|
|
|
60
64
|
finishedAt: this.#now().toISOString(),
|
|
61
65
|
error,
|
|
62
66
|
failureKind,
|
|
67
|
+
...errorOutput(action.error),
|
|
63
68
|
});
|
|
64
69
|
if (failureKind === 'transient' && run.attempts < this.#maxAttempts) {
|
|
65
70
|
await this.runs.update(run.id, {
|
|
@@ -137,12 +142,7 @@ export class AutomationRunExecutor {
|
|
|
137
142
|
const timer = setInterval(renew, this.#heartbeatMs);
|
|
138
143
|
timer.unref?.();
|
|
139
144
|
try {
|
|
140
|
-
const output = await this.
|
|
141
|
-
actionId: step.actionId,
|
|
142
|
-
payload: resolveActionInput(step.input, run.input),
|
|
143
|
-
idempotencyKey: `${run.id}:${step.id}`,
|
|
144
|
-
signal: controller.signal,
|
|
145
|
-
});
|
|
145
|
+
const output = await this.#executeStep(run, step, controller.signal);
|
|
146
146
|
return leaseLost ? { leaseLost: true } : { leaseLost: false, output };
|
|
147
147
|
}
|
|
148
148
|
catch (error) {
|
|
@@ -154,35 +154,30 @@ export class AutomationRunExecutor {
|
|
|
154
154
|
await heartbeat;
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
#executeStep(run, step, signal) {
|
|
158
|
+
const idempotencyKey = `${run.id}:${step.id}`;
|
|
159
|
+
if ('kind' in step) {
|
|
160
|
+
if (!this.#processes) {
|
|
161
|
+
throw Object.assign(new Error('Automation process execution is not installed on this machine'), {
|
|
162
|
+
code: 'process_execution_unavailable',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const { id: _id, stdin, ...execution } = step;
|
|
166
|
+
return this.#processes.execute({
|
|
167
|
+
...execution,
|
|
168
|
+
...(stdin === undefined ? {} : { stdin: processStdin(resolveInputTemplate(stdin, run.input)) }),
|
|
169
|
+
idempotencyKey,
|
|
170
|
+
signal,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return this.actions.call({
|
|
174
|
+
actionId: step.actionId,
|
|
175
|
+
payload: resolveInputTemplate(step.input, run.input),
|
|
176
|
+
idempotencyKey,
|
|
177
|
+
signal,
|
|
178
|
+
});
|
|
170
179
|
}
|
|
171
|
-
const message = safeError(error).toLowerCase();
|
|
172
|
-
return message.includes('fetch failed') || message.includes('network error');
|
|
173
180
|
}
|
|
174
|
-
const TRANSIENT_CODES = new Set([
|
|
175
|
-
'ECONNREFUSED',
|
|
176
|
-
'ECONNRESET',
|
|
177
|
-
'EAI_AGAIN',
|
|
178
|
-
'ENETDOWN',
|
|
179
|
-
'ENETUNREACH',
|
|
180
|
-
'ETIMEDOUT',
|
|
181
|
-
'UND_ERR_CONNECT_TIMEOUT',
|
|
182
|
-
'UND_ERR_HEADERS_TIMEOUT',
|
|
183
|
-
'UND_ERR_SOCKET',
|
|
184
|
-
'internal',
|
|
185
|
-
]);
|
|
186
181
|
function defaultRetryDelayMs(attempt) {
|
|
187
182
|
return Math.min(5 * 60_000, 2_000 * (2 ** Math.max(0, attempt - 1)));
|
|
188
183
|
}
|
|
@@ -191,6 +186,6 @@ function integer(value, minimum, name) {
|
|
|
191
186
|
throw new Error(`${name} must be an integer of at least ${minimum}`);
|
|
192
187
|
return value;
|
|
193
188
|
}
|
|
194
|
-
function
|
|
195
|
-
return
|
|
189
|
+
function processStdin(value) {
|
|
190
|
+
return typeof value === 'string' ? value : `${JSON.stringify(value)}\n`;
|
|
196
191
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,10 +2,14 @@ export { createAutomationClient, type AutomationAuthorization, type AutomationHe
|
|
|
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
|
-
export { runAutomationCli } from './cli.js';
|
|
5
|
+
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, type AutomationCliIo, type AutomationCliMainOptions, type ShellAutomationCommandOptions, } from './cli.js';
|
|
6
|
+
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, type AutomationCommandAnnotations, type AutomationCommandDefinition, type AutomationCommandName, type AutomationCommands, } from './command-surface.js';
|
|
7
|
+
export { automationFailure, type AutomationFailureEnvelope } from './command-result.js';
|
|
8
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
6
9
|
export { createAutomationMcpServer } from './mcp.js';
|
|
10
|
+
export { createAutomationToolSurface, type AutomationToolSurface, type AutomationView, type CreateAutomationDefinition, type UpdateAutomationDefinition, type WorkflowChange, } from './tool-surface.js';
|
|
7
11
|
export { SupabaseAutomationCrudRepository, type SupabaseRpcClient } from './supabase-crud.js';
|
|
8
|
-
export type { Automation, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationPlan, AutomationRunJournal, AutomationStepRun, AutomationStepStatus, AutomationRun, AutomationScope, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, Json, ListAutomations, ListRuns, Page, PageQuery, RunStatus, RunAutomationNow, ScheduleTrigger, Trigger, ToolActionStep, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow, } from './contract.js';
|
|
12
|
+
export type { Automation, AutomationActionPlan, AutomationCrud, AutomationCrudService as AutomationCrudServiceContract, AutomationPrincipal, AutomationPlan, AutomationRunJournal, AutomationScriptRuntime, AutomationStep, AutomationTypedPlan, AutomationStepRun, AutomationStepStatus, AutomationRun, AutomationScope, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, CodeScriptStep, DirectCommandStep, Json, ListAutomations, ListRuns, Page, PageQuery, RunStatus, RunAutomationNow, ScheduleTrigger, Trigger, ToolActionStep, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, WebhookTrigger, Workflow, } from './contract.js';
|
|
9
13
|
export { Automations, EventEndpointNotFoundError, EventRejectedError } from './automations.js';
|
|
10
14
|
export { createAutomationEventsApi, type AutomationEventReceipt, type AutomationEventsApi, type AutomationEventsApiConfig, } from './events-http.js';
|
|
11
15
|
export { nextCronAt, validateCron } from './schedule.js';
|
|
@@ -16,7 +20,8 @@ export { createMachineRuns } from './machine.js';
|
|
|
16
20
|
export type { AutomationMachinePrincipal, ClaimedAutomationRun, MachineRunRepository, MachineRuns, MachineRunUpdate, } from './machine.js';
|
|
17
21
|
export { createMachineRunsApi } from './machine-http.js';
|
|
18
22
|
export { createMachineRunsClient, type AutomationRequestHeaders } from './machine-client.js';
|
|
19
|
-
export { AutomationRunExecutor, automationPlan, type AutomationActionPort } from './executor.js';
|
|
23
|
+
export { AutomationRunExecutor, automationPlan, type AutomationActionPort, type AutomationProcessExecution, type AutomationProcessPort, type AutomationRunExecutorOptions, } from './executor.js';
|
|
24
|
+
export { createNodeAutomationProcessHost, type NodeAutomationProcessHostOptions, } from './node-process-host.js';
|
|
20
25
|
export { createAutomationMachineRunner, type AutomationMachineRunnerOptions, } from './runner.js';
|
|
21
26
|
export { compiledAutomationPlan } from './plan.js';
|
|
22
27
|
export type { SupabaseRpcClient as SupabaseStoreRpcClient } from './supabase-store.js';
|
package/dist/src/index.js
CHANGED
|
@@ -9,8 +9,12 @@ export { createAutomationClient } from './client.js';
|
|
|
9
9
|
export { AutomationCrudService } from './crud.js';
|
|
10
10
|
export { AutomationError, ConflictError, ForbiddenError, NotFoundError, ValidationError, } from './errors.js';
|
|
11
11
|
export { createAutomationApi } from './http.js';
|
|
12
|
-
export { runAutomationCli } from './cli.js';
|
|
12
|
+
export { automationCliHelp, createShellAutomationCommands, runAutomationCli, runAutomationCliMain, shellAutomationEndpoint, } from './cli.js';
|
|
13
|
+
export { automationCommandDefinition, automationCommandDefinitions, createAutomationCommands, } from './command-surface.js';
|
|
14
|
+
export { automationFailure } from './command-result.js';
|
|
15
|
+
export { MAX_CLI_RUNTIME_RESPONSE_BYTES, MAX_RUN_JOURNAL_BYTES, RUN_JOURNAL_OUTPUT_BUDGET_BYTES, } from './execution-limits.js';
|
|
13
16
|
export { createAutomationMcpServer } from './mcp.js';
|
|
17
|
+
export { createAutomationToolSurface, } from './tool-surface.js';
|
|
14
18
|
export { SupabaseAutomationCrudRepository } from './supabase-crud.js';
|
|
15
19
|
export { Automations, EventEndpointNotFoundError, EventRejectedError } from './automations.js';
|
|
16
20
|
export { createAutomationEventsApi, } from './events-http.js';
|
|
@@ -21,6 +25,7 @@ export { SupabaseMachineRunRepository } from './supabase-machine.js';
|
|
|
21
25
|
export { createMachineRuns } from './machine.js';
|
|
22
26
|
export { createMachineRunsApi } from './machine-http.js';
|
|
23
27
|
export { createMachineRunsClient } from './machine-client.js';
|
|
24
|
-
export { AutomationRunExecutor, automationPlan } from './executor.js';
|
|
28
|
+
export { AutomationRunExecutor, automationPlan, } from './executor.js';
|
|
29
|
+
export { createNodeAutomationProcessHost, } from './node-process-host.js';
|
|
25
30
|
export { createAutomationMachineRunner, } from './runner.js';
|
|
26
31
|
export { compiledAutomationPlan } from './plan.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Json } from './contract.js';
|
|
2
|
-
/** Resolve the one explicit dynamic value permitted in
|
|
3
|
-
export declare function
|
|
2
|
+
/** Resolve the one explicit dynamic value permitted in immutable step input. */
|
|
3
|
+
export declare function resolveInputTemplate(template: Json, runInput: Json): Json;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const RUN_INPUT_REFERENCE = '$runInput';
|
|
2
|
-
/** Resolve the one explicit dynamic value permitted in
|
|
3
|
-
export function
|
|
2
|
+
/** Resolve the one explicit dynamic value permitted in immutable step input. */
|
|
3
|
+
export function resolveInputTemplate(template, runInput) {
|
|
4
4
|
if (Array.isArray(template))
|
|
5
|
-
return template.map((value) =>
|
|
5
|
+
return template.map((value) => resolveInputTemplate(value, runInput));
|
|
6
6
|
if (!template || typeof template !== 'object')
|
|
7
7
|
return template;
|
|
8
8
|
const entries = Object.entries(template);
|
|
@@ -14,6 +14,6 @@ export function resolveActionInput(template, runInput) {
|
|
|
14
14
|
}
|
|
15
15
|
return Object.fromEntries(entries.map(([key, value]) => [
|
|
16
16
|
key,
|
|
17
|
-
|
|
17
|
+
resolveInputTemplate(value, runInput),
|
|
18
18
|
]));
|
|
19
19
|
}
|
package/dist/src/mcp-main.js
CHANGED
|
File without changes
|
package/dist/src/mcp.js
CHANGED
|
@@ -1,75 +1,34 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod/v3';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { automationCommandDefinitions, createAutomationCommands, } from './command-surface.js';
|
|
4
|
+
import { automationFailure } from './command-result.js';
|
|
5
5
|
import { createAutomationToolSurface } from './tool-surface.js';
|
|
6
6
|
export { createAutomationToolSurface };
|
|
7
|
-
const resultSchema = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
update: z.array(z.object({ id: identifierSchema, patch: updateWebhookTriggerSchema }).strict()).max(100).optional(),
|
|
16
|
-
delete: z.array(identifierSchema).max(100).optional(),
|
|
17
|
-
}).strict();
|
|
18
|
-
const workflowChangeSchema = z.union([
|
|
19
|
-
z.object({ create: createWorkflowSchema }).strict(),
|
|
20
|
-
z.object({ update: updateWorkflowSchema }).strict(),
|
|
21
|
-
z.object({ delete: z.literal(true) }).strict(),
|
|
22
|
-
]);
|
|
7
|
+
const resultSchema = {
|
|
8
|
+
result: z.unknown().optional(),
|
|
9
|
+
error: z.object({
|
|
10
|
+
code: z.string(),
|
|
11
|
+
message: z.string(),
|
|
12
|
+
status: z.number().optional(),
|
|
13
|
+
}).strict().optional(),
|
|
14
|
+
};
|
|
23
15
|
export function createAutomationMcpServer(sdk) {
|
|
24
16
|
const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
schedules: z.array(createScheduleTriggerSchema).max(100).optional(),
|
|
29
|
-
webhooks: z.array(createWebhookTriggerSchema).max(100).optional(),
|
|
30
|
-
workflow: createWorkflowSchema.optional(),
|
|
31
|
-
}, write(false), (input) => tools.create(input));
|
|
32
|
-
register(server, 'amalgm_automations_list', 'List automations', 'List the caller\'s automations with optional target, enabled-state, and pagination filters.', {
|
|
33
|
-
...listAutomationsSchema.shape,
|
|
34
|
-
}, read(), (query) => tools.list(query));
|
|
35
|
-
register(server, 'amalgm_automations_get', 'Get automation', 'Get one complete automation definition with its typed triggers and workflow. Recent run history is optional.', {
|
|
36
|
-
automation_id: identifierSchema,
|
|
37
|
-
include_runs: z.boolean().optional(),
|
|
38
|
-
run_query: listRunsSchema.optional(),
|
|
39
|
-
}, read(), ({ automation_id, include_runs, run_query }) => (tools.get(automation_id, include_runs || run_query ? (run_query ?? { limit: 20 }) : false)));
|
|
40
|
-
register(server, 'amalgm_automations_update', 'Update automation', 'Update an automation and grouped trigger or workflow changes in one task-level call. Resource changes are staged while the automation is disabled; a failed change leaves a safe disabled draft.', {
|
|
41
|
-
automation_id: identifierSchema,
|
|
42
|
-
patch: updateAutomationSchema.optional(),
|
|
43
|
-
schedules: scheduleChangesSchema.optional(),
|
|
44
|
-
webhooks: webhookChangesSchema.optional(),
|
|
45
|
-
workflow: workflowChangeSchema.optional(),
|
|
46
|
-
}, write(false), ({ automation_id, ...input }) => tools.update(automation_id, input));
|
|
47
|
-
register(server, 'amalgm_automations_delete', 'Delete automation', 'Delete an automation and its current triggers and workflow. Historical runs remain.', {
|
|
48
|
-
automation_id: identifierSchema,
|
|
49
|
-
}, destructive(), ({ automation_id }) => tools.delete(automation_id));
|
|
50
|
-
register(server, 'amalgm_automations_run_now', 'Run automation now', 'Durably admit one manual run using the automation\'s current enabled configuration and compiled workflow. The selected machine executes it through the normal pending-run queue; this call never executes the workflow inline.', {
|
|
51
|
-
automation_id: identifierSchema,
|
|
52
|
-
input: runAutomationNowSchema.shape.input,
|
|
53
|
-
idempotency_key: runAutomationNowSchema.shape.idempotencyKey,
|
|
54
|
-
}, write(false), ({ automation_id, input, idempotency_key }) => tools.runNow(automation_id, {
|
|
55
|
-
...(input === undefined ? {} : { input }),
|
|
56
|
-
...(idempotency_key === undefined ? {} : { idempotencyKey: idempotency_key }),
|
|
57
|
-
}));
|
|
17
|
+
const commands = createAutomationCommands(sdk);
|
|
18
|
+
for (const definition of automationCommandDefinitions)
|
|
19
|
+
register(server, commands, definition);
|
|
58
20
|
return server;
|
|
59
21
|
}
|
|
60
|
-
function register(server,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
title,
|
|
66
|
-
description,
|
|
67
|
-
inputSchema,
|
|
22
|
+
function register(server, commands, definition) {
|
|
23
|
+
server.registerTool(definition.mcpName, {
|
|
24
|
+
title: definition.title,
|
|
25
|
+
description: definition.description,
|
|
26
|
+
inputSchema: definition.inputShape,
|
|
68
27
|
outputSchema: resultSchema,
|
|
69
|
-
annotations,
|
|
28
|
+
annotations: definition.annotations,
|
|
70
29
|
}, async (input) => {
|
|
71
30
|
try {
|
|
72
|
-
const result = await
|
|
31
|
+
const result = await commands.execute(definition.name, input);
|
|
73
32
|
return toolSuccess(result);
|
|
74
33
|
}
|
|
75
34
|
catch (error) {
|
|
@@ -84,30 +43,14 @@ function toolSuccess(result) {
|
|
|
84
43
|
};
|
|
85
44
|
}
|
|
86
45
|
function toolFailure(error) {
|
|
87
|
-
const
|
|
88
|
-
const code
|
|
89
|
-
const status = error instanceof AutomationError ? error.status : undefined;
|
|
46
|
+
const failure = automationFailure(error);
|
|
47
|
+
const { code, message, status } = failure.error;
|
|
90
48
|
return {
|
|
91
49
|
isError: true,
|
|
92
50
|
content: [{
|
|
93
51
|
type: 'text',
|
|
94
52
|
text: `${code}: ${message}${status === undefined ? '' : ` (HTTP ${status})`}`,
|
|
95
53
|
}],
|
|
96
|
-
structuredContent: {
|
|
97
|
-
error: {
|
|
98
|
-
code,
|
|
99
|
-
message,
|
|
100
|
-
...(status === undefined ? {} : { status }),
|
|
101
|
-
},
|
|
102
|
-
},
|
|
54
|
+
structuredContent: { ...failure },
|
|
103
55
|
};
|
|
104
56
|
}
|
|
105
|
-
const read = () => ({
|
|
106
|
-
readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false,
|
|
107
|
-
});
|
|
108
|
-
const write = (idempotentHint) => ({
|
|
109
|
-
readOnlyHint: false, destructiveHint: false, idempotentHint, openWorldHint: false,
|
|
110
|
-
});
|
|
111
|
-
const destructive = () => ({
|
|
112
|
-
readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false,
|
|
113
|
-
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AutomationProcessPort } from './execution-contract.js';
|
|
2
|
+
export interface NodeAutomationProcessHostOptions {
|
|
3
|
+
/** Used only when a step omits cwd. Shell should supply the user's OS home. */
|
|
4
|
+
readonly defaultCwd?: string;
|
|
5
|
+
/** Machine-host environment inherited by child processes. */
|
|
6
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
7
|
+
readonly shellCommand?: string;
|
|
8
|
+
readonly nodeCommand?: string;
|
|
9
|
+
readonly pythonCommand?: string;
|
|
10
|
+
readonly terminateGraceMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Node machine adapter for the SDK's process capability. */
|
|
13
|
+
export declare function createNodeAutomationProcessHost(options?: NodeAutomationProcessHostOptions): AutomationProcessPort;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { captureProcess, processError, processOutput, stringEnvironment, } from './node-process-runner.js';
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 30 * 60_000;
|
|
6
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
7
|
+
const DEFAULT_TERMINATE_GRACE_MS = 1_000;
|
|
8
|
+
/** Node machine adapter for the SDK's process capability. */
|
|
9
|
+
export function createNodeAutomationProcessHost(options = {}) {
|
|
10
|
+
const defaultCwd = options.defaultCwd ?? process.cwd();
|
|
11
|
+
if (!isAbsolute(defaultCwd))
|
|
12
|
+
throw new Error('Automation process defaultCwd must be absolute');
|
|
13
|
+
const environment = stringEnvironment(options.environment ?? process.env);
|
|
14
|
+
const terminateGraceMs = positiveInteger(options.terminateGraceMs ?? DEFAULT_TERMINATE_GRACE_MS, 'terminateGraceMs');
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
execute: (input) => executeProcess(input, {
|
|
17
|
+
defaultCwd,
|
|
18
|
+
environment,
|
|
19
|
+
shellCommand: options.shellCommand,
|
|
20
|
+
nodeCommand: options.nodeCommand,
|
|
21
|
+
pythonCommand: options.pythonCommand,
|
|
22
|
+
terminateGraceMs,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function executeProcess(input, options) {
|
|
27
|
+
input.signal.throwIfAborted();
|
|
28
|
+
const prepared = await prepareProcess(input, options);
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
result = await captureProcess(prepared.invocation, {
|
|
32
|
+
...input,
|
|
33
|
+
cwd: input.cwd ?? options.defaultCwd,
|
|
34
|
+
env: {
|
|
35
|
+
...options.environment,
|
|
36
|
+
...input.env,
|
|
37
|
+
AMALGM_AUTOMATION_IDEMPOTENCY_KEY: input.idempotencyKey,
|
|
38
|
+
},
|
|
39
|
+
}, {
|
|
40
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
41
|
+
maximumBytes: DEFAULT_MAX_OUTPUT_BYTES,
|
|
42
|
+
terminateGraceMs: options.terminateGraceMs,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await prepared.cleanup();
|
|
47
|
+
}
|
|
48
|
+
const output = processOutput(result);
|
|
49
|
+
if (result.aborted)
|
|
50
|
+
throw processError('process_aborted', 'Automation process was cancelled', output);
|
|
51
|
+
if (result.timedOut) {
|
|
52
|
+
throw processError('process_timeout', `Automation process timed out after ${input.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, output);
|
|
53
|
+
}
|
|
54
|
+
if (result.code !== 0) {
|
|
55
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
56
|
+
throw processError('process_exit', `Automation process exited with ${result.code ?? result.signal ?? 'unknown'}${detail ? `: ${oneLine(detail)}` : ''}`, output);
|
|
57
|
+
}
|
|
58
|
+
return output;
|
|
59
|
+
}
|
|
60
|
+
async function prepareProcess(input, options) {
|
|
61
|
+
if (input.kind === 'command') {
|
|
62
|
+
return {
|
|
63
|
+
invocation: { command: input.command, args: [...(input.args ?? [])] },
|
|
64
|
+
cleanup: async () => undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const directory = await mkdtemp(join(tmpdir(), 'amalgm-automation-'));
|
|
68
|
+
const extension = input.runtime === 'node' ? 'mjs' : input.runtime === 'python' ? 'py' : 'sh';
|
|
69
|
+
const script = join(directory, `step.${extension}`);
|
|
70
|
+
try {
|
|
71
|
+
await writeFile(script, input.source, { encoding: 'utf8', mode: 0o600 });
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
await rm(directory, { recursive: true, force: true });
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const cleanup = () => rm(directory, { recursive: true, force: true });
|
|
78
|
+
if (input.runtime === 'node') {
|
|
79
|
+
return {
|
|
80
|
+
invocation: {
|
|
81
|
+
command: options.nodeCommand ?? process.execPath,
|
|
82
|
+
args: [script, ...(input.args ?? [])],
|
|
83
|
+
},
|
|
84
|
+
cleanup,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (input.runtime === 'python') {
|
|
88
|
+
return {
|
|
89
|
+
invocation: {
|
|
90
|
+
command: options.pythonCommand ?? 'python3',
|
|
91
|
+
args: [script, ...(input.args ?? [])],
|
|
92
|
+
},
|
|
93
|
+
cleanup,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
invocation: process.platform === 'win32'
|
|
98
|
+
? {
|
|
99
|
+
command: options.shellCommand ?? 'powershell.exe',
|
|
100
|
+
args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-File', script, ...(input.args ?? [])],
|
|
101
|
+
}
|
|
102
|
+
: {
|
|
103
|
+
command: options.shellCommand ?? '/bin/sh',
|
|
104
|
+
args: [script, ...(input.args ?? [])],
|
|
105
|
+
},
|
|
106
|
+
cleanup,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function positiveInteger(value, label) {
|
|
110
|
+
if (!Number.isInteger(value) || value < 1)
|
|
111
|
+
throw new Error(`${label} must be a positive integer`);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function oneLine(value) {
|
|
115
|
+
return value.replace(/[\r\n]+/g, ' ').slice(0, 500);
|
|
116
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Json } from './contract.js';
|
|
2
|
+
import type { AutomationProcessExecution } from './execution-contract.js';
|
|
3
|
+
export interface ProcessInvocation {
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly args: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CapturedProcess {
|
|
8
|
+
readonly code: number | null;
|
|
9
|
+
readonly signal: NodeJS.Signals | null;
|
|
10
|
+
readonly stdout: string;
|
|
11
|
+
readonly stderr: string;
|
|
12
|
+
readonly truncated: boolean;
|
|
13
|
+
readonly timedOut: boolean;
|
|
14
|
+
readonly aborted: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function captureProcess(invocation: ProcessInvocation, input: AutomationProcessExecution, defaults: Readonly<{
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
maximumBytes: number;
|
|
19
|
+
terminateGraceMs: number;
|
|
20
|
+
}>): Promise<CapturedProcess>;
|
|
21
|
+
export declare function processOutput(result: CapturedProcess): Json;
|
|
22
|
+
export declare function processError(code: string, message: string, output: Json): Error;
|
|
23
|
+
export declare function stringEnvironment(source: NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export function captureProcess(invocation, input, defaults) {
|
|
3
|
+
const maximumBytes = input.maxOutputBytes ?? defaults.maximumBytes;
|
|
4
|
+
const timeoutMs = input.timeoutMs ?? defaults.timeoutMs;
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let child;
|
|
7
|
+
try {
|
|
8
|
+
child = spawn(invocation.command, invocation.args, {
|
|
9
|
+
cwd: input.cwd ?? process.cwd(),
|
|
10
|
+
env: input.env,
|
|
11
|
+
detached: process.platform !== 'win32',
|
|
12
|
+
shell: false,
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
reject(spawnError(error));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const stdout = [];
|
|
22
|
+
const stderr = [];
|
|
23
|
+
let capturedBytes = 0;
|
|
24
|
+
let truncated = false;
|
|
25
|
+
let timedOut = false;
|
|
26
|
+
let aborted = false;
|
|
27
|
+
let terminating = false;
|
|
28
|
+
let escalation;
|
|
29
|
+
let settled = false;
|
|
30
|
+
const capture = (target) => (chunk) => {
|
|
31
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
32
|
+
const remaining = maximumBytes - capturedBytes;
|
|
33
|
+
if (remaining > 0) {
|
|
34
|
+
const kept = bytes.subarray(0, remaining);
|
|
35
|
+
target.push(kept);
|
|
36
|
+
capturedBytes += kept.byteLength;
|
|
37
|
+
}
|
|
38
|
+
if (bytes.byteLength > remaining)
|
|
39
|
+
truncated = true;
|
|
40
|
+
};
|
|
41
|
+
child.stdout?.on('data', capture(stdout));
|
|
42
|
+
child.stderr?.on('data', capture(stderr));
|
|
43
|
+
child.stdin?.on('error', () => undefined);
|
|
44
|
+
const terminate = () => {
|
|
45
|
+
if (terminating)
|
|
46
|
+
return;
|
|
47
|
+
terminating = true;
|
|
48
|
+
stopProcess(child, 'SIGTERM');
|
|
49
|
+
escalation = setTimeout(() => stopProcess(child, 'SIGKILL'), defaults.terminateGraceMs);
|
|
50
|
+
escalation.unref?.();
|
|
51
|
+
};
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
aborted = true;
|
|
54
|
+
terminate();
|
|
55
|
+
};
|
|
56
|
+
input.signal.addEventListener('abort', onAbort, { once: true });
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
timedOut = true;
|
|
59
|
+
terminate();
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
timeout.unref?.();
|
|
62
|
+
child.once('error', (error) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
cleanup();
|
|
67
|
+
reject(spawnError(error));
|
|
68
|
+
});
|
|
69
|
+
child.once('close', (code, signal) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
cleanup();
|
|
74
|
+
resolve({
|
|
75
|
+
code,
|
|
76
|
+
signal,
|
|
77
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
78
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
79
|
+
truncated,
|
|
80
|
+
timedOut,
|
|
81
|
+
aborted,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
child.stdin?.end(input.stdin);
|
|
85
|
+
if (input.signal.aborted)
|
|
86
|
+
onAbort();
|
|
87
|
+
function cleanup() {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (escalation)
|
|
90
|
+
clearTimeout(escalation);
|
|
91
|
+
input.signal.removeEventListener('abort', onAbort);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function processOutput(result) {
|
|
96
|
+
return {
|
|
97
|
+
exitCode: result.code,
|
|
98
|
+
signal: result.signal,
|
|
99
|
+
stdout: result.stdout,
|
|
100
|
+
stderr: result.stderr,
|
|
101
|
+
truncated: result.truncated,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function processError(code, message, output) {
|
|
105
|
+
return Object.assign(new Error(message), { code, output });
|
|
106
|
+
}
|
|
107
|
+
export function stringEnvironment(source) {
|
|
108
|
+
return Object.fromEntries(Object.entries(source).filter((entry) => (typeof entry[1] === 'string' && !PRIVATE_HOST_ENV.has(entry[0]))));
|
|
109
|
+
}
|
|
110
|
+
const PRIVATE_HOST_ENV = new Set([
|
|
111
|
+
'AMALGM_AUTOMATIONS_AUTHORIZATION',
|
|
112
|
+
'AMALGM_COMPUTER_AUTH_TOKEN',
|
|
113
|
+
'AMALGM_RUNTIME_TOKEN',
|
|
114
|
+
'AMALGM_TUNNEL_TOKEN',
|
|
115
|
+
]);
|
|
116
|
+
function spawnError(error) {
|
|
117
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
118
|
+
return Object.assign(new Error(`Automation process could not start: ${source.message}`), {
|
|
119
|
+
code: source.code ?? 'process_spawn_failed',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function stopProcess(child, signal) {
|
|
123
|
+
if (child.exitCode !== null)
|
|
124
|
+
return;
|
|
125
|
+
if (process.platform !== 'win32' && child.pid) {
|
|
126
|
+
try {
|
|
127
|
+
process.kill(-child.pid, signal);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// The child may not have established its process group yet.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
child.kill(signal);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// The close event remains authoritative.
|
|
139
|
+
}
|
|
140
|
+
}
|