amalgm 0.1.69 → 0.1.70

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.69",
3
+ "version": "0.1.70",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -774,7 +774,7 @@ var HARNESSES = {
774
774
  logoKey: "opencode",
775
775
  models: OPENCODE_MODELS,
776
776
  defaultModelId: DEFAULT_MODEL_IDS.opencode,
777
- supportedAuthMethods: ["amalgm", "byok"],
777
+ supportedAuthMethods: ["amalgm", "byok", "provider_auth"],
778
778
  dynamicModels: true
779
779
  },
780
780
  codex: {
@@ -34,7 +34,7 @@ const DEFAULT_AUTH_METHODS = {
34
34
  const SUPPORTED_AUTH_BY_HARNESS = {
35
35
  claude_code: ['amalgm', 'byok', 'provider_auth'],
36
36
  codex: ['amalgm', 'byok', 'provider_auth'],
37
- opencode: ['amalgm', 'byok'],
37
+ opencode: ['amalgm', 'byok', 'provider_auth'],
38
38
  pi: ['amalgm', 'byok', 'provider_auth'],
39
39
  amp: ['provider_auth'],
40
40
  cursor: ['provider_auth'],
@@ -36,7 +36,7 @@ class ClaudeAdapter {
36
36
  const systemPrompt = composeSystemPrompt(contract);
37
37
  const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
38
38
  const settings = {
39
- ...(claudeNativeHookSettings({ cwd: contract.cwd }) || {}),
39
+ ...(contract.authMethod === 'provider_auth' ? (claudeNativeHookSettings({ cwd: contract.cwd }) || {}) : {}),
40
40
  ...(contract.fastMode ? { fastMode: true, fastModePerSessionOptIn: true } : {}),
41
41
  };
42
42
  return {
@@ -363,7 +363,9 @@ function writeConfig(contract, syncInfo) {
363
363
  const home = contract.auth.runtimeHome;
364
364
  if (!home) return;
365
365
  fs.mkdirSync(home, { recursive: true });
366
- if (syncInfo === undefined) syncInfo = syncCodexNativeConfig(home);
366
+ if (syncInfo === undefined) {
367
+ syncInfo = contract.authMethod === 'provider_auth' ? syncCodexNativeConfig(home) : null;
368
+ }
367
369
  const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
368
370
  const configPath = path.join(home, 'config.toml');
369
371
  if (contract.authMethod === 'provider_auth') {
@@ -39,7 +39,7 @@ function splitModel(model, auth) {
39
39
  function configFor(contract) {
40
40
  const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
41
41
  const systemPrompt = composeSystemPrompt(contract);
42
- return {
42
+ const config = {
43
43
  logLevel: process.env.OPENCODE_LOG_LEVEL || 'ERROR',
44
44
  enabled_providers: [model.providerID],
45
45
  disabled_providers: [],
@@ -75,7 +75,9 @@ function configFor(contract) {
75
75
  title: { disable: true },
76
76
  },
77
77
  mcp: toOpenCodeMcpConfig(contract),
78
- provider: {
78
+ };
79
+ if (contract.authMethod !== 'provider_auth') {
80
+ config.provider = {
79
81
  [model.providerID]: {
80
82
  options: {
81
83
  baseURL: contract.auth.baseUrl,
@@ -85,8 +87,9 @@ function configFor(contract) {
85
87
  chunkTimeout: 300000,
86
88
  },
87
89
  },
88
- },
89
- };
90
+ };
91
+ }
92
+ return config;
90
93
  }
91
94
 
92
95
  function resultData(result) {
@@ -9,7 +9,7 @@ const { resolveCredential } = require('./credentials/store');
9
9
  const AUTH_BY_HARNESS = {
10
10
  claude_code: ['amalgm', 'byok', 'provider_auth'],
11
11
  codex: ['amalgm', 'byok', 'provider_auth'],
12
- opencode: ['amalgm', 'byok'],
12
+ opencode: ['amalgm', 'byok', 'provider_auth'],
13
13
  };
14
14
 
15
15
  function shellValue(raw) {
@@ -109,35 +109,15 @@ function providerAuthIdentity(harness) {
109
109
  return `${harness || 'provider'}:provider-default`;
110
110
  }
111
111
 
112
- function deriveAuthProfileId({ method, harness, userId, tokenFingerprint }) {
113
- if (method === 'amalgm') {
114
- return `amalgm-${fingerprint(userId || process.env.AMALGM_USER_ID || 'local') || 'local'}`;
115
- }
116
- if (method === 'byok') {
117
- return `byok-${tokenFingerprint || 'default'}`;
118
- }
119
- if (method === 'provider_auth') {
120
- return `provider-${fingerprint(providerAuthIdentity(harness)) || 'default'}`;
121
- }
122
- return 'default';
123
- }
124
-
125
- function runtimeHome({ amalgmDir, harness, authProfileId }) {
126
- return path.join(amalgmDir, 'cli-homes', safePathSegment(harness), safePathSegment(authProfileId));
127
- }
128
-
129
- function pinnedRuntimeHome(amalgmDir, cliHomePath) {
130
- if (typeof cliHomePath !== 'string' || !cliHomePath.trim()) return null;
131
- const root = path.resolve(amalgmDir);
132
- const resolved = path.resolve(cliHomePath.trim());
133
- return resolved === root || resolved.startsWith(`${root}${path.sep}`) ? resolved : null;
112
+ function runtimeHome({ amalgmDir, harness }) {
113
+ return path.join(amalgmDir, 'cli-homes', safePathSegment(harness), 'home');
134
114
  }
135
115
 
136
116
  function providerAuthFingerprint(harness) {
137
117
  return fingerprint(providerAuthIdentity(harness));
138
118
  }
139
119
 
140
- function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, userId, credentialId, modelId, cliHomePath, authProfileId }) {
120
+ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, userId, credentialId, modelId }) {
141
121
  const method = coerceAuth(harness, authMethod);
142
122
  const byok = readByokFile(amalgmDir);
143
123
  let baseUrl = null;
@@ -166,7 +146,6 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
166
146
  } else if (method === 'provider_auth') {
167
147
  tokenFingerprint = providerAuthFingerprint(harness);
168
148
  }
169
- const profileId = authProfileId || deriveAuthProfileId({ method, harness, userId, tokenFingerprint });
170
149
  return {
171
150
  method,
172
151
  baseUrl,
@@ -176,10 +155,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
176
155
  forwardBaseUrl,
177
156
  tokenRef,
178
157
  tokenFingerprint,
179
- authProfileId: profileId,
180
- runtimeHome:
181
- pinnedRuntimeHome(amalgmDir, cliHomePath)
182
- || runtimeHome({ amalgmDir, harness, authProfileId: profileId }),
158
+ runtimeHome: runtimeHome({ amalgmDir, harness }),
183
159
  };
184
160
  }
185
161
 
@@ -203,6 +179,12 @@ function runtimeEnv(contract, baseEnv = process.env) {
203
179
  env.CLAUDE_CONFIG_DIR = contract.auth.runtimeHome;
204
180
  env.OPENCODE_HOME = contract.auth.runtimeHome;
205
181
  env.OPENCODE_CONFIG_DIR = contract.auth.runtimeHome;
182
+ if (contract.harness === 'opencode') {
183
+ env.XDG_CONFIG_HOME = path.join(contract.auth.runtimeHome, '.config');
184
+ env.XDG_DATA_HOME = path.join(contract.auth.runtimeHome, '.local', 'share');
185
+ env.XDG_STATE_HOME = path.join(contract.auth.runtimeHome, '.local', 'state');
186
+ env.XDG_CACHE_HOME = path.join(contract.auth.runtimeHome, '.cache');
187
+ }
206
188
  if (contract.harness === 'claude_code' && contract.authMethod === 'provider_auth') {
207
189
  delete env.CLAUDE_CONFIG_DIR;
208
190
  }
@@ -347,8 +347,6 @@ function createContract(payload, options = {}) {
347
347
  userId,
348
348
  credentialId: payload.credentialId || payload.byokCredentialId || null,
349
349
  modelId,
350
- cliHomePath: payload.cliHomePath || payload.cli_home_path || null,
351
- authProfileId: payload.authProfileId || payload.auth_profile_id || null,
352
350
  });
353
351
  const contract = {
354
352
  sessionId,
@@ -179,7 +179,6 @@ class ChatCore {
179
179
  auth: {
180
180
  baseUrl: contract.auth.baseUrl,
181
181
  tokenFingerprint: contract.auth.tokenFingerprint,
182
- authProfileId: contract.auth.authProfileId,
183
182
  runtimeHome: contract.auth.runtimeHome,
184
183
  },
185
184
  },
@@ -1,37 +1,54 @@
1
1
  'use strict';
2
2
 
3
3
  const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
4
7
  const test = require('node:test');
5
8
  const { authEnvelope, runtimeEnv } = require('../auth');
6
9
 
7
- test('amalgm auth uses a pinned CLI home across sessions and proxy token refreshes', () => {
8
- const first = authEnvelope({
9
- harness: 'codex',
10
- authMethod: 'amalgm',
11
- sessionId: 'session-one',
12
- proxyToken: 'proxy-token-one',
13
- localBaseUrl: 'http://127.0.0.1:8084',
14
- proxyBaseUrl: 'https://proxy.example.test',
15
- amalgmDir: '/tmp/amalgm-test',
16
- userId: 'user-123',
17
- });
18
- const second = authEnvelope({
19
- harness: 'codex',
20
- authMethod: 'amalgm',
21
- sessionId: 'session-two',
22
- proxyToken: 'proxy-token-two',
23
- localBaseUrl: 'http://127.0.0.1:8084',
24
- proxyBaseUrl: 'https://proxy.example.test',
25
- amalgmDir: '/tmp/amalgm-test',
26
- userId: 'user-123',
27
- });
10
+ test('codex always uses one canonical CLI home across auth methods and token refreshes', () => {
11
+ const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-auth-test-'));
12
+ fs.writeFileSync(path.join(amalgmDir, '.amalgm-credentials'), 'OPENAI_API_KEY=sk-test\n');
13
+ try {
14
+ const first = authEnvelope({
15
+ harness: 'codex',
16
+ authMethod: 'amalgm',
17
+ sessionId: 'session-one',
18
+ proxyToken: 'proxy-token-one',
19
+ localBaseUrl: 'http://127.0.0.1:8084',
20
+ proxyBaseUrl: 'https://proxy.example.test',
21
+ amalgmDir,
22
+ userId: 'user-123',
23
+ });
24
+ const second = authEnvelope({
25
+ harness: 'codex',
26
+ authMethod: 'provider_auth',
27
+ sessionId: 'session-two',
28
+ proxyToken: 'proxy-token-two',
29
+ localBaseUrl: 'http://127.0.0.1:8084',
30
+ proxyBaseUrl: 'https://proxy.example.test',
31
+ amalgmDir,
32
+ userId: 'user-123',
33
+ });
34
+ const third = authEnvelope({
35
+ harness: 'codex',
36
+ authMethod: 'byok',
37
+ sessionId: 'session-three',
38
+ amalgmDir,
39
+ userId: 'user-123',
40
+ });
28
41
 
29
- assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
30
- assert.equal(first.runtimeHome, second.runtimeHome);
31
- assert.match(first.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/codex\/amalgm-[0-9a-f]{16}$/);
42
+ assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
43
+ assert.equal(first.runtimeHome, second.runtimeHome);
44
+ assert.equal(second.runtimeHome, third.runtimeHome);
45
+ assert.equal(first.runtimeHome, path.join(amalgmDir, 'cli-homes', 'codex', 'home'));
46
+ } finally {
47
+ fs.rmSync(amalgmDir, { recursive: true, force: true });
48
+ }
32
49
  });
33
50
 
34
- test('opencode amalgm auth uses a pinned CLI home across sessions and proxy token refreshes', () => {
51
+ test('opencode always uses one canonical CLI home across auth methods and token refreshes', () => {
35
52
  const first = authEnvelope({
36
53
  harness: 'opencode',
37
54
  authMethod: 'amalgm',
@@ -44,7 +61,7 @@ test('opencode amalgm auth uses a pinned CLI home across sessions and proxy toke
44
61
  });
45
62
  const second = authEnvelope({
46
63
  harness: 'opencode',
47
- authMethod: 'amalgm',
64
+ authMethod: 'provider_auth',
48
65
  sessionId: 'session-two',
49
66
  proxyToken: 'proxy-token-two',
50
67
  localBaseUrl: 'http://127.0.0.1:8084',
@@ -56,13 +73,15 @@ test('opencode amalgm auth uses a pinned CLI home across sessions and proxy toke
56
73
 
57
74
  assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
58
75
  assert.equal(first.runtimeHome, second.runtimeHome);
59
- assert.match(first.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/opencode\/amalgm-[0-9a-f]{16}$/);
76
+ assert.equal(first.runtimeHome, '/tmp/amalgm-test/cli-homes/opencode/home');
60
77
  assert.equal(env.HOME, first.runtimeHome);
61
78
  assert.equal(env.OPENCODE_HOME, first.runtimeHome);
62
79
  assert.equal(env.OPENCODE_CONFIG_DIR, first.runtimeHome);
80
+ assert.equal(env.XDG_CONFIG_HOME, '/tmp/amalgm-test/cli-homes/opencode/home/.config');
81
+ assert.equal(env.XDG_DATA_HOME, '/tmp/amalgm-test/cli-homes/opencode/home/.local/share');
63
82
  });
64
83
 
65
- test('claude provider auth uses a pinned CLI home with native config copied separately', () => {
84
+ test('claude always uses one canonical CLI home', () => {
66
85
  const envelope = authEnvelope({
67
86
  harness: 'claude_code',
68
87
  authMethod: 'provider_auth',
@@ -70,7 +89,7 @@ test('claude provider auth uses a pinned CLI home with native config copied sepa
70
89
  amalgmDir: '/tmp/amalgm-test',
71
90
  });
72
91
 
73
- assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/claude_code\/provider-[0-9a-f]{16}$/);
92
+ assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/claude_code/home');
74
93
 
75
94
  const env = runtimeEnv({
76
95
  harness: 'claude_code',
@@ -88,19 +107,7 @@ test('claude provider auth uses a pinned CLI home with native config copied sepa
88
107
  assert.equal(env.ANTHROPIC_API_KEY, undefined);
89
108
  });
90
109
 
91
- test('codex provider auth uses a pinned provider home', () => {
92
- const envelope = authEnvelope({
93
- harness: 'codex',
94
- authMethod: 'provider_auth',
95
- sessionId: 'session-test',
96
- amalgmDir: '/tmp/amalgm-test',
97
- });
98
-
99
- assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/cli-homes\/codex\/provider-[0-9a-f]{16}$/);
100
- assert.equal(envelope.runtimeHome.includes('/runtime/session-test/'), false);
101
- });
102
-
103
- test('stored CLI home path wins over derived path', () => {
110
+ test('provided CLI home path cannot move an agent off its canonical home', () => {
104
111
  const envelope = authEnvelope({
105
112
  harness: 'codex',
106
113
  authMethod: 'amalgm',
@@ -114,6 +121,5 @@ test('stored CLI home path wins over derived path', () => {
114
121
  authProfileId: 'manual-profile',
115
122
  });
116
123
 
117
- assert.equal(envelope.authProfileId, 'manual-profile');
118
- assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/manual-profile');
124
+ assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/home');
119
125
  });
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const { spawnSync } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const test = require('node:test');
9
+ const nativeBinaries = require('../tooling/native-binaries');
10
+
11
+ function packagePath(root, packageName) {
12
+ return path.join(root, ...String(packageName).split('/'));
13
+ }
14
+
15
+ function writePackage(root, packageName) {
16
+ fs.mkdirSync(root, { recursive: true });
17
+ fs.writeFileSync(path.join(root, 'package.json'), `${JSON.stringify({ name: packageName, version: '0.0.0' }, null, 2)}\n`);
18
+ }
19
+
20
+ function writeExecutable(file) {
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ if (process.platform === 'win32') {
23
+ fs.writeFileSync(file, '@echo off\r\nexit /b 0\r\n');
24
+ } else {
25
+ fs.writeFileSync(file, '#!/bin/sh\nexit 0\n');
26
+ fs.chmodSync(file, 0o755);
27
+ }
28
+ }
29
+
30
+ function installFakePackagedAgentDeps(resourcesPath) {
31
+ const nodeModules = path.join(resourcesPath, 'app.asar.unpacked', 'node_modules');
32
+ const claudePackage = nativeBinaries.__private.claudeTargetPackage();
33
+ const codexTarget = nativeBinaries.__private.codexTarget();
34
+ const openCodePackage = nativeBinaries.__private.openCodePackageCandidates()[0];
35
+ assert.ok(claudePackage, 'current platform should have a Claude native package');
36
+ assert.ok(codexTarget, 'current platform should have a Codex native package');
37
+ assert.ok(openCodePackage, 'current platform should have an OpenCode native package');
38
+
39
+ writePackage(path.join(nodeModules, '@anthropic-ai', 'claude-agent-sdk'), '@anthropic-ai/claude-agent-sdk');
40
+ writePackage(path.join(nodeModules, '@openai', 'codex'), '@openai/codex');
41
+ writePackage(path.join(nodeModules, 'opencode-ai'), 'opencode-ai');
42
+
43
+ const claudeRoot = packagePath(
44
+ path.join(nodeModules, '@anthropic-ai', 'claude-agent-sdk', 'node_modules'),
45
+ claudePackage,
46
+ );
47
+ writePackage(claudeRoot, claudePackage);
48
+ const claude = path.join(claudeRoot, process.platform === 'win32' ? 'claude.exe' : 'claude');
49
+ writeExecutable(claude);
50
+
51
+ const codexRoot = packagePath(
52
+ path.join(nodeModules, '@openai', 'codex', 'node_modules'),
53
+ codexTarget.packageName,
54
+ );
55
+ writePackage(codexRoot, codexTarget.packageName);
56
+ const codex = path.join(codexRoot, 'vendor', codexTarget.triple, 'codex', codexTarget.binaryName);
57
+ writeExecutable(codex);
58
+
59
+ const openCodeRoot = packagePath(
60
+ path.join(nodeModules, 'opencode-ai', 'node_modules'),
61
+ openCodePackage,
62
+ );
63
+ writePackage(openCodeRoot, openCodePackage);
64
+ const opencode = path.join(openCodeRoot, 'bin', process.platform === 'win32' ? 'opencode.exe' : 'opencode');
65
+ writeExecutable(opencode);
66
+
67
+ return { claude, codex, opencode };
68
+ }
69
+
70
+ function withFakePackagedResources(fn) {
71
+ const previousResourcesPath = Object.getOwnPropertyDescriptor(process, 'resourcesPath');
72
+ const previousEnv = {
73
+ AMALGM_AGENT_BIN_DIR: process.env.AMALGM_AGENT_BIN_DIR,
74
+ AMALGM_NATIVE_NODE_MODULES: process.env.AMALGM_NATIVE_NODE_MODULES,
75
+ CLAUDE_CODE_BINARY: process.env.CLAUDE_CODE_BINARY,
76
+ PATH: process.env.PATH,
77
+ };
78
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-native-binaries-'));
79
+ const resourcesPath = path.join(root, 'resources');
80
+ const binDir = path.join(root, 'bin');
81
+ try {
82
+ Object.defineProperty(process, 'resourcesPath', {
83
+ value: resourcesPath,
84
+ configurable: true,
85
+ });
86
+ process.env.AMALGM_AGENT_BIN_DIR = binDir;
87
+ process.env.PATH = '';
88
+ delete process.env.AMALGM_NATIVE_NODE_MODULES;
89
+ delete process.env.CLAUDE_CODE_BINARY;
90
+ return fn({ root, resourcesPath, binDir });
91
+ } finally {
92
+ if (previousResourcesPath) {
93
+ Object.defineProperty(process, 'resourcesPath', previousResourcesPath);
94
+ } else {
95
+ delete process.resourcesPath;
96
+ }
97
+ for (const [key, value] of Object.entries(previousEnv)) {
98
+ if (value === undefined) delete process.env[key];
99
+ else process.env[key] = value;
100
+ }
101
+ fs.rmSync(root, { recursive: true, force: true });
102
+ }
103
+ }
104
+
105
+ test('packaged nested agent dependencies resolve to exact executable paths and spawnable shims', () => {
106
+ withFakePackagedResources(({ resourcesPath, binDir }) => {
107
+ const expected = installFakePackagedAgentDeps(resourcesPath);
108
+
109
+ assert.equal(nativeBinaries.__private.bundledClaudeBinary(), expected.claude);
110
+ assert.equal(nativeBinaries.bundledCodexBinary(), expected.codex);
111
+ assert.equal(nativeBinaries.bundledOpenCodeBinary(), expected.opencode);
112
+
113
+ const result = nativeBinaries.ensureAgentCommandShims({ quiet: true, logger: { log() {} } });
114
+ assert.equal(result.ok, true);
115
+ for (const command of ['claude', 'codex', 'opencode']) {
116
+ const shim = path.join(binDir, process.platform === 'win32' ? `${command}.cmd` : command);
117
+ assert.equal(fs.existsSync(shim), true);
118
+ const spawned = spawnSync(shim, ['--smoke'], {
119
+ stdio: 'ignore',
120
+ shell: process.platform === 'win32',
121
+ windowsHide: true,
122
+ });
123
+ assert.equal(spawned.status, 0, `${command} shim should spawn`);
124
+ }
125
+ });
126
+ });
@@ -11,6 +11,7 @@ const {
11
11
  claudeNativeHookSettings,
12
12
  syncClaudeNativeConfig,
13
13
  syncCodexNativeConfig,
14
+ syncOpenCodeNativeConfig,
14
15
  } = require('../tooling/native-config');
15
16
  const { prepareHarnessRuntime } = require('../tooling/runtime-home');
16
17
 
@@ -108,6 +109,50 @@ test('prepared opencode runtime always has an isolated managed home', () => {
108
109
  });
109
110
  });
110
111
 
112
+ test('provider auth imports opencode native config into managed home', () => {
113
+ withNativeHome((home) => {
114
+ const configDir = path.join(home, '.config', 'opencode');
115
+ const dataDir = path.join(home, '.local', 'share', 'opencode');
116
+ fs.mkdirSync(configDir, { recursive: true });
117
+ fs.mkdirSync(dataDir, { recursive: true });
118
+ fs.writeFileSync(path.join(configDir, 'opencode.json'), '{"mcp":{}}');
119
+ fs.writeFileSync(path.join(dataDir, 'auth.json'), '{"provider":"test"}');
120
+
121
+ const runtimeHome = path.join(home, 'managed', 'opencode-home');
122
+ const prepared = prepareHarnessRuntime({
123
+ harness: 'opencode',
124
+ authMethod: 'provider_auth',
125
+ auth: { runtimeHome },
126
+ }, { PATH: '/bin', HOME: home });
127
+
128
+ assert.equal(prepared.runtimeHome, runtimeHome);
129
+ assert.equal(prepared.env.HOME, runtimeHome);
130
+ assert.equal(prepared.env.OPENCODE_HOME, runtimeHome);
131
+ assert.equal(prepared.env.XDG_CONFIG_HOME, path.join(runtimeHome, '.config'));
132
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.config', 'opencode', 'opencode.json')), true);
133
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'auth.json')), true);
134
+ });
135
+ });
136
+
137
+ test('non-user auth prepares a home without importing native config', () => {
138
+ withNativeHome((home) => {
139
+ const source = path.join(home, '.codex');
140
+ fs.mkdirSync(source, { recursive: true });
141
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "native"');
142
+
143
+ const runtimeHome = path.join(home, 'managed', 'codex-home');
144
+ const prepared = prepareHarnessRuntime({
145
+ harness: 'codex',
146
+ authMethod: 'amalgm',
147
+ auth: { runtimeHome, tokenRef: 'test-token', baseUrl: 'https://example.test' },
148
+ }, { PATH: '/bin', HOME: home });
149
+
150
+ assert.equal(prepared.syncInfo, null);
151
+ assert.equal(fs.existsSync(runtimeHome), true);
152
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), false);
153
+ });
154
+ });
155
+
111
156
  test('codex provider config keeps native mcp config and overrides managed model provider', () => {
112
157
  withNativeHome((home) => {
113
158
  const source = path.join(home, '.codex');
@@ -147,7 +192,7 @@ test('codex provider config keeps native mcp config and overrides managed model
147
192
  });
148
193
  });
149
194
 
150
- test('codex amalgm config keeps native mcp config and overrides managed model provider', () => {
195
+ test('codex amalgm config does not import native user config', () => {
151
196
  withNativeHome((home) => {
152
197
  const source = path.join(home, '.codex');
153
198
  fs.mkdirSync(source, { recursive: true });
@@ -176,12 +221,11 @@ test('codex amalgm config keeps native mcp config and overrides managed model pr
176
221
 
177
222
  const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
178
223
  assert.match(config, /model_provider = "amalgm"/);
179
- assert.match(config, /codex_hooks = true/);
180
224
  assert.doesNotMatch(config, /native-provider/);
181
- assert.match(config, /\[mcp_servers\.native\]/);
182
- assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
183
- assert.match(config, /Authorization = "Bearer test"/);
184
- assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
225
+ assert.doesNotMatch(config, /codex_hooks = true/);
226
+ assert.doesNotMatch(config, /\[mcp_servers\.native\]/);
227
+ assert.doesNotMatch(config, /Authorization = "Bearer test"/);
228
+ assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), false);
185
229
  });
186
230
  });
187
231
 
@@ -219,7 +263,7 @@ test('claude extracts native hooks without enabling full filesystem settings', (
219
263
  });
220
264
  });
221
265
 
222
- test('claude native sync skips debug symlinks', () => {
266
+ test('claude native sync does not delete existing debug symlinks', () => {
223
267
  withNativeHome((home) => {
224
268
  const source = path.join(home, '.claude');
225
269
  fs.mkdirSync(path.join(source, 'debug'), { recursive: true });
@@ -237,7 +281,41 @@ test('claude native sync skips debug symlinks', () => {
237
281
  assert.equal(result.sourceDir, source);
238
282
  assert.equal(fs.existsSync(path.join(runtimeHome, 'settings.json')), true);
239
283
  assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'trace.txt')), false);
240
- assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'latest')), false);
284
+ assert.equal(fs.lstatSync(path.join(runtimeHome, 'debug', 'latest')).isSymbolicLink(), true);
285
+ });
286
+ });
287
+
288
+ test('native sync never replaces existing in-home dot dirs', () => {
289
+ withNativeHome((home) => {
290
+ const source = path.join(home, '.codex');
291
+ fs.mkdirSync(source, { recursive: true });
292
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
293
+
294
+ const runtimeHome = path.join(home, 'runtime-home');
295
+ fs.mkdirSync(path.join(runtimeHome, '.codex'), { recursive: true });
296
+ fs.writeFileSync(path.join(runtimeHome, '.codex', 'sentinel'), 'keep');
297
+
298
+ syncCodexNativeConfig(runtimeHome);
299
+
300
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'sentinel')), true);
301
+ assert.equal(fs.lstatSync(path.join(runtimeHome, '.codex')).isDirectory(), true);
302
+ });
303
+ });
304
+
305
+ test('opencode native sync preserves existing session state', () => {
306
+ withNativeHome((home) => {
307
+ const dataDir = path.join(home, '.local', 'share', 'opencode');
308
+ fs.mkdirSync(dataDir, { recursive: true });
309
+ fs.writeFileSync(path.join(dataDir, 'auth.json'), '{"provider":"test"}');
310
+
311
+ const runtimeHome = path.join(home, 'runtime-home');
312
+ fs.mkdirSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session'), { recursive: true });
313
+ fs.writeFileSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session', 'sentinel'), 'keep');
314
+
315
+ syncOpenCodeNativeConfig(runtimeHome);
316
+
317
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'auth.json')), true);
318
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session', 'sentinel')), true);
241
319
  });
242
320
  });
243
321
 
@@ -11,13 +11,22 @@ function amalgmDir() {
11
11
 
12
12
  function nativeNodeModulesDirs() {
13
13
  const resourcesPath = process.resourcesPath || '';
14
- return unique([
15
- process.env.AMALGM_NATIVE_NODE_MODULES,
16
- path.join(amalgmDir(), 'native', 'node_modules'),
14
+ const resourceNodeModules = [
17
15
  resourcesPath ? path.join(resourcesPath, 'app.asar.unpacked', 'node_modules') : '',
18
16
  resourcesPath ? path.join(resourcesPath, 'app.asar', 'node_modules') : '',
19
17
  resourcesPath ? path.join(resourcesPath, 'cli', 'node_modules') : '',
20
18
  resourcesPath ? path.join(resourcesPath, 'runtime', 'node_modules') : '',
19
+ ].filter(Boolean);
20
+ const wrapperPackages = [
21
+ '@anthropic-ai/claude-agent-sdk',
22
+ '@openai/codex',
23
+ 'opencode-ai',
24
+ ];
25
+ return unique([
26
+ process.env.AMALGM_NATIVE_NODE_MODULES,
27
+ path.join(amalgmDir(), 'native', 'node_modules'),
28
+ ...resourceNodeModules,
29
+ ...resourceNodeModules.flatMap((dir) => wrapperPackages.map((packageName) => path.join(dir, packageName, 'node_modules'))),
21
30
  ].filter(Boolean));
22
31
  }
23
32
 
@@ -619,6 +628,14 @@ function ensureNativeBinaries(options = {}) {
619
628
  }
620
629
 
621
630
  module.exports = {
631
+ __private: {
632
+ bundledClaudeBinary,
633
+ claudeTargetPackage,
634
+ codexTarget,
635
+ nativeNodeModulesDirs,
636
+ openCodePackageCandidates,
637
+ packageRoot,
638
+ },
622
639
  binaryStatus,
623
640
  bundledCodexPathDirs,
624
641
  bundledClaudeBinary: resolveClaudeBinary,
@@ -57,20 +57,12 @@ function shouldCopyConfigPath(root, source) {
57
57
  function copyConfigTree(sourceDir, targetDir) {
58
58
  if (!sourceDir || !targetDir || !exists(sourceDir)) return false;
59
59
  if (path.resolve(sourceDir) === path.resolve(targetDir)) return true;
60
- fs.mkdirSync(targetDir, { recursive: true });
61
- fs.cpSync(sourceDir, targetDir, {
62
- recursive: true,
63
- force: true,
64
- errorOnExist: false,
65
- dereference: false,
66
- filter: (source) => shouldCopyConfigPath(sourceDir, source),
67
- });
68
- return true;
60
+ return copyDirBounded(sourceDir, targetDir, { maxFiles: 500, maxBytes: 25 * 1024 * 1024 }).copied;
69
61
  }
70
62
 
71
63
  function copyFileIfPresent(source, target) {
72
64
  if (!exists(source)) return false;
73
- fs.mkdirSync(path.dirname(target), { recursive: true });
65
+ if (!ensureDirectory(path.dirname(target)) || !canWriteFileTarget(target)) return false;
74
66
  fs.copyFileSync(source, target);
75
67
  try {
76
68
  fs.chmodSync(target, fs.statSync(source).mode & 0o777);
@@ -86,10 +78,8 @@ function ensureHomeAlias(runtimeHome, dotDirName) {
86
78
  if (stat.isSymbolicLink()) {
87
79
  const target = fs.readlinkSync(alias);
88
80
  if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
89
- fs.rmSync(alias, { recursive: true, force: true });
90
- } else {
91
- fs.rmSync(alias, { recursive: true, force: true });
92
81
  }
82
+ return;
93
83
  } catch {}
94
84
  try {
95
85
  fs.symlinkSync('.', alias, 'dir');
@@ -98,28 +88,6 @@ function ensureHomeAlias(runtimeHome, dotDirName) {
98
88
  }
99
89
  }
100
90
 
101
- function removeOutboundSymlink(root, relativePath) {
102
- const file = path.join(root, relativePath);
103
- let stat;
104
- try {
105
- stat = fs.lstatSync(file);
106
- } catch {
107
- return;
108
- }
109
- if (!stat.isSymbolicLink()) return;
110
- let target;
111
- try {
112
- target = fs.readlinkSync(file);
113
- } catch {
114
- return;
115
- }
116
- const resolvedRoot = path.resolve(root);
117
- const resolvedTarget = path.resolve(path.dirname(file), target);
118
- if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) {
119
- fs.rmSync(file, { force: true });
120
- }
121
- }
122
-
123
91
  function ensureNativeKeychainAlias(runtimeHome) {
124
92
  if (process.platform !== 'darwin' || !runtimeHome) return false;
125
93
  const source = path.join(nativeHome(), 'Library', 'Keychains');
@@ -131,7 +99,7 @@ function ensureNativeKeychainAlias(runtimeHome) {
131
99
  const linkTarget = fs.readlinkSync(target);
132
100
  if (path.resolve(path.dirname(target), linkTarget) === path.resolve(source)) return true;
133
101
  }
134
- fs.rmSync(target, { recursive: true, force: true });
102
+ return false;
135
103
  } catch {}
136
104
  fs.mkdirSync(path.dirname(target), { recursive: true });
137
105
  try {
@@ -146,6 +114,29 @@ function nativeHome() {
146
114
  return process.env.AMALGM_NATIVE_HOME || os.homedir();
147
115
  }
148
116
 
117
+ function ensureDirectory(dir) {
118
+ try {
119
+ const stat = fs.lstatSync(dir);
120
+ return stat.isDirectory();
121
+ } catch {
122
+ try {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ }
130
+
131
+ function canWriteFileTarget(target) {
132
+ try {
133
+ const stat = fs.lstatSync(target);
134
+ return stat.isFile();
135
+ } catch {
136
+ return true;
137
+ }
138
+ }
139
+
149
140
  function copyDirBounded(sourceDir, targetDir, options = {}) {
150
141
  if (!sourceDir || !targetDir || !exists(sourceDir)) return { copied: false, files: 0, bytes: 0, truncated: false };
151
142
  const maxFiles = Number(options.maxFiles || 200);
@@ -163,7 +154,7 @@ function copyDirBounded(sourceDir, targetDir, options = {}) {
163
154
  }
164
155
  if (stat.isSymbolicLink()) return;
165
156
  if (stat.isDirectory()) {
166
- fs.mkdirSync(target, { recursive: true });
157
+ if (!ensureDirectory(target)) return;
167
158
  for (const entry of fs.readdirSync(source)) {
168
159
  walk(path.join(source, entry), path.join(target, entry));
169
160
  if (state.truncated) break;
@@ -175,7 +166,7 @@ function copyDirBounded(sourceDir, targetDir, options = {}) {
175
166
  state.truncated = true;
176
167
  return;
177
168
  }
178
- fs.mkdirSync(path.dirname(target), { recursive: true });
169
+ if (!ensureDirectory(path.dirname(target)) || !canWriteFileTarget(target)) return;
179
170
  fs.copyFileSync(source, target);
180
171
  state.files += 1;
181
172
  state.bytes += stat.size;
@@ -218,7 +209,6 @@ function syncClaudeNativeConfig(runtimeHome) {
218
209
  if (path.resolve(runtimeHome) === path.resolve(home)) return null;
219
210
  const sourceDir = path.join(home, '.claude');
220
211
  fs.mkdirSync(runtimeHome, { recursive: true });
221
- removeOutboundSymlink(runtimeHome, path.join('debug', 'latest'));
222
212
  const copied = copyConfigTree(sourceDir, runtimeHome);
223
213
  ensureHomeAlias(runtimeHome, '.claude');
224
214
  ensureNativeKeychainAlias(runtimeHome);
@@ -227,6 +217,27 @@ function syncClaudeNativeConfig(runtimeHome) {
227
217
  return copied ? { sourceDir, runtimeHome } : null;
228
218
  }
229
219
 
220
+ function syncOpenCodeNativeConfig(runtimeHome) {
221
+ if (!runtimeHome) return null;
222
+ const home = nativeHome();
223
+ fs.mkdirSync(runtimeHome, { recursive: true });
224
+ const config = copyConfigTree(
225
+ path.join(home, '.config', 'opencode'),
226
+ path.join(runtimeHome, '.config', 'opencode'),
227
+ );
228
+ const data = copyConfigTree(
229
+ path.join(home, '.local', 'share', 'opencode'),
230
+ path.join(runtimeHome, '.local', 'share', 'opencode'),
231
+ );
232
+ const dot = copyConfigTree(path.join(home, '.opencode'), path.join(runtimeHome, '.opencode'));
233
+ if (!config && !data && !dot) return null;
234
+ return {
235
+ sourceDir: path.join(home, '.config', 'opencode'),
236
+ runtimeHome,
237
+ copied: config || data || dot,
238
+ };
239
+ }
240
+
230
241
  function readJsonFile(file) {
231
242
  try {
232
243
  return JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -271,8 +282,10 @@ function claudeNativeHookSettings(options = {}) {
271
282
  function syncNativeHarnessConfig(contract) {
272
283
  const runtimeHome = contract?.auth?.runtimeHome;
273
284
  if (!runtimeHome) return null;
285
+ if (contract.authMethod !== 'provider_auth') return null;
274
286
  if (contract.harness === 'codex') return syncCodexNativeConfig(runtimeHome);
275
287
  if (contract.harness === 'claude_code') return syncClaudeNativeConfig(runtimeHome);
288
+ if (contract.harness === 'opencode') return syncOpenCodeNativeConfig(runtimeHome);
276
289
  return null;
277
290
  }
278
291
 
@@ -282,6 +295,7 @@ module.exports = {
282
295
  copyConfigTree,
283
296
  copyDirBounded,
284
297
  copyFileIfPresent,
298
+ ensureDirectory,
285
299
  ensureNativeKeychainAlias,
286
300
  ensureHomeAlias,
287
301
  shouldCopyConfigPath,
@@ -290,4 +304,5 @@ module.exports = {
290
304
  syncClaudeNativeConfig,
291
305
  syncCodexNativeConfig,
292
306
  syncNativeHarnessConfig,
307
+ syncOpenCodeNativeConfig,
293
308
  };
@@ -15,7 +15,9 @@ function managedRuntimeHome(contract) {
15
15
  function prepareHarnessRuntime(contract, baseEnv = process.env) {
16
16
  const runtimeHome = managedRuntimeHome(contract);
17
17
  fs.mkdirSync(runtimeHome, { recursive: true });
18
- const syncInfo = syncNativeHarnessConfig(contract);
18
+ const syncInfo = contract?.authMethod === 'provider_auth'
19
+ ? syncNativeHarnessConfig(contract)
20
+ : null;
19
21
  return {
20
22
  runtimeHome,
21
23
  syncInfo,
@@ -18,7 +18,7 @@ const VALID_AUTH_MODES = ['amalgm', 'provider_auth', 'byok'];
18
18
  const SUPPORTED_AUTH_BY_HARNESS = {
19
19
  claude_code: ['amalgm', 'byok', 'provider_auth'],
20
20
  codex: ['amalgm', 'byok', 'provider_auth'],
21
- opencode: ['amalgm', 'byok'],
21
+ opencode: ['amalgm', 'byok', 'provider_auth'],
22
22
  pi: ['amalgm', 'byok', 'provider_auth'],
23
23
  };
24
24