@amalgm/automations 0.4.0 → 0.4.2

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
@@ -64,6 +64,9 @@
64
64
  aliases for one another.
65
65
  23. Automations is a standalone hosted service. Gateway owns none of its API,
66
66
  scheduling, claim, execution, or persistence path.
67
+ Its host validates Core's complete environment binding before opening its
68
+ database, issuer, public API or webhook authority; local has no managed
69
+ service fallback.
67
70
  24. The durable run ledger is the offline queue. The platform never keeps a
68
71
  second online-machine delivery buffer and never drops an unclaimed run.
69
72
  25. A run advances through its immutable plan in order. Every step transition
@@ -91,7 +94,8 @@
91
94
  33. MCP and CLI project one task-level command catalog: create, list, get,
92
95
  update, delete, and run-now. Command names, input schemas, and invocation
93
96
  behavior are defined once and neither adapter invents resource-level
94
- lifecycle operations.
97
+ lifecycle operations. Unknown configuration fields are rejected, never
98
+ discarded by an adapter; declared JSON payloads remain open data.
95
99
  34. A CLI running for a signed-in user reaches Automations through Shell's
96
100
  authenticated loopback MCP route. It may possess the local runtime admission
97
101
  token, but never a durable machine credential, device key, cached access
@@ -140,3 +144,6 @@
140
144
  for that user and machine; a notification is never proof of execution.
141
145
  48. Losing upstream notification coverage closes downstream streams, and every
142
146
  reconnect checks retained work before becoming idle again.
147
+ 49. A complete definition validates all supplied configuration before its
148
+ first write; a later service failure preserves an identifiable disabled
149
+ draft and the original failure code.
package/PURPOSE.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
+ Managed releases bind to one exact published Core version and build from the
6
+ locked dependency graph on the supported Node toolchain.
7
+
5
8
  Automations is the sole owner of Amalgm automation behavior and data. It lets an
6
9
  authenticated user create, inspect, change, delete, and manually run automation
7
10
  configuration — an automation belongs to one user and one target, may own any
@@ -10,6 +13,11 @@ have each trigger occurrence run on the selected existing Amalgm machine.
10
13
  Supabase holds everything except execution: the automation, its triggers, the
11
14
  complete workflow, and permanent run history.
12
15
 
16
+ Each hosted environment reaches only its declared UI issuer, database and
17
+ webhook authority. Main and preview initially share the database by owner
18
+ direction; their public API and issuer identities remain distinct. Durable
19
+ claims remain the single authority for work even when hosts share data.
20
+
13
21
  The product has two composable halves over that one state:
14
22
 
15
23
  - **The configuration control plane** — one ergonomic, auth-bound SDK contract
@@ -36,6 +44,12 @@ the run ledger. Reconnecting re-establishes notification coverage before checkin
36
44
  the ledger. Connection health and active execution leases have bounded timers;
37
45
  idle machines have no periodic work-claim timer.
38
46
 
47
+ Complete create and update requests validate their supplied schedules, workflow
48
+ plans, and field names before writing configuration. Unknown fields cannot
49
+ silently become a different request. If a service fails after staging begins,
50
+ the disabled draft remains inspectable and the error keeps that service's code,
51
+ so the agent can repair the same definition rather than repeat its creation.
52
+
39
53
  The agent CLI is the command-line projection of the same task-level command
40
54
  surface as MCP: create, list, get, update, delete, and run-now. Each command
41
55
  accepts the same JSON object as its corresponding MCP tool and returns the same
@@ -56,6 +70,12 @@ the user reach aayush@amalgm.ai with bounded, redacted diagnostic evidence when
56
70
  the failing boundary cannot be repaired. The packaged skill is the source for
57
71
  installed copies and public setup/support guidance.
58
72
 
73
+ The standalone service publishes a public skill discovery index and an
74
+ integrity-checked archive built from that same packaged skill. Installation
75
+ needs no private repository access, and the archive includes its referenced
76
+ instructions. Public distribution owns no copy of authentication or workflow
77
+ behavior.
78
+
59
79
  The CLI is global configuration control, not directory-local state. The agent
60
80
  or person creating an automation may invoke it from any directory; the selected
61
81
  machine and the persisted workflow decide where effects occur later. A process
package/README.md CHANGED
@@ -98,15 +98,32 @@ one JSON line on stderr shaped as
98
98
  `status`, and exits nonzero. Use `--stdin` for inputs containing credentials;
99
99
  `--input` and `--file` are also supported.
