@amalgm/automations 0.2.4 → 0.2.5

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 CHANGED
@@ -78,3 +78,8 @@
78
78
  30. Run Now is triggerless manual admission. It atomically snapshots the
79
79
  current enabled automation and compiled workflow into the pending ledger;
80
80
  it never executes inline or changes a trigger's schedule state.
81
+ 31. A step input is an immutable JSON template. The exact singleton
82
+ `{ "$runInput": true }` explicitly resolves to the run's immutable input;
83
+ no trigger payload is ambient and no action rediscovers it.
84
+ 32. An adapter preserves the owning service's error code and status. Transport
85
+ errors may add presentation, but never relabel authorization as validation.
package/PURPOSE.md CHANGED
@@ -27,13 +27,15 @@ The product has two composable halves over that one state:
27
27
  needs to know whether a machine is online: an unclaimed run is the complete
28
28
  offline queue.
29
29
 
30
- One run is one immutable plan plus one durable ordered step journal. The
30
+ One run is one immutable plan, one immutable trigger input, plus one durable ordered step journal. The
31
31
  machine records a step as running before invoking its action and records its
32
32
  output before advancing. Reconnect resumes at the first step that is not
33
33
  already complete. A transient network failure releases the same run back to
34
34
  the queue with a bounded future retry time; a configuration, authorization, or
35
35
  action error fails it. Stable per-step idempotency keys make an uncertain
36
- network acknowledgement safe to repeat.
36
+ network acknowledgement safe to repeat. A workflow can place that run input in
37
+ an action payload only through the explicit `{ "$runInput": true }` template
38
+ value, so static configuration and occurrence data never blur together.
37
39
 
38
40
  Automations is its own hosted service and Fly machine. Its API and scheduler
39
41
  share the same SDK and Supabase authority; neither is composed into, proxied by,
@@ -61,9 +61,10 @@ function createRequester(baseUrl, authorization, additionalHeaders, fetch, reque
61
61
  if (response.status === 404 && nullable)
62
62
  return null;
63
63
  if (!response.ok) {
64
- throw new AutomationError(payload.code === 'conflict' || payload.code === 'forbidden' || payload.code === 'internal' || payload.code === 'not_found'
65
- ? payload.code
66
- : response.status >= 500 ? 'internal' : 'validation', payload.error || `Automations API returned ${response.status}`);
64
+ const code = payload.code?.trim() || (response.status === 403 ? 'forbidden'
65
+ : response.status >= 500 ? 'internal'
66
+ : 'validation');
67
+ throw new AutomationError(code, payload.error || `Automations API returned ${response.status}`, response.status);
67
68
  }
68
69
  return payload;
69
70
  };
@@ -1,7 +1,8 @@
1
1
  export type AutomationErrorCode = 'conflict' | 'forbidden' | 'internal' | 'not_found' | 'validation';
2
2
  export declare class AutomationError extends Error {
3
- readonly code: AutomationErrorCode;
4
- constructor(code: AutomationErrorCode, message: string);
3
+ readonly code: AutomationErrorCode | string;
4
+ readonly status?: number | undefined;
5
+ constructor(code: AutomationErrorCode | string, message: string, status?: number | undefined);
5
6
  }
