amalgm 0.1.114 → 0.1.115

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.114",
3
+ "version": "0.1.115",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -120,6 +120,25 @@ function runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout }) {
120
120
  );
121
121
  }
122
122
 
123
+ function legacyRuntimeHomeSources({ amalgmDir, harness, runtimeHome }) {
124
+ const harnessHome = path.join(amalgmDir, 'cli-homes', safePathSegment(harness));
125
+ const canonical = path.resolve(runtimeHome || '');
126
+ const sources = [path.join(harnessHome, 'home')];
127
+ try {
128
+ for (const entry of fs.readdirSync(harnessHome, { withFileTypes: true })) {
129
+ if (!entry.isDirectory()) continue;
130
+ if (entry.name === 'agents' || entry.name === 'home') continue;
131
+ if (!/^(amalgm|provider|byok)-/.test(entry.name)) continue;
132
+ sources.push(path.join(harnessHome, entry.name));
133
+ }
134
+ } catch {
135
+ // The harness home is created lazily.
136
+ }
137
+ return [...new Set(sources
138
+ .map((source) => path.resolve(source))
139
+ .filter((source) => source && source !== canonical && !canonical.startsWith(`${source}${path.sep}`)))];
140
+ }
141
+
123
142
  function providerAuthFingerprint(harness) {
124
143
  return fingerprint(providerAuthIdentity(harness));
125
144
  }
@@ -153,6 +172,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
153
172
  } else if (method === 'provider_auth') {
154
173
  tokenFingerprint = providerAuthFingerprint(harness);
155
174
  }
175
+ const runtimeHomePath = runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout });
156
176
  return {
157
177
  method,
158
178
  baseUrl,
@@ -162,7 +182,8 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
162
182
  forwardBaseUrl,
163
183
  tokenRef,
164
184
  tokenFingerprint,
165
- runtimeHome: runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout }),
185
+ runtimeHome: runtimeHomePath,
186
+ runtimeHomeSources: legacyRuntimeHomeSources({ amalgmDir, harness, runtimeHome: runtimeHomePath }),
166
187
  };
167
188
  }
168
189
 
@@ -222,5 +243,6 @@ module.exports = {
222
243
  authEnvelope,
223
244
  coerceAuth,
224
245
  fingerprint,
246
+ legacyRuntimeHomeSources,
225
247
  runtimeEnv,
226
248
  };
@@ -5,7 +5,8 @@ const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const test = require('node:test');
8
- const { authEnvelope, runtimeEnv } = require('../auth');
8
+ const { authEnvelope, legacyRuntimeHomeSources, runtimeEnv } = require('../auth');
9
+ const { matchRuntimeHome } = require('../tooling/runtime-home');
9
10
 