100
100
 
101
- When Shell invokes the adapter, it supplies `AMALGM_MCP_URL` and
102
- `AMALGM_RUNTIME_TOKEN` from its running user runtime. The CLI sends that
103
- runtime token only to the loopback `/mcp/automations` route. Shell retains the
101
+ Shell supplies the selected running user's loopback connection directly to
102
+ the SDK adapter. The CLI sends its runtime token only to the loopback
103
+ `/mcp/automations` route. Shell retains the
104
104
  durable machine credential and device key and creates the short-lived access
105
105
  token and fresh DPoP proof for the hosted request. For transition
106
106
  compatibility, the standalone entry point still accepts the prior paired
107
107
  `AMALGM_AUTOMATIONS_API_URL` and `AMALGM_AUTOMATIONS_AUTHORIZATION`
108
108
  environment variables.
109
109
 
110
+ For public local MCP clients, Shell 0.1.176+ provides
111
+ `amalgm automations mcp --user person@example.com`. It exposes the same six
112
+ tools over stdio and discovers the current runtime connection per call.
113
+ `amalgm status --all` lists local account registrations. No pasted token or
114
+ private repository access is part of either flow; the standalone
115
+ `amalgm-automations-mcp` entry point remains for custom authenticated hosts.
116
+
117
+ Install the complete portable skill with Node.js 22.20+:
118
+
119
+ ```bash
120
+ npx skills add https://automations.amalgm.ai --skill amalgm-automations
121
+ ```
122
+
123
+ The host serves standard discovery at `/.well-known/agent-skills/index.json`
124
+ and a checksum-addressed archive built from `skills/amalgm-automations`.
125
+ The archive includes its references and agent metadata and requires no login.
126
+
110
127
  The hosted service accepts verified Supabase user sessions for browser control
111
128
  and Core-issued `amalgm-automations` DPoP grants for Shell. It does not run in
112
129
  or depend on Amalgm Gateway.
@@ -1,12 +1,16 @@
1
+ import { isLoopbackEndpoint, resolveRuntimeEndpoint, validateRuntimeEndpointEnvironment } from '@amalgm/core/binding';
1
2
  export function automationsHostConfig(env = process.env) {
3
+ const publicOrigin = required(env.AMALGAM_PUBLIC_ORIGIN, 'AMALGAM_PUBLIC_ORIGIN');
4
+ const isolatedOrigin = isLoopbackEndpoint(publicOrigin) ? publicOrigin : undefined;
5
+ const binding = validateRuntimeEndpointEnvironment(env, isolatedOrigin);
2
6
  return Object.freeze({
3
7
  port: integer(env.PORT, 8080, 1, 65_535),
4
- publicOrigin: url(env.AMALGAM_PUBLIC_ORIGIN, 'AMALGAM_PUBLIC_ORIGIN'),
5
- webhookOrigin: url(env.AUTOMATIONS_WEBHOOK_ORIGIN, 'AUTOMATIONS_WEBHOOK_ORIGIN'),
8
+ publicOrigin: resolveRuntimeEndpoint(binding, 'automations', publicOrigin, isolatedOrigin),
9
+ webhookOrigin: resolveRuntimeEndpoint(binding, 'automations', required(env.AUTOMATIONS_WEBHOOK_ORIGIN, 'AUTOMATIONS_WEBHOOK_ORIGIN'), isolatedOrigin),
6
10
  webhookTokenKey: secret(env.AUTOMATIONS_WEBHOOK_TOKEN_KEY, 'AUTOMATIONS_WEBHOOK_TOKEN_KEY'),
7
- supabaseUrl: url(env.SUPABASE_URL ?? env.NEXT_PUBLIC_SUPABASE_URL, 'SUPABASE_URL'),
11
+ supabaseUrl: resolveRuntimeEndpoint(binding, 'supabase', required(env.SUPABASE_URL ?? env.NEXT_PUBLIC_SUPABASE_URL, 'SUPABASE_URL'), isolatedOrigin),
8
12
  supabaseServiceRoleKey: required(env.SUPABASE_SERVICE_ROLE_KEY, 'SUPABASE_SERVICE_ROLE_KEY'),
9
- authorizationIssuer: url(env.AMALGM_AUTHORIZATION_ISSUER, 'AMALGM_AUTHORIZATION_ISSUER'),
13
+ authorizationIssuer: resolveRuntimeEndpoint(binding, 'app', required(env.AMALGM_AUTHORIZATION_ISSUER, 'AMALGM_AUTHORIZATION_ISSUER'), isolatedOrigin),
10
14
  schedulerIntervalMs: integer(env.AUTOMATIONS_SCHEDULER_INTERVAL_MS, 1_000, 250, 60_000),
11
15
  });
12
16
  }
