amalgm 0.1.122 → 0.1.123
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/auth-store.js +73 -17
- package/lib/paths.js +4 -0
- package/lib/tunnel-chat.js +105 -12
- package/lib/tunnel-events.js +106 -12
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/apps/advertise.js +27 -3
- package/runtime/scripts/amalgm-mcp/config.js +18 -0
- package/runtime/scripts/local-gateway.js +30 -2
- package/runtime/scripts/proxy-token-store.js +33 -4
package/lib/auth-store.js
CHANGED
|
@@ -10,8 +10,11 @@ const {
|
|
|
10
10
|
AMALGM_RUNTIME_USER_SCOPE,
|
|
11
11
|
AUTH_FILE,
|
|
12
12
|
COMPUTER_FILE,
|
|
13
|
+
RUNTIME_AUTH_FILE,
|
|
14
|
+
RUNTIME_COMPUTER_FILE,
|
|
13
15
|
} = require('./paths');
|
|
14
16
|
const {
|
|
17
|
+
normalizeRuntimeLabel,
|
|
15
18
|
sanitizeScopeSegment,
|
|
16
19
|
scopedAmalgmDir,
|
|
17
20
|
} = require('./runtime-identity');
|
|
@@ -69,10 +72,10 @@ function hasComputerSecrets(record) {
|
|
|
69
72
|
return Array.from(SECRET_COMPUTER_KEYS).some((key) => cleanString(record[key]));
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
function publicComputerRecord(record) {
|
|
75
|
+
function publicComputerRecord(record, authFile = AUTH_FILE) {
|
|
73
76
|
const out = {
|
|
74
77
|
...(record || {}),
|
|
75
|
-
auth_file: path.basename(
|
|
78
|
+
auth_file: path.basename(authFile),
|
|
76
79
|
};
|
|
77
80
|
for (const key of SECRET_COMPUTER_KEYS) delete out[key];
|
|
78
81
|
return out;
|
|
@@ -172,15 +175,49 @@ function mergeRecordAuth(record, auth) {
|
|
|
172
175
|
return merged;
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
function runtimeLabelFromRecord(record) {
|
|
179
|
+
return normalizeRuntimeLabel(record?.runtime_label || record?.label || record?.branch);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recordMatchesCurrentRuntime(record) {
|
|
183
|
+
const label = runtimeLabelFromRecord(record);
|
|
184
|
+
if (!label) return AMALGM_RUNTIME_LABEL === 'main';
|
|
185
|
+
return label === AMALGM_RUNTIME_LABEL;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertRecordMatchesUserScope(record) {
|
|
189
|
+
const userId = cleanString(record?.user_id);
|
|
190
|
+
const expectedScope = cleanString(AMALGM_RUNTIME_USER_SCOPE);
|
|
191
|
+
if (!userId || !expectedScope || expectedScope === 'local') return;
|
|
192
|
+
if (userId === expectedScope) return;
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Refusing to save ${AMALGM_RUNTIME_LABEL} computer credentials for user ${userId} in scope ${expectedScope}.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadRecordPair(recordFile, authFile) {
|
|
199
|
+
const record = readJson(recordFile, null);
|
|
200
|
+
if (!record || typeof record !== 'object') return null;
|
|
201
|
+
return mergeRecordAuth(record, readJson(authFile, null));
|
|
202
|
+
}
|
|
203
|
+
|
|
175
204
|
function loadComputerRecord(options = {}) {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
const runtimeRecord = loadRecordPair(RUNTIME_COMPUTER_FILE, RUNTIME_AUTH_FILE);
|
|
206
|
+
if (runtimeRecord) return runtimeRecord;
|
|
207
|
+
|
|
208
|
+
const scopedRecord = readJson(COMPUTER_FILE, null);
|
|
209
|
+
if (scopedRecord && typeof scopedRecord === 'object' && recordMatchesCurrentRuntime(scopedRecord)) {
|
|
210
|
+
const merged = mergeRecordAuth(scopedRecord, readJson(AUTH_FILE, null));
|
|
211
|
+
if (options.migrate && (AMALGM_RUNTIME_LABEL === 'main' || hasComputerSecrets(scopedRecord))) {
|
|
212
|
+
saveComputerRecord(merged);
|
|
213
|
+
}
|
|
214
|
+
return merged;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const legacyRecord = loadLegacyComputerRecord();
|
|
218
|
+
if (!legacyRecord || typeof legacyRecord !== 'object') return null;
|
|
219
|
+
const merged = mergeRecordAuth(legacyRecord, readLegacyAuth());
|
|
220
|
+
if (options.migrate && (hasComputerSecrets(legacyRecord) || legacyRecord)) {
|
|
184
221
|
saveComputerRecord(merged);
|
|
185
222
|
}
|
|
186
223
|
return merged;
|
|
@@ -200,12 +237,27 @@ function readLegacyAuth() {
|
|
|
200
237
|
}
|
|
201
238
|
|
|
202
239
|
function saveComputerRecord(record) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
writeJsonSecret(
|
|
208
|
-
|
|
240
|
+
assertRecordMatchesUserScope(record);
|
|
241
|
+
|
|
242
|
+
const runtimeAuth = authFromRecord(record, readJson(RUNTIME_AUTH_FILE, null));
|
|
243
|
+
const runtimeRecord = publicComputerRecord(record, RUNTIME_AUTH_FILE);
|
|
244
|
+
writeJsonSecret(RUNTIME_COMPUTER_FILE, runtimeRecord);
|
|
245
|
+
writeJsonSecret(RUNTIME_AUTH_FILE, runtimeAuth);
|
|
246
|
+
|
|
247
|
+
if (AMALGM_RUNTIME_LABEL === 'main') {
|
|
248
|
+
const sharedAuth = authFromRecord(record, readJson(AUTH_FILE, null));
|
|
249
|
+
const sharedRecord = publicComputerRecord(record, AUTH_FILE);
|
|
250
|
+
writeJsonSecret(COMPUTER_FILE, sharedRecord);
|
|
251
|
+
writeJsonSecret(AUTH_FILE, sharedAuth);
|
|
252
|
+
mirrorLocalRecordToUserScope(sharedRecord, sharedAuth);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const nextRecord = AMALGM_RUNTIME_LABEL === 'main'
|
|
256
|
+
? publicComputerRecord(record, AUTH_FILE)
|
|
257
|
+
: runtimeRecord;
|
|
258
|
+
const nextAuth = AMALGM_RUNTIME_LABEL === 'main'
|
|
259
|
+
? authFromRecord(record, readJson(AUTH_FILE, null))
|
|
260
|
+
: runtimeAuth;
|
|
209
261
|
return mergeRecordAuth(nextRecord, nextAuth);
|
|
210
262
|
}
|
|
211
263
|
|
|
@@ -219,7 +271,9 @@ function mirrorLocalRecordToUserScope(record, auth) {
|
|
|
219
271
|
}
|
|
220
272
|
|
|
221
273
|
function deleteComputerAuth() {
|
|
222
|
-
|
|
274
|
+
const files = [RUNTIME_COMPUTER_FILE, RUNTIME_AUTH_FILE];
|
|
275
|
+
if (AMALGM_RUNTIME_LABEL === 'main') files.push(COMPUTER_FILE, AUTH_FILE);
|
|
276
|
+
for (const file of files) {
|
|
223
277
|
try {
|
|
224
278
|
fs.unlinkSync(file);
|
|
225
279
|
} catch {
|
|
@@ -253,5 +307,7 @@ module.exports = {
|
|
|
253
307
|
loadComputerRecord,
|
|
254
308
|
proxyTokenFromRecord,
|
|
255
309
|
proxyTokenNeedsRefresh,
|
|
310
|
+
RUNTIME_AUTH_FILE,
|
|
311
|
+
RUNTIME_COMPUTER_FILE,
|
|
256
312
|
saveComputerRecord,
|
|
257
313
|
};
|
package/lib/paths.js
CHANGED
|
@@ -37,6 +37,8 @@ const AMALGM_RUNTIME_INSTANCE_ID = process.env.AMALGM_RUNTIME_INSTANCE_ID
|
|
|
37
37
|
const DEVICE_FILE = path.join(AMALGM_HOME, 'device.json');
|
|
38
38
|
const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
|
|
39
39
|
const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
|
|
40
|
+
const RUNTIME_COMPUTER_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'computer.json');
|
|
41
|
+
const RUNTIME_AUTH_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'auth.json');
|
|
40
42
|
const RUNTIME_TOKEN_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'runtime-token.json');
|
|
41
43
|
const PID_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm.pid');
|
|
42
44
|
const RUNTIME_STATE_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'runtime-state.json');
|
|
@@ -66,6 +68,8 @@ module.exports = {
|
|
|
66
68
|
PACKAGE_ROOT,
|
|
67
69
|
PID_FILE,
|
|
68
70
|
RUNTIME_DIR,
|
|
71
|
+
RUNTIME_AUTH_FILE,
|
|
72
|
+
RUNTIME_COMPUTER_FILE,
|
|
69
73
|
RUNTIME_STATE_FILE,
|
|
70
74
|
RUNTIME_TOKEN_FILE,
|
|
71
75
|
SERVICE_DIR,
|
package/lib/tunnel-chat.js
CHANGED
|
@@ -11,15 +11,52 @@ const {
|
|
|
11
11
|
} = require('./paths');
|
|
12
12
|
const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const DEFAULT_GATEWAY_PORT = runtimeServiceByName('local-gateway').defaultPort;
|
|
15
|
+
const DEFAULT_PORT_MONITOR_PORT = runtimeServiceByName('port-monitor').defaultPort;
|
|
16
|
+
const DEFAULT_MCP_PORT = runtimeServiceByName('amalgm-mcp').defaultPort;
|
|
17
|
+
const DEFAULT_CHAT_SERVER_PORT = runtimeServiceByName('chat-server').defaultPort;
|
|
18
|
+
const DEFAULT_TARGET_PORT = DEFAULT_CHAT_SERVER_PORT;
|
|
15
19
|
const DEFAULT_CHAT_TUNNEL_URL = 'wss://amalgm-chat-gateway.fly.dev/wire';
|
|
16
20
|
const CONNECT_TIMEOUT_MS = 20_000;
|
|
17
21
|
const WATCHDOG_INTERVAL_MS = 15_000;
|
|
18
22
|
const HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const DEFAULT_RUNTIME_PORTS = new Map([
|
|
24
|
+
['gateway', DEFAULT_GATEWAY_PORT],
|
|
25
|
+
['portMonitor', DEFAULT_PORT_MONITOR_PORT],
|
|
26
|
+
['mcp', DEFAULT_MCP_PORT],
|
|
27
|
+
['chat', DEFAULT_CHAT_SERVER_PORT],
|
|
22
28
|
]);
|
|
29
|
+
const GATEWAY_PREFIXES = ['/__amalgm', '/diagnostics', '/pty'];
|
|
30
|
+
const GATEWAY_EXACT_PATHS = new Set(['/', '/healthz', '/runtime-state', '/pty/session', '/pty/resize']);
|
|
31
|
+
const PORT_MONITOR_PREFIXES = ['/api/ports', '/ports', '/port-monitor'];
|
|
32
|
+
const CHAT_PREFIXES = ['/chat', '/raw-temp', '/active', '/stop', '/destroy', '/egress', '/mcp-relay'];
|
|
33
|
+
const MCP_PREFIXES = [
|
|
34
|
+
'/mcp',
|
|
35
|
+
'/events',
|
|
36
|
+
'/tasks',
|
|
37
|
+
'/automations',
|
|
38
|
+
'/event-triggers',
|
|
39
|
+
'/workspace',
|
|
40
|
+
'/project-context',
|
|
41
|
+
'/github',
|
|
42
|
+
'/fs',
|
|
43
|
+
'/mcp-connections',
|
|
44
|
+
'/toolbox',
|
|
45
|
+
'/state',
|
|
46
|
+
'/local',
|
|
47
|
+
'/credentials',
|
|
48
|
+
'/chat-payloads',
|
|
49
|
+
'/email',
|
|
50
|
+
'/slack',
|
|
51
|
+
'/user-api-keys',
|
|
52
|
+
'/apps',
|
|
53
|
+
'/artifacts',
|
|
54
|
+
'/agents',
|
|
55
|
+
'/agent-config',
|
|
56
|
+
'/agent-bundles',
|
|
57
|
+
'/user-preferences',
|
|
58
|
+
'/browser',
|
|
59
|
+
];
|
|
23
60
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
24
61
|
'connection',
|
|
25
62
|
'keep-alive',
|
|
@@ -74,23 +111,69 @@ function readRuntimePorts() {
|
|
|
74
111
|
const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
|
|
75
112
|
return {
|
|
76
113
|
gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || runtimePort('local-gateway'),
|
|
114
|
+
portMonitor: Number(parsed?.ports?.port_monitor) || runtimePort('port-monitor'),
|
|
77
115
|
mcp: Number(parsed?.ports?.amalgm_mcp) || runtimePort('amalgm-mcp'),
|
|
78
116
|
chat: Number(parsed?.ports?.chat_server) || runtimePort('chat-server'),
|
|
79
117
|
};
|
|
80
118
|
} catch {
|
|
81
119
|
return {
|
|
82
120
|
gateway: runtimePort('local-gateway'),
|
|
121
|
+
portMonitor: runtimePort('port-monitor'),
|
|
83
122
|
mcp: runtimePort('amalgm-mcp'),
|
|
84
123
|
chat: runtimePort('chat-server'),
|
|
85
124
|
};
|
|
86
125
|
}
|
|
87
126
|
}
|
|
88
127
|
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
function validPort(value) {
|
|
129
|
+
const port = Number(value);
|
|
130
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function prefixMatches(pathname, prefix) {
|
|
134
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function framePathname(framePath) {
|
|
138
|
+
try {
|
|
139
|
+
return new URL(framePath || '/', 'http://127.0.0.1').pathname || '/';
|
|
140
|
+
} catch {
|
|
141
|
+
return '/';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function serviceForRuntimePath(framePath) {
|
|
146
|
+
const pathname = framePathname(framePath);
|
|
147
|
+
if (GATEWAY_EXACT_PATHS.has(pathname) || GATEWAY_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) {
|
|
148
|
+
return 'gateway';
|
|
149
|
+
}
|
|
150
|
+
if (PORT_MONITOR_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'portMonitor';
|
|
151
|
+
if (CHAT_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'chat';
|
|
152
|
+
if (MCP_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'mcp';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function defaultServiceForPort(port) {
|
|
157
|
+
if (port === DEFAULT_GATEWAY_PORT) return 'gateway';
|
|
158
|
+
if (port === DEFAULT_PORT_MONITOR_PORT) return 'portMonitor';
|
|
159
|
+
if (port === DEFAULT_MCP_PORT) return 'mcp';
|
|
160
|
+
if (port === DEFAULT_CHAT_SERVER_PORT) return 'chat';
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runtimePortForService(runtimePorts, service) {
|
|
165
|
+
return validPort(runtimePorts?.[service]) || validPort(DEFAULT_RUNTIME_PORTS.get(service));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isDeclaredRuntimePort(port, runtimePorts) {
|
|
169
|
+
return ['gateway', 'portMonitor', 'mcp', 'chat'].some((service) => validPort(runtimePorts?.[service]) === port);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function allowedTargetPorts(runtimePorts = readRuntimePorts()) {
|
|
173
|
+
const ports = new Set();
|
|
174
|
+
for (const service of ['gateway', 'portMonitor', 'mcp', 'chat']) {
|
|
175
|
+
const port = runtimePortForService(runtimePorts, service);
|
|
176
|
+
if (port) ports.add(port);
|
|
94
177
|
}
|
|
95
178
|
return ports;
|
|
96
179
|
}
|
|
@@ -196,9 +279,19 @@ function createChatTunnel({ record, foreground = false }) {
|
|
|
196
279
|
}
|
|
197
280
|
|
|
198
281
|
function resolveTargetPort(frame) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
return
|
|
282
|
+
const runtimePorts = readRuntimePorts();
|
|
283
|
+
const requested = validPort(frame.target_port);
|
|
284
|
+
if (requested && isDeclaredRuntimePort(requested, runtimePorts)) return requested;
|
|
285
|
+
|
|
286
|
+
const defaultService = requested ? defaultServiceForPort(requested) : 'chat';
|
|
287
|
+
const pathService = (!requested || defaultService) ? serviceForRuntimePath(frame.path) : null;
|
|
288
|
+
const service = pathService || defaultService;
|
|
289
|
+
if (service) {
|
|
290
|
+
return runtimePortForService(runtimePorts, service);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!requested) return null;
|
|
294
|
+
return allowedTargetPorts(runtimePorts).has(requested) ? requested : null;
|
|
202
295
|
}
|
|
203
296
|
|
|
204
297
|
function scheduleReconnect() {
|
package/lib/tunnel-events.js
CHANGED
|
@@ -14,14 +14,51 @@ const {
|
|
|
14
14
|
} = require('./paths');
|
|
15
15
|
const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const DEFAULT_GATEWAY_PORT = runtimeServiceByName('local-gateway').defaultPort;
|
|
18
|
+
const DEFAULT_PORT_MONITOR_PORT = runtimeServiceByName('port-monitor').defaultPort;
|
|
19
|
+
const DEFAULT_MCP_PORT = runtimeServiceByName('amalgm-mcp').defaultPort;
|
|
20
|
+
const DEFAULT_CHAT_SERVER_PORT = runtimeServiceByName('chat-server').defaultPort;
|
|
21
|
+
const DEFAULT_TARGET_PORT = DEFAULT_MCP_PORT;
|
|
18
22
|
const CONNECT_TIMEOUT_MS = 20_000;
|
|
19
23
|
const WATCHDOG_INTERVAL_MS = 15_000;
|
|
20
24
|
const HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const DEFAULT_RUNTIME_PORTS = new Map([
|
|
26
|
+
['gateway', DEFAULT_GATEWAY_PORT],
|
|
27
|
+
['portMonitor', DEFAULT_PORT_MONITOR_PORT],
|
|
28
|
+
['mcp', DEFAULT_MCP_PORT],
|
|
29
|
+
['chat', DEFAULT_CHAT_SERVER_PORT],
|
|
24
30
|
]);
|
|
31
|
+
const GATEWAY_PREFIXES = ['/__amalgm', '/diagnostics', '/pty'];
|
|
32
|
+
const GATEWAY_EXACT_PATHS = new Set(['/', '/healthz', '/runtime-state', '/pty/session', '/pty/resize']);
|
|
33
|
+
const PORT_MONITOR_PREFIXES = ['/api/ports', '/ports', '/port-monitor'];
|
|
34
|
+
const CHAT_PREFIXES = ['/chat', '/raw-temp', '/active', '/stop', '/destroy', '/egress', '/mcp-relay'];
|
|
35
|
+
const MCP_PREFIXES = [
|
|
36
|
+
'/mcp',
|
|
37
|
+
'/events',
|
|
38
|
+
'/tasks',
|
|
39
|
+
'/automations',
|
|
40
|
+
'/event-triggers',
|
|
41
|
+
'/workspace',
|
|
42
|
+
'/project-context',
|
|
43
|
+
'/github',
|
|
44
|
+
'/fs',
|
|
45
|
+
'/mcp-connections',
|
|
46
|
+
'/toolbox',
|
|
47
|
+
'/state',
|
|
48
|
+
'/local',
|
|
49
|
+
'/credentials',
|
|
50
|
+
'/chat-payloads',
|
|
51
|
+
'/email',
|
|
52
|
+
'/slack',
|
|
53
|
+
'/user-api-keys',
|
|
54
|
+
'/apps',
|
|
55
|
+
'/artifacts',
|
|
56
|
+
'/agents',
|
|
57
|
+
'/agent-config',
|
|
58
|
+
'/agent-bundles',
|
|
59
|
+
'/user-preferences',
|
|
60
|
+
'/browser',
|
|
61
|
+
];
|
|
25
62
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
26
63
|
'connection',
|
|
27
64
|
'keep-alive',
|
|
@@ -183,23 +220,69 @@ function readRuntimePorts() {
|
|
|
183
220
|
const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
|
|
184
221
|
return {
|
|
185
222
|
gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || runtimePort('local-gateway'),
|
|
223
|
+
portMonitor: Number(parsed?.ports?.port_monitor) || runtimePort('port-monitor'),
|
|
186
224
|
mcp: Number(parsed?.ports?.amalgm_mcp) || runtimePort('amalgm-mcp'),
|
|
187
225
|
chat: Number(parsed?.ports?.chat_server) || runtimePort('chat-server'),
|
|
188
226
|
};
|
|
189
227
|
} catch {
|
|
190
228
|
return {
|
|
191
229
|
gateway: runtimePort('local-gateway'),
|
|
230
|
+
portMonitor: runtimePort('port-monitor'),
|
|
192
231
|
mcp: runtimePort('amalgm-mcp'),
|
|
193
232
|
chat: runtimePort('chat-server'),
|
|
194
233
|
};
|
|
195
234
|
}
|
|
196
235
|
}
|
|
197
236
|
|
|
198
|
-
function
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
237
|
+
function validPort(value) {
|
|
238
|
+
const port = Number(value);
|
|
239
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function prefixMatches(pathname, prefix) {
|
|
243
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function framePathname(framePath) {
|
|
247
|
+
try {
|
|
248
|
+
return new URL(framePath || '/', 'http://127.0.0.1').pathname || '/';
|
|
249
|
+
} catch {
|
|
250
|
+
return '/';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function serviceForRuntimePath(framePath) {
|
|
255
|
+
const pathname = framePathname(framePath);
|
|
256
|
+
if (GATEWAY_EXACT_PATHS.has(pathname) || GATEWAY_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) {
|
|
257
|
+
return 'gateway';
|
|
258
|
+
}
|
|
259
|
+
if (PORT_MONITOR_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'portMonitor';
|
|
260
|
+
if (CHAT_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'chat';
|
|
261
|
+
if (MCP_PREFIXES.some((prefix) => prefixMatches(pathname, prefix))) return 'mcp';
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function defaultServiceForPort(port) {
|
|
266
|
+
if (port === DEFAULT_GATEWAY_PORT) return 'gateway';
|
|
267
|
+
if (port === DEFAULT_PORT_MONITOR_PORT) return 'portMonitor';
|
|
268
|
+
if (port === DEFAULT_MCP_PORT) return 'mcp';
|
|
269
|
+
if (port === DEFAULT_CHAT_SERVER_PORT) return 'chat';
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function runtimePortForService(runtimePorts, service) {
|
|
274
|
+
return validPort(runtimePorts?.[service]) || validPort(DEFAULT_RUNTIME_PORTS.get(service));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isDeclaredRuntimePort(port, runtimePorts) {
|
|
278
|
+
return ['gateway', 'portMonitor', 'mcp', 'chat'].some((service) => validPort(runtimePorts?.[service]) === port);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function allowedTargetPorts(runtimePorts = readRuntimePorts()) {
|
|
282
|
+
const ports = new Set();
|
|
283
|
+
for (const service of ['gateway', 'portMonitor', 'mcp', 'chat']) {
|
|
284
|
+
const port = runtimePortForService(runtimePorts, service);
|
|
285
|
+
if (port) ports.add(port);
|
|
203
286
|
}
|
|
204
287
|
for (const route of readAppRoutes()) ports.add(route.port);
|
|
205
288
|
return ports;
|
|
@@ -216,9 +299,20 @@ function runtimeGatewayPort() {
|
|
|
216
299
|
}
|
|
217
300
|
|
|
218
301
|
function resolveTargetPort(frame, fallbackPort = defaultTargetPort()) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
302
|
+
const runtimePorts = readRuntimePorts();
|
|
303
|
+
const requested = validPort(frame.target_port);
|
|
304
|
+
if (requested && isDeclaredRuntimePort(requested, runtimePorts)) return requested;
|
|
305
|
+
|
|
306
|
+
const fallbackService = fallbackPort === defaultTargetPort() ? 'mcp' : defaultServiceForPort(fallbackPort);
|
|
307
|
+
const defaultService = requested ? defaultServiceForPort(requested) : fallbackService;
|
|
308
|
+
const pathService = (!requested || defaultService) ? serviceForRuntimePath(frame.path) : null;
|
|
309
|
+
const service = pathService || defaultService;
|
|
310
|
+
if (service) {
|
|
311
|
+
return runtimePortForService(runtimePorts, service);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!requested) return null;
|
|
315
|
+
return allowedTargetPorts(runtimePorts).has(requested) ? requested : null;
|
|
222
316
|
}
|
|
223
317
|
|
|
224
318
|
function createEventTunnel({ record, foreground = false }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const {
|
|
2
2
|
COMPUTER_AUTH_FILE,
|
|
3
3
|
COMPUTER_RECORD_FILE,
|
|
4
|
+
RUNTIME_COMPUTER_AUTH_FILE,
|
|
5
|
+
RUNTIME_COMPUTER_RECORD_FILE,
|
|
6
|
+
AMALGM_RUNTIME_LABEL,
|
|
4
7
|
GATEWAY_BASE_URL,
|
|
5
8
|
GATEWAY_INTERNAL_SECRET,
|
|
6
9
|
APP_ROUTE_SYNC_INTERVAL_MS,
|
|
@@ -19,10 +22,24 @@ function warnOnce(key, message) {
|
|
|
19
22
|
console.warn(message);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
function
|
|
23
|
-
const
|
|
25
|
+
function normalizeRuntimeLabel(value) {
|
|
26
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
27
|
+
if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
|
|
28
|
+
if (['local', 'dev', 'development'].includes(raw)) return 'local';
|
|
29
|
+
return 'main';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function recordMatchesRuntime(record) {
|
|
33
|
+
const label = normalizeRuntimeLabel(record?.runtime_label || record?.label || record?.branch);
|
|
34
|
+
if (!label) return AMALGM_RUNTIME_LABEL === 'main';
|
|
35
|
+
return label === AMALGM_RUNTIME_LABEL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadComputerRecordPair(recordFile, authFile) {
|
|
39
|
+
const record = readJson(recordFile, null);
|
|
24
40
|
if (!record || typeof record !== 'object') return null;
|
|
25
|
-
|
|
41
|
+
if (!recordMatchesRuntime(record)) return null;
|
|
42
|
+
const auth = readJson(authFile, null);
|
|
26
43
|
if (!auth || typeof auth !== 'object') return record;
|
|
27
44
|
if (auth.computer_id && record.computer_id && auth.computer_id !== record.computer_id) return record;
|
|
28
45
|
return {
|
|
@@ -31,6 +48,13 @@ function loadComputerRecord() {
|
|
|
31
48
|
};
|
|
32
49
|
}
|
|
33
50
|
|
|
51
|
+
function loadComputerRecord() {
|
|
52
|
+
return (
|
|
53
|
+
loadComputerRecordPair(RUNTIME_COMPUTER_RECORD_FILE, RUNTIME_COMPUTER_AUTH_FILE)
|
|
54
|
+
|| loadComputerRecordPair(COMPUTER_RECORD_FILE, COMPUTER_AUTH_FILE)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
async function postAppRoutes(computerId, routes, authHeaders) {
|
|
35
59
|
const body = JSON.stringify({ routes });
|
|
36
60
|
const headers = {
|
|
@@ -15,6 +15,9 @@ const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
|
15
15
|
|
|
16
16
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
17
17
|
const AMALGM_HOME = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
|
|
18
|
+
const AMALGM_RUNTIME_LABEL = normalizeRuntimeLabel(process.env.AMALGM_RUNTIME_LABEL || process.env.AMALGM_BRANCH);
|
|
19
|
+
const AMALGM_RUNTIME_STATE_DIR = process.env.AMALGM_RUNTIME_STATE_DIR
|
|
20
|
+
|| path.join(AMALGM_DIR, 'runtimes', AMALGM_RUNTIME_LABEL);
|
|
18
21
|
|
|
19
22
|
// Storage file/dir layout under AMALGM_DIR.
|
|
20
23
|
const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
|
|
@@ -26,6 +29,9 @@ const APPS_FILE = path.join(STORAGE_DIR, 'apps.json');
|
|
|
26
29
|
const LEGACY_ARTIFACTS_DIR = path.join(STORAGE_DIR, 'artifacts');
|
|
27
30
|
const LEGACY_ARTIFACTS_FILE = path.join(STORAGE_DIR, 'artifacts.json');
|
|
28
31
|
const COMPUTER_RECORD_FILE = path.join(STORAGE_DIR, 'computer.json');
|
|
32
|
+
const COMPUTER_AUTH_FILE = path.join(STORAGE_DIR, 'auth.json');
|
|
33
|
+
const RUNTIME_COMPUTER_RECORD_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'computer.json');
|
|
34
|
+
const RUNTIME_COMPUTER_AUTH_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'auth.json');
|
|
29
35
|
const TASK_RUNS_DIR = path.join(STORAGE_DIR, 'task-runs');
|
|
30
36
|
const EVENT_TRIGGERS_FILE = path.join(STORAGE_DIR, 'event-triggers.json');
|
|
31
37
|
const WORKFLOWS_FILE = path.join(STORAGE_DIR, 'workflows.json');
|
|
@@ -92,6 +98,13 @@ const APP_ROUTE_SYNC_INTERVAL_MS = parseInt(
|
|
|
92
98
|
10,
|
|
93
99
|
);
|
|
94
100
|
|
|
101
|
+
function normalizeRuntimeLabel(value) {
|
|
102
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
103
|
+
if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
|
|
104
|
+
if (['local', 'dev', 'development'].includes(raw)) return 'local';
|
|
105
|
+
return 'main';
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
function copyIfUseful(source, target) {
|
|
96
109
|
if (!fs.existsSync(source)) return;
|
|
97
110
|
if (fs.existsSync(target) && fs.statSync(target).size > 64 * 1024) return;
|
|
@@ -127,6 +140,8 @@ module.exports = {
|
|
|
127
140
|
MCP_PROTOCOL_VERSION,
|
|
128
141
|
AMALGM_DIR,
|
|
129
142
|
AMALGM_HOME,
|
|
143
|
+
AMALGM_RUNTIME_LABEL,
|
|
144
|
+
AMALGM_RUNTIME_STATE_DIR,
|
|
130
145
|
STORAGE_DIR,
|
|
131
146
|
LOCAL_DB_FILE,
|
|
132
147
|
TASKS_FILE,
|
|
@@ -136,6 +151,9 @@ module.exports = {
|
|
|
136
151
|
LEGACY_ARTIFACTS_DIR,
|
|
137
152
|
LEGACY_ARTIFACTS_FILE,
|
|
138
153
|
COMPUTER_RECORD_FILE,
|
|
154
|
+
COMPUTER_AUTH_FILE,
|
|
155
|
+
RUNTIME_COMPUTER_RECORD_FILE,
|
|
156
|
+
RUNTIME_COMPUTER_AUTH_FILE,
|
|
139
157
|
TASK_RUNS_DIR,
|
|
140
158
|
EVENT_TRIGGERS_FILE,
|
|
141
159
|
WORKFLOWS_FILE,
|
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
const { runtimePort } = require('../lib/runtime-manifest');
|
|
26
26
|
const {
|
|
27
27
|
runtimeLogDir,
|
|
28
|
+
runtimeStateDir,
|
|
28
29
|
runtimeStateFile,
|
|
29
30
|
} = require('./lib/runtime-paths');
|
|
30
31
|
|
|
@@ -50,6 +51,7 @@ const pty = loadPty();
|
|
|
50
51
|
|
|
51
52
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
52
53
|
const STATE_FILE = runtimeStateFile();
|
|
54
|
+
const RUNTIME_COMPUTER_FILE = path.join(runtimeStateDir(), 'computer.json');
|
|
53
55
|
const APPS_FILE = path.join(AMALGM_DIR, 'apps.json');
|
|
54
56
|
const LEGACY_ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
|
|
55
57
|
const LOG_DIR = runtimeLogDir();
|
|
@@ -159,11 +161,13 @@ const MCP_PREFIXES = [
|
|
|
159
161
|
'/artifacts',
|
|
160
162
|
'/agents',
|
|
161
163
|
'/agent-config',
|
|
164
|
+
'/agent-bundles',
|
|
162
165
|
'/user-preferences',
|
|
163
166
|
'/browser',
|
|
164
167
|
];
|
|
165
168
|
|
|
166
169
|
const PORT_MONITOR_PREFIXES = [
|
|
170
|
+
'/api/ports',
|
|
167
171
|
'/ports',
|
|
168
172
|
'/port-monitor',
|
|
169
173
|
];
|
|
@@ -339,8 +343,29 @@ function ensurePtySpawnHelperExecutable() {
|
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
|
|
346
|
+
function normalizeRuntimeLabel(value) {
|
|
347
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
348
|
+
if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
|
|
349
|
+
if (['local', 'dev', 'development'].includes(raw)) return 'local';
|
|
350
|
+
return 'main';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function computerRecordMatchesRuntime(record) {
|
|
354
|
+
const label = normalizeRuntimeLabel(record?.runtime_label || record?.label || record?.branch);
|
|
355
|
+
return label === normalizeRuntimeLabel(RUNTIME_LABEL);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function loadComputerRecord() {
|
|
359
|
+
const runtimeComputer = readJson(RUNTIME_COMPUTER_FILE, null);
|
|
360
|
+
if (runtimeComputer && computerRecordMatchesRuntime(runtimeComputer)) return runtimeComputer;
|
|
361
|
+
|
|
362
|
+
const sharedComputer = readJson(path.join(AMALGM_DIR, 'computer.json'), null);
|
|
363
|
+
if (sharedComputer && computerRecordMatchesRuntime(sharedComputer)) return sharedComputer;
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
342
367
|
function writeRuntimeState(actualPort) {
|
|
343
|
-
const computer =
|
|
368
|
+
const computer = loadComputerRecord();
|
|
344
369
|
const previous = readJson(STATE_FILE, {});
|
|
345
370
|
if (!runtimeStateOwnedHere(previous)) {
|
|
346
371
|
console.warn('[local-gateway] runtime-state belongs to another Amalgm runtime; skipping write');
|
|
@@ -606,9 +631,12 @@ function serviceForPath(pathname) {
|
|
|
606
631
|
return { port: SERVICE_PORTS.mcp, stripPrefix: '' };
|
|
607
632
|
}
|
|
608
633
|
if (PORT_MONITOR_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
|
|
634
|
+
const stripPrefix = pathname.startsWith('/port-monitor')
|
|
635
|
+
? '/port-monitor'
|
|
636
|
+
: (pathname.startsWith('/ports') ? '/ports' : '');
|
|
609
637
|
return {
|
|
610
638
|
port: SERVICE_PORTS.portMonitor,
|
|
611
|
-
stripPrefix
|
|
639
|
+
stripPrefix,
|
|
612
640
|
};
|
|
613
641
|
}
|
|
614
642
|
return null;
|
|
@@ -8,8 +8,13 @@ const DEFAULT_PROXY_BASE_URL = 'https://amalgm-api-proxy-v2.fly.dev';
|
|
|
8
8
|
const REFRESH_BUFFER_MS = 10 * 60 * 1000;
|
|
9
9
|
|
|
10
10
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
11
|
+
const AMALGM_RUNTIME_LABEL = normalizeRuntimeLabel(process.env.AMALGM_RUNTIME_LABEL || process.env.AMALGM_BRANCH);
|
|
12
|
+
const AMALGM_RUNTIME_STATE_DIR = process.env.AMALGM_RUNTIME_STATE_DIR
|
|
13
|
+
|| path.join(AMALGM_DIR, 'runtimes', AMALGM_RUNTIME_LABEL);
|
|
11
14
|
const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
|
|
12
15
|
const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
|
|
16
|
+
const RUNTIME_COMPUTER_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'computer.json');
|
|
17
|
+
const RUNTIME_AUTH_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'auth.json');
|
|
13
18
|
|
|
14
19
|
let refreshPromise = null;
|
|
15
20
|
|
|
@@ -37,15 +42,38 @@ function cleanString(value) {
|
|
|
37
42
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
|
|
45
|
+
function normalizeRuntimeLabel(value) {
|
|
46
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
47
|
+
if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
|
|
48
|
+
if (['local', 'dev', 'development'].includes(raw)) return 'local';
|
|
49
|
+
return 'main';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function recordMatchesRuntime(record) {
|
|
53
|
+
const label = normalizeRuntimeLabel(record?.runtime_label || record?.label || record?.branch);
|
|
54
|
+
if (!label) return AMALGM_RUNTIME_LABEL === 'main';
|
|
55
|
+
return label === AMALGM_RUNTIME_LABEL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadAuthPair(recordFile, authFile) {
|
|
59
|
+
const record = readJson(recordFile, null);
|
|
60
|
+
if (!record || typeof record !== 'object') return null;
|
|
61
|
+
if (!recordMatchesRuntime(record)) return null;
|
|
62
|
+
const auth = readJson(authFile, null) || {};
|
|
43
63
|
if (record.computer_id && auth.computer_id && record.computer_id !== auth.computer_id) {
|
|
44
64
|
return { record, auth: {} };
|
|
45
65
|
}
|
|
46
66
|
return { record, auth };
|
|
47
67
|
}
|
|
48
68
|
|
|
69
|
+
function loadAuthContext() {
|
|
70
|
+
return (
|
|
71
|
+
loadAuthPair(RUNTIME_COMPUTER_FILE, RUNTIME_AUTH_FILE)
|
|
72
|
+
|| loadAuthPair(COMPUTER_FILE, AUTH_FILE)
|
|
73
|
+
|| { record: {}, auth: {} }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
function proxyRefreshContext(record, auth) {
|
|
50
78
|
return {
|
|
51
79
|
machineToken:
|
|
@@ -128,7 +156,8 @@ async function refreshProxyToken() {
|
|
|
128
156
|
},
|
|
129
157
|
updated_at: now,
|
|
130
158
|
};
|
|
131
|
-
|
|
159
|
+
const targetAuthFile = fs.existsSync(RUNTIME_COMPUTER_FILE) ? RUNTIME_AUTH_FILE : AUTH_FILE;
|
|
160
|
+
writeJsonSecret(targetAuthFile, nextAuth);
|
|
132
161
|
return cleanString(nextAuth.proxy.token);
|
|
133
162
|
}
|
|
134
163
|
|