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 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(AUTH_FILE),
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 record = readJson(COMPUTER_FILE, null);
177
- const scopedRecord = record && typeof record === 'object' ? record : null;
178
- const legacyRecord = !scopedRecord ? loadLegacyComputerRecord() : null;
179
- const activeRecord = scopedRecord || legacyRecord;
180
- if (!activeRecord || typeof activeRecord !== 'object') return null;
181
- const auth = scopedRecord ? readJson(AUTH_FILE, null) : readLegacyAuth();
182
- const merged = mergeRecordAuth(activeRecord, auth);
183
- if (options.migrate && (hasComputerSecrets(activeRecord) || legacyRecord)) {
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
- const existingAuth = readJson(AUTH_FILE, null);
204
- const nextAuth = authFromRecord(record, existingAuth);
205
- const nextRecord = publicComputerRecord(record);
206
- writeJsonSecret(COMPUTER_FILE, nextRecord);
207
- writeJsonSecret(AUTH_FILE, nextAuth);
208
- mirrorLocalRecordToUserScope(nextRecord, nextAuth);
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
- for (const file of [COMPUTER_FILE, AUTH_FILE]) {
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,
@@ -11,15 +11,52 @@ const {
11
11
  } = require('./paths');
12
12
  const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
13
13
 
14
- const DEFAULT_TARGET_PORT = runtimeServiceByName('chat-server').defaultPort;
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 ALLOWED_TARGET_PORTS = new Set([
20
- runtimeServiceByName('amalgm-mcp').defaultPort,
21
- runtimeServiceByName('chat-server').defaultPort,
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 allowedTargetPorts() {
90
- const ports = new Set(ALLOWED_TARGET_PORTS);
91
- const runtimePorts = readRuntimePorts();
92
- for (const port of [runtimePorts.gateway, runtimePorts.mcp, runtimePorts.chat]) {
93
- if (Number.isInteger(port) && port > 0 && port <= 65535) ports.add(port);
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 candidate = frame.target_port == null ? defaultTargetPort() : Number(frame.target_port);
200
- if (!Number.isInteger(candidate) || !allowedTargetPorts().has(candidate)) return null;
201
- return candidate;
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() {
@@ -14,14 +14,51 @@ const {
14
14
  } = require('./paths');
15
15
  const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
16
16
 
17
- const DEFAULT_TARGET_PORT = runtimeServiceByName('amalgm-mcp').defaultPort;
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 CORE_TARGET_PORTS = new Set([
22
- runtimeServiceByName('amalgm-mcp').defaultPort,
23
- runtimeServiceByName('chat-server').defaultPort,
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 allowedTargetPorts() {
199
- const ports = new Set(CORE_TARGET_PORTS);
200
- const runtimePorts = readRuntimePorts();
201
- for (const port of [runtimePorts.gateway, runtimePorts.mcp, runtimePorts.chat]) {
202
- if (Number.isInteger(port) && port > 0 && port <= 65535) ports.add(port);
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 candidate = frame.target_port == null ? fallbackPort : Number(frame.target_port);
220
- if (!Number.isInteger(candidate) || candidate <= 0 || candidate > 65535) return null;
221
- return allowedTargetPorts().has(candidate) ? candidate : null;
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,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.122",
3
+ "version": "0.1.123",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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 loadComputerRecord() {
23
- const record = readJson(COMPUTER_RECORD_FILE, null);
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
- const auth = readJson(COMPUTER_AUTH_FILE, null);
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 = readJson(path.join(AMALGM_DIR, 'computer.json'), null);
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: pathname.startsWith('/port-monitor') ? '/port-monitor' : '/ports',
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 loadAuthContext() {
41
- const record = readJson(COMPUTER_FILE, null) || {};
42
- const auth = readJson(AUTH_FILE, null) || {};
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
- writeJsonSecret(AUTH_FILE, nextAuth);
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