amalgm 0.1.44 → 0.1.47
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/README.md +0 -1
- package/lib/cli.js +0 -2
- package/lib/runtime-manifest.js +1 -129
- package/lib/service.js +0 -1
- package/lib/supervisor.js +0 -3
- package/lib/tunnel-chat.js +12 -12
- package/lib/tunnel-events.js +12 -12
- package/package.json +2 -2
- package/runtime/lib/runtime-manifest.js +159 -0
- package/runtime/scripts/amalgm-mcp/agents/hooks.js +182 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +6 -1
- package/runtime/scripts/amalgm-mcp/agents/store.js +61 -31
- package/runtime/scripts/amalgm-mcp/agents/talk.js +12 -22
- package/runtime/scripts/amalgm-mcp/agents/tools.js +8 -13
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +3 -2
- package/runtime/scripts/amalgm-mcp/config.js +3 -2
- package/runtime/scripts/amalgm-mcp/index.js +2 -2
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +12 -48
- package/runtime/scripts/chat-core/adapters/claude.js +3 -1
- package/runtime/scripts/chat-core/adapters/codex.js +173 -29
- package/runtime/scripts/chat-core/auth.js +1 -1
- package/runtime/scripts/chat-core/contract.js +2 -1
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +3 -3
- package/runtime/scripts/chat-core/tooling/native-config.js +133 -0
- package/runtime/scripts/chat-server/config.js +2 -1
- package/runtime/scripts/local-gateway.js +17 -17
- package/runtime/scripts/port-monitor.js +7 -8
- package/runtime/scripts/fs-watcher.js +0 -923
|
@@ -9,8 +9,9 @@ const { promptText } = require('../input');
|
|
|
9
9
|
const { runtimeEnv } = require('../auth');
|
|
10
10
|
const { codexErrorMessage, normalizeCodexNotification } = require('../normalizers/codex');
|
|
11
11
|
const { recordNativeEvent } = require('../recorder');
|
|
12
|
-
const { toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
12
|
+
const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
13
13
|
const { bundledCodexBinary, bundledCodexPathDirs, executableExists } = require('../tooling/native-binaries');
|
|
14
|
+
const { syncCodexNativeConfig } = require('../tooling/native-config');
|
|
14
15
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
15
16
|
|
|
16
17
|
class JsonLineRpc {
|
|
@@ -176,11 +177,167 @@ function isCompactCommand(text) {
|
|
|
176
177
|
return String(text || '').trim().toLowerCase() === '/compact';
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function readTextFile(file) {
|
|
181
|
+
try {
|
|
182
|
+
return fs.readFileSync(file, 'utf8');
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function tomlSectionName(line) {
|
|
189
|
+
const match = String(line || '').match(/^\s*\[([^\]]+)\]\s*$/);
|
|
190
|
+
return match ? match[1].trim() : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function removeTopLevelKeys(toml, keys) {
|
|
194
|
+
const remove = new Set(keys);
|
|
195
|
+
let inTopLevel = true;
|
|
196
|
+
return String(toml || '')
|
|
197
|
+
.split(/\r?\n/)
|
|
198
|
+
.filter((line) => {
|
|
199
|
+
if (tomlSectionName(line)) inTopLevel = false;
|
|
200
|
+
if (!inTopLevel) return true;
|
|
201
|
+
const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*=/);
|
|
202
|
+
return !match || !remove.has(match[1]);
|
|
203
|
+
})
|
|
204
|
+
.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function removeTomlSections(toml, sectionNames) {
|
|
208
|
+
const remove = new Set(sectionNames);
|
|
209
|
+
let skipping = false;
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const line of String(toml || '').split(/\r?\n/)) {
|
|
212
|
+
const section = tomlSectionName(line);
|
|
213
|
+
if (section) {
|
|
214
|
+
skipping = [...remove].some((name) => section === name || section.startsWith(`${name}.`));
|
|
215
|
+
}
|
|
216
|
+
if (!skipping) out.push(line);
|
|
217
|
+
}
|
|
218
|
+
return out.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ensureFeatureBoolean(toml, key, value) {
|
|
222
|
+
const lines = String(toml || '').split(/\r?\n/);
|
|
223
|
+
const sectionIndex = lines.findIndex((line) => tomlSectionName(line) === 'features');
|
|
224
|
+
const rendered = `${key} = ${value ? 'true' : 'false'}`;
|
|
225
|
+
if (sectionIndex === -1) {
|
|
226
|
+
return `${String(toml || '').trimEnd()}\n\n[features]\n${rendered}\n`;
|
|
227
|
+
}
|
|
228
|
+
let insertAt = lines.length;
|
|
229
|
+
for (let i = sectionIndex + 1; i < lines.length; i += 1) {
|
|
230
|
+
if (tomlSectionName(lines[i])) {
|
|
231
|
+
insertAt = i;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (new RegExp(`^\\s*${key}\\s*=`).test(lines[i])) {
|
|
235
|
+
lines[i] = rendered;
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
lines.splice(insertAt, 0, rendered);
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function tomlQuoted(value) {
|
|
244
|
+
return JSON.stringify(String(value || ''));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hookTrustBlocks(toml) {
|
|
248
|
+
const lines = String(toml || '').split(/\r?\n/);
|
|
249
|
+
const blocks = [];
|
|
250
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
251
|
+
const section = tomlSectionName(lines[i]);
|
|
252
|
+
const match = section?.match(/^hooks\.state\."([^"]+)"$/);
|
|
253
|
+
if (!match) continue;
|
|
254
|
+
let end = lines.length;
|
|
255
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
256
|
+
if (tomlSectionName(lines[j])) {
|
|
257
|
+
end = j;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
blocks.push({ key: match[1], lines: lines.slice(i, end) });
|
|
262
|
+
}
|
|
263
|
+
return blocks;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
|
|
267
|
+
if (!sourceDir || !runtimeHome) return toml;
|
|
268
|
+
const sourceHookPrefix = `${path.join(sourceDir, 'hooks.json')}:`;
|
|
269
|
+
const runtimePrefixes = [
|
|
270
|
+
`${path.join(runtimeHome, 'hooks.json')}:`,
|
|
271
|
+
`${path.join(runtimeHome, '.codex', 'hooks.json')}:`,
|
|
272
|
+
];
|
|
273
|
+
const blocks = hookTrustBlocks(toml);
|
|
274
|
+
if (blocks.length === 0) return toml;
|
|
275
|
+
const existing = new Set(blocks.map((block) => block.key));
|
|
276
|
+
const additions = [];
|
|
277
|
+
for (const block of blocks) {
|
|
278
|
+
if (!block.key.startsWith(sourceHookPrefix)) continue;
|
|
279
|
+
const suffix = block.key.slice(sourceHookPrefix.length);
|
|
280
|
+
for (const prefix of runtimePrefixes) {
|
|
281
|
+
const nextKey = `${prefix}${suffix}`;
|
|
282
|
+
if (existing.has(nextKey)) continue;
|
|
283
|
+
existing.add(nextKey);
|
|
284
|
+
const lines = [...block.lines];
|
|
285
|
+
lines[0] = `[hooks.state.${tomlQuoted(nextKey)}]`;
|
|
286
|
+
additions.push(lines.join('\n'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (additions.length === 0) return toml;
|
|
290
|
+
const parent = /\n\s*\[hooks\.state\]\s*(?:\n|$)/.test(`\n${toml}`) ? '' : '\n[hooks.state]\n';
|
|
291
|
+
return `${String(toml || '').trimEnd()}${parent}\n${additions.join('\n\n')}\n`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function generatedMcpSectionNames(contract) {
|
|
295
|
+
return relayedMcpServers(contract).map((server) => `mcp_servers.${server.name}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
299
|
+
const mcpToml = toCodexMcpToml(contract);
|
|
300
|
+
const topLevelKeys = [
|
|
301
|
+
...(contract.authMethod === 'provider_auth' ? [] : ['model_provider']),
|
|
302
|
+
...(contract.authMethod === 'amalgm' ? ['model_context_window', 'model_auto_compact_token_limit'] : []),
|
|
303
|
+
];
|
|
304
|
+
let config = removeTopLevelKeys(existingConfig, topLevelKeys);
|
|
305
|
+
config = removeTomlSections(config, [
|
|
306
|
+
...(contract.authMethod === 'amalgm' ? ['model_providers.amalgm'] : []),
|
|
307
|
+
...generatedMcpSectionNames(contract),
|
|
308
|
+
]);
|
|
309
|
+
config = mirrorCodexHookTrust(config, syncInfo?.sourceDir, contract.auth.runtimeHome);
|
|
310
|
+
if (fs.existsSync(path.join(contract.auth.runtimeHome, 'hooks.json'))) {
|
|
311
|
+
config = ensureFeatureBoolean(config, 'codex_hooks', true);
|
|
312
|
+
}
|
|
313
|
+
const generated = contract.authMethod === 'amalgm'
|
|
314
|
+
? [
|
|
315
|
+
'model_provider = "amalgm"',
|
|
316
|
+
...modelWindowConfigLines(contract),
|
|
317
|
+
'',
|
|
318
|
+
'[model_providers.amalgm]',
|
|
319
|
+
'name = "amalgm"',
|
|
320
|
+
`base_url = "${contract.auth.baseUrl}"`,
|
|
321
|
+
'env_key = "OPENAI_API_KEY"',
|
|
322
|
+
'wire_api = "responses"',
|
|
323
|
+
'requires_openai_auth = false',
|
|
324
|
+
].join('\n')
|
|
325
|
+
: contract.authMethod === 'byok'
|
|
326
|
+
? 'model_provider = "openai"'
|
|
327
|
+
: '';
|
|
328
|
+
return [
|
|
329
|
+
generated.trimEnd(),
|
|
330
|
+
String(config || '').trim(),
|
|
331
|
+
String(mcpToml || '').trim(),
|
|
332
|
+
].filter(Boolean).join('\n\n') + '\n';
|
|
333
|
+
}
|
|
334
|
+
|
|
179
335
|
function writeConfig(contract) {
|
|
180
336
|
const home = contract.auth.runtimeHome;
|
|
181
337
|
if (!home) return;
|
|
182
338
|
fs.mkdirSync(home, { recursive: true });
|
|
183
|
-
const
|
|
339
|
+
const syncInfo = syncCodexNativeConfig(home);
|
|
340
|
+
const configPath = path.join(home, 'config.toml');
|
|
184
341
|
if (contract.authMethod === 'provider_auth') {
|
|
185
342
|
const sourceAuth = path.join(os.homedir(), '.codex', 'auth.json');
|
|
186
343
|
const targetAuth = path.join(home, 'auth.json');
|
|
@@ -188,34 +345,10 @@ function writeConfig(contract) {
|
|
|
188
345
|
fs.copyFileSync(sourceAuth, targetAuth);
|
|
189
346
|
fs.chmodSync(targetAuth, 0o600);
|
|
190
347
|
}
|
|
191
|
-
fs.writeFileSync(
|
|
192
|
-
'model_provider = "openai"',
|
|
193
|
-
'',
|
|
194
|
-
mcpToml,
|
|
195
|
-
].join('\n'), { mode: 0o600 });
|
|
348
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
|
|
196
349
|
return;
|
|
197
350
|
}
|
|
198
|
-
|
|
199
|
-
fs.writeFileSync(path.join(home, 'config.toml'), [
|
|
200
|
-
'model_provider = "amalgm"',
|
|
201
|
-
...modelWindowConfigLines(contract),
|
|
202
|
-
'',
|
|
203
|
-
'[model_providers.amalgm]',
|
|
204
|
-
'name = "amalgm"',
|
|
205
|
-
`base_url = "${contract.auth.baseUrl}"`,
|
|
206
|
-
'env_key = "OPENAI_API_KEY"',
|
|
207
|
-
'wire_api = "responses"',
|
|
208
|
-
'requires_openai_auth = false',
|
|
209
|
-
'',
|
|
210
|
-
mcpToml,
|
|
211
|
-
].join('\n'), { mode: 0o600 });
|
|
212
|
-
} else {
|
|
213
|
-
fs.writeFileSync(path.join(home, 'config.toml'), [
|
|
214
|
-
'model_provider = "openai"',
|
|
215
|
-
'',
|
|
216
|
-
mcpToml,
|
|
217
|
-
].join('\n'), { mode: 0o600 });
|
|
218
|
-
}
|
|
351
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
|
|
219
352
|
fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
|
|
220
353
|
auth_mode: 'apikey',
|
|
221
354
|
OPENAI_API_KEY: contract.auth.tokenRef,
|
|
@@ -350,4 +483,15 @@ class CodexAdapter {
|
|
|
350
483
|
}
|
|
351
484
|
}
|
|
352
485
|
|
|
353
|
-
module.exports = {
|
|
486
|
+
module.exports = {
|
|
487
|
+
CodexAdapter,
|
|
488
|
+
__private: {
|
|
489
|
+
buildCodexConfig,
|
|
490
|
+
ensureFeatureBoolean,
|
|
491
|
+
mirrorCodexHookTrust,
|
|
492
|
+
modelWindowConfigLines,
|
|
493
|
+
removeTomlSections,
|
|
494
|
+
removeTopLevelKeys,
|
|
495
|
+
writeConfig,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
@@ -70,7 +70,7 @@ function coerceAuth(harness, requested) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
|
|
73
|
-
if (authMethod === 'provider_auth' &&
|
|
73
|
+
if (authMethod === 'provider_auth' && !['claude_code', 'codex'].includes(harness)) return null;
|
|
74
74
|
const suffix = tokenFingerprint || 'default';
|
|
75
75
|
return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
|
|
76
76
|
}
|
|
@@ -5,6 +5,7 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { AMALGM_DIR, DEFAULT_CWD, PROXY_BASE_URL, PROXY_TOKEN } = require('../chat-server/config');
|
|
7
7
|
const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
|
|
8
|
+
const { runtimePort } = require('../../lib/runtime-manifest');
|
|
8
9
|
|
|
9
10
|
function agentToHarness(agent) {
|
|
10
11
|
const clean = String(agent || '').trim();
|
|
@@ -323,7 +324,7 @@ function createContract(payload, options = {}) {
|
|
|
323
324
|
const fastMode = payload.fastMode === true;
|
|
324
325
|
const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
|
|
325
326
|
const cwd = resolveCwd(payload.cwd, options);
|
|
326
|
-
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port ||
|
|
327
|
+
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || runtimePort('chat-server')}`;
|
|
327
328
|
const origin = normalizeOrigin({ ...payload, cwd });
|
|
328
329
|
const auth = authEnvelope({
|
|
329
330
|
harness,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { getRuntimeToken } = require('../../runtime-auth');
|
|
4
|
+
const { runtimePort } = require('../../../lib/runtime-manifest');
|
|
4
5
|
|
|
5
6
|
function headerArrayToRecord(headers) {
|
|
6
7
|
const out = {};
|
|
@@ -49,12 +50,11 @@ function normalizeServer(server) {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function localBaseUrl(contract) {
|
|
52
|
-
return String(contract.localBaseUrl || `http://127.0.0.1:${
|
|
53
|
+
return String(contract.localBaseUrl || `http://127.0.0.1:${runtimePort('chat-server')}`).replace(/\/$/, '');
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function amalgmMcpServer(contract) {
|
|
56
|
-
const
|
|
57
|
-
const url = process.env.AMALGM_MCP_URL || `http://127.0.0.1:${port}/mcp`;
|
|
57
|
+
const url = process.env.AMALGM_MCP_URL || `http://127.0.0.1:${runtimePort('amalgm-mcp')}/mcp`;
|
|
58
58
|
const headers = [
|
|
59
59
|
{ name: 'x-amalgm-runtime-token', value: getRuntimeToken() },
|
|
60
60
|
{ name: 'x-amalgm-session-id', value: contract.sessionId },
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const EXCLUDED_DIR_NAMES = new Set([
|
|
8
|
+
'.cache',
|
|
9
|
+
'.npm',
|
|
10
|
+
'.tmp',
|
|
11
|
+
'cache',
|
|
12
|
+
'log',
|
|
13
|
+
'logs',
|
|
14
|
+
'projects',
|
|
15
|
+
'sessions',
|
|
16
|
+
'shell_snapshots',
|
|
17
|
+
'tmp',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const EXCLUDED_FILE_PATTERNS = [
|
|
21
|
+
/^logs?_\d+\.sqlite(?:-.+)?$/i,
|
|
22
|
+
/^state_\d+\.sqlite(?:-.+)?$/i,
|
|
23
|
+
/^.*\.db-(?:shm|wal)$/i,
|
|
24
|
+
/^.*\.sqlite-(?:shm|wal)$/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function exists(p) {
|
|
28
|
+
try {
|
|
29
|
+
fs.accessSync(p);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldCopyConfigPath(root, source) {
|
|
37
|
+
const relative = path.relative(root, source);
|
|
38
|
+
if (!relative || relative === '.') return true;
|
|
39
|
+
const parts = relative.split(path.sep);
|
|
40
|
+
if (parts.some((part) => EXCLUDED_DIR_NAMES.has(part))) return false;
|
|
41
|
+
const basename = path.basename(source);
|
|
42
|
+
return !EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyConfigTree(sourceDir, targetDir) {
|
|
46
|
+
if (!sourceDir || !targetDir || !exists(sourceDir)) return false;
|
|
47
|
+
if (path.resolve(sourceDir) === path.resolve(targetDir)) return true;
|
|
48
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
49
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
50
|
+
recursive: true,
|
|
51
|
+
force: true,
|
|
52
|
+
errorOnExist: false,
|
|
53
|
+
dereference: false,
|
|
54
|
+
filter: (source) => shouldCopyConfigPath(sourceDir, source),
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function copyFileIfPresent(source, target) {
|
|
60
|
+
if (!exists(source)) return false;
|
|
61
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
62
|
+
fs.copyFileSync(source, target);
|
|
63
|
+
try {
|
|
64
|
+
fs.chmodSync(target, fs.statSync(source).mode & 0o777);
|
|
65
|
+
} catch {}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
|
|
70
|
+
if (!runtimeHome || !dotDirName || !sourceDir || !exists(sourceDir)) return;
|
|
71
|
+
const alias = path.join(runtimeHome, dotDirName);
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.lstatSync(alias);
|
|
74
|
+
if (stat.isSymbolicLink()) {
|
|
75
|
+
const target = fs.readlinkSync(alias);
|
|
76
|
+
if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
|
|
77
|
+
fs.rmSync(alias, { recursive: true, force: true });
|
|
78
|
+
} else if (stat.isDirectory()) {
|
|
79
|
+
copyConfigTree(sourceDir, alias);
|
|
80
|
+
return;
|
|
81
|
+
} else {
|
|
82
|
+
fs.rmSync(alias, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
try {
|
|
86
|
+
fs.symlinkSync('.', alias, 'dir');
|
|
87
|
+
} catch {
|
|
88
|
+
copyConfigTree(sourceDir, alias);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncCodexNativeConfig(runtimeHome) {
|
|
93
|
+
if (!runtimeHome) return null;
|
|
94
|
+
const nativeHome = os.homedir();
|
|
95
|
+
const sourceDir = path.join(nativeHome, '.codex');
|
|
96
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
97
|
+
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
98
|
+
copyIntoHomeAlias(runtimeHome, '.codex', sourceDir);
|
|
99
|
+
copyConfigTree(path.join(nativeHome, '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
|
|
100
|
+
copyFileIfPresent(path.join(nativeHome, '.codex-supermemory.log'), path.join(runtimeHome, '.codex-supermemory.log'));
|
|
101
|
+
return copied ? { sourceDir, runtimeHome } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function syncClaudeNativeConfig(runtimeHome) {
|
|
105
|
+
if (!runtimeHome) return null;
|
|
106
|
+
const nativeHome = os.homedir();
|
|
107
|
+
const sourceDir = path.join(nativeHome, '.claude');
|
|
108
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
109
|
+
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
110
|
+
copyIntoHomeAlias(runtimeHome, '.claude', sourceDir);
|
|
111
|
+
copyFileIfPresent(path.join(nativeHome, '.claude.json'), path.join(runtimeHome, '.claude.json'));
|
|
112
|
+
copyConfigTree(path.join(nativeHome, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
|
|
113
|
+
return copied ? { sourceDir, runtimeHome } : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function syncNativeHarnessConfig(contract) {
|
|
117
|
+
const runtimeHome = contract?.auth?.runtimeHome;
|
|
118
|
+
if (!runtimeHome) return null;
|
|
119
|
+
if (contract.harness === 'codex') return syncCodexNativeConfig(runtimeHome);
|
|
120
|
+
if (contract.harness === 'claude_code') return syncClaudeNativeConfig(runtimeHome);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
__private: {
|
|
126
|
+
copyConfigTree,
|
|
127
|
+
copyFileIfPresent,
|
|
128
|
+
shouldCopyConfigPath,
|
|
129
|
+
},
|
|
130
|
+
syncClaudeNativeConfig,
|
|
131
|
+
syncCodexNativeConfig,
|
|
132
|
+
syncNativeHarnessConfig,
|
|
133
|
+
};
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
proxyBaseUrl,
|
|
15
15
|
readProxyToken,
|
|
16
16
|
} = require('../proxy-token-store');
|
|
17
|
+
const { runtimePort } = require('../../lib/runtime-manifest');
|
|
17
18
|
|
|
18
19
|
// ── Paths ──────────────────────────────────────────────────────────────────
|
|
19
20
|
// Set by the orchestrator (entrypoint.sh or electron/main.ts).
|
|
@@ -24,7 +25,7 @@ const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || '/workspace';
|
|
|
24
25
|
|
|
25
26
|
// ── Server ──────────────────────────────────────────────────────────────────
|
|
26
27
|
|
|
27
|
-
const PORT =
|
|
28
|
+
const PORT = runtimePort('chat-server');
|
|
28
29
|
const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
|
|
29
30
|
|
|
30
31
|
// ── API keys / proxy ────────────────────────────────────────────────────────
|
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
isAuthorizedRuntimeRequest,
|
|
23
23
|
runtimeAuthHeaders,
|
|
24
24
|
} = require('./runtime-auth');
|
|
25
|
+
const { runtimePort } = require('../lib/runtime-manifest');
|
|
25
26
|
|
|
26
27
|
function loadPty() {
|
|
27
28
|
const candidates = [
|
|
@@ -51,7 +52,7 @@ const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
|
|
|
51
52
|
const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
|
|
52
53
|
const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
|
|
53
54
|
const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
|
|
54
|
-
const PORT =
|
|
55
|
+
const PORT = runtimePort('local-gateway');
|
|
55
56
|
const RUNTIME_TOKEN_HEADER = 'x-amalgm-runtime-token';
|
|
56
57
|
const DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES = 256 * 1024;
|
|
57
58
|
const MAX_DIAGNOSTIC_LOG_TAIL_BYTES = 2 * 1024 * 1024;
|
|
@@ -61,7 +62,6 @@ const DIAGNOSTIC_LOG_FILES = Object.freeze({
|
|
|
61
62
|
'chat-server': 'chat-server.log',
|
|
62
63
|
'local-gateway': 'local-gateway.log',
|
|
63
64
|
'amalgm-mcp': 'amalgm-mcp.log',
|
|
64
|
-
'fs-watcher': 'fs-watcher.log',
|
|
65
65
|
'port-monitor': 'port-monitor.log',
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -77,10 +77,9 @@ const DIAGNOSTIC_LOG_ALIASES = Object.freeze({
|
|
|
77
77
|
|
|
78
78
|
const SERVICE_PORTS = {
|
|
79
79
|
gateway: PORT,
|
|
80
|
-
portMonitor:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
chat: Number.parseInt(process.env.CHAT_SERVER_PORT || '8084', 10),
|
|
80
|
+
portMonitor: runtimePort('port-monitor'),
|
|
81
|
+
mcp: runtimePort('amalgm-mcp'),
|
|
82
|
+
chat: runtimePort('chat-server'),
|
|
84
83
|
};
|
|
85
84
|
const LOCAL_AGENT_APP_PORT = Number.parseInt(process.env.AMALGM_LOCAL_APP_PORT || '3456', 10);
|
|
86
85
|
|
|
@@ -275,6 +274,14 @@ function readJson(file, fallback = null) {
|
|
|
275
274
|
}
|
|
276
275
|
}
|
|
277
276
|
|
|
277
|
+
function runtimeStateOwnedHere(previous) {
|
|
278
|
+
if (!previous || typeof previous !== 'object' || Object.keys(previous).length === 0) return true;
|
|
279
|
+
return previous.pid === process.pid
|
|
280
|
+
|| previous.gateway_pid === process.pid
|
|
281
|
+
|| previous.pid === process.ppid
|
|
282
|
+
|| previous.supervisor_pid === process.ppid;
|
|
283
|
+
}
|
|
284
|
+
|
|
278
285
|
function ensurePtySpawnHelperExecutable() {
|
|
279
286
|
try {
|
|
280
287
|
const nodePtyRoot = path.dirname(require.resolve(`${pty.packageName}/package.json`));
|
|
@@ -294,6 +301,10 @@ function ensurePtySpawnHelperExecutable() {
|
|
|
294
301
|
function writeRuntimeState(actualPort) {
|
|
295
302
|
const computer = readJson(path.join(AMALGM_DIR, 'computer.json'), null);
|
|
296
303
|
const previous = readJson(STATE_FILE, {});
|
|
304
|
+
if (!runtimeStateOwnedHere(previous)) {
|
|
305
|
+
console.warn('[local-gateway] runtime-state belongs to another Amalgm runtime; skipping write');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
297
308
|
writeJsonSecret(STATE_FILE, {
|
|
298
309
|
...previous,
|
|
299
310
|
schema_version: 1,
|
|
@@ -312,7 +323,6 @@ function writeRuntimeState(actualPort) {
|
|
|
312
323
|
ports: {
|
|
313
324
|
gateway: actualPort,
|
|
314
325
|
port_monitor: SERVICE_PORTS.portMonitor,
|
|
315
|
-
fs_watcher: SERVICE_PORTS.fsWatcher,
|
|
316
326
|
amalgm_mcp: SERVICE_PORTS.mcp,
|
|
317
327
|
chat_server: SERVICE_PORTS.chat,
|
|
318
328
|
},
|
|
@@ -550,12 +560,6 @@ function serviceForPath(pathname) {
|
|
|
550
560
|
if (MCP_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
|
|
551
561
|
return { port: SERVICE_PORTS.mcp, stripPrefix: '' };
|
|
552
562
|
}
|
|
553
|
-
if (pathname === '/fs-watcher' || pathname.startsWith('/fs-watcher/')) {
|
|
554
|
-
return {
|
|
555
|
-
port: SERVICE_PORTS.fsWatcher,
|
|
556
|
-
stripPrefix: '/fs-watcher',
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
563
|
if (PORT_MONITOR_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
|
|
560
564
|
return {
|
|
561
565
|
port: SERVICE_PORTS.portMonitor,
|
|
@@ -971,10 +975,6 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
971
975
|
bridgePtyWebSocket(req, ws, url.pathname);
|
|
972
976
|
return;
|
|
973
977
|
}
|
|
974
|
-
if (url.pathname === '/ws/fs' || url.pathname === '/fs-watcher/ws') {
|
|
975
|
-
proxyWebSocket(req, ws, { port: SERVICE_PORTS.fsWatcher, stripPrefix: '' }, '/');
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
978
|
const target = serviceForPath(url.pathname);
|
|
979
979
|
if (!target) {
|
|
980
980
|
ws.close(1008, 'Unknown local gateway route');
|
|
@@ -4,27 +4,26 @@
|
|
|
4
4
|
* Polls `ss -tlnp` every 2s and pushes
|
|
5
5
|
* port_opened / port_closed events over SSE.
|
|
6
6
|
*
|
|
7
|
-
* Runs alongside chat-server on
|
|
7
|
+
* Runs alongside chat-server on the port-monitor runtime port.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const http = require('http');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { execSync } = require('child_process');
|
|
13
13
|
const { authorizeRuntimeHttp } = require('./runtime-auth');
|
|
14
|
+
const { runtimePort } = require('../lib/runtime-manifest');
|
|
14
15
|
|
|
15
|
-
const PORT =
|
|
16
|
+
const PORT = runtimePort('port-monitor');
|
|
16
17
|
const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
|
|
17
18
|
const AGENT_PORT = parseInt(process.env.PORT || '8080');
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const AMALGM_GATEWAY_PORT = parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
|
|
19
|
+
const AMALGM_MCP_PORT = runtimePort('amalgm-mcp');
|
|
20
|
+
const CHAT_SERVER_PORT = runtimePort('chat-server');
|
|
21
|
+
const AMALGM_GATEWAY_PORT = runtimePort('local-gateway');
|
|
22
22
|
const LOCAL_AGENT_APP_PORT = parseInt(process.env.AMALGM_LOCAL_APP_PORT || '3456', 10);
|
|
23
23
|
const INTERNAL_SERVICE_PORTS = new Set([
|
|
24
24
|
AGENT_PORT,
|
|
25
25
|
LOCAL_AGENT_APP_PORT,
|
|
26
26
|
PORT,
|
|
27
|
-
FS_WATCHER_PORT,
|
|
28
27
|
AMALGM_MCP_PORT,
|
|
29
28
|
CHAT_SERVER_PORT,
|
|
30
29
|
AMALGM_GATEWAY_PORT,
|
|
@@ -33,7 +32,7 @@ const INTERNAL_SERVICE_PORTS = new Set([
|
|
|
33
32
|
const COMMON_DEV_PORTS = new Set([3000, 3001, 3002, 3003, 4000, 4200, 4321, 5000, 5001, 5173, 5174, 6006, 7000, 8000, 8001, 8888]);
|
|
34
33
|
const BLOCKED_PREVIEW_PROCESSES = new Set(['.opencode', 'controlce', 'figma_age', 'figma_agent', 'loom', 'opencode', 'rapportd']);
|
|
35
34
|
const DEV_PREVIEW_PROCESS_RE = /^(node|bun|deno|npm|pnpm|yarn|vite|next|astro|nuxt|svelte|remix|webpack|rspack|parcel|serve|http-server|tsx|python|python3|uvicorn|gunicorn|flask|django|ruby|rails|puma|php|java|go|air|cargo|trunk|dotnet|nginx)$/i;
|
|
36
|
-
const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|
|
|
35
|
+
const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|chat-server|local-gateway|amalgm-mcp(?:\/|\\)index)\.js(?:\s|$)/i;
|
|
37
36
|
const BLOCKED_COMMAND_RE = /(?:agent-browser|chromium|chrome)(?:\s|$)/i;
|
|
38
37
|
|
|
39
38
|
const knownPorts = new Map();
|