amalgm 0.1.61 → 0.1.63
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
CHANGED
|
@@ -231,6 +231,7 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
231
231
|
let connectStartedAt = 0;
|
|
232
232
|
let lastGatewayFrameAt = 0;
|
|
233
233
|
const upstreamSockets = new Map();
|
|
234
|
+
const upstreamStreams = new Map();
|
|
234
235
|
|
|
235
236
|
function log(message) {
|
|
236
237
|
const line = `[event-tunnel] ${message}`;
|
|
@@ -386,6 +387,71 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
386
387
|
req.end(body);
|
|
387
388
|
}
|
|
388
389
|
|
|
390
|
+
function forwardStream(frame) {
|
|
391
|
+
const targetPort = resolveTargetPort(frame);
|
|
392
|
+
if (!targetPort) {
|
|
393
|
+
send({
|
|
394
|
+
type: 'stream_res_error',
|
|
395
|
+
req_id: frame.req_id,
|
|
396
|
+
status: 403,
|
|
397
|
+
message: 'Target port is not allowed',
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const body = frame.body_b64 ? Buffer.from(frame.body_b64, 'base64') : Buffer.alloc(0);
|
|
402
|
+
const req = http.request(
|
|
403
|
+
{
|
|
404
|
+
hostname: '127.0.0.1',
|
|
405
|
+
port: targetPort,
|
|
406
|
+
method: frame.method || 'GET',
|
|
407
|
+
path: frame.path || '/',
|
|
408
|
+
headers: localHeaders(frame.headers),
|
|
409
|
+
},
|
|
410
|
+
(res) => {
|
|
411
|
+
send({
|
|
412
|
+
type: 'stream_res_start',
|
|
413
|
+
req_id: frame.req_id,
|
|
414
|
+
status: res.statusCode || 200,
|
|
415
|
+
headers: responseHeaders(res.headers),
|
|
416
|
+
});
|
|
417
|
+
res.on('data', (chunk) => {
|
|
418
|
+
send({
|
|
419
|
+
type: 'stream_res_data',
|
|
420
|
+
req_id: frame.req_id,
|
|
421
|
+
chunk_b64: Buffer.from(chunk).toString('base64'),
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
res.on('end', () => {
|
|
425
|
+
upstreamStreams.delete(frame.req_id);
|
|
426
|
+
send({ type: 'stream_res_end', req_id: frame.req_id });
|
|
427
|
+
});
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
upstreamStreams.set(frame.req_id, req);
|
|
432
|
+
|
|
433
|
+
req.on('error', (error) => {
|
|
434
|
+
if (!upstreamStreams.has(frame.req_id)) return;
|
|
435
|
+
upstreamStreams.delete(frame.req_id);
|
|
436
|
+
send({
|
|
437
|
+
type: 'stream_res_error',
|
|
438
|
+
req_id: frame.req_id,
|
|
439
|
+
status: 502,
|
|
440
|
+
message: error.message || 'Local stream request failed',
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (body.length > 0) req.write(body);
|
|
445
|
+
req.end();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function cancelStream(frame) {
|
|
449
|
+
const req = upstreamStreams.get(frame.req_id);
|
|
450
|
+
if (!req) return;
|
|
451
|
+
upstreamStreams.delete(frame.req_id);
|
|
452
|
+
req.destroy(new Error('stream canceled'));
|
|
453
|
+
}
|
|
454
|
+
|
|
389
455
|
function openPreviewSocket(frame) {
|
|
390
456
|
const targetPort = resolveTargetPort(frame);
|
|
391
457
|
if (!targetPort) {
|
|
@@ -436,6 +502,14 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
436
502
|
handleRequest(frame);
|
|
437
503
|
return;
|
|
438
504
|
}
|
|
505
|
+
if (frame.type === 'stream_req') {
|
|
506
|
+
forwardStream(frame);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (frame.type === 'stream_cancel') {
|
|
510
|
+
cancelStream(frame);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
439
513
|
if (frame.type === 'ws_open') {
|
|
440
514
|
openPreviewSocket(frame);
|
|
441
515
|
return;
|
|
@@ -518,6 +592,14 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
518
592
|
}
|
|
519
593
|
}
|
|
520
594
|
upstreamSockets.clear();
|
|
595
|
+
for (const stream of upstreamStreams.values()) {
|
|
596
|
+
try {
|
|
597
|
+
stream.destroy(new Error('event tunnel stopped'));
|
|
598
|
+
} catch {
|
|
599
|
+
// noop
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
upstreamStreams.clear();
|
|
521
603
|
void postPresence(record, false);
|
|
522
604
|
if (ws) {
|
|
523
605
|
try {
|
package/package.json
CHANGED
|
@@ -338,12 +338,16 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
|
338
338
|
].filter(Boolean).join('\n\n') + '\n';
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
function baseNativeConfigForContract(contract, syncInfo) {
|
|
342
|
+
return readTextFile(syncInfo?.sourceConfigPath);
|
|
343
|
+
}
|
|
344
|
+
|
|
341
345
|
function writeConfig(contract) {
|
|
342
346
|
const home = contract.auth.runtimeHome;
|
|
343
347
|
if (!home) return;
|
|
344
348
|
fs.mkdirSync(home, { recursive: true });
|
|
345
349
|
const syncInfo = syncCodexNativeConfig(home);
|
|
346
|
-
const
|
|
350
|
+
const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
|
|
347
351
|
const configPath = path.join(home, 'config.toml');
|
|
348
352
|
if (contract.authMethod === 'provider_auth') {
|
|
349
353
|
const sourceAuth = path.join(syncInfo?.sourceDir || path.join(os.homedir(), '.codex'), 'auth.json');
|
|
@@ -352,10 +356,10 @@ function writeConfig(contract) {
|
|
|
352
356
|
fs.copyFileSync(sourceAuth, targetAuth);
|
|
353
357
|
fs.chmodSync(targetAuth, 0o600);
|
|
354
358
|
}
|
|
355
|
-
fs.writeFileSync(configPath, buildCodexConfig(contract,
|
|
359
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, nativeConfig, syncInfo), { mode: 0o600 });
|
|
356
360
|
return;
|
|
357
361
|
}
|
|
358
|
-
fs.writeFileSync(configPath, buildCodexConfig(contract,
|
|
362
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, nativeConfig, syncInfo), { mode: 0o600 });
|
|
359
363
|
fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
|
|
360
364
|
auth_mode: 'apikey',
|
|
361
365
|
OPENAI_API_KEY: contract.auth.tokenRef,
|
|
@@ -37,6 +37,8 @@ test('codex native sync copies hook support without bulk runtime state', () => {
|
|
|
37
37
|
fs.mkdirSync(path.join(source, 'worktrees'), { recursive: true });
|
|
38
38
|
fs.mkdirSync(path.join(source, 'plugins'), { recursive: true });
|
|
39
39
|
fs.mkdirSync(path.join(source, 'supermemory'), { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
|
|
41
|
+
fs.writeFileSync(path.join(source, 'preferences.json'), '{"theme":"dark"}');
|
|
40
42
|
fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
|
|
41
43
|
fs.writeFileSync(path.join(source, 'supermemory.json'), '{"projectContainerTag":"test"}');
|
|
42
44
|
fs.writeFileSync(path.join(source, 'supermemory', 'recall.js'), 'console.log("recall")');
|
|
@@ -51,6 +53,8 @@ test('codex native sync copies hook support without bulk runtime state', () => {
|
|
|
51
53
|
const result = syncCodexNativeConfig(runtimeHome);
|
|
52
54
|
|
|
53
55
|
assert.equal(result.sourceDir, source);
|
|
56
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), true);
|
|
57
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'preferences.json')), true);
|
|
54
58
|
assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
|
|
55
59
|
assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory.json')), true);
|
|
56
60
|
assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory', 'recall.js')), true);
|
|
@@ -61,7 +65,7 @@ test('codex native sync copies hook support without bulk runtime state', () => {
|
|
|
61
65
|
});
|
|
62
66
|
});
|
|
63
67
|
|
|
64
|
-
test('codex provider config keeps
|
|
68
|
+
test('codex provider config keeps native mcp config and overrides managed model provider', () => {
|
|
65
69
|
withNativeHome((home) => {
|
|
66
70
|
const source = path.join(home, '.codex');
|
|
67
71
|
fs.mkdirSync(source, { recursive: true });
|
|
@@ -72,7 +76,10 @@ test('codex provider config keeps hooks but drops native model and mcp config',
|
|
|
72
76
|
'model_provider = "native-provider"',
|
|
73
77
|
'',
|
|
74
78
|
'[mcp_servers.native]',
|
|
75
|
-
'
|
|
79
|
+
'url = "https://example.com/mcp"',
|
|
80
|
+
'',
|
|
81
|
+
'[mcp_servers.native.http_headers]',
|
|
82
|
+
'Authorization = "Bearer test"',
|
|
76
83
|
'',
|
|
77
84
|
`[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
|
|
78
85
|
'trusted = true',
|
|
@@ -90,7 +97,47 @@ test('codex provider config keeps hooks but drops native model and mcp config',
|
|
|
90
97
|
assert.match(config, /model_provider = "openai"/);
|
|
91
98
|
assert.match(config, /codex_hooks = true/);
|
|
92
99
|
assert.doesNotMatch(config, /native-provider/);
|
|
93
|
-
assert.
|
|
100
|
+
assert.match(config, /\[mcp_servers\.native\]/);
|
|
101
|
+
assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
|
|
102
|
+
assert.match(config, /Authorization = "Bearer test"/);
|
|
103
|
+
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('codex amalgm config keeps native mcp config and overrides managed model provider', () => {
|
|
108
|
+
withNativeHome((home) => {
|
|
109
|
+
const source = path.join(home, '.codex');
|
|
110
|
+
fs.mkdirSync(source, { recursive: true });
|
|
111
|
+
const hooksPath = path.join(source, 'hooks.json');
|
|
112
|
+
fs.writeFileSync(hooksPath, '{"hooks":{"UserPromptSubmit":[]}}');
|
|
113
|
+
fs.writeFileSync(path.join(source, 'config.toml'), [
|
|
114
|
+
'model_provider = "native-provider"',
|
|
115
|
+
'',
|
|
116
|
+
'[mcp_servers.native]',
|
|
117
|
+
'url = "https://example.com/mcp"',
|
|
118
|
+
'',
|
|
119
|
+
'[mcp_servers.native.http_headers]',
|
|
120
|
+
'Authorization = "Bearer test"',
|
|
121
|
+
'',
|
|
122
|
+
`[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
|
|
123
|
+
'trusted = true',
|
|
124
|
+
'',
|
|
125
|
+
].join('\n'));
|
|
126
|
+
|
|
127
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
128
|
+
codexPrivate.writeConfig({
|
|
129
|
+
authMethod: 'amalgm',
|
|
130
|
+
auth: { runtimeHome, baseUrl: 'https://amalgm.example/v1', tokenRef: 'test-token' },
|
|
131
|
+
mcpServers: [],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
|
|
135
|
+
assert.match(config, /model_provider = "amalgm"/);
|
|
136
|
+
assert.match(config, /codex_hooks = true/);
|
|
137
|
+
assert.doesNotMatch(config, /native-provider/);
|
|
138
|
+
assert.match(config, /\[mcp_servers\.native\]/);
|
|
139
|
+
assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
|
|
140
|
+
assert.match(config, /Authorization = "Bearer test"/);
|
|
94
141
|
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
|
|
95
142
|
});
|
|
96
143
|
});
|
|
@@ -179,7 +179,10 @@ function syncCodexNativeConfig(runtimeHome) {
|
|
|
179
179
|
if (!exists(sourceDir)) return null;
|
|
180
180
|
|
|
181
181
|
pruneLegacyCodexRuntimeHome(runtimeHome);
|
|
182
|
+
const nativeConfig = copyDirBounded(sourceDir, runtimeHome);
|
|
182
183
|
const copiedFiles = [
|
|
184
|
+
copyFileIfPresent(path.join(sourceDir, 'config.toml'), path.join(runtimeHome, 'config.toml')),
|
|
185
|
+
copyFileIfPresent(path.join(sourceDir, 'auth.json'), path.join(runtimeHome, 'auth.json')),
|
|
183
186
|
copyFileIfPresent(path.join(sourceDir, 'hooks.json'), path.join(runtimeHome, 'hooks.json')),
|
|
184
187
|
copyFileIfPresent(path.join(sourceDir, 'supermemory.json'), path.join(runtimeHome, 'supermemory.json')),
|
|
185
188
|
].filter(Boolean).length;
|
|
@@ -191,8 +194,8 @@ function syncCodexNativeConfig(runtimeHome) {
|
|
|
191
194
|
sourceDir,
|
|
192
195
|
runtimeHome,
|
|
193
196
|
sourceConfigPath: path.join(sourceDir, 'config.toml'),
|
|
194
|
-
copied: copiedFiles + (supermemory.copied ? 1 : 0) > 0,
|
|
195
|
-
truncated: supermemory.truncated,
|
|
197
|
+
copied: nativeConfig.copied || copiedFiles + (supermemory.copied ? 1 : 0) > 0,
|
|
198
|
+
truncated: nativeConfig.truncated || supermemory.truncated,
|
|
196
199
|
};
|
|
197
200
|
}
|
|
198
201
|
|