amalgm 0.1.51 → 0.1.52

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.
Files changed (70) hide show
  1. package/lib/tunnel-events.js +48 -23
  2. package/package.json +2 -2
  3. package/runtime/lib/harnesses.js +12 -4
  4. package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
  5. package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
  6. package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
  7. package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
  8. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
  9. package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
  10. package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
  11. package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
  13. package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
  14. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
  15. package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
  16. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
  17. package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
  18. package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
  19. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
  20. package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
  21. package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
  22. package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
  23. package/runtime/scripts/amalgm-mcp/config.js +33 -48
  24. package/runtime/scripts/amalgm-mcp/deps.js +1 -31
  25. package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
  26. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
  27. package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
  28. package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
  29. package/runtime/scripts/amalgm-mcp/index.js +12 -14
  30. package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
  31. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
  32. package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
  33. package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
  34. package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
  37. package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
  38. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
  39. package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
  40. package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
  41. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
  42. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
  43. package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
  44. package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
  45. package/runtime/scripts/chat-core/adapters/claude.js +2 -1
  46. package/runtime/scripts/chat-core/auth.js +82 -12
  47. package/runtime/scripts/chat-core/contract.js +5 -1
  48. package/runtime/scripts/chat-core/engine.js +103 -62
  49. package/runtime/scripts/chat-core/event-schema.js +8 -0
  50. package/runtime/scripts/chat-core/events.js +5 -0
  51. package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
  52. package/runtime/scripts/chat-core/parts.js +21 -6
  53. package/runtime/scripts/chat-core/sse.js +3 -0
  54. package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
  55. package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
  56. package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
  57. package/runtime/scripts/chat-core/tool-shape.js +4 -4
  58. package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
  59. package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
  60. package/runtime/scripts/local-gateway.js +34 -27
  61. package/runtime/scripts/platform-context.txt +76 -94
  62. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
  63. package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
  64. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
  65. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
  66. package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
  67. package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
  68. package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
  69. package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
  70. package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Browser MCP tools. In desktop local mode they drive Amalgm's visible
3
- * Electron browser surface; elsewhere they fall back to Playwright.
2
+ * Browser MCP tools. In the desktop app they drive Amalgm's visible
3
+ * Electron browser surface; elsewhere they fall back to agent-browser.
4
4
  */
5
5
 
6
6
  const { textResult, errorResult } = require('../lib/tool-result');
