amalgm 0.1.75 → 0.1.76
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 +1 -1
- package/runtime/scripts/amalgm-mcp/agent-config/importers/claude.js +52 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/codex.js +36 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/common.js +115 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/index.js +14 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/opencode.js +59 -0
- package/runtime/scripts/amalgm-mcp/agent-config/renderers/hooks.js +55 -0
- package/runtime/scripts/amalgm-mcp/agent-config/renderers/instructions.js +54 -0
- package/runtime/scripts/amalgm-mcp/agent-config/rest.js +168 -0
- package/runtime/scripts/amalgm-mcp/agent-config/schema.js +248 -0
- package/runtime/scripts/amalgm-mcp/agent-config/store.js +123 -0
- package/runtime/scripts/amalgm-mcp/agents/hooks.js +2 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +45 -2
- package/runtime/scripts/amalgm-mcp/agents/talk.js +20 -4
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +52 -9
- package/runtime/scripts/amalgm-mcp/automations/tools.js +3 -3
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
- package/runtime/scripts/amalgm-mcp/lib/chat-payloads.js +101 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +10 -10
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/agents.js +13 -0
- package/runtime/scripts/amalgm-mcp/server/routes/chat-payloads.js +37 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +13 -3
- package/runtime/scripts/amalgm-mcp/tests/agent-config.test.js +80 -0
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +17 -5
- package/runtime/scripts/amalgm-mcp/tests/chat-payloads.test.js +71 -0
- package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +9 -9
- package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +29 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +21 -16
- package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +9 -7
- package/runtime/scripts/amalgm-mcp/toolbox/selection.js +21 -1
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +16 -1
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +8 -2
- package/runtime/scripts/chat-core/adapters/claude.js +2 -2
- package/runtime/scripts/chat-core/adapters/codex.js +82 -12
- package/runtime/scripts/chat-core/auth.js +13 -4
- package/runtime/scripts/chat-core/contract.js +18 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +27 -2
- package/runtime/scripts/chat-core/tests/native-config.test.js +65 -12
- package/runtime/scripts/chat-core/tooling/native-config.js +73 -0
- package/runtime/scripts/chat-core/tooling/runtime-home.js +2 -2
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -1
- package/runtime/scripts/local-gateway.js +1 -0
|
@@ -160,16 +160,18 @@ test('toolbox service lists compact registry views', () => {
|
|
|
160
160
|
test('toolbox service overlays first-party system catalog entries', () => {
|
|
161
161
|
const service = createToolboxService(createRepository());
|
|
162
162
|
const catalog = service.readCatalog();
|
|
163
|
-
const browser = service.getTool('
|
|
164
|
-
const navigate = service.getAction('
|
|
163
|
+
const browser = service.getTool('browser', { includeActions: false });
|
|
164
|
+
const navigate = service.getAction('browser.browser_navigate');
|
|
165
165
|
|
|
166
166
|
assert.equal(catalog.tools.amalgm, undefined);
|
|
167
167
|
assert.equal(catalog.toolActions['amalgm.notify_user'], undefined);
|
|
168
|
-
assert.equal(catalog.tools['amalgm.browser']
|
|
169
|
-
assert.equal(browser.
|
|
168
|
+
assert.equal(catalog.tools['amalgm.browser'], undefined);
|
|
169
|
+
assert.equal(catalog.tools.browser.origin, 'system');
|
|
170
|
+
assert.equal(catalog.tools.browser.owner, 'amalgm');
|
|
171
|
+
assert.equal(browser.tool.name, 'Browser');
|
|
170
172
|
assert.equal(browser.actions, undefined);
|
|
171
|
-
assert.equal(navigate.action.sourceAction.mcpToolName, '
|
|
172
|
-
assert.equal(service.listTools({ origin: 'system' }).tools.some((tool) => tool.id === '
|
|
173
|
+
assert.equal(navigate.action.sourceAction.mcpToolName, 'toolbox__browser_browser_navigate');
|
|
174
|
+
assert.equal(service.listTools({ origin: 'system' }).tools.some((tool) => tool.id === 'browser'), true);
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
test('toolbox service resolves only runnable CLI/API actions', () => {
|
|
@@ -196,7 +198,7 @@ test('toolbox service derives only external MCP ids from selected tools', () =>
|
|
|
196
198
|
assert.deepEqual(
|
|
197
199
|
service.externalMcpToolIdsForSelection([
|
|
198
200
|
'remote.mcp',
|
|
199
|
-
'
|
|
201
|
+
'browser',
|
|
200
202
|
'api.weather',
|
|
201
203
|
'missing.tool',
|
|
202
204
|
]),
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const FIRST_PARTY_TOOL_IDS = new Set([
|
|
4
|
+
'automations',
|
|
5
|
+
'notifications',
|
|
6
|
+
'agents',
|
|
7
|
+
'apps',
|
|
8
|
+
'browser',
|
|
9
|
+
]);
|
|
10
|
+
|
|
3
11
|
function selectedToolIdsForContext(context) {
|
|
4
12
|
const tools = context?.sessionMetadata?.tools;
|
|
5
13
|
if (!tools || tools.mode !== 'selected') return null;
|
|
@@ -11,9 +19,21 @@ function selectedToolIdsForContext(context) {
|
|
|
11
19
|
return new Set(selected.map((value) => String(value || '').trim()).filter(Boolean));
|
|
12
20
|
}
|
|
13
21
|
|
|
22
|
+
function selectedHas(selectedToolIds, id) {
|
|
23
|
+
if (!id) return false;
|
|
24
|
+
if (selectedToolIds.has(id)) return true;
|
|
25
|
+
const value = String(id || '').trim();
|
|
26
|
+
const toolId = value.split('.')[0];
|
|
27
|
+
if (FIRST_PARTY_TOOL_IDS.has(toolId) && selectedToolIds.has(`amalgm.${value}`)) return true;
|
|
28
|
+
if (value === 'notifications.notify_user' && selectedToolIds.has('amalgm.notify_user')) return true;
|
|
29
|
+
if (value === 'agents.talk_to_agent' && selectedToolIds.has('amalgm.talk_to_agent')) return true;
|
|
30
|
+
if (FIRST_PARTY_TOOL_IDS.has(value) && selectedToolIds.has(`amalgm.${value}`)) return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
function selectedContainsToolOrAction(selectedToolIds, toolId, actionId) {
|
|
15
35
|
if (!selectedToolIds) return true;
|
|
16
|
-
return selectedToolIds
|
|
36
|
+
return selectedHas(selectedToolIds, toolId) || selectedHas(selectedToolIds, actionId);
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
module.exports = {
|
|
@@ -12,6 +12,13 @@ const { PORT } = require('../config');
|
|
|
12
12
|
const { toolboxMcpToolName } = require('./names');
|
|
13
13
|
|
|
14
14
|
const SYSTEM_TIMESTAMP = '1970-01-01T00:00:00.000Z';
|
|
15
|
+
const LEGACY_FIRST_PARTY_TOOL_IDS = new Set([
|
|
16
|
+
'amalgm.automations',
|
|
17
|
+
'amalgm.notifications',
|
|
18
|
+
'amalgm.agents',
|
|
19
|
+
'amalgm.apps',
|
|
20
|
+
'amalgm.browser',
|
|
21
|
+
]);
|
|
15
22
|
|
|
16
23
|
function coreToolGroups() {
|
|
17
24
|
return require('../server/core-tools').CORE_TOOL_GROUPS || [];
|
|
@@ -83,8 +90,16 @@ function persistedCatalogWithoutLegacyAggregate(toolbox) {
|
|
|
83
90
|
const tools = { ...(toolbox?.tools || {}) };
|
|
84
91
|
const toolActions = { ...(toolbox?.toolActions || {}) };
|
|
85
92
|
delete tools.amalgm;
|
|
93
|
+
for (const toolId of LEGACY_FIRST_PARTY_TOOL_IDS) delete tools[toolId];
|
|
86
94
|
for (const [actionId, action] of Object.entries(toolActions)) {
|
|
87
|
-
|
|
95
|
+
const legacyGroupId = actionId.split('.').slice(0, 2).join('.');
|
|
96
|
+
if (
|
|
97
|
+
action?.toolId === 'amalgm'
|
|
98
|
+
|| LEGACY_FIRST_PARTY_TOOL_IDS.has(action?.toolId)
|
|
99
|
+
|| LEGACY_FIRST_PARTY_TOOL_IDS.has(legacyGroupId)
|
|
100
|
+
) {
|
|
101
|
+
delete toolActions[actionId];
|
|
102
|
+
}
|
|
88
103
|
}
|
|
89
104
|
return { tools, toolActions };
|
|
90
105
|
}
|
|
@@ -136,7 +136,7 @@ function makeBuilders() {
|
|
|
136
136
|
const args = isObject(promptOrArgs)
|
|
137
137
|
? { agent, ...promptOrArgs }
|
|
138
138
|
: { agent, prompt: String(promptOrArgs || '') };
|
|
139
|
-
return builders.tool(name, '
|
|
139
|
+
return builders.tool(name, 'agents.talk_to_agent', args, options);
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
builders.workflow = (definition) => definition;
|
|
@@ -10,7 +10,12 @@ const { recordEventRun } = require('../events/store');
|
|
|
10
10
|
const { compileWorkflowText } = require('./compiler');
|
|
11
11
|
const { loadWorkflows } = require('./store');
|
|
12
12
|
const { callToolboxMcpTool, toolboxMcpToolName } = require('../toolbox/runner');
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
resolveFirstPartyToolAction,
|
|
15
|
+
resolveToolboxAction,
|
|
16
|
+
suggestWorkflowToolActions,
|
|
17
|
+
workflowActionKeys,
|
|
18
|
+
} = require('../automations/tool-actions');
|
|
14
19
|
|
|
15
20
|
const DEFAULT_CELL_TIMEOUT_MS = 120_000;
|
|
16
21
|
const DEFAULT_CODE_TIMEOUT_MS = 15_000;
|
|
@@ -281,7 +286,8 @@ function mcpResultToOutput(result, actionKey) {
|
|
|
281
286
|
async function runToolCell(cell, args, allowlist) {
|
|
282
287
|
const actionKey = `${cell.toolId}.${cell.actionName}`;
|
|
283
288
|
const allowedActions = Array.isArray(allowlist.actions) ? allowlist.actions : [];
|
|
284
|
-
|
|
289
|
+
const allowedKeys = new Set(workflowActionKeys(cell.toolId, cell.actionName));
|
|
290
|
+
if (allowedActions.length > 0 && !allowedActions.some((allowed) => allowedKeys.has(allowed))) {
|
|
285
291
|
throw new Error(`Workflow is not allowed to call ${actionKey}.`);
|
|
286
292
|
}
|
|
287
293
|
|
|
@@ -7,10 +7,10 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
|
|
|
7
7
|
const { recordNativeEvent } = require('../recorder');
|
|
8
8
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
9
9
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
10
|
-
const { claudeNativeHookSettings } = require('../tooling/native-config');
|
|
11
10
|
const { importPackage } = require('../tooling/package-import');
|
|
12
11
|
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
13
12
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
13
|
+
const { toClaudeSettings } = require('../../amalgm-mcp/agent-config/renderers/hooks');
|
|
14
14
|
|
|
15
15
|
function redactSecrets(text) {
|
|
16
16
|
return String(text || '')
|
|
@@ -36,7 +36,7 @@ class ClaudeAdapter {
|
|
|
36
36
|
const systemPrompt = composeSystemPrompt(contract);
|
|
37
37
|
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
38
38
|
const settings = {
|
|
39
|
-
...(contract.
|
|
39
|
+
...toClaudeSettings(contract.agentConfig),
|
|
40
40
|
...(contract.fastMode ? { fastMode: true, fastModePerSessionOptIn: true } : {}),
|
|
41
41
|
};
|
|
42
42
|
return {
|
|
@@ -10,9 +10,10 @@ const { codexErrorMessage, normalizeCodexNotification } = require('../normalizer
|
|
|
10
10
|
const { recordNativeEvent } = require('../recorder');
|
|
11
11
|
const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
12
12
|
const { bundledCodexBinary, bundledCodexPathDirs, executableExists, findOnPath } = require('../tooling/native-binaries');
|
|
13
|
-
const {
|
|
13
|
+
const { syncCodexProviderAuth } = require('../tooling/native-config');
|
|
14
14
|
const { prepareHarnessRuntime } = require('../tooling/runtime-home');
|
|
15
15
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
16
|
+
const { hasHooks, toCodexHooksJson, toHookMap } = require('../../amalgm-mcp/agent-config/renderers/hooks');
|
|
16
17
|
|
|
17
18
|
class JsonLineRpc {
|
|
18
19
|
constructor({ binary, cwd, env }) {
|
|
@@ -202,6 +203,53 @@ function readTextFile(file) {
|
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
function ensureCodexHomeAlias(home) {
|
|
207
|
+
if (!home) return null;
|
|
208
|
+
const alias = path.join(home, '.codex');
|
|
209
|
+
try {
|
|
210
|
+
const stat = fs.lstatSync(alias);
|
|
211
|
+
return stat.isDirectory() || stat.isSymbolicLink() ? alias : null;
|
|
212
|
+
} catch {}
|
|
213
|
+
try {
|
|
214
|
+
fs.symlinkSync('.', alias, 'dir');
|
|
215
|
+
return alias;
|
|
216
|
+
} catch {
|
|
217
|
+
try {
|
|
218
|
+
fs.mkdirSync(alias, { recursive: true });
|
|
219
|
+
return alias;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function codexConfigDirs(home) {
|
|
227
|
+
fs.mkdirSync(home, { recursive: true });
|
|
228
|
+
const dirs = [home];
|
|
229
|
+
const alias = ensureCodexHomeAlias(home);
|
|
230
|
+
if (alias) dirs.push(alias);
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
return dirs.filter((dir) => {
|
|
233
|
+
let key;
|
|
234
|
+
try {
|
|
235
|
+
key = fs.realpathSync(dir);
|
|
236
|
+
} catch {
|
|
237
|
+
key = path.resolve(dir);
|
|
238
|
+
}
|
|
239
|
+
if (seen.has(key)) return false;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function writeCodexHomeFile(home, filename, contents, mode = 0o600) {
|
|
246
|
+
for (const dir of codexConfigDirs(home)) {
|
|
247
|
+
const target = path.join(dir, filename);
|
|
248
|
+
fs.writeFileSync(target, contents, { mode });
|
|
249
|
+
try { fs.chmodSync(target, mode); } catch {}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
205
253
|
function tomlSectionName(line) {
|
|
206
254
|
const match = String(line || '').match(/^\s*\[([^\]]+)\]\s*$/);
|
|
207
255
|
return match ? match[1].trim() : null;
|
|
@@ -312,6 +360,27 @@ function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
|
|
|
312
360
|
return `${String(toml || '').trimEnd()}${parent}\n${additions.join('\n\n')}\n`;
|
|
313
361
|
}
|
|
314
362
|
|
|
363
|
+
function generatedCodexHookTrust(contract) {
|
|
364
|
+
const home = contract.auth.runtimeHome;
|
|
365
|
+
if (!home || !hasHooks(contract.agentConfig)) return '';
|
|
366
|
+
const hookMap = toHookMap(contract.agentConfig);
|
|
367
|
+
const hookFiles = [
|
|
368
|
+
path.join(home, 'hooks.json'),
|
|
369
|
+
path.join(home, '.codex', 'hooks.json'),
|
|
370
|
+
];
|
|
371
|
+
const lines = ['[hooks.state]'];
|
|
372
|
+
for (const hooksPath of hookFiles) {
|
|
373
|
+
for (const [event, entries] of Object.entries(hookMap)) {
|
|
374
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
375
|
+
lines.push('');
|
|
376
|
+
lines.push(`[hooks.state.${tomlQuoted(`${hooksPath}:${event}:${index}`)}]`);
|
|
377
|
+
lines.push('trusted = true');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
315
384
|
function generatedMcpSectionNames(contract) {
|
|
316
385
|
return relayedMcpServers(contract).map((server) => `mcp_servers.${server.name}`);
|
|
317
386
|
}
|
|
@@ -327,8 +396,7 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
|
327
396
|
...(contract.authMethod === 'amalgm' ? ['model_providers.amalgm'] : []),
|
|
328
397
|
...generatedMcpSectionNames(contract),
|
|
329
398
|
]);
|
|
330
|
-
|
|
331
|
-
if (fs.existsSync(path.join(contract.auth.runtimeHome, 'hooks.json'))) {
|
|
399
|
+
if (hasHooks(contract.agentConfig)) {
|
|
332
400
|
config = ensureFeatureBoolean(config, 'codex_hooks', true);
|
|
333
401
|
}
|
|
334
402
|
const generated = contract.authMethod === 'amalgm'
|
|
@@ -351,6 +419,7 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
|
351
419
|
return [
|
|
352
420
|
generated.trimEnd(),
|
|
353
421
|
String(config || '').trim(),
|
|
422
|
+
generatedCodexHookTrust(contract).trim(),
|
|
354
423
|
String(mcpToml || '').trim(),
|
|
355
424
|
].filter(Boolean).join('\n\n') + '\n';
|
|
356
425
|
}
|
|
@@ -363,26 +432,27 @@ function writeConfig(contract, syncInfo) {
|
|
|
363
432
|
const home = contract.auth.runtimeHome;
|
|
364
433
|
if (!home) return;
|
|
365
434
|
fs.mkdirSync(home, { recursive: true });
|
|
435
|
+
ensureCodexHomeAlias(home);
|
|
366
436
|
if (syncInfo === undefined) {
|
|
367
|
-
syncInfo = contract.authMethod === 'provider_auth' ?
|
|
437
|
+
syncInfo = contract.authMethod === 'provider_auth' ? syncCodexProviderAuth(home) : null;
|
|
368
438
|
}
|
|
369
439
|
const nativeConfig = baseNativeConfigForContract(contract, syncInfo);
|
|
370
|
-
|
|
440
|
+
if (hasHooks(contract.agentConfig)) {
|
|
441
|
+
writeCodexHomeFile(home, 'hooks.json', toCodexHooksJson(contract.agentConfig), 0o600);
|
|
442
|
+
}
|
|
371
443
|
if (contract.authMethod === 'provider_auth') {
|
|
372
444
|
const sourceAuth = path.join(syncInfo?.sourceDir || path.join(os.homedir(), '.codex'), 'auth.json');
|
|
373
|
-
const targetAuth = path.join(home, 'auth.json');
|
|
374
445
|
if (fs.existsSync(sourceAuth)) {
|
|
375
|
-
fs.
|
|
376
|
-
fs.chmodSync(targetAuth, 0o600);
|
|
446
|
+
writeCodexHomeFile(home, 'auth.json', fs.readFileSync(sourceAuth), 0o600);
|
|
377
447
|
}
|
|
378
|
-
|
|
448
|
+
writeCodexHomeFile(home, 'config.toml', buildCodexConfig(contract, nativeConfig, syncInfo), 0o600);
|
|
379
449
|
return;
|
|
380
450
|
}
|
|
381
|
-
|
|
382
|
-
|
|
451
|
+
writeCodexHomeFile(home, 'config.toml', buildCodexConfig(contract, nativeConfig, syncInfo), 0o600);
|
|
452
|
+
writeCodexHomeFile(home, 'auth.json', JSON.stringify({
|
|
383
453
|
auth_mode: 'apikey',
|
|
384
454
|
OPENAI_API_KEY: contract.auth.tokenRef,
|
|
385
|
-
}, null, 2),
|
|
455
|
+
}, null, 2), 0o600);
|
|
386
456
|
}
|
|
387
457
|
|
|
388
458
|
class CodexAdapter {
|
|
@@ -109,15 +109,24 @@ function providerAuthIdentity(harness) {
|
|
|
109
109
|
return `${harness || 'provider'}:provider-default`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function runtimeHome({ amalgmDir, harness }) {
|
|
113
|
-
|
|
112
|
+
function runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout }) {
|
|
113
|
+
const harnessHome = path.join(amalgmDir, 'cli-homes', safePathSegment(harness));
|
|
114
|
+
if (runtimeHomeLayout === 'per-agent') {
|
|
115
|
+
return path.join(
|
|
116
|
+
harnessHome,
|
|
117
|
+
'agents',
|
|
118
|
+
safePathSegment(agentConfigId || harness),
|
|
119
|
+
'home',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return path.join(harnessHome, 'home');
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
function providerAuthFingerprint(harness) {
|
|
117
126
|
return fingerprint(providerAuthIdentity(harness));
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, userId, credentialId, modelId }) {
|
|
129
|
+
function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, userId, credentialId, modelId, agentConfigId, runtimeHomeLayout }) {
|
|
121
130
|
const method = coerceAuth(harness, authMethod);
|
|
122
131
|
const byok = readByokFile(amalgmDir);
|
|
123
132
|
let baseUrl = null;
|
|
@@ -155,7 +164,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
|
|
|
155
164
|
forwardBaseUrl,
|
|
156
165
|
tokenRef,
|
|
157
166
|
tokenFingerprint,
|
|
158
|
-
runtimeHome: runtimeHome({ amalgmDir, harness }),
|
|
167
|
+
runtimeHome: runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout }),
|
|
159
168
|
};
|
|
160
169
|
}
|
|
161
170
|
|
|
@@ -7,6 +7,7 @@ const { AMALGM_DIR, DEFAULT_CWD, PROXY_BASE_URL, PROXY_TOKEN } = require('../cha
|
|
|
7
7
|
const { canonicalProjectPath } = require('../lib/project-paths');
|
|
8
8
|
const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
|
|
9
9
|
const { runtimePort } = require('../../lib/runtime-manifest');
|
|
10
|
+
const { normalizeAgentConfig } = require('../amalgm-mcp/agent-config/schema');
|
|
10
11
|
|
|
11
12
|
function agentToHarness(agent) {
|
|
12
13
|
const clean = String(agent || '').trim();
|
|
@@ -206,6 +207,7 @@ function frozenRuntimeFields(input) {
|
|
|
206
207
|
authMethod: input.authMethod,
|
|
207
208
|
auth: authIdentity,
|
|
208
209
|
systemPrompt: fingerprint(input.systemPrompt || ''),
|
|
210
|
+
agentConfig: fingerprint(JSON.stringify(input.agentConfig || null)),
|
|
209
211
|
origin: fingerprint(JSON.stringify(input.origin || null)),
|
|
210
212
|
mcpServers: fingerprint(JSON.stringify(mcpServers)),
|
|
211
213
|
};
|
|
@@ -350,6 +352,17 @@ function createContract(payload, options = {}) {
|
|
|
350
352
|
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || runtimePort('chat-server')}`;
|
|
351
353
|
const origin = normalizeOrigin({ ...payload, cwd });
|
|
352
354
|
const userId = payload.userId || options.userId || 'unknown-user';
|
|
355
|
+
const payloadChatAgent = payload.chatInput && typeof payload.chatInput === 'object' && !Array.isArray(payload.chatInput)
|
|
356
|
+
? payload.chatInput.agent
|
|
357
|
+
: null;
|
|
358
|
+
const agentConfigId = nullableString(
|
|
359
|
+
payload.agentConfigId
|
|
360
|
+
|| payload.amalgmAgentId
|
|
361
|
+
|| payload.agentConfig?.agentId
|
|
362
|
+
|| payloadChatAgent?.customAgentId,
|
|
363
|
+
) || harness;
|
|
364
|
+
const agentConfig = normalizeAgentConfig(payload.agentConfig || { agentId: agentConfigId });
|
|
365
|
+
const runtimeHomeLayout = agentConfig.runtimeHomeLayout === 'per-agent' ? 'per-agent' : 'legacy';
|
|
353
366
|
const auth = authEnvelope({
|
|
354
367
|
harness,
|
|
355
368
|
authMethod,
|
|
@@ -361,6 +374,8 @@ function createContract(payload, options = {}) {
|
|
|
361
374
|
userId,
|
|
362
375
|
credentialId: payload.credentialId || payload.byokCredentialId || null,
|
|
363
376
|
modelId,
|
|
377
|
+
agentConfigId,
|
|
378
|
+
runtimeHomeLayout,
|
|
364
379
|
});
|
|
365
380
|
const contract = {
|
|
366
381
|
sessionId,
|
|
@@ -369,6 +384,9 @@ function createContract(payload, options = {}) {
|
|
|
369
384
|
assistantMessageId,
|
|
370
385
|
userMessageId: payload.userMessageId || null,
|
|
371
386
|
agentId: payload.agentId || (harness === 'claude_code' ? 'claude' : harness),
|
|
387
|
+
agentConfigId,
|
|
388
|
+
agentConfig,
|
|
389
|
+
runtimeHomeLayout,
|
|
372
390
|
harness,
|
|
373
391
|
authMethod: auth.method,
|
|
374
392
|
modelId,
|
|
@@ -7,7 +7,7 @@ const path = require('node:path');
|
|
|
7
7
|
const test = require('node:test');
|
|
8
8
|
const { authEnvelope, runtimeEnv } = require('../auth');
|
|
9
9
|
|
|
10
|
-
test('codex
|
|
10
|
+
test('codex keeps legacy CLI home unless per-agent layout is explicit', () => {
|
|
11
11
|
const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-auth-test-'));
|
|
12
12
|
fs.writeFileSync(path.join(amalgmDir, '.amalgm-credentials'), 'OPENAI_API_KEY=sk-test\n');
|
|
13
13
|
try {
|
|
@@ -38,11 +38,36 @@ test('codex always uses one canonical CLI home across auth methods and token ref
|
|
|
38
38
|
amalgmDir,
|
|
39
39
|
userId: 'user-123',
|
|
40
40
|
});
|
|
41
|
-
|
|
41
|
+
const otherAgent = authEnvelope({
|
|
42
|
+
harness: 'codex',
|
|
43
|
+
authMethod: 'provider_auth',
|
|
44
|
+
sessionId: 'session-four',
|
|
45
|
+
proxyToken: 'proxy-token-two',
|
|
46
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
47
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
48
|
+
amalgmDir,
|
|
49
|
+
userId: 'user-123',
|
|
50
|
+
agentConfigId: 'agent-two',
|
|
51
|
+
});
|
|
52
|
+
const perAgent = authEnvelope({
|
|
53
|
+
harness: 'codex',
|
|
54
|
+
authMethod: 'provider_auth',
|
|
55
|
+
sessionId: 'session-five',
|
|
56
|
+
proxyToken: 'proxy-token-three',
|
|
57
|
+
localBaseUrl: 'http://127.0.0.1:8084',
|
|
58
|
+
proxyBaseUrl: 'https://proxy.example.test',
|
|
59
|
+
amalgmDir,
|
|
60
|
+
userId: 'user-123',
|
|
61
|
+
agentConfigId: 'agent-two',
|
|
62
|
+
runtimeHomeLayout: 'per-agent',
|
|
63
|
+
});
|
|
42
64
|
assert.notEqual(first.tokenFingerprint, second.tokenFingerprint);
|
|
43
65
|
assert.equal(first.runtimeHome, second.runtimeHome);
|
|
44
66
|
assert.equal(second.runtimeHome, third.runtimeHome);
|
|
45
67
|
assert.equal(first.runtimeHome, path.join(amalgmDir, 'cli-homes', 'codex', 'home'));
|
|
68
|
+
assert.equal(otherAgent.runtimeHome, first.runtimeHome);
|
|
69
|
+
assert.equal(perAgent.runtimeHome, path.join(amalgmDir, 'cli-homes', 'codex', 'agents', 'agent-two', 'home'));
|
|
70
|
+
assert.notEqual(first.runtimeHome, perAgent.runtimeHome);
|
|
46
71
|
} finally {
|
|
47
72
|
fs.rmSync(amalgmDir, { recursive: true, force: true });
|
|
48
73
|
}
|
|
@@ -68,12 +68,13 @@ test('codex native sync copies config without deleting existing runtime state',
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
test('prepared codex runtime
|
|
71
|
+
test('prepared codex runtime imports provider auth without native config prefs', () => {
|
|
72
72
|
withNativeHome((home) => {
|
|
73
73
|
const source = path.join(home, '.codex');
|
|
74
74
|
fs.mkdirSync(source, { recursive: true });
|
|
75
75
|
fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
|
|
76
76
|
fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
|
|
77
|
+
fs.writeFileSync(path.join(source, 'auth.json'), '{"auth_mode":"chatgpt"}');
|
|
77
78
|
|
|
78
79
|
const runtimeHome = path.join(home, 'managed', 'codex-home');
|
|
79
80
|
const prepared = prepareHarnessRuntime({
|
|
@@ -86,9 +87,12 @@ test('prepared codex runtime uses managed home while importing native config', (
|
|
|
86
87
|
assert.equal(prepared.env.HOME, runtimeHome);
|
|
87
88
|
assert.equal(prepared.env.CODEX_HOME, runtimeHome);
|
|
88
89
|
assert.equal(prepared.syncInfo.runtimeHome, runtimeHome);
|
|
89
|
-
assert.equal(
|
|
90
|
-
assert.equal(fs.existsSync(path.join(runtimeHome, '
|
|
91
|
-
assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', '
|
|
90
|
+
assert.equal(prepared.syncInfo.authOnly, true);
|
|
91
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'auth.json')), true);
|
|
92
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'auth.json')), true);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), false);
|
|
94
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), false);
|
|
95
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'config.toml')), false);
|
|
92
96
|
});
|
|
93
97
|
});
|
|
94
98
|
|
|
@@ -109,7 +113,7 @@ test('prepared opencode runtime always has an isolated managed home', () => {
|
|
|
109
113
|
});
|
|
110
114
|
});
|
|
111
115
|
|
|
112
|
-
test('provider auth imports opencode native config
|
|
116
|
+
test('provider auth imports opencode auth without native config prefs', () => {
|
|
113
117
|
withNativeHome((home) => {
|
|
114
118
|
const configDir = path.join(home, '.config', 'opencode');
|
|
115
119
|
const dataDir = path.join(home, '.local', 'share', 'opencode');
|
|
@@ -129,7 +133,8 @@ test('provider auth imports opencode native config into managed home', () => {
|
|
|
129
133
|
assert.equal(prepared.env.HOME, runtimeHome);
|
|
130
134
|
assert.equal(prepared.env.OPENCODE_HOME, runtimeHome);
|
|
131
135
|
assert.equal(prepared.env.XDG_CONFIG_HOME, path.join(runtimeHome, '.config'));
|
|
132
|
-
assert.equal(
|
|
136
|
+
assert.equal(prepared.syncInfo.authOnly, true);
|
|
137
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, '.config', 'opencode', 'opencode.json')), false);
|
|
133
138
|
assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'auth.json')), true);
|
|
134
139
|
});
|
|
135
140
|
});
|
|
@@ -153,7 +158,7 @@ test('non-user auth prepares a home without importing native config', () => {
|
|
|
153
158
|
});
|
|
154
159
|
});
|
|
155
160
|
|
|
156
|
-
test('codex provider config
|
|
161
|
+
test('codex provider config ignores native prefs and renders agent hooks', () => {
|
|
157
162
|
withNativeHome((home) => {
|
|
158
163
|
const source = path.join(home, '.codex');
|
|
159
164
|
fs.mkdirSync(source, { recursive: true });
|
|
@@ -178,6 +183,15 @@ test('codex provider config keeps native mcp config and overrides managed model
|
|
|
178
183
|
codexPrivate.writeConfig({
|
|
179
184
|
authMethod: 'provider_auth',
|
|
180
185
|
auth: { runtimeHome },
|
|
186
|
+
agentConfig: {
|
|
187
|
+
agentId: 'agent-test',
|
|
188
|
+
hooks: [{
|
|
189
|
+
type: 'command',
|
|
190
|
+
event: 'UserPromptSubmit',
|
|
191
|
+
command: 'echo managed',
|
|
192
|
+
timeout: 1,
|
|
193
|
+
}],
|
|
194
|
+
},
|
|
181
195
|
mcpServers: [],
|
|
182
196
|
});
|
|
183
197
|
|
|
@@ -185,10 +199,12 @@ test('codex provider config keeps native mcp config and overrides managed model
|
|
|
185
199
|
assert.match(config, /model_provider = "openai"/);
|
|
186
200
|
assert.match(config, /codex_hooks = true/);
|
|
187
201
|
assert.doesNotMatch(config, /native-provider/);
|
|
188
|
-
assert.
|
|
189
|
-
assert.
|
|
190
|
-
assert.
|
|
202
|
+
assert.doesNotMatch(config, /\[mcp_servers\.native\]/);
|
|
203
|
+
assert.doesNotMatch(config, /\[mcp_servers\.native\.http_headers\]/);
|
|
204
|
+
assert.doesNotMatch(config, /Authorization = "Bearer test"/);
|
|
191
205
|
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
|
|
206
|
+
const hooks = fs.readFileSync(path.join(runtimeHome, 'hooks.json'), 'utf8');
|
|
207
|
+
assert.match(hooks, /echo managed/);
|
|
192
208
|
});
|
|
193
209
|
});
|
|
194
210
|
|
|
@@ -229,7 +245,35 @@ test('codex amalgm config does not import native user config', () => {
|
|
|
229
245
|
});
|
|
230
246
|
});
|
|
231
247
|
|
|
232
|
-
test('
|
|
248
|
+
test('codex amalgm config overwrites stale real dot-codex auth files', () => {
|
|
249
|
+
withNativeHome((home) => {
|
|
250
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
251
|
+
fs.mkdirSync(path.join(runtimeHome, '.codex'), { recursive: true });
|
|
252
|
+
fs.writeFileSync(path.join(runtimeHome, '.codex', 'auth.json'), JSON.stringify({
|
|
253
|
+
auth_mode: 'chatgpt',
|
|
254
|
+
account: { email: 'user@example.test' },
|
|
255
|
+
}));
|
|
256
|
+
fs.writeFileSync(path.join(runtimeHome, '.codex', 'config.toml'), 'model_provider = "openai"\n');
|
|
257
|
+
|
|
258
|
+
codexPrivate.writeConfig({
|
|
259
|
+
authMethod: 'amalgm',
|
|
260
|
+
auth: { runtimeHome, baseUrl: 'https://amalgm.example/v1', tokenRef: 'test-token' },
|
|
261
|
+
mcpServers: [],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
for (const configDir of [runtimeHome, path.join(runtimeHome, '.codex')]) {
|
|
265
|
+
const auth = JSON.parse(fs.readFileSync(path.join(configDir, 'auth.json'), 'utf8'));
|
|
266
|
+
const config = fs.readFileSync(path.join(configDir, 'config.toml'), 'utf8');
|
|
267
|
+
assert.equal(auth.auth_mode, 'apikey');
|
|
268
|
+
assert.equal(auth.OPENAI_API_KEY, 'test-token');
|
|
269
|
+
assert.match(config, /model_provider = "amalgm"/);
|
|
270
|
+
assert.match(config, /requires_openai_auth = false/);
|
|
271
|
+
assert.doesNotMatch(config, /model_provider = "openai"/);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('claude renders agent hooks without importing native permission settings', () => {
|
|
233
277
|
withNativeHome((home) => {
|
|
234
278
|
const source = path.join(home, '.claude');
|
|
235
279
|
fs.mkdirSync(source, { recursive: true });
|
|
@@ -254,12 +298,21 @@ test('claude extracts native hooks without enabling full filesystem settings', (
|
|
|
254
298
|
auth: { runtimeHome },
|
|
255
299
|
cwd: home,
|
|
256
300
|
cliModel: 'anthropic/claude-opus-4.8',
|
|
301
|
+
agentConfig: {
|
|
302
|
+
agentId: 'agent-test',
|
|
303
|
+
hooks: [{
|
|
304
|
+
type: 'command',
|
|
305
|
+
event: 'UserPromptSubmit',
|
|
306
|
+
command: 'echo managed',
|
|
307
|
+
timeout: 1,
|
|
308
|
+
}],
|
|
309
|
+
},
|
|
257
310
|
mcpServers: [],
|
|
258
311
|
});
|
|
259
312
|
|
|
260
313
|
assert.deepEqual(options.settingSources, []);
|
|
261
314
|
assert.equal(options.settings.permissions, undefined);
|
|
262
|
-
assert.equal(options.settings.hooks.UserPromptSubmit[0].hooks[0].command, 'echo
|
|
315
|
+
assert.equal(options.settings.hooks.UserPromptSubmit[0].hooks[0].command, 'echo managed');
|
|
263
316
|
});
|
|
264
317
|
});
|
|
265
318
|
|