@amalgm/automations 0.2.3 → 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
@@ -24,9 +24,11 @@
24
24
  9. Supabase is authoritative for automations, triggers, workflow source, and
25
25
  permanent run history. A definition edit or deletion never rewrites prior
26
26
  runs.
27
- 10. A webhook URL is a rotatable bearer capability for exactly one trigger. Its
28
- random locator is not sufficient without the hosted service's HMAC, and
29
- caller input never supplies an owner or target.
27
+ 10. A webhook URL is a rotatable bearer capability for exactly one trigger.
28
+ Possession plus any configured provider proof admits that trigger; source
29
+ and event labels never form a second routing gate. Its random locator is not
30
+ sufficient without the hosted service's HMAC, and caller input never
31
+ supplies an owner or target.
30
32
  11. Optional provider signing secrets are persisted but write-only: normal
31
33
  reads reveal only that a secret is configured.
32
34
  12. Run history is read-only in the control plane and always scoped to its
@@ -76,3 +78,8 @@
76
78
  30. Run Now is triggerless manual admission. It atomically snapshots the
77
79
  current enabled automation and compiled workflow into the pending ledger;
78
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,
@@ -47,9 +49,10 @@ trigger owns one opaque URL under `automations.amalgm.ai`; the URL resolves the
47
49
  trigger, owner, and target, so a caller never supplies routing identity. The
48
50
  adapter bounds and parses the body, optionally verifies the provider's signing
49
51
  secret, deduplicates explicit provider delivery ids, and returns only after the
50
- run has been stored in Supabase. The signed URL can be rotated without changing
51
- the automation, and its usable bearer token cannot be reconstructed from the
52
- database alone.
52
+ run has been stored in Supabase. Provider source and event labels describe the
53
+ occurrence; they are not a second routing gate after the URL has selected the
54
+ trigger. The signed URL can be rotated without changing the automation, and its
55
+ usable bearer token cannot be reconstructed from the database alone.
53
56
 
54
57
  Amalgm supplies a resolved authenticated principal from its user session or
55
58
  HMAC-refresh flow; future API keys resolve to the same principal capability.
package/README.md CHANGED
@@ -99,6 +99,8 @@ Public webhook admission uses the URL returned on each webhook trigger:
99
99
  POST https://automations.amalgm.ai/e/<opaque-capability>
100
100
  ```
101
101
 
102
+ That URL selects the exact trigger. Provider source and event labels are
103
+ recorded as occurrence metadata, not used as another routing condition.
102
104
  `Idempotency-Key` and common provider delivery-id headers deduplicate retries
103
105
  against the same trigger. The caller never supplies a user or target id.
104
106
 
@@ -1,5 +1,5 @@
1
1
  import { nextCronAt } from './schedule.js';
2
- import { eventReferences, matchesEvent, verifyEventSecret } from './webhook.js';
2
+ import { eventReferences, verifyEventSecret } from './webhook.js';
3
3
  const noop = () => { };
4
4
  export class EventRejectedError extends Error {
5
5
  constructor() {
@@ -33,14 +33,7 @@ export class Automations {
33
33
  const references = input.source && input.event
34
34
  ? { primary: { source: input.source, event: input.event } }
35
35
  : eventReferences(input.headers, input.payload);
36
- let reference = references.primary;
37
- let matches = matchesEvent(trigger, reference.source, reference.event);
38
- if (!matches && references.fallback) {
39
- reference = references.fallback;
40
- matches = matchesEvent(trigger, reference.source, reference.event);
41
- }
42
- if (!matches)
43
- return { run: null, ...reference };
36
+ const reference = references.primary;
44
37
  const now = input.now || new Date();
45
38
  const runInput = { kind: 'event', ...reference, payload: input.payload };
46
39
  const run = await this.store.enqueueEvent(trigger, runInput, now, input.deliveryKey);
@@ -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.3",
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.