amalgm 0.1.51 → 0.1.53
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/lib/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +547 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-binaries.js +34 -9
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser MCP tools. In desktop
|
|
3
|
-
* Electron browser surface; elsewhere they fall back to
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
30
|
-
const
|
|
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:${
|
|
33
|
+
const CHAT_SERVER_URL = `http://localhost:${process.env.CHAT_SERVER_PORT || 8084}`;
|
|
51
34
|
|
|
52
|
-
const SCHEDULER_INTERVAL_MS =
|
|
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
|
|
62
|
-
const
|
|
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 =
|
|
47
|
+
const PROXY_TOKEN = process.env.AMALGM_PROXY_TOKEN
|
|
72
48
|
|| (process.env.AMALGM_RUNTIME_SOURCE === 'npm' ? '' : process.env.ANTHROPIC_API_KEY || '');
|
|
73
|
-
const
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
APPS_DOMAIN,
|
|
121
|
+
APP_PORT_MIN,
|
|
122
|
+
APP_PORT_MAX,
|
|
138
123
|
GATEWAY_BASE_URL,
|
|
139
124
|
GATEWAY_INTERNAL_SECRET,
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
*
|
|
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 {
|
|
16
|
-
const {
|
|
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
|
|
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(
|
|
88
|
+
console.log(
|
|
89
|
+
`[AmalgmMCP:Events] Matched ${matchedTriggers.length} trigger(s) for ${eventSource}.${eventName}`,
|
|
90
|
+
);
|
|
71
91
|
|
|
72
92
|
const eventObj = {
|
|
73
|
-
source:
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
+
markTriggerFired(matchedTrigger.id, { source: 'events:ingress' });
|
|
110
|
+
}
|
|
109
111
|
|
|
110
|
-
return sendJson(200, {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
60
|
-
if (
|
|
61
|
-
|
|
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
|
|
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
|
};
|