10
11
  test('codex uses one stable per-agent CLI home across auth methods', () => {
11
12
  const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-auth-test-'));
@@ -139,3 +140,69 @@ test('provided CLI home path cannot move an agent off its canonical home', () =>
139
140
 
140
141
  assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/agents/codex/home');
141
142
  });
143
+
144
+ test('per-agent auth envelope exposes legacy homes as match sources', () => {
145
+ const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-home-sources-'));
146
+ try {
147
+ const harnessHome = path.join(amalgmDir, 'cli-homes', 'codex');
148
+ fs.mkdirSync(path.join(harnessHome, 'home'), { recursive: true });
149
+ fs.mkdirSync(path.join(harnessHome, 'provider-abc123'), { recursive: true });
150
+ fs.mkdirSync(path.join(harnessHome, 'amalgm-def456'), { recursive: true });
151
+ fs.mkdirSync(path.join(harnessHome, 'agents', 'codex', 'home'), { recursive: true });
152
+
153
+ const envelope = authEnvelope({
154
+ harness: 'codex',
155
+ authMethod: 'provider_auth',
156
+ sessionId: 'session-test',
157
+ amalgmDir,
158
+ agentConfigId: 'codex',
159
+ });
160
+
161
+ assert.equal(envelope.runtimeHome, path.join(harnessHome, 'agents', 'codex', 'home'));
162
+ assert.deepEqual(envelope.runtimeHomeSources.sort(), [
163
+ path.join(harnessHome, 'amalgm-def456'),
164
+ path.join(harnessHome, 'home'),
165
+ path.join(harnessHome, 'provider-abc123'),
166
+ ].sort());
167
+ assert.deepEqual(legacyRuntimeHomeSources({
168
+ amalgmDir,
169
+ harness: 'codex',
170
+ runtimeHome: envelope.runtimeHome,
171
+ }).sort(), envelope.runtimeHomeSources.sort());
172
+ } finally {
173
+ fs.rmSync(amalgmDir, { recursive: true, force: true });
174
+ }
175
+ });
176
+
177
+ test('runtime home matcher preserves agent-id home and imports missing legacy sessions', () => {
178
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-home-match-'));
179
+ try {
180
+ const target = path.join(root, 'cli-homes', 'codex', 'agents', 'codex', 'home');
181
+ const legacy = path.join(root, 'cli-homes', 'codex', 'home');
182
+ const provider = path.join(root, 'cli-homes', 'codex', 'provider-abc123');
183
+ const legacySession = path.join(legacy, 'sessions', '2026', '06', '15', 'rollout-old.jsonl');
184
+ const targetSession = path.join(target, 'sessions', '2026', '06', '15', 'rollout-new.jsonl');
185
+ const providerSession = path.join(provider, 'sessions', '2026', '06', '16', 'rollout-provider.jsonl');
186
+ const overlapping = path.join(legacy, 'config.toml');
187
+ const targetOverlapping = path.join(target, 'config.toml');
188
+ fs.mkdirSync(path.dirname(legacySession), { recursive: true });
189
+ fs.mkdirSync(path.dirname(targetSession), { recursive: true });
190
+ fs.mkdirSync(path.dirname(providerSession), { recursive: true });
191
+ fs.writeFileSync(legacySession, 'old session\n');
192
+ fs.writeFileSync(targetSession, 'new session\n');
193
+ fs.writeFileSync(providerSession, 'provider session\n');
194
+ fs.writeFileSync(overlapping, 'legacy config\n');
195
+ fs.writeFileSync(targetOverlapping, 'target config\n');
196
+
197
+ const summary = matchRuntimeHome(target, [legacy, provider]);
198
+
199
+ assert.equal(fs.readFileSync(path.join(target, 'sessions', '2026', '06', '15', 'rollout-old.jsonl'), 'utf8'), 'old session\n');
200
+ assert.equal(fs.readFileSync(targetSession, 'utf8'), 'new session\n');
201
+ assert.equal(fs.readFileSync(path.join(target, 'sessions', '2026', '06', '16', 'rollout-provider.jsonl'), 'utf8'), 'provider session\n');
202
+ assert.equal(fs.readFileSync(targetOverlapping, 'utf8'), 'target config\n');
203
+ assert.ok(summary.length >= 1);
204
+ assert.equal(matchRuntimeHome(target, [legacy, provider]).length, 0);
205
+ } finally {
206
+ fs.rmSync(root, { recursive: true, force: true });
207
+ }
208
+ });
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const path = require('path');
4
5
  const { runtimeEnv } = require('../auth');
5
6
  const { syncNativeProviderAuth } = require('./native-config');
6
7
 
@@ -12,20 +13,93 @@ function managedRuntimeHome(contract) {
12
13
  return runtimeHome;
13
14
  }
14
15
 
16
+ function isSubpath(parent, child) {
17
+ const relative = path.relative(parent, child);
18
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
19
+ }
20
+
21
+ function mergeMissingEntries(source, target, stats) {
22
+ const sourceStat = fs.lstatSync(source);
23
+ if (fs.existsSync(target)) {
24
+ if (!sourceStat.isDirectory() || !fs.lstatSync(target).isDirectory()) {
25
+ stats.skipped += 1;
26
+ return;
27
+ }
28
+ for (const entry of fs.readdirSync(source)) {
29
+ mergeMissingEntries(path.join(source, entry), path.join(target, entry), stats);
30
+ }
31
+ return;
32
+ }
33
+
34
+ if (sourceStat.isSymbolicLink()) {
35
+ fs.symlinkSync(fs.readlinkSync(source), target);
36
+ stats.copied += 1;
37
+ return;
38
+ }
39
+
40
+ if (sourceStat.isDirectory()) {
41
+ fs.mkdirSync(target, { recursive: true, mode: sourceStat.mode });
42
+ stats.dirs += 1;
43
+ for (const entry of fs.readdirSync(source)) {
44
+ mergeMissingEntries(path.join(source, entry), path.join(target, entry), stats);
45
+ }
46
+ return;
47
+ }
48
+
49
+ fs.mkdirSync(path.dirname(target), { recursive: true });
50
+ fs.copyFileSync(source, target);
51
+ try {
52
+ fs.chmodSync(target, sourceStat.mode);
53
+ } catch {
54
+ // Best effort; content preservation matters more than exact mode metadata.
55
+ }
56
+ stats.copied += 1;
57
+ }
58
+
59
+ function matchRuntimeHome(runtimeHome, sources = []) {
60
+ const target = path.resolve(runtimeHome);
61
+ fs.mkdirSync(target, { recursive: true });
62
+ const marker = path.join(target, '.amalgm-home-match.json');
63
+ if (fs.existsSync(marker) && process.env.AMALGM_FORCE_HOME_MATCH !== '1') return [];
64
+ const summaries = [];
65
+ const checked = [];
66
+ for (const rawSource of Array.isArray(sources) ? sources : []) {
67
+ if (typeof rawSource !== 'string' || !rawSource.trim()) continue;
68
+ const source = path.resolve(rawSource);
69
+ if (source === target || isSubpath(target, source) || isSubpath(source, target)) continue;
70
+ if (!fs.existsSync(source)) continue;
71
+ checked.push(source);
72
+ const stats = { source, copied: 0, dirs: 0, skipped: 0 };
73
+ mergeMissingEntries(source, target, stats);
74
+ if (stats.copied || stats.dirs) summaries.push(stats);
75
+ }
76
+ if (checked.length) {
77
+ fs.writeFileSync(marker, `${JSON.stringify({
78
+ matched_at: new Date().toISOString(),
79
+ checked,
80
+ sources: summaries,
81
+ }, null, 2)}\n`);
82
+ }
83
+ return summaries;
84
+ }
85
+
15
86
  function prepareHarnessRuntime(contract, baseEnv = process.env) {
16
87
  const runtimeHome = managedRuntimeHome(contract);
17
88
  fs.mkdirSync(runtimeHome, { recursive: true });
89
+ const homeMatch = matchRuntimeHome(runtimeHome, contract?.auth?.runtimeHomeSources);
18
90
  const syncInfo = contract?.authMethod === 'provider_auth'
19
91
  ? syncNativeProviderAuth(contract)
20
92
  : null;
21
93
  return {
22
94
  runtimeHome,
95
+ homeMatch,
23
96
  syncInfo,
24
97
  env: runtimeEnv(contract, baseEnv),
25
98
  };
26
99
  }
27
100
 
28
101
  module.exports = {
102
+ matchRuntimeHome,
29
103
  managedRuntimeHome,
30
104
  prepareHarnessRuntime,
31
105
  };