6
7
  export declare class ValidationError extends AutomationError {
7
8
  constructor(message: string);
@@ -1,8 +1,10 @@
1
1
  export class AutomationError extends Error {
2
2
  code;
3
- constructor(code, message) {
3
+ status;
4
+ constructor(code, message, status) {
4
5
  super(message);
5
6
  this.code = code;
7
+ this.status = status;
6
8
  this.name = 'AutomationError';
7
9
  }
8
10
  }
@@ -1,3 +1,4 @@
1
+ import { resolveActionInput } from './input-template.js';
1
2
  import { automationPlan } from './plan.js';
2
3
  import { completed, emptyJournal, journalJson, putStep, runJournal, stepAttempt, } from './run-journal.js';
3
4
  export { automationPlan } from './plan.js';
@@ -138,7 +139,7 @@ export class AutomationRunExecutor {
138
139
  try {
139
140
  const output = await this.actions.call({
140
141
  actionId: step.actionId,
141
- payload: step.input,
142
+ payload: resolveActionInput(step.input, run.input),
142
143
  idempotencyKey: `${run.id}:${step.id}`,
143
144
  signal: controller.signal,
144
145
  });
@@ -0,0 +1,3 @@
1
+ import type { Json } from './contract.js';
2
+ /** Resolve the one explicit dynamic value permitted in an immutable action input. */
3
+ export declare function resolveActionInput(template: Json, runInput: Json): Json;
@@ -0,0 +1,19 @@
1
+ const RUN_INPUT_REFERENCE = '$runInput';
2
+ /** Resolve the one explicit dynamic value permitted in an immutable action input. */
3
+ export function resolveActionInput(template, runInput) {
4
+ if (Array.isArray(template))
5
+ return template.map((value) => resolveActionInput(value, runInput));
6
+ if (!template || typeof template !== 'object')
7
+ return template;
8
+ const entries = Object.entries(template);
9
+ if (entries.length === 1 && entries[0]?.[0] === RUN_INPUT_REFERENCE) {
10
+ if (entries[0][1] !== true) {
11
+ throw new Error(`Workflow ${RUN_INPUT_REFERENCE} reference must equal true`);
12
+ }
13
+ return runInput;
14
+ }
15
+ return Object.fromEntries(entries.map(([key, value]) => [
16
+ key,
17
+ resolveActionInput(value, runInput),
18
+ ]));
19
+ }
@@ -17,7 +17,9 @@ export function createMachineRunsClient(options) {
17
17
  });
18
18
  const payload = await response.json().catch(() => ({}));
19
19
  if (!response.ok)
20
- throw new AutomationError(response.status === 403 ? 'forbidden' : response.status >= 500 ? 'internal' : 'validation', payload.error ?? `Automations API returned ${response.status}`);
20
+ throw new AutomationError(payload.code?.trim() || (response.status === 403 ? 'forbidden'
21
+ : response.status >= 500 ? 'internal'
22
+ : 'validation'), payload.error ?? `Automations API returned ${response.status}`, response.status);
21
23
  return payload;
22
24
  };
23
25
  return Object.freeze({
package/dist/src/mcp.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod/v3';
3
+ import { AutomationError } from './errors.js';
3
4
  import { createAutomationSchema, createScheduleTriggerSchema, createWebhookTriggerSchema, createWorkflowSchema, identifierSchema, listAutomationsSchema, listRunsSchema, runAutomationNowSchema, updateAutomationSchema, updateScheduleTriggerSchema, updateWebhookTriggerSchema, updateWorkflowSchema, } from './schema.js';
4
5
  import { createAutomationToolSurface } from './tool-surface.js';
5
6
  export { createAutomationToolSurface };
@@ -83,9 +84,22 @@ function toolSuccess(result) {
83
84
  };
84
85
  }
85
86
  function toolFailure(error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ const code = error instanceof AutomationError ? error.code : 'internal';
89
+ const status = error instanceof AutomationError ? error.status : undefined;
86
90
  return {
87
91
  isError: true,
88
- content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
92
+ content: [{
93
+ type: 'text',
94
+ text: `${code}: ${message}${status === undefined ? '' : ` (HTTP ${status})`}`,
95
+ }],
96
+ structuredContent: {
97
+ error: {
98
+ code,
99
+ message,
100
+ ...(status === undefined ? {} : { status }),
101
+ },
102
+ },
89
103
  };
90
104
  }
91
105
  const read = () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -54,6 +54,12 @@ user asks for the outcome.
54
54
 
55
55
  Action discovery belongs to the Tools product, not Automations.
56
56
 
57
+ For webhook-driven work, pass the admitted provider payload into an action by
58
+ placing the exact value `{ "$runInput": true }` at the desired point in that
59
+ step's `input`. For example, an agent-review action can use a static `message`
60
+ and set `context` to `{ "$runInput": true }`. Never paste an example delivery
61
+ into the workflow: every admitted run must use its own durable input.
62
+
57
63
  `pending` means a run is durable and waiting for its selected machine. `sent`
58
64
  or `running` means that machine holds a lease. `completed` and `failed` are
59
65
  terminal. Do not infer delivery from the schedule alone; inspect runs when the
@@ -63,3 +69,10 @@ Each webhook trigger returns one copyable `webhookUrl`; use that URL as the
63
69
  provider destination. A separate provider signing secret is optional and
64
70
  write-only. Never echo, log, or place either credential in a workflow. Rotate
65
71
  the URL when its bearer capability may have been exposed.
72
+
73
+ For GitHub, register the returned URL as a repository webhook with JSON content
74
+ type and select only the requested events (normally `push`). If a provider
75
+ secret was configured on the trigger, use the same value as GitHub's webhook
76
+ secret. Creating the Amalgm trigger does not silently mutate GitHub; finish the
77
+ provider registration in the authenticated GitHub surface available to the
78
+ agent, then report both sides as configured.