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 +2 -2
- package/runtime/lib/harnesses.js +1 -1
- package/runtime/scripts/amalgm-mcp/automations/store.js +3 -3
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +3 -0
- package/runtime/scripts/chat-core/adapters/claude.js +6 -6
- package/runtime/scripts/chat-core/adapters/codex.js +27 -11
- package/runtime/scripts/chat-core/adapters/opencode.js +19 -10
- package/runtime/scripts/chat-core/auth.js +11 -29
- package/runtime/scripts/chat-core/contract.js +0 -2
- package/runtime/scripts/chat-core/engine.js +0 -1
- package/runtime/scripts/chat-core/tests/auth.test.js +50 -44
- package/runtime/scripts/chat-core/tests/native-binaries.test.js +126 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +131 -9
- package/runtime/scripts/chat-core/tooling/native-binaries.js +52 -11
- package/runtime/scripts/chat-core/tooling/native-config.js +54 -39
- package/runtime/scripts/chat-core/tooling/runtime-home.js +31 -0
- package/runtime/scripts/credential-adapter.js +1 -1
- package/runtime/scripts/platform-context.txt +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
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"
|
package/runtime/lib/harnesses.js
CHANGED
|
@@ -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 '
|
|
465
|
+
return 'active';
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
function triggerStatus(trigger) {
|
|
469
469
|
if (!trigger || trigger.enabled === false) return 'paused';
|
|
470
|
-
return '
|
|
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 '
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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(
|
|
151
|
-
const env =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
@@ -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('
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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: '
|
|
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.
|
|
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
|
|
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.
|
|
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('
|
|
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.
|
|
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
|
|
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.
|
|
140
|
-
assert.
|
|
141
|
-
assert.
|
|
142
|
-
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')),
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
...resourceNodeModules,
|
|
29
|
+
...resourceNodeModules.flatMap((dir) => wrapperPackages.map((packageName) => path.join(dir, packageName, 'node_modules'))),
|
|
30
|
+
].filter(Boolean));
|
|
17
31
|
}
|
|
18
32
|
|
|
19
|
-
function
|
|
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(
|
|
22
|
-
return
|
|
48
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
49
|
+
return candidate;
|
|
23
50
|
} catch {
|
|
24
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
208
|
+
Render format: `[label](amalgm://KIND/ENCODED_PATH_OR_ID)`
|
|
209
209
|
|
|
210
210
|
Kinds:
|
|
211
|
-
- File: `[Component.tsx](amalgm://file
|
|
212
|
-
- Folder: `[components](amalgm://folder
|
|
213
|
-
- Skill: `[email-formatter](amalgm://skill
|
|
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
|
|
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
|
|
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>
|