@@ -21,14 +25,6 @@ function secret(value, name) {
21
25
  throw new Error(`${name} must contain at least 32 bytes`);
22
26
  return result;
23
27
  }
24
- function url(value, name) {
25
- const parsed = new URL(required(value, name));
26
- const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
27
- if (parsed.protocol !== 'https:' && !(local && parsed.protocol === 'http:')) {
28
- throw new Error(`${name} must use HTTPS`);
29
- }
30
- return parsed.toString().replace(/\/$/, '');
31
- }
32
28
  function integer(value, fallback, minimum, maximum) {
33
29
  const parsed = value === undefined ? fallback : Number(value);
34
30
  if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
package/dist/host/main.js CHANGED
@@ -14,6 +14,7 @@ import { automationsHostConfig } from './config.js';
14
14
  import { createAutomationsHost } from './server.js';
15
15
  import { createMachineRunNotifications } from '../src/machine-notifications.js';
16
16
  import { subscribeToRunChanges } from './notifications.js';
17
+ import { createPublicSkillApi } from './skill.js';
17
18
  const config = automationsHostConfig();
18
19
  const supabase = createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
19
20
  auth: { persistSession: false, autoRefreshToken: false },
@@ -48,6 +49,7 @@ const host = createAutomationsHost({
48
49
  controlApi,
49
50
  machineApi,
50
51
  eventsApi,
52
+ publicSkillApi: createPublicSkillApi(),
51
53
  fireSchedules: () => delivery.fireDueCrons(),
52
54
  schedulerIntervalMs: config.schedulerIntervalMs,
53
55
  log,
@@ -4,6 +4,7 @@ export declare function createAutomationsHost(options: {
4
4
  readonly controlApi: (request: Request) => Promise<Response>;
5
5
  readonly machineApi: (request: Request) => Promise<Response>;
6
6
  readonly eventsApi?: (request: Request) => Promise<Response>;
7
+ readonly publicSkillApi?: (request: Request) => Promise<Response>;
7
8
  readonly fireSchedules: () => Promise<unknown>;
8
9
  readonly schedulerIntervalMs: number;
9
10
  readonly maxRequestBodyBytes?: number;
@@ -25,9 +25,11 @@ export function createAutomationsHost(options) {
25
25
  return await send(outgoing, Response.json({ ok: true }), controller.signal);
26
26
  const request = await webRequest(incoming, options.publicOrigin, options.maxRequestBodyBytes ?? 2 * 1024 * 1024, controller.signal);
27
27
  const pathname = new URL(request.url).pathname;
28
- const api = pathname.startsWith('/e/') && options.eventsApi
29
- ? options.eventsApi
30
- : pathname.startsWith('/v1/machine/') ? options.machineApi : options.controlApi;
28
+ const api = pathname.startsWith('/.well-known/agent-skills/') && options.publicSkillApi
29
+ ? options.publicSkillApi
30
+ : pathname.startsWith('/e/') && options.eventsApi
31
+ ? options.eventsApi
32
+ : pathname.startsWith('/v1/machine/') ? options.machineApi : options.controlApi;
31
33
  await send(outgoing, await api(request), controller.signal);
32
34
  }
33
35
  catch (error) {
@@ -0,0 +1,2 @@
1
+ /** Only the two packaged public artifacts are addressable; no arbitrary file reads. */
2
+ export declare function createPublicSkillApi(directory?: URL): (request: Request) => Promise<Response>;
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ const prefix = '/.well-known/agent-skills/';
3
+ /** Only the two packaged public artifacts are addressable; no arbitrary file reads. */
4
+ export function createPublicSkillApi(directory = new URL('../skills/', import.meta.url)) {
5
+ const index = readFileSync(new URL('index.json', directory));
6
+ const entry = JSON.parse(index.toString('utf8')).skills[0];
7
+ const filename = entry.url.replace(/^\.\//, '');
8
+ if (!/^amalgm-automations\.[a-f0-9]{64}\.tgz$/.test(filename))
9
+ throw new Error('Invalid skill artifact');
10
+ const files = new Map([
11
+ [`${prefix}index.json`, { bytes: index, type: 'application/json', cache: 'no-cache' }],
12
+ [`${prefix}${filename}`, {
13
+ bytes: readFileSync(new URL(filename, directory)), type: 'application/gzip',
14
+ cache: 'public, max-age=31536000, immutable',
15
+ }],
16
+ ]);
17
+ return async (request) => {
18
+ const file = files.get(new URL(request.url).pathname);
19
+ if (!file)
20
+ return new Response('Not found', { status: 404 });
21
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
22
+ return new Response('Method not allowed', { status: 405, headers: { allow: 'GET, HEAD' } });
23
+ }
24
+ return new Response(request.method === 'HEAD' ? null : new Uint8Array(file.bytes), {
25
+ headers: {
26
+ 'content-type': file.type,
27
+ 'content-length': String(file.bytes.byteLength),
28
+ 'cache-control': file.cache,
29
+ 'x-content-type-options': 'nosniff',
30
+ },
31
+ });
32
+ };
33
+ }
@@ -0,0 +1 @@
1
+ {"$schema":"https://schemas.agentskills.io/discovery/0.2.0/schema.json","skills":[{"name":"amalgm-automations","description":"Set up, operate, and troubleshoot Amalgm Automations through MCP or the amalgm automations CLI. Use for installation, Google sign-in, scheduled or webhook workflows, Run Now, run history, and Automations support.","type":"archive","url":"./amalgm-automations.f64db3414c22f330c2c83a5d1dc76949d7a95f3c9ba06a94c5fbd9c4024d4acd.tgz","digest":"sha256:f64db3414c22f330c2c83a5d1dc76949d7a95f3c9ba06a94c5fbd9c4024d4acd"}]}
@@ -8,6 +8,6 @@ export interface AutomationCliIo extends AutomationCliInputPorts {
8
8
  stdout?: Output;
9
9
  stderr?: Output;
10
10
  }
11
- export declare const automationCliHelp = "amalgm automations \u2014 durable automation control for agents\n\nUsage:\n amalgm automations <command> [--input JSON | --file PATH | --stdin]\n amalgm-automations <command> [--input JSON | --file PATH | --stdin]\n\nCommands (identical to the Automations MCP task surface):\n create Create one complete definition\n list List the user's automations\n get Get one complete definition and optional run history\n update Apply grouped definition changes\n delete Delete current configuration; run history remains\n run-now Admit one durable manual run\n\nInput is one JSON object with the corresponding MCP tool's fields. Commands\nwithout options receive {}. Output is one JSON success or error envelope.\nUse --stdin when input contains credentials such as a webhook signing secret.\n\nSetup and support:\n Already connected? Reuse the running Amalgm Shell.\n First login: amalgm login --no-open\n Open the printed link, sign in with Google, and approve this computer.\n Keep the login process running; it hosts Shell after approval.\n Check: amalgm status --user EMAIL\n Resume a registered computer: amalgm run --user EMAIL\n Verify access: amalgm automations list --user EMAIL --input '{\"limit\":1}'\n No local installation? https://amalgm.ai/setup\n Support: aayush@amalgm.ai\n Never copy private connection credentials into a command or support email.\n\nWorkflow step lanes:\n version 1 action-only: {\"id\":\"...\",\"actionId\":\"product.action\",\"input\":{...}}\n version 2 action, command, or script steps:\n command {\"id\":\"...\",\"kind\":\"command\",\"command\":\"codex\",\"args\":[\"exec\",\"...\"],\"cwd\":\"/absolute/path\"}\n script {\"id\":\"...\",\"kind\":\"script\",\"runtime\":\"shell|node|python\",\"source\":\"...\",\"cwd\":\"/absolute/path\"}\n";
11
+ export declare const automationCliHelp = "amalgm automations \u2014 durable automation control for agents\n\nUsage:\n amalgm automations <command> [--input JSON | --file PATH | --stdin]\n amalgm-automations <command> [--input JSON | --file PATH | --stdin]\n\nCommands (identical to the Automations MCP task surface):\n create Create one complete definition\n list List the user's automations\n get Get one complete definition and optional run history\n update Apply grouped definition changes\n delete Delete current configuration; run history remains\n run-now Admit one durable manual run\n\nInput is one JSON object with the corresponding MCP tool's fields. Commands\nwithout options receive {}. Output is one JSON success or error envelope.\nUse --stdin when input contains credentials such as a webhook signing secret.\n\nSetup and support:\n Already connected? Reuse the running Amalgm Shell.\n First login: amalgm login --no-open\n Open the printed link, sign in with Google, and approve this computer.\n Keep the login process running; it hosts Shell after approval.\n Check: amalgm status --user EMAIL\n List local accounts: amalgm status --all\n Resume a registered computer: amalgm run --user EMAIL\n Verify access: amalgm automations list --user EMAIL --input '{\"limit\":1}'\n No local installation? https://amalgm.ai/setup\n Support: aayush@amalgm.ai\n Never copy private connection credentials into a command or support email.\n\nExternal agent MCP setup (after Shell login):\n amalgm automations mcp --user EMAIL\n Configure your client to launch this stdio command. No credential fields.\n\nWorkflow step lanes:\n version 1 action-only: {\"id\":\"...\",\"actionId\":\"product.action\",\"input\":{...}}\n version 2 action, command, or script steps:\n command {\"id\":\"...\",\"kind\":\"command\",\"command\":\"codex\",\"args\":[\"exec\",\"...\"],\"cwd\":\"/absolute/path\"}\n script {\"id\":\"...\",\"kind\":\"script\",\"runtime\":\"shell|node|python\",\"source\":\"...\",\"cwd\":\"/absolute/path\"}\n";
12
12
  export declare function runAutomationCli(argv: string[], backend: AutomationCrud | AutomationCommands, io?: AutomationCliIo): Promise<number>;
13
13
  export {};
@@ -26,12 +26,17 @@ Setup and support:
26
26
  Open the printed link, sign in with Google, and approve this computer.
27
27
  Keep the login process running; it hosts Shell after approval.
28
28
  Check: amalgm status --user EMAIL
29
+ List local accounts: amalgm status --all
29
30
  Resume a registered computer: amalgm run --user EMAIL
30
31
  Verify access: amalgm automations list --user EMAIL --input '{"limit":1}'
31
32
  No local installation? https://amalgm.ai/setup
32
33
  Support: aayush@amalgm.ai
33
34
  Never copy private connection credentials into a command or support email.
34
35
 
36
+ External agent MCP setup (after Shell login):
37
+ amalgm automations mcp --user EMAIL
38
+ Configure your client to launch this stdio command. No credential fields.
39
+
35
40
  Workflow step lanes:
36
41
  version 1 action-only: {"id":"...","actionId":"product.action","input":{...}}
37
42
  version 2 action, command, or script steps:
@@ -22,7 +22,6 @@ export function triggerOperations(context) {
22
22
  await context.exists(automationId);
23
23
  const parsed = parseCreateScheduleTrigger(input);
24
24
  const timezone = parsed.timezone || 'UTC';
25
- schedule(parsed.cron, timezone);
26
25
  if (parsed.id)
27
26
  await unique(automationId, parsed.id);
28
27
  return repository.createScheduleTrigger(principal.userId, automationId, {
package/dist/src/mcp.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type AutomationCommands } from './command-surface.js';
2
3
  import type { AutomationCrud } from './contract.js';
3
4
  import { createAutomationToolSurface } from './tool-surface.js';
4
5
  export { createAutomationToolSurface };
5
6
  export type { AutomationToolSurface, AutomationView, CreateAutomationDefinition, UpdateAutomationDefinition, WorkflowChange, } from './tool-surface.js';
6
- export declare function createAutomationMcpServer(sdk: AutomationCrud): McpServer;
7
+ export declare function createAutomationMcpServer(sdk: AutomationCrud | AutomationCommands): McpServer;
package/dist/src/mcp.js CHANGED
@@ -14,7 +14,7 @@ const resultSchema = {
14
14
  };
15
15
  export function createAutomationMcpServer(sdk) {
16
16
  const server = new McpServer({ name: 'amalgm-automations-mcp-server', version: '0.1.0' });
17
- const commands = createAutomationCommands(sdk);
17
+ const commands = 'execute' in sdk ? sdk : createAutomationCommands(sdk);
18
18
  for (const definition of automationCommandDefinitions)
19
19
  register(server, commands, definition);
20
20
  return server;
@@ -23,7 +23,9 @@ function register(server, commands, definition) {
23
23
  server.registerTool(definition.mcpName, {
24
24
  title: definition.title,
25
25
  description: definition.description,
26
- inputSchema: definition.inputShape,
26
+ // Forward unknown top-level fields to the SDK's strict parser. The MCP
27
+ // SDK's default object parser strips them before our command sees them.
28
+ inputSchema: z.object(definition.inputShape).passthrough(),
27
29
  outputSchema: resultSchema,
28
30
  annotations: definition.annotations,
29
31
  }, async (input) => {
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v3';
2
2
  import { ValidationError } from './errors.js';
3
3
  import { compiledAutomationPlan } from './plan.js';
4
- import { IDENTIFIER } from './validation.js';
4
+ import { IDENTIFIER, schedule } from './validation.js';
5
5
  const maxPageSize = 100;
6
6
  const identifierMessage = 'Identifier is invalid';
7
7
  const text = (maximum, label) => z.string()
@@ -96,10 +96,16 @@ export function parseListAutomations(value) {
96
96
  return parse(listAutomationsSchema, value);
97
97
  }
98
98
  export function parseCreateScheduleTrigger(value) {
99
- return parse(createScheduleTriggerSchema, value);
99
+ const trigger = parse(createScheduleTriggerSchema, value);
100
+ schedule(trigger.cron, trigger.timezone ?? 'UTC');
101
+ return trigger;
100
102
  }
101
103
  export function parseUpdateScheduleTrigger(value) {
102
- return parse(updateScheduleTriggerSchema, value);
104
+ const patch = parse(updateScheduleTriggerSchema, value);
105
+ if (patch.cron !== undefined || patch.timezone !== undefined) {
106
+ schedule(patch.cron ?? '* * * * *', patch.timezone ?? 'UTC');
107
+ }
108
+ return patch;
103
109
  }
104
110
  export function parseCreateWebhookTrigger(value) {
105
111
  return parse(createWebhookTriggerSchema, value);
@@ -1,4 +1,5 @@
1
- import { NotFoundError, ValidationError } from './errors.js';
1
+ import { AutomationError, NotFoundError, ValidationError } from './errors.js';
2
+ import { parseCreateAutomation, parseCreateScheduleTrigger, parseCreateWebhookTrigger, parseCreateWorkflow, parseUpdateAutomation, parseUpdateScheduleTrigger, parseUpdateWebhookTrigger, parseUpdateWorkflow, } from './schema.js';
2
3
  export function createAutomationToolSurface(sdk) {
3
4
  const view = async (automationId, runs = false) => {
4
5
  const automation = await sdk.automations.get(automationId);
@@ -14,6 +15,11 @@ export function createAutomationToolSurface(sdk) {
14
15
  const surface = {
15
16
  async create(input) {
16
17
  const { schedules = [], webhooks = [], workflow, ...automationInput } = input;
18
+ parseCreateAutomation(automationInput);
19
+ schedules.forEach(parseCreateScheduleTrigger);
20
+ webhooks.forEach(parseCreateWebhookTrigger);
21
+ if (workflow)
22
+ parseCreateWorkflow(workflow);
17
23
  validateCreateIds(schedules, webhooks);
18
24
  const staged = schedules.length > 0 || webhooks.length > 0 || workflow !== undefined;
19
25
  const requestedEnabled = automationInput.enabled !== false;
@@ -40,6 +46,7 @@ export function createAutomationToolSurface(sdk) {
40
46
  get: view,
41
47
  async update(automationId, input) {
42
48
  requireChanges(input);
49
+ validateUpdate(input);
43
50
  validateChanges(input.schedules, 'schedule');
44
51
  validateChanges(input.webhooks, 'webhook');
45
52
  const current = await sdk.automations.get(automationId);
@@ -98,6 +105,18 @@ function requireChanges(input) {
98
105
  throw new ValidationError('Automation update must include a change');
99
106
  }
100
107
  }
108
+ function validateUpdate(input) {
109
+ if (input.patch)
110
+ parseUpdateAutomation(input.patch);
111
+ input.schedules?.create?.forEach(parseCreateScheduleTrigger);
112
+ input.schedules?.update?.forEach(({ patch }) => parseUpdateScheduleTrigger(patch));
113
+ input.webhooks?.create?.forEach(parseCreateWebhookTrigger);
114
+ input.webhooks?.update?.forEach(({ patch }) => parseUpdateWebhookTrigger(patch));
115
+ if (input.workflow && 'create' in input.workflow)
116
+ parseCreateWorkflow(input.workflow.create);
117
+ if (input.workflow && 'update' in input.workflow)
118
+ parseUpdateWorkflow(input.workflow.update);
119
+ }
101
120
  function hasChanges(changes) {
102
121
  return Boolean(changes && [changes.create, changes.update, changes.delete]
103
122
  .some((items) => items && items.length > 0));
@@ -122,5 +141,5 @@ function validateCreateIds(schedules, webhooks) {
122
141
  }
123
142
  function disabledDraftError(automationId, error) {
124
143
  const reason = error instanceof Error ? error.message : String(error);
125
- return new Error(`Automation ${automationId} remains disabled because configuration failed: ${reason}`);
144
+ return new AutomationError(error instanceof AutomationError ? error.code : 'internal', `Automation ${automationId} remains disabled because configuration failed: ${reason}`, error instanceof AutomationError ? error.status : undefined);
126
145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/automations",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -45,7 +45,7 @@
45
45
  "README.md"
46
46
  ],
47
47
  "scripts": {
48
- "build": "rm -rf dist && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts",
48
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsx scripts/mark-executables.ts && tsx scripts/build-public-skill.ts",
49
49
  "check": "tsx scripts/check-tree.ts && tsc -p tsconfig.json --noEmit",
50
50
  "test": "tsx --test test/*.test.ts",
51
51
  "test:supabase": "tsx --test test/integration/postgres.test.ts",
@@ -56,10 +56,10 @@
56
56
  "start": "node dist/host/main.js"
57
57
  },
58
58
  "engines": {
59
- "node": ">=20"
59
+ "node": ">=24"
60
60
  },
61
61
  "dependencies": {
62
- "@amalgm/core": "0.4.4",
62
+ "@amalgm/core": "0.4.7",
63
63
  "@modelcontextprotocol/sdk": "^1.30.0",
64
64
  "@supabase/supabase-js": "2.57.4",
65
65
  "cron-parser": "^5.4.0",
@@ -72,13 +72,16 @@ input.
72
72
  complex source. Do not expose those values in command arguments or logs.
73
73
  - Delete only after the exact id is established and deletion is within the
74
74
  user's request. Deleting configuration intentionally retains run history.
75
+ Omit `id` when creating a new automation; explicitly reusing a deleted id
76
+ refers to the same logical identity and retains its history and retry keys.
75
77
 
76
78
  ## Build one complete definition
77
79
 
78
80
  Create the automation, its schedule/webhook triggers, and its optional workflow
79
- in one task-level call. The service stages multi-resource changes safely and
80
- enables the definition only after setup succeeds. If setup fails, report the
81
- returned disabled draft id rather than silently creating a replacement.
81
+ in one task-level call. Invalid supplied configuration fails before any write.
82
+ The service stages valid multi-resource changes and enables the definition
83
+ only after setup succeeds. If a later service failure returns a disabled draft
84
+ id, get and repair that draft with `update`; do not create a replacement.
82
85
 
83
86
  Use a finite `maxOccurrences` when the user's request is bounded. Do not turn
84
87
  "ten times" into an unbounded schedule plus a future cleanup promise.
@@ -102,7 +105,7 @@ executables through the selected machine; never guess either.
102
105
  not evidence that work completed.
103
106
 
104
107
  1. Supply a stable, caller-chosen `idempotency_key` whenever the request might
105
- be retried.
108
+ be retried. Use a new key for each new logical run.
106
109
  2. If admission is retried, send the same automation id, input, and key. The
107
110
  returned run id must remain the same.
108
111
  3. Poll `get` with `include_runs: true` or a `run_query` until the admitted run
@@ -15,6 +15,8 @@ Failures use stderr, a nonzero exit, and:
15
15
  ```
16
16
 
17
17
  Do not scrape prose or infer success from exit code alone; parse the envelope.
18
+ Unknown configuration fields are rejected. Arbitrary JSON inside workflow
19
+ action input and Run Now input remains valid payload data.
18
20
 
19
21
  ## create
20
22
 
@@ -62,6 +64,11 @@ At least one executable step is required when `compiled` is supplied. Omit
62
64
  webhook URL is the provider destination; a configured signing secret is
63
65
  write-only on later reads.
64
66
 
67
+ Omit `id` for a new automation. An explicit id names a persistent logical
68
+ identity: deleting and recreating it retains that identity's run history and
69
+ Run Now idempotency keys. Use a new key for each new logical run and reuse it
70
+ only when retrying that run.
71
+
65
72
  CLI example:
66
73
 
67
74
  ```bash
@@ -5,6 +5,19 @@ an error. Use Shell's commands for setup and Automations reads to verify access.
5
5
  The agent handles the commands; the person chooses their Google account and
6
6
  approves connecting their computer in the browser.
7
7
 
8
+ ## Install this skill
9
+
10
+ The public download includes this skill and all its references; it needs no
11
+ GitHub account or access to Amalgm's source repositories. With Node.js 22.20+
12
+ (or Amalgm's supplied Node runtime), run:
13
+
14
+ ```bash
15
+ npx skills add https://automations.amalgm.ai --skill amalgm-automations
16
+ ```
17
+
18
+ Choose the intended agent in the installer's prompt. Installing the skill
19
+ supplies guidance; use the connection steps below to access Automations.
20
+
8
21
  ## Start with the connection you have
9
22
 
10
23
  If Automations MCP already answers `list` with `{"limit":1}`, continue the task.
@@ -34,6 +47,28 @@ A successful Automations response is a JSON `result` envelope; an empty list
34
47
  is valid. The six Automations commands use that envelope. Shell's `status`
35
48
  returns a different JSON shape; `login` and `run` report progress as text.
36
49
 
50
+ For a local stdio MCP client, configure this server using the actual selected
51
+ account email (Shell 0.1.176 or newer):
52
+
53
+ ```json
54
+ {
55
+ "mcpServers": {
56
+ "amalgm-automations": {
57
+ "command": "amalgm",
58
+ "args": ["automations", "mcp", "--user", "person@example.com"]
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ This is the server entry for clients using `mcpServers` JSON; clients with a
65
+ different configuration format use the same command and arguments. Use the
66
+ exact installed launcher path if the client cannot find `amalgm` on PATH.
67
+ Shell supplies the current local connection on every tool call, including
68
+ after a runtime restart. Do not add credentials or environment variables.
69
+ Listing tools works before login; calling them requires a running signed-in
70
+ runtime. After configuring the client, verify its Automations `list` tool.
71
+
37
72
  ## Install only when needed
38
73
 
39
74
  Check for an existing app-managed installation before adding another copy.
@@ -105,8 +140,11 @@ do not reopen approval until they choose to continue.
105
140
 
106
141
  ## Reuse or resume a registered computer
107
142
 
108
- Once the account email is known, inspect it explicitly. Replace the example
109
- email below with the user's actual selected account:
143
+ Discover registered accounts with `amalgm status --all` (Shell 0.1.176+).
144
+ It returns email and registration state only. A sole ready account is selected
145
+ implicitly; multiple ready accounts require `--user`. Once the account email
146
+ is known, inspect it explicitly. Replace the example email below with the
147
+ user's actual selected account:
110
148
 
111
149
  ```bash
112
150
  amalgm status --user person@example.com
@@ -140,14 +178,15 @@ pending; it does not erase their history.
140
178
 
141
179
  ## Diagnose the failing boundary
142
180
 
143
- Read the error's code, message, and status together; some Shell failures are
144
- wrapped as `internal`. Keep automation and run ids when available. Try the
181
+ Read the error's code, message, and status together. Keep automation and run
182
+ ids when available. Try the
145
183
  appropriate repair and verify again. If the same failure persists with no new
146
184
  evidence, explain it and offer support rather than looping through login,
147
185
  reinstallation, or repeated writes.
148
186
 
149
187
  | Failure | What to do |
150
188
  | --- | --- |
189
+ | `user_not_registered` or `user_selection_required` | Use `amalgm status --all` to discover local accounts, then select the intended email or complete login for it. |
151
190
  | `runtime_unavailable`, connection refused, or Shell says it is not running | Inspect the selected account's status and use the resume/setup decision above. A standalone adapter asking for environment credentials should be replaced by the global `amalgm automations` path. |
152
191
  | `runtime_unauthorized` | Retry the read once through the public Shell command, which obtains the current local connection. If it still fails, keep the error for support; never extract or replace tokens manually. |
153
192
  | `runtime_transport` or `runtime_protocol` | Check local readiness, connectivity, and the installed version. A timeout or service outage is not evidence that the user needs a new account. |
@@ -159,8 +198,10 @@ reinstallation, or repeated writes.
159
198
 
160
199
  For a failed mutation, inspect existing state before retrying. Reuse the same
161
200
  Run Now input and idempotency key after an uncertain response. Preserve a
162
- returned disabled draft id. A retry must not turn one requested automation or
163
- run into several.
201
+ returned disabled draft id: get that definition, then repair it with `update`
202
+ instead of retrying `create`. Invalid supplied configuration is rejected
203
+ before any write; a later service failure can still leave a disabled draft.
204
+ A retry must not turn one requested automation or run into several.
164
205
 
165
206
  ## Get support
166
207