amalgm 0.1.68 → 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.68",
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,
@@ -17,7 +17,7 @@
17
17
  "sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
18
18
  "prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
19
19
  "pack:dry": "npm pack --dry-run",
20
- "check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
20
+ "check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=20"
@@ -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: {
@@ -462,12 +462,12 @@ function upsertAutomationRow(db, automation) {
462
462
  function workflowStatus(workflow) {
463
463
  if (!workflow || workflow.enabled === false) return 'paused';
464
464
  if (Array.isArray(workflow.compilerErrors) && workflow.compilerErrors.length > 0) return 'error';
465
- return 'idle';
465
+ return 'active';
466
466
  }
467
467
 
468
468
  function triggerStatus(trigger) {
469
469
  if (!trigger || trigger.enabled === false) return 'paused';
470
- return 'idle';
470
+ return 'active';
471
471
  }
472
472
 
473
473
  function automationStatus(automation, triggers, workflow) {
@@ -475,7 +475,7 @@ function automationStatus(automation, triggers, workflow) {
475
475
  if (workflowStatus(workflow) === 'error') return 'error';
476
476
  if (triggers.some((trigger) => triggerStatus(trigger) === 'error')) return 'error';
477
477
  if (triggers.length > 0 && triggers.every((trigger) => trigger.enabled === false)) return 'paused';
478
- return 'idle';
478
+ return 'active';
479
479
  }
480
480
 
481
481
  function composeAutomation(base, triggers, workflow) {
@@ -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'],
@@ -44,6 +44,9 @@ test('automation stores trigger plus workflow in local live SQLite resources', (
44
44
  assert.equal(automation.triggers.length, 1);
45
45
  assert.equal(automation.workflows.length, 1);
46
46
  assert.equal(automation.triggers[0].kind, 'event');
47
+ assert.equal(automation.status, 'active');
48
+ assert.equal(automation.triggers[0].status, 'active');
49
+ assert.equal(automation.workflows[0].status, 'active');
47
50
 
48
51
  const snapshot = buildSnapshot('automations,triggers,workflows');
49
52
  assert.equal(snapshot.resources.automations.some((item) => item.id === 'automation-webhook'), true);
@@ -3,13 +3,13 @@
3
3
  const path = require('path');
4
4
  const { done, errorEvent, reasoningStarted, usageFinal } = require('../events');
5
5
  const { promptText } = require('../input');
6
- const { runtimeEnv } = require('../auth');
7
6
  const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude } = require('../normalizers/claude');
8
7
  const { recordNativeEvent } = require('../recorder');
9
8
  const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
10
9
  const { bundledClaudeBinary } = require('../tooling/native-binaries');
11
- const { claudeNativeHookSettings, syncNativeHarnessConfig } = require('../tooling/native-config');
10
+ const { claudeNativeHookSettings } = require('../tooling/native-config');
12
11
  const { importPackage } = require('../tooling/package-import');
12
+ const { prepareHarnessRuntime } = require('../tooling/runtime-home');
13
13
  const { composeSystemPrompt } = require('../tooling/system-prompt');
14
14
 
15
15
  function redactSecrets(text) {
@@ -32,16 +32,16 @@ class ClaudeAdapter {
32
32
  }
33
33
 
34
34
  options(contract, extra = {}) {
35
- syncNativeHarnessConfig(contract);
35
+ const runtime = prepareHarnessRuntime(contract);
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 {
43
43
  cwd: contract.cwd,
44
- env: runtimeEnv(contract),
44
+ env: runtime.env,
45
45
  model: contract.cliModel,
46
46
  ...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
47
47
  ...(Object.keys(settings).length > 0 ? { settings } : {}),
@@ -51,7 +51,7 @@ class ClaudeAdapter {
51
51
  permissionMode: 'bypassPermissions',
52
52
  allowDangerouslySkipPermissions: true,
53
53
  ...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
54
- ? { debug: true, debugFile: path.join(contract.auth.runtimeHome || process.cwd(), 'claude-debug.log') }
54
+ ? { debug: true, debugFile: path.join(runtime.runtimeHome, 'claude-debug.log') }
55
55
  : {}),
56
56
  settingSources: [],
57
57
  strictMcpConfig: false,
@@ -6,12 +6,12 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const { done, errorEvent } = require('../events');
8
8
  const { promptText } = require('../input');
9
- const { runtimeEnv } = require('../auth');
10
9
  const { codexErrorMessage, normalizeCodexNotification } = require('../normalizers/codex');
11
10
  const { recordNativeEvent } = require('../recorder');
12
11
  const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
13
- const { bundledCodexBinary, bundledCodexPathDirs, executableExists } = require('../tooling/native-binaries');
12
+ const { bundledCodexBinary, bundledCodexPathDirs, executableExists, findOnPath } = require('../tooling/native-binaries');
14
13
  const { syncCodexNativeConfig } = require('../tooling/native-config');
14
+ const { prepareHarnessRuntime } = require('../tooling/runtime-home');
15
15
  const { composeSystemPrompt } = require('../tooling/system-prompt');
16
16
 
17
17
  class JsonLineRpc {
@@ -139,16 +139,29 @@ class JsonLineRpc {
139
139
 
140
140
  function resolveBinary() {
141
141
  const home = process.env.HOME || os.homedir();
142
- for (const candidate of [process.env.CODEX_BINARY, bundledCodexBinary(), path.join(home, '.npm-global/bin/codex'), '/opt/homebrew/bin/codex', '/usr/local/bin/codex', 'codex'].filter(Boolean)) {
143
- if (candidate.includes('/') && !executableExists(candidate)) continue;
142
+ const candidates = [
143
+ process.env.CODEX_BINARY,
144
+ bundledCodexBinary(),
145
+ path.join(home, '.npm-global/bin/codex'),
146
+ '/opt/homebrew/bin/codex',
147
+ '/usr/local/bin/codex',
148
+ findOnPath('codex'),
149
+ ].filter(Boolean);
150
+ const failures = [];
151
+ for (const candidate of candidates) {
152
+ if (!executableExists(candidate)) {
153
+ failures.push(`${candidate} (missing or not executable)`);
154
+ continue;
155
+ }
144
156
  const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
145
157
  if (res.status === 0) return candidate;
158
+ failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
146
159
  }
147
- return 'codex';
160
+ throw new Error(`Codex CLI binary is not available. Checked: ${failures.join(', ') || 'no candidates'}. Reinstall Amalgm or run amalgm update so the bundled Codex native package is present.`);
148
161
  }
149
162
 
150
- function codexEnv(contract, binary) {
151
- const env = runtimeEnv(contract);
163
+ function codexEnv(baseEnv, binary) {
164
+ const env = { ...(baseEnv || {}) };
152
165
  if (binary !== bundledCodexBinary()) return env;
153
166
  const pathDirs = bundledCodexPathDirs();
154
167
  if (pathDirs.length > 0) {
@@ -346,11 +359,13 @@ function baseNativeConfigForContract(contract, syncInfo) {
346
359
  return readTextFile(syncInfo?.sourceConfigPath);
347
360
  }
348
361
 
349
- function writeConfig(contract) {
362
+ function writeConfig(contract, syncInfo) {
350
363
  const home = contract.auth.runtimeHome;
351
364
  if (!home) return;
352
365
  fs.mkdirSync(home, { recursive: true });
353
- const syncInfo = syncCodexNativeConfig(home);
366
+ if (syncInfo === undefined) {
367
+ syncInfo = contract.authMethod === 'provider_auth' ? syncCodexNativeConfig(home) : null;
368
+ }
354
369
  const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
355
370
  const configPath = path.join(home, 'config.toml');
356
371
  if (contract.authMethod === 'provider_auth') {
@@ -372,9 +387,10 @@ function writeConfig(contract) {
372
387
 
373
388
  class CodexAdapter {
374
389
  async create(contract) {
375
- writeConfig(contract);
390
+ const runtime = prepareHarnessRuntime(contract);
391
+ writeConfig(contract, runtime.syncInfo);
376
392
  const binary = resolveBinary();
377
- const client = new JsonLineRpc({ binary, cwd: contract.cwd, env: codexEnv(contract, binary) });
393
+ const client = new JsonLineRpc({ binary, cwd: contract.cwd, env: codexEnv(runtime.env, binary) });
378
394
  await client.request('initialize', { clientInfo: { name: 'amalgm-chat-core', version: '1.0.0' }, capabilities: { experimentalApi: true } });
379
395
  client.notify('initialized');
380
396
  const threadOptions = {
@@ -7,12 +7,12 @@ const os = require('os');
7
7
  const path = require('path');
8
8
  const { done, errorEvent, reasoningStarted } = require('../events');
9
9
  const { openCodeParts } = require('../input');
10
- const { runtimeEnv } = require('../auth');
11
10
  const { normalizeOpenCodeEvent, normalizeOpenCodePromptResult } = require('../normalizers/opencode');
12
11
  const { recordNativeEvent } = require('../recorder');
13
12
  const { toOpenCodeMcpConfig } = require('../tooling/mcp-bundle');
14
- const { bundledOpenCodeBinary, executableExists } = require('../tooling/native-binaries');
13
+ const { bundledOpenCodeBinary, executableExists, findOnPath } = require('../tooling/native-binaries');
15
14
  const { importPackage } = require('../tooling/package-import');
15
+ const { prepareHarnessRuntime } = require('../tooling/runtime-home');
16
16
  const { composeSystemPrompt } = require('../tooling/system-prompt');
17
17
 
18
18
  function splitModel(model, 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) {
@@ -102,22 +105,28 @@ function resolveBinary() {
102
105
  path.join(home, '.npm-global/bin/opencode'),
103
106
  '/opt/homebrew/bin/opencode',
104
107
  '/usr/local/bin/opencode',
105
- 'opencode',
108
+ findOnPath('opencode'),
106
109
  ].filter(Boolean);
110
+ const failures = [];
107
111
  for (const candidate of candidates) {
108
- if (candidate.includes('/') && !executableExists(candidate)) continue;
112
+ if (!executableExists(candidate)) {
113
+ failures.push(`${candidate} (missing or not executable)`);
114
+ continue;
115
+ }
109
116
  const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
110
117
  if (res.status === 0) return candidate;
118
+ failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
111
119
  }
112
- return 'opencode';
120
+ throw new Error(`OpenCode CLI binary is not available. Checked: ${failures.join(', ') || 'no candidates'}. Reinstall Amalgm or run amalgm update so the bundled OpenCode native package is present.`);
113
121
  }
114
122
 
115
123
  async function startOpenCodeServer(contract) {
124
+ const runtime = prepareHarnessRuntime(contract);
116
125
  const binary = resolveBinary();
117
126
  const config = configFor(contract);
118
127
  const port = await freePort();
119
128
  const env = {
120
- ...runtimeEnv(contract),
129
+ ...runtime.env,
121
130
  OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
122
131
  };
123
132
  const args = ['serve', '--pure', '--hostname=127.0.0.1', `--port=${port}`];
@@ -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,7 +11,9 @@ const {
11
11
  claudeNativeHookSettings,
12
12
  syncClaudeNativeConfig,
13
13
  syncCodexNativeConfig,
14
+ syncOpenCodeNativeConfig,
14
15
  } = require('../tooling/native-config');
16
+ const { prepareHarnessRuntime } = require('../tooling/runtime-home');
15
17
 
16
18
  function withNativeHome(fn) {
17
19
  const previousNativeHome = process.env.AMALGM_NATIVE_HOME;
@@ -66,6 +68,91 @@ test('codex native sync copies config without deleting existing runtime state',
66
68
  });
67
69
  });
68
70
 
71
+ test('prepared codex runtime uses managed home while importing native config', () => {
72
+ withNativeHome((home) => {
73
+ const source = path.join(home, '.codex');
74
+ fs.mkdirSync(source, { recursive: true });
75
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
76
+ fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
77
+
78
+ const runtimeHome = path.join(home, 'managed', 'codex-home');
79
+ const prepared = prepareHarnessRuntime({
80
+ harness: 'codex',
81
+ authMethod: 'provider_auth',
82
+ auth: { runtimeHome },
83
+ }, { PATH: '/bin', HOME: home });
84
+
85
+ assert.equal(prepared.runtimeHome, runtimeHome);
86
+ assert.equal(prepared.env.HOME, runtimeHome);
87
+ assert.equal(prepared.env.CODEX_HOME, runtimeHome);
88
+ assert.equal(prepared.syncInfo.runtimeHome, runtimeHome);
89
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), true);
90
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
91
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'config.toml')), true);
92
+ });
93
+ });
94
+
95
+ test('prepared opencode runtime always has an isolated managed home', () => {
96
+ withNativeHome((home) => {
97
+ const runtimeHome = path.join(home, 'managed', 'opencode-home');
98
+ const prepared = prepareHarnessRuntime({
99
+ harness: 'opencode',
100
+ authMethod: 'amalgm',
101
+ auth: { runtimeHome, tokenRef: 'test-token', baseUrl: 'https://example.test' },
102
+ }, { PATH: '/bin', HOME: home });
103
+
104
+ assert.equal(prepared.runtimeHome, runtimeHome);
105
+ assert.equal(prepared.env.HOME, runtimeHome);
106
+ assert.equal(prepared.env.OPENCODE_HOME, runtimeHome);
107
+ assert.equal(prepared.env.OPENCODE_CONFIG_DIR, runtimeHome);
108
+ assert.equal(fs.existsSync(runtimeHome), true);
109
+ });
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
+
69
156
  test('codex provider config keeps native mcp config and overrides managed model provider', () => {
70
157
  withNativeHome((home) => {
71
158
  const source = path.join(home, '.codex');
@@ -105,7 +192,7 @@ test('codex provider config keeps native mcp config and overrides managed model
105
192
  });
106
193
  });
107
194
 
108
- test('codex amalgm config keeps native mcp config and overrides managed model provider', () => {
195
+ test('codex amalgm config does not import native user config', () => {
109
196
  withNativeHome((home) => {
110
197
  const source = path.join(home, '.codex');
111
198
  fs.mkdirSync(source, { recursive: true });
@@ -134,12 +221,11 @@ test('codex amalgm config keeps native mcp config and overrides managed model pr
134
221
 
135
222
  const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
136
223
  assert.match(config, /model_provider = "amalgm"/);
137
- assert.match(config, /codex_hooks = true/);
138
224
  assert.doesNotMatch(config, /native-provider/);
139
- assert.match(config, /\[mcp_servers\.native\]/);
140
- assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
141
- assert.match(config, /Authorization = "Bearer test"/);
142
- 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);
143
229
  });
144
230
  });
145
231
 
@@ -161,9 +247,11 @@ test('claude extracts native hooks without enabling full filesystem settings', (
161
247
  assert.equal(hookSettings.permissions, undefined);
162
248
 
163
249
  const adapter = new ClaudeAdapter();
250
+ const runtimeHome = path.join(home, 'runtime-home');
164
251
  const options = adapter.options({
252
+ harness: 'claude_code',
165
253
  authMethod: 'provider_auth',
166
- auth: { runtimeHome: null },
254
+ auth: { runtimeHome },
167
255
  cwd: home,
168
256
  cliModel: 'anthropic/claude-opus-4.7',
169
257
  mcpServers: [],
@@ -175,7 +263,7 @@ test('claude extracts native hooks without enabling full filesystem settings', (
175
263
  });
176
264
  });
177
265
 
178
- test('claude native sync skips debug symlinks', () => {
266
+ test('claude native sync does not delete existing debug symlinks', () => {
179
267
  withNativeHome((home) => {
180
268
  const source = path.join(home, '.claude');
181
269
  fs.mkdirSync(path.join(source, 'debug'), { recursive: true });
@@ -193,7 +281,41 @@ test('claude native sync skips debug symlinks', () => {
193
281
  assert.equal(result.sourceDir, source);
194
282
  assert.equal(fs.existsSync(path.join(runtimeHome, 'settings.json')), true);
195
283
  assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'trace.txt')), false);
196
- 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);
197
319
  });
198
320
  });
199
321
 
@@ -10,21 +10,52 @@ function amalgmDir() {
10
10
  }
11
11
 
12
12
  function nativeNodeModulesDirs() {
13
- return [
13
+ const resourcesPath = process.resourcesPath || '';
14
+ const resourceNodeModules = [
15
+ resourcesPath ? path.join(resourcesPath, 'app.asar.unpacked', 'node_modules') : '',
16
+ resourcesPath ? path.join(resourcesPath, 'app.asar', 'node_modules') : '',
17
+ resourcesPath ? path.join(resourcesPath, 'cli', 'node_modules') : '',
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([
14
26
  process.env.AMALGM_NATIVE_NODE_MODULES,
15
27
  path.join(amalgmDir(), 'native', 'node_modules'),
16
- ].filter(Boolean);
28
+ ...resourceNodeModules,
29
+ ...resourceNodeModules.flatMap((dir) => wrapperPackages.map((packageName) => path.join(dir, packageName, 'node_modules'))),
30
+ ].filter(Boolean));
17
31
  }
18
32
 
19
- function executableExists(file) {
33
+ function asarUnpackedPath(file) {
34
+ const marker = `${path.sep}app.asar${path.sep}`;
35
+ const value = String(file || '');
36
+ if (!value.includes(marker)) return value;
37
+ return value.replace(marker, `${path.sep}app.asar.unpacked${path.sep}`);
38
+ }
39
+
40
+ function existingAsarUnpackedPath(file) {
41
+ const unpacked = asarUnpackedPath(file);
42
+ return unpacked !== file && fs.existsSync(unpacked) ? unpacked : file;
43
+ }
44
+
45
+ function executablePath(file) {
46
+ const candidate = existingAsarUnpackedPath(file);
20
47
  try {
21
- fs.accessSync(file, fs.constants.X_OK);
22
- return true;
48
+ fs.accessSync(candidate, fs.constants.X_OK);
49
+ return candidate;
23
50
  } catch {
24
- return false;
51
+ return null;
25
52
  }
26
53
  }
27
54
 
55
+ function executableExists(file) {
56
+ return Boolean(executablePath(file));
57
+ }
58
+
28
59
  function findPackageJson(packageName) {
29
60
  const candidateDirs = [
30
61
  ...nativeNodeModulesDirs(),
@@ -39,9 +70,9 @@ function findPackageJson(packageName) {
39
70
 
40
71
  function packageRoot(packageName) {
41
72
  const manifest = findPackageJson(packageName);
42
- if (manifest) return path.dirname(manifest);
73
+ if (manifest) return path.dirname(existingAsarUnpackedPath(manifest));
43
74
  try {
44
- return path.dirname(require.resolve(`${packageName}/package.json`));
75
+ return path.dirname(existingAsarUnpackedPath(require.resolve(`${packageName}/package.json`)));
45
76
  } catch {
46
77
  return null;
47
78
  }
@@ -77,7 +108,7 @@ function packageBin(packageName, binName) {
77
108
  : manifest.bin?.[binName];
78
109
  if (!bin) return null;
79
110
  const candidate = path.join(root, bin);
80
- return executableExists(candidate) ? candidate : null;
111
+ return executablePath(candidate);
81
112
  } catch {
82
113
  return null;
83
114
  }
@@ -160,7 +191,7 @@ function bundledClaudeBinary() {
160
191
  const root = packageRoot(packageName);
161
192
  if (!root) return null;
162
193
  const candidate = path.join(root, process.platform === 'win32' ? 'claude.exe' : 'claude');
163
- return executableExists(candidate) ? candidate : null;
194
+ return executablePath(candidate);
164
195
  }
165
196
 
166
197
  function resolveClaudeBinary() {
@@ -224,7 +255,8 @@ function bundledCodexBinary() {
224
255
  const vendorRoot = codexVendorRoot();
225
256
  if (target && vendorRoot) {
226
257
  const candidate = path.join(vendorRoot, target.triple, 'codex', target.binaryName);
227
- if (executableExists(candidate)) return candidate;
258
+ const executable = executablePath(candidate);
259
+ if (executable) return executable;
228
260
  }
229
261
  return packageBin('@openai/codex', 'codex');
230
262
  }
@@ -596,6 +628,14 @@ function ensureNativeBinaries(options = {}) {
596
628
  }
597
629
 
598
630
  module.exports = {
631
+ __private: {
632
+ bundledClaudeBinary,
633
+ claudeTargetPackage,
634
+ codexTarget,
635
+ nativeNodeModulesDirs,
636
+ openCodePackageCandidates,
637
+ packageRoot,
638
+ },
599
639
  binaryStatus,
600
640
  bundledCodexPathDirs,
601
641
  bundledClaudeBinary: resolveClaudeBinary,
@@ -605,4 +645,5 @@ module.exports = {
605
645
  ensureAgentCommandShims,
606
646
  ensureNativeBinaries,
607
647
  executableExists,
648
+ findOnPath,
608
649
  };
@@ -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
  };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { runtimeEnv } = require('../auth');
5
+ const { syncNativeHarnessConfig } = require('./native-config');
6
+
7
+ function managedRuntimeHome(contract) {
8
+ const runtimeHome = contract?.auth?.runtimeHome;
9
+ if (typeof runtimeHome !== 'string' || !runtimeHome.trim()) {
10
+ throw new Error(`${contract?.harness || 'Agent'} is missing a managed CLI home`);
11
+ }
12
+ return runtimeHome;
13
+ }
14
+
15
+ function prepareHarnessRuntime(contract, baseEnv = process.env) {
16
+ const runtimeHome = managedRuntimeHome(contract);
17
+ fs.mkdirSync(runtimeHome, { recursive: true });
18
+ const syncInfo = contract?.authMethod === 'provider_auth'
19
+ ? syncNativeHarnessConfig(contract)
20
+ : null;
21
+ return {
22
+ runtimeHome,
23
+ syncInfo,
24
+ env: runtimeEnv(contract, baseEnv),
25
+ };
26
+ }
27
+
28
+ module.exports = {
29
+ managedRuntimeHome,
30
+ prepareHarnessRuntime,
31
+ };
@@ -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
 
@@ -205,12 +205,12 @@ Types:
205
205
 
206
206
  When you reference a file, folder, skill, or agent that exists on the user's local volume, emit it as an `amalgm://` markdown link. The frontend renders these as inline citation chips by default, with a hover card showing the path and (for skills) description. Skills are files too: if you only have the `SKILL.md` path, a normal file citation is valid. This is the same format the user's `@`-menu produces, and it's how agents talk to each other about local resources.
207
207
 
208
- Render format: `[label](amalgm://KIND/PATH_OR_ID)`
208
+ Render format: `[label](amalgm://KIND/ENCODED_PATH_OR_ID)`
209
209
 
210
210
  Kinds:
211
- - File: `[Component.tsx](amalgm://file//workspace/app/Component.tsx)`
212
- - Folder: `[components](amalgm://folder//workspace/app/components)`
213
- - Skill: `[email-formatter](amalgm://skill//workspace/.agents/skills/email-formatter "Formats outgoing emails with consistent voice")`
211
+ - File: `[Component.tsx](amalgm://file/%2Fworkspace%2Fapp%2FComponent.tsx)`
212
+ - Folder: `[components](amalgm://folder/%2Fworkspace%2Fapp%2Fcomponents)`
213
+ - Skill: `[email-formatter](amalgm://skill/%2Fworkspace%2F.agents%2Fskills%2Femail-formatter "Formats outgoing emails with consistent voice")`
214
214
  - Agent: `[Helper](amalgm://agent/AGENT_ID)`
215
215
 
216
216
  For skills, append a one-line description as the markdown link title (the part in quotes after the URL). The description teaches downstream agents and the user what the skill does so they can decide whether to load it. Keep the description under ~140 chars.
@@ -218,11 +218,12 @@ For skills, append a one-line description as the markdown link title (the part i
218
218
  For files and folders, the path alone is enough — no description needed. The user (and any agent reading your output) sees a chip; clicking it opens the file or folder.
219
219
 
220
220
  IMPORTANT:
221
- - Use the absolute path the user gave you. Don't rewrite, normalize, or shorten it.
221
+ - Use the absolute path the user gave you as the decoded path, but percent-encode it inside the URL. Spaces must be `%20`, slashes in paths should be `%2F`, and literal `)` characters must be encoded. Do not use raw spaces inside markdown link destinations.
222
+ - Don't rewrite, normalize, or shorten the decoded path.
222
223
  - Don't wrap citations in backticks. Don't explain the syntax.
223
224
  - When the user `@`-mentions something in their prompt, that input arrives as the same `amalgm://` link format — you can echo it back unchanged when referring to that resource.
224
225
 
225
- Example response (what you output): "I updated the [chat-server](amalgm://folder//workspace/scripts/chat-server) entrypoint and pulled in the [retry-policy](amalgm://skill//workspace/.agents/skills/retry-policy "Adds bounded exponential backoff to flaky outbound calls") skill for the new HTTP client."
226
+ Example response (what you output): "I updated the [chat-server](amalgm://folder/%2Fworkspace%2Fscripts%2Fchat-server) entrypoint and pulled in the [retry-policy](amalgm://skill/%2Fworkspace%2F.agents%2Fskills%2Fretry-policy "Adds bounded exponential backoff to flaky outbound calls") skill for the new HTTP client."
226
227
 
227
228
  The user sees two clickable chips inline; the agent reading your transcript sees structured references it can act on.
228
229
  </amalgm-platform>