@@ -39,6 +39,7 @@ const tabTargetProperties = {
39
39
 
40
40
  const locatorProperties = {
41
41
  ...tabTargetProperties,
42
+ ref: { type: 'string', description: 'Snapshot ref such as @e1' },
42
43
  selector: { type: 'string', description: 'CSS selector for the target element' },
43
44
  text: { type: 'string', description: 'Visible text to match' },
44
45
  role: { type: 'string', description: 'ARIA role, e.g. button, link, textbox' },
@@ -224,7 +225,7 @@ module.exports = [
224
225
  type: 'object',
225
226
  properties: {
226
227
  ...tabTargetProperties,
227
- full_page: { type: 'boolean', description: 'Capture the full scrollable page (Playwright fallback only; Electron captures viewport)' },
228
+ full_page: { type: 'boolean', description: 'Capture the full scrollable page' },
228
229
  },
229
230
  },
230
231
  async handler(args, ctx) {
@@ -261,8 +262,8 @@ module.exports = [
261
262
  description: 'Click an element by selector, text, role/name, label, placeholder, or testId.',
262
263
  inputSchema: { type: 'object', properties: locatorProperties },
263
264
  async handler(args, ctx) {
264
- if (!args.selector && !args.text && !args.role && !args.label && !args.placeholder && !args.testId && !args.name) {
265
- return errorResult('A selector, text, role, label, placeholder, testId, or name is required');
265
+ if (!args.ref && !args.selector && !args.text && !args.role && !args.label && !args.placeholder && !args.testId && !args.name) {
266
+ return errorResult('A ref, selector, text, role, label, placeholder, testId, or name is required');
266
267
  }
267
268
  try {
268
269
  const result = await clickBrowser(args, ctx);
@@ -320,6 +321,7 @@ module.exports = [
320
321
  type: 'object',
321
322
  properties: {
322
323
  ...tabTargetProperties,
324
+ ref: { type: 'string', description: 'Snapshot ref such as @e1' },
323
325
  selector: { type: 'string', description: 'CSS selector to extract (omit for full page)' },
324
326
  },
325
327
  },
@@ -482,7 +484,7 @@ module.exports = [
482
484
  },
483
485
  {
484
486
  name: 'browser_cua_move',
485
- description: 'Move the visible automation cursor to viewport coordinates.',
487
+ description: 'Move the browser mouse to viewport coordinates.',
486
488
  inputSchema: {
487
489
  type: 'object',
488
490
  properties: { ...tabTargetProperties, x: { type: 'number' }, y: { type: 'number' }, keys: { type: 'array', items: { type: 'string' } } },
@@ -625,7 +627,7 @@ module.exports = [
625
627
  },
626
628
  {
627
629
  name: 'browser_start_video',
628
- description: 'Start recording the visible browser session to a local WebM video. Saved when browser_stop_video is called.',
630
+ description: 'Start recording the browser session to a local WebM video. Saved when browser_stop_video is called.',
629
631
  inputSchema: {
630
632
  type: 'object',
631
633
  properties: {
@@ -8,15 +8,8 @@
8
8
 
9
9
  const path = require('path');
10
10
  const os = require('os');
11
- const fs = require('fs');
12
- const {
13
- DEFAULT_PROXY_BASE_URL,
14
- proxyBaseUrl,
15
- readProxyToken,
16
- } = require('../proxy-token-store');
17
- const { runtimePort } = require('../../lib/runtime-manifest');
18
11
 
19
- const PORT = runtimePort('amalgm-mcp');
12
+ const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
20
13
  const MCP_PROTOCOL_VERSION = '2024-11-05';
21
14
 
22
15
  const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
@@ -26,30 +19,20 @@ const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
26
19
  const LOCAL_DB_FILE = path.join(STORAGE_DIR, 'amalgm.db');
27
20
  const TASKS_FILE = path.join(STORAGE_DIR, 'tasks.json');
28
21
  const AGENTS_FILE = path.join(STORAGE_DIR, 'agents.json');
29
- const ARTIFACTS_DIR = path.join(STORAGE_DIR, 'artifacts');
30
- const ARTIFACTS_FILE = path.join(STORAGE_DIR, 'artifacts.json');
22
+ const APPS_DIR = path.join(STORAGE_DIR, 'apps');
23
+ const APPS_FILE = path.join(STORAGE_DIR, 'apps.json');
24
+ const LEGACY_ARTIFACTS_DIR = path.join(STORAGE_DIR, 'artifacts');
25
+ const LEGACY_ARTIFACTS_FILE = path.join(STORAGE_DIR, 'artifacts.json');
31
26
  const COMPUTER_RECORD_FILE = path.join(STORAGE_DIR, 'computer.json');
32
- const COMPUTER_AUTH_FILE = path.join(STORAGE_DIR, 'auth.json');
33
27
  const TASK_RUNS_DIR = path.join(STORAGE_DIR, 'task-runs');
34
28
  const EVENT_TRIGGERS_FILE = path.join(STORAGE_DIR, 'event-triggers.json');
29
+ const WORKFLOWS_FILE = path.join(STORAGE_DIR, 'workflows.json');
35
30
  const AGENT_CONVOS_DIR = path.join(STORAGE_DIR, 'agent-convos');
36
31
 
37
- function readJson(file) {
38
- try {
39
- return JSON.parse(fs.readFileSync(file, 'utf8'));
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- function cleanString(value) {
46
- return typeof value === 'string' && value.trim() ? value.trim() : '';
47
- }
48
-
49
32
  // Chat server (local Next.js/Electron) — same process tree; no cloud hop.
50
- const CHAT_SERVER_URL = `http://localhost:${runtimePort('chat-server')}`;
33
+ const CHAT_SERVER_URL = `http://localhost:${process.env.CHAT_SERVER_PORT || 8084}`;
51
34
 
52
- const SCHEDULER_INTERVAL_MS = 60_000;
35
+ const SCHEDULER_INTERVAL_MS = 30_000;
53
36
 
54
37
  // Default working directory for automated runs (was /workspace in the container).
55
38
  const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
@@ -58,19 +41,13 @@ const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
58
41
  // Sensitive provider keys live on the Fly.io api-proxy. When running locally
59
42
  // without signing in, none of this is configured and hasSupabase() returns false.
60
43
 
61
- const savedComputerRecord = readJson(COMPUTER_RECORD_FILE) || {};
62
- const savedComputerAuth = readJson(COMPUTER_AUTH_FILE) || {};
63
- const AMALGM_USER_ID = cleanString(process.env.AMALGM_USER_ID)
64
- || cleanString(savedComputerRecord.user_id)
65
- || cleanString(savedComputerAuth.user_id);
66
- const AMALGM_COMPUTER_ID = cleanString(process.env.AMALGM_COMPUTER_ID)
67
- || cleanString(savedComputerRecord.computer_id)
68
- || cleanString(savedComputerAuth.computer_id)
69
- || cleanString(process.env.AMALGM_CONTAINER_ID);
44
+ const AMALGM_USER_ID = process.env.AMALGM_USER_ID || '';
45
+ const AMALGM_COMPUTER_ID = process.env.AMALGM_COMPUTER_ID || process.env.AMALGM_CONTAINER_ID || '';
70
46
 
71
- const PROXY_TOKEN = readProxyToken()
47
+ const PROXY_TOKEN = process.env.AMALGM_PROXY_TOKEN
72
48
  || (process.env.AMALGM_RUNTIME_SOURCE === 'npm' ? '' : process.env.ANTHROPIC_API_KEY || '');
73
- const PROXY_BASE_URL = proxyBaseUrl() || (PROXY_TOKEN ? DEFAULT_PROXY_BASE_URL : '');
49
+ const DEFAULT_PROXY_BASE_URL = 'https://amalgm-api-proxy-v2.fly.dev';
50
+ const PROXY_BASE_URL = process.env.AMALGM_PROXY_URL || (PROXY_TOKEN ? DEFAULT_PROXY_BASE_URL : '');
74
51
  const SUPABASE_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/supabase` : '';
75
52
 
76
53
  const SUPABASE_URL_DIRECT =
@@ -89,9 +66,15 @@ const SUPABASE_AUTH_HEADERS = SUPABASE_PROXY_URL
89
66
  const EVENTS_PUBLIC_URL =
90
67
  process.env.AMALGM_EVENTS_PUBLIC_URL || `http://localhost:${PORT}/events`;
91
68
 
92
- const ARTIFACTS_DOMAIN = process.env.AMALGM_ARTIFACTS_DOMAIN || 'artifacts.amalgm.ai';
93
- const ARTIFACT_PORT_MIN = parseInt(process.env.AMALGM_ARTIFACT_PORT_MIN || '9100', 10);
94
- const ARTIFACT_PORT_MAX = parseInt(process.env.AMALGM_ARTIFACT_PORT_MAX || '9199', 10);
69
+ const APPS_DOMAIN = (
70
+ process.env.AMALGM_APPS_DOMAIN
71
+ || process.env.NEXT_PUBLIC_APPS_DOMAIN
72
+ || process.env.AMALGM_ARTIFACTS_DOMAIN
73
+ || process.env.NEXT_PUBLIC_ARTIFACTS_DOMAIN
74
+ || 'apps.amalgm.ai'
75
+ );
76
+ const APP_PORT_MIN = parseInt(process.env.AMALGM_APP_PORT_MIN || process.env.AMALGM_ARTIFACT_PORT_MIN || '9100', 10);
77
+ const APP_PORT_MAX = parseInt(process.env.AMALGM_APP_PORT_MAX || process.env.AMALGM_ARTIFACT_PORT_MAX || '9199', 10);
95
78
  const GATEWAY_BASE_URL = (
96
79
  process.env.AMALGM_GATEWAY_INTERNAL_URL
97
80
  || process.env.AMALGM_GATEWAY_URL
@@ -102,8 +85,8 @@ const GATEWAY_INTERNAL_SECRET =
102
85
  || process.env.PROXY_ADMIN_SECRET
103
86
  || process.env.ADMIN_SECRET
104
87
  || '';
105
- const ARTIFACT_ROUTE_SYNC_INTERVAL_MS = parseInt(
106
- process.env.AMALGM_ARTIFACT_ROUTE_SYNC_INTERVAL_MS || '30000',
88
+ const APP_ROUTE_SYNC_INTERVAL_MS = parseInt(
89
+ process.env.AMALGM_APP_ROUTE_SYNC_INTERVAL_MS || process.env.AMALGM_ARTIFACT_ROUTE_SYNC_INTERVAL_MS || '30000',
107
90
  10,
108
91
  );
109
92
 
@@ -115,12 +98,14 @@ module.exports = {
115
98
  LOCAL_DB_FILE,
116
99
  TASKS_FILE,
117
100
  AGENTS_FILE,
118
- ARTIFACTS_DIR,
119
- ARTIFACTS_FILE,
101
+ APPS_DIR,
102
+ APPS_FILE,
103
+ LEGACY_ARTIFACTS_DIR,
104
+ LEGACY_ARTIFACTS_FILE,
120
105
  COMPUTER_RECORD_FILE,
121
- COMPUTER_AUTH_FILE,
122
106
  TASK_RUNS_DIR,
123
107
  EVENT_TRIGGERS_FILE,
108
+ WORKFLOWS_FILE,
124
109
  AGENT_CONVOS_DIR,
125
110
  CHAT_SERVER_URL,
126
111
  SCHEDULER_INTERVAL_MS,
@@ -132,10 +117,10 @@ module.exports = {
132
117
  SUPABASE_URL,
133
118
  SUPABASE_AUTH_HEADERS,
134
119
  EVENTS_PUBLIC_URL,
135
- ARTIFACTS_DOMAIN,
136
- ARTIFACT_PORT_MIN,
137
- ARTIFACT_PORT_MAX,
120
+ APPS_DOMAIN,
121
+ APP_PORT_MIN,
122
+ APP_PORT_MAX,
138
123
  GATEWAY_BASE_URL,
139
124
  GATEWAY_INTERNAL_SECRET,
140
- ARTIFACT_ROUTE_SYNC_INTERVAL_MS,
125
+ APP_ROUTE_SYNC_INTERVAL_MS,
141
126
  };
@@ -1,15 +1,3 @@
1
- /**
2
- * Deps — resolves optional native deps (cron-parser, playwright).
3
- *
4
- * The container-era code looked in /opt/amalgm-mcp-dep/ first and then the
5
- * global require path. Locally we only need the global require path — these
6
- * are npm deps of the main repo.
7
- *
8
- * Playwright is lazy: we don't require() it until a browser_* tool is called.
9
- * This keeps boot fast and lets the MCP server run on machines that haven't
10
- * run `npx playwright install chromium` yet.
11
- */
12
-
13
1
  function loadCronParser() {
14
2
  try {
15
3
  return require('cron-parser');
@@ -19,22 +7,4 @@ function loadCronParser() {
19
7
  }
20
8
  }
21
9
 
22
- let _playwrightMod = null;
23
-
24
- function loadPlaywright() {
25
- if (_playwrightMod) return _playwrightMod;
26
- for (const mod of ['playwright', 'playwright-core']) {
27
- try {
28
- _playwrightMod = require(mod);
29
- console.log(`[AmalgmMCP] Loaded Playwright from: ${mod}`);
30
- return _playwrightMod;
31
- } catch {
32
- // Try next candidate.
33
- }
34
- }
35
- throw new Error(
36
- 'Playwright is not installed. Run: npm install playwright && npx playwright install chromium',
37
- );
38
- }
39
-
40
- module.exports = { loadCronParser, loadPlaywright };
10
+ module.exports = { loadCronParser };
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * /events — webhook ingress.
3
3
  *
4
- * Accepts raw POST body, requires a verified signature, and on a hit fires an
5
- * agent run via events/executor.js and returns 200 with
4
+ * Accepts raw POST body, requires a verified signature, and on a hit fires a
5
+ * automation workflow run via automations/runner.js and returns 200 with
6
6
  * `{ ok, event, triggered }`.
7
7
  */
8
8
 
9
9
  const {
10
10
  extractSignature,
11
+ matchAllBySignature,
11
12
  matchBySignature,
12
13
  pickSourceLabel,
13
14
  collectPassthroughHeaders,
14
15
  } = require('./matcher');
15
- const { loadEventTriggers, saveEventTriggers } = require('./store');
16
- const { executeArtifactEvent } = require('./executor');
16
+ const { listEventTriggersForIngress, markTriggerFired } = require('../automations/store');
17
+ const { executeAutomationForTrigger } = require('../automations/runner');
17
18
  const ring = require('./ring-buffer');
18
19
 
19
20
  /**
@@ -44,20 +45,37 @@ async function handleEventsPost(req, sendJson) {
44
45
  return sendJson(401, { error: 'Webhook signature required' });
45
46
  }
46
47
 
47
- const triggerData = loadEventTriggers();
48
- const matchedTrigger = matchBySignature(triggerData.triggers, signature, rawBody);
49
- if (!matchedTrigger) {
50
- console.warn('[AmalgmMCP:Events] No trigger matched the webhook signature');
51
- return sendJson(401, { error: 'Invalid webhook signature — no matching trigger' });
52
- }
53
-
48
+ const triggers = listEventTriggersForIngress();
54
49
  const githubEvent = req.headers['x-github-event'];
50
+ const eventSource = pickSourceLabel(req.headers);
51
+ const eventName =
52
+ githubEvent ||
53
+ req.headers['x-linear-event'] ||
54
+ req.headers['x-gitlab-event'] ||
55
+ 'webhook';
56
+
55
57
  // GitHub webhook handshake: acknowledge verified ping events immediately.
56
58
  if (githubEvent === 'ping') {
59
+ const pingTrigger = matchBySignature(triggers, signature, rawBody);
60
+ if (!pingTrigger) {
61
+ console.warn('[AmalgmMCP:Events] No trigger matched the GitHub ping signature');
62
+ return sendJson(401, { error: 'Invalid webhook signature — no matching trigger' });
63
+ }
57
64
  console.log('[AmalgmMCP:Events] GitHub ping — pong');
58
65
  return sendJson(200, { ok: true, message: 'pong' });
59
66
  }
60
67
 
68
+ const matchedTriggers = matchAllBySignature(
69
+ triggers,
70
+ signature,
71
+ rawBody,
72
+ { source: eventSource, event: eventName },
73
+ );
74
+ if (!matchedTriggers?.length) {
75
+ console.warn(`[AmalgmMCP:Events] No trigger matched ${eventSource}.${eventName}`);
76
+ return sendJson(401, { error: 'Invalid webhook signature or source/event — no matching trigger' });
77
+ }
78
+
61
79
  const eventHeaders = collectPassthroughHeaders(req.headers);
62
80
 
63
81
  let parsedBody;
@@ -67,47 +85,37 @@ async function handleEventsPost(req, sendJson) {
67
85
  parsedBody = rawBody;
68
86
  }
69
87
 
70
- console.log(`[AmalgmMCP:Events] Matched trigger "${matchedTrigger.name}" (${matchedTrigger.id})`);
88
+ console.log(
89
+ `[AmalgmMCP:Events] Matched ${matchedTriggers.length} trigger(s) for ${eventSource}.${eventName}`,
90
+ );
71
91
 
72
92
  const eventObj = {
73
- source: pickSourceLabel(req.headers),
74
- event:
75
- githubEvent ||
76
- req.headers['x-linear-event'] ||
77
- req.headers['x-gitlab-event'] ||
78
- 'webhook',
93
+ source: eventSource,
94
+ event: eventName,
79
95
  payload: parsedBody,
80
96
  headers: eventHeaders,
81
97
  timestamp: new Date().toISOString(),
82
98
  };
83
99
  ring.push(eventObj);
84
100
 
85
- const eventDef = {
86
- name: matchedTrigger.name,
87
- description: matchedTrigger.name,
88
- agent_prompt: matchedTrigger.agent_prompt,
89
- harness: matchedTrigger.harness,
90
- model: matchedTrigger.model,
91
- authMethod: matchedTrigger.authMethod,
92
- chatInput: matchedTrigger.chatInput,
93
- };
94
- const syntheticArtifact = { id: matchedTrigger.id, name: matchedTrigger.name };
95
- const fullPayload = { body: parsedBody, headers: eventHeaders };
96
-
97
- executeArtifactEvent(syntheticArtifact, eventDef, fullPayload, {
98
- triggerId: matchedTrigger.id,
99
- projectPath: matchedTrigger.projectPath,
100
- }).catch((err) => {
101
- console.error(
102
- `[AmalgmMCP:Events] Trigger agent run failed for "${matchedTrigger.name}":`,
103
- err.message,
104
- );
105
- });
101
+ for (const matchedTrigger of matchedTriggers) {
102
+ executeAutomationForTrigger(matchedTrigger.id, eventObj).catch((err) => {
103
+ console.error(
104
+ `[AmalgmMCP:Events] Automation workflow failed for trigger "${matchedTrigger.name}":`,
105
+ err.message,
106
+ );
107
+ });
106
108
 
107
- matchedTrigger.lastFiredAt = new Date().toISOString();
108
- saveEventTriggers(triggerData);
109
+ markTriggerFired(matchedTrigger.id, { source: 'events:ingress' });
110
+ }
109
111
 
110
- return sendJson(200, { ok: true, event: eventObj, triggered: true });
112
+ return sendJson(200, {
113
+ ok: true,
114
+ event: eventObj,
115
+ triggered: true,
116
+ triggerCount: matchedTriggers.length,
117
+ triggerIds: matchedTriggers.map((trigger) => trigger.id),
118
+ });
111
119
  }
112
120
 
113
121
  module.exports = { handleEventsPost };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ function findEngineRepoPath() {
8
+ const explicit = process.env.AMALGM_ENGINE_REPO_PATH;
9
+ if (explicit && fs.existsSync(path.join(explicit, 'packages', 'amalgm', 'package.json'))) {
10
+ return explicit;
11
+ }
12
+
13
+ const candidate = path.resolve(__dirname, '../../../..');
14
+ if (fs.existsSync(path.join(candidate, 'packages', 'amalgm', 'package.json'))) {
15
+ return candidate;
16
+ }
17
+
18
+ return null;
19
+ }
20
+
21
+ function engineNpmPublishWorkflow(repoPath) {
22
+ const escapedRepoPath = JSON.stringify(repoPath);
23
+ const repoUrl = process.env.AMALGM_ENGINE_REPO_URL || 'https://github.com/amalgm-inc/amalgm-engine.git';
24
+ const cloneLine = JSON.stringify(` git clone ${repoUrl} .`);
25
+ return `export default workflow({
26
+ trigger: event("github.push"),
27
+
28
+ allowlist: {
29
+ localCompute: true,
30
+ secrets: ["NODE_AUTH_TOKEN"],
31
+ actions: ["amalgm.notify_user"]
32
+ },
33
+
34
+ limits: {
35
+ maxConcurrentRuns: 1,
36
+ queueLimit: 5,
37
+ cellTimeoutMs: 600000
38
+ },
39
+
40
+ cells: [
41
+ code("filter_push", async ({ event, stop }) => {
42
+ const payload = event.payload
43
+ if (payload.ref !== "refs/heads/main") stop()
44
+
45
+ const commits = Array.isArray(payload.commits) ? payload.commits : []
46
+ const changed = Array.from(new Set([
47
+ ...(payload.head_commit?.added || []),
48
+ ...(payload.head_commit?.modified || []),
49
+ ...(payload.head_commit?.removed || []),
50
+ ...commits.flatMap((commit) => [
51
+ ...(commit.added || []),
52
+ ...(commit.modified || []),
53
+ ...(commit.removed || [])
54
+ ])
55
+ ]))
56
+
57
+ const relevant = changed.some((file) =>
58
+ file.startsWith("packages/amalgm/") ||
59
+ file.startsWith("runtime/") ||
60
+ file === "scripts/sync-npm-package-runtime.mjs"
61
+ )
62
+
63
+ if (!relevant) stop()
64
+
65
+ return {
66
+ sha: payload.after,
67
+ ref: payload.ref,
68
+ files: changed
69
+ }
70
+ }),
71
+
72
+ cli("update_repo", {
73
+ cwd: ${escapedRepoPath},
74
+ command: [
75
+ "if [ ! -d .git ]; then",
76
+ ${cloneLine},
77
+ "fi",
78
+ "git fetch origin main",
79
+ "git checkout main",
80
+ "git pull --ff-only origin main"
81
+ ].join("\\n")
82
+ }),
83
+
84
+ cli("sync_runtime", {
85
+ cwd: ${escapedRepoPath},
86
+ command: "node scripts/sync-npm-package-runtime.mjs"
87
+ }),
88
+
89
+ cli("stamp_version", {
90
+ cwd: ${escapedRepoPath} + "/packages/amalgm",
91
+ command: "npm version --allow-same-version --no-git-tag-version \\"0.1.$(date +%s)\\""
92
+ }),
93
+
94
+ cli("check_package", {
95
+ cwd: ${escapedRepoPath} + "/packages/amalgm",
96
+ command: "npm run check"
97
+ }),
98
+
99
+ cli("publish_package", {
100
+ cwd: ${escapedRepoPath} + "/packages/amalgm",
101
+ command: [
102
+ ": \\"${'$'}{NODE_AUTH_TOKEN:?Missing NODE_AUTH_TOKEN}\\"",
103
+ "printf '//registry.npmjs.org/:_authToken=${'$'}{NODE_AUTH_TOKEN}\\\\n' > .npmrc",
104
+ "trap 'rm -f .npmrc' EXIT",
105
+ "VERSION=\\"$(node -p \\"require('./package.json').version\\")\\"",
106
+ "if npm view \\"amalgm@${'$'}{VERSION}\\" version >/dev/null 2>&1; then",
107
+ " echo \\"amalgm@${'$'}{VERSION} already exists; skipping publish\\"",
108
+ "else",
109
+ " npm publish --access public --tag latest",
110
+ "fi",
111
+ "npm dist-tag add \\"amalgm@${'$'}{VERSION}\\" canary"
112
+ ].join("\\n"),
113
+ env: {
114
+ NODE_AUTH_TOKEN: ({ secrets }) => secrets.NODE_AUTH_TOKEN
115
+ },
116
+ timeoutMs: 600000
117
+ }),
118
+
119
+ tool("notify_user", "amalgm", "notify_user", {
120
+ subject: "amalgm npm publish complete",
121
+ level: "success",
122
+ message: ({ outputs }) => {
123
+ const publish = outputs.publish_package || {}
124
+ return [
125
+ "amalgm npm publish workflow completed.",
126
+ "",
127
+ "~~~",
128
+ String(publish.stdout || "").trim() || "No stdout captured.",
129
+ "~~~"
130
+ ].join("\\n")
131
+ }
132
+ })
133
+ ]
134
+ })`;
135
+ }
136
+
137
+ function internalWorkflowSeeds() {
138
+ if (!findEngineRepoPath()) return [];
139
+ const repoPath = process.env.AMALGM_ENGINE_PUBLISH_REPO_PATH
140
+ || path.join(os.homedir(), '.amalgm', 'workflow-checkouts', 'amalgm-engine-publish');
141
+
142
+ return [
143
+ {
144
+ id: 'internal-amalgm-engine-npm-publish',
145
+ name: 'Publish amalgm npm package',
146
+ description: 'Internal workflow: publish the amalgm npm package when amalgm-engine is pushed to main.',
147
+ source: 'github',
148
+ event: 'push',
149
+ projectPath: repoPath,
150
+ enabled: true,
151
+ internal: true,
152
+ workflowText: engineNpmPublishWorkflow(repoPath),
153
+ allowlist: {
154
+ localCompute: true,
155
+ secrets: ['NODE_AUTH_TOKEN'],
156
+ actions: ['amalgm.notify_user'],
157
+ },
158
+ limits: {
159
+ maxConcurrentRuns: 1,
160
+ queueLimit: 5,
161
+ cellTimeoutMs: 600000,
162
+ },
163
+ },
164
+ ];
165
+ }
166
+
167
+ module.exports = {
168
+ internalWorkflowSeeds,
169
+ };
@@ -50,25 +50,54 @@ function computeSignature(secret, rawBody) {
50
50
  }
51
51
 
52
52
  /**
53
- * Return the trigger whose secret verifies the provided auth proof, or null.
53
+ * Check whether a trigger's source/event selector matches an inbound event.
54
+ * Empty or "*" means "any" for either side.
54
55
  */
55
- function matchBySignature(triggers, signature, rawBody) {
56
+ function matchesEventRef(trigger, eventRef = {}) {
57
+ if (!trigger) return false;
58
+ const actualSource = String(eventRef.source || '').trim();
59
+ const actualEvent = String(eventRef.event || '').trim();
60
+ const wantedSource = String(trigger.source || '*').trim();
61
+ const wantedEvent = String(trigger.event || '*').trim();
62
+ return (
63
+ (!wantedSource || wantedSource === '*' || wantedSource === actualSource)
64
+ && (!wantedEvent || wantedEvent === '*' || wantedEvent === actualEvent)
65
+ );
66
+ }
67
+
68
+ function verifiesSignature(trigger, signature, rawBody) {
69
+ if (!trigger?.enabled || !trigger.secret) return false;
70
+ if (
71
+ signature.header === 'authorization'
72
+ || TOKEN_HEADERS.includes(signature.header)
73
+ ) {
74
+ return timingSafeEqual(signature.value, trigger.secret);
75
+ }
76
+
77
+ const expected = computeSignature(trigger.secret, rawBody);
78
+ return timingSafeEqual(signature.value, expected);
79
+ }
80
+
81
+ /**
82
+ * Return every trigger whose secret and optional source/event selector match.
83
+ */
84
+ function matchAllBySignature(triggers, signature, rawBody, eventRef = null) {
56
85
  if (!signature || typeof signature.value !== 'string') return null;
57
86
 
87
+ const matches = [];
58
88
  for (const trigger of triggers) {
59
- if (!trigger.enabled || !trigger.secret) continue;
60
- if (
61
- signature.header === 'authorization'
62
- || TOKEN_HEADERS.includes(signature.header)
63
- ) {
64
- if (timingSafeEqual(signature.value, trigger.secret)) return trigger;
65
- continue;
66
- }
67
-
68
- const expected = computeSignature(trigger.secret, rawBody);
69
- if (timingSafeEqual(signature.value, expected)) return trigger;
89
+ if (!verifiesSignature(trigger, signature, rawBody)) continue;
90
+ if (eventRef && !matchesEventRef(trigger, eventRef)) continue;
91
+ matches.push(trigger);
70
92
  }
71
- return null;
93
+ return matches;
94
+ }
95
+
96
+ /**
97
+ * Return the first trigger whose secret verifies the provided auth proof, or null.
98
+ */
99
+ function matchBySignature(triggers, signature, rawBody, eventRef = null) {
100
+ return matchAllBySignature(triggers, signature, rawBody, eventRef)?.[0] || null;
72
101
  }
73
102
 
74
103
  /**
@@ -119,7 +148,9 @@ function collectPassthroughHeaders(reqHeaders) {
119
148
  module.exports = {
120
149
  extractSignature,
121
150
  computeSignature,
151
+ matchAllBySignature,
122
152
  matchBySignature,
153
+ matchesEventRef,
123
154
  pickSourceLabel,
124
155
  collectPassthroughHeaders,
125
156
  };