amalgm 0.1.46 → 0.1.48
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/agents/hooks.js +182 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +6 -1
- package/runtime/scripts/amalgm-mcp/agents/store.js +61 -31
- package/runtime/scripts/amalgm-mcp/agents/talk.js +12 -22
- package/runtime/scripts/amalgm-mcp/agents/tools.js +8 -13
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +12 -48
- package/runtime/scripts/chat-core/adapters/claude.js +3 -1
- package/runtime/scripts/chat-core/adapters/codex.js +173 -29
- package/runtime/scripts/chat-core/auth.js +2 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +42 -0
- package/runtime/scripts/chat-core/tooling/native-config.js +133 -0
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const PHASE_BY_NATIVE_EVENT = {
|
|
8
|
+
UserPromptSubmit: 'userSubmit',
|
|
9
|
+
UserSubmit: 'userSubmit',
|
|
10
|
+
userSubmit: 'userSubmit',
|
|
11
|
+
Stop: 'responseComplete',
|
|
12
|
+
ResponseComplete: 'responseComplete',
|
|
13
|
+
responseComplete: 'responseComplete',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function readJson(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function compactCommand(command) {
|
|
25
|
+
const trimmed = String(command || '').trim();
|
|
26
|
+
if (!trimmed) return '';
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
return home ? trimmed.replaceAll(home, '~') : trimmed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandLabel(command) {
|
|
32
|
+
const compact = compactCommand(command);
|
|
33
|
+
if (!compact) return 'Hook';
|
|
34
|
+
const parts = compact.split(/\s+/);
|
|
35
|
+
const script = parts.find((part) => /[./][^ ]+\.(?:js|mjs|cjs|sh|py|ts)$/i.test(part));
|
|
36
|
+
return script ? path.basename(script) : compact;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendHook(acc, sourcePath, harnessId, event, command, matcher, options = {}) {
|
|
40
|
+
const cleanCommand = String(command || '').trim();
|
|
41
|
+
if (!cleanCommand) return;
|
|
42
|
+
const phase = PHASE_BY_NATIVE_EVENT[event] || event;
|
|
43
|
+
const timeout = Number(options.timeout);
|
|
44
|
+
acc.push({
|
|
45
|
+
id: `${harnessId}:${sourcePath}:${event}:${acc.length}`,
|
|
46
|
+
harnessId,
|
|
47
|
+
source: 'native',
|
|
48
|
+
sourcePath,
|
|
49
|
+
event,
|
|
50
|
+
phase,
|
|
51
|
+
type: typeof options.type === 'string' && options.type.trim() ? options.type.trim() : 'command',
|
|
52
|
+
command: cleanCommand,
|
|
53
|
+
label: commandLabel(cleanCommand),
|
|
54
|
+
matcher: typeof matcher === 'string' ? matcher : '',
|
|
55
|
+
...(Number.isFinite(timeout) ? { timeout } : {}),
|
|
56
|
+
...(typeof options.statusMessage === 'string' && options.statusMessage.trim()
|
|
57
|
+
? { statusMessage: options.statusMessage.trim() }
|
|
58
|
+
: {}),
|
|
59
|
+
enabled: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isHookEventName(event) {
|
|
64
|
+
const clean = String(event || '').trim();
|
|
65
|
+
return Boolean(
|
|
66
|
+
PHASE_BY_NATIVE_EVENT[clean]
|
|
67
|
+
|| /(?:prompt|submit|response|complete|stop|hook)/i.test(clean),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasCommandShape(value) {
|
|
72
|
+
return !!value
|
|
73
|
+
&& typeof value === 'object'
|
|
74
|
+
&& !Array.isArray(value)
|
|
75
|
+
&& (
|
|
76
|
+
typeof value.command === 'string'
|
|
77
|
+
|| typeof value.event === 'string'
|
|
78
|
+
|| Array.isArray(value.hooks)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractCommands(value, context, acc) {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
if (!isHookEventName(context.event)) return;
|
|
85
|
+
appendHook(acc, context.sourcePath, context.harnessId, context.event, value, context.matcher);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
for (const item of value) extractCommands(item, context, acc);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!value || typeof value !== 'object') return;
|
|
95
|
+
|
|
96
|
+
const matcher = typeof value.matcher === 'string' ? value.matcher : context.matcher;
|
|
97
|
+
const event = typeof value.event === 'string' ? value.event : context.event;
|
|
98
|
+
if (typeof value.command === 'string') {
|
|
99
|
+
appendHook(acc, context.sourcePath, context.harnessId, event, value.command, matcher, value);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value.hooks)) {
|
|
102
|
+
for (const hook of value.hooks) extractCommands(hook, { ...context, event, matcher }, acc);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hooksFromObject(rawHooks, context) {
|
|
107
|
+
const hooks = [];
|
|
108
|
+
if (Array.isArray(rawHooks)) {
|
|
109
|
+
extractCommands(rawHooks, context, hooks);
|
|
110
|
+
return hooks;
|
|
111
|
+
}
|
|
112
|
+
if (!rawHooks || typeof rawHooks !== 'object') return hooks;
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(rawHooks.hooks)) {
|
|
115
|
+
extractCommands(rawHooks.hooks, context, hooks);
|
|
116
|
+
} else if (rawHooks.hooks && typeof rawHooks.hooks === 'object') {
|
|
117
|
+
hooks.push(...hooksFromObject(rawHooks.hooks, context));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const [event, value] of Object.entries(rawHooks)) {
|
|
121
|
+
if (event === 'hooks') continue;
|
|
122
|
+
if (!isHookEventName(event) && !hasCommandShape(value) && !Array.isArray(value)) continue;
|
|
123
|
+
extractCommands(value, { ...context, event }, hooks);
|
|
124
|
+
}
|
|
125
|
+
return hooks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectCodexHooks(homeDir) {
|
|
129
|
+
const sourcePath = path.join(homeDir, '.codex', 'hooks.json');
|
|
130
|
+
const data = readJson(sourcePath);
|
|
131
|
+
const hooks = hooksFromObject(data?.hooks ?? data, {
|
|
132
|
+
harnessId: 'codex',
|
|
133
|
+
sourcePath,
|
|
134
|
+
event: '',
|
|
135
|
+
matcher: '',
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
harnessId: 'codex',
|
|
139
|
+
sources: fs.existsSync(sourcePath) ? [sourcePath] : [],
|
|
140
|
+
hooks,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectClaudeHooks(homeDir) {
|
|
145
|
+
const sourcePaths = [
|
|
146
|
+
path.join(homeDir, '.claude', 'settings.json'),
|
|
147
|
+
path.join(homeDir, '.claude', 'settings.local.json'),
|
|
148
|
+
path.join(homeDir, '.claude.json'),
|
|
149
|
+
];
|
|
150
|
+
const hooks = [];
|
|
151
|
+
const sources = [];
|
|
152
|
+
|
|
153
|
+
for (const sourcePath of sourcePaths) {
|
|
154
|
+
const data = readJson(sourcePath);
|
|
155
|
+
if (!data) continue;
|
|
156
|
+
sources.push(sourcePath);
|
|
157
|
+
hooks.push(...hooksFromObject(data.hooks, {
|
|
158
|
+
harnessId: 'claude_code',
|
|
159
|
+
sourcePath,
|
|
160
|
+
event: '',
|
|
161
|
+
matcher: '',
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
harnessId: 'claude_code',
|
|
167
|
+
sources,
|
|
168
|
+
hooks,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function collectNativeHooks() {
|
|
173
|
+
const homeDir = os.homedir();
|
|
174
|
+
return {
|
|
175
|
+
codex: collectCodexHooks(homeDir),
|
|
176
|
+
claude_code: collectClaudeHooks(homeDir),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
collectNativeHooks,
|
|
182
|
+
};
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
resolveAgent,
|
|
10
10
|
updateAgent,
|
|
11
11
|
} = require('./store');
|
|
12
|
+
const { hydrateModelPreferences } = require('../lib/prefs');
|
|
12
13
|
const credentialAdapter = require('../../credential-adapter');
|
|
13
14
|
|
|
14
15
|
function coerceAuthMethodForHarness(harnessId, authMethod) {
|
|
@@ -50,6 +51,7 @@ function normalizeToolConfig(tools, legacyMcp) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
async function handleList(sendJson) {
|
|
54
|
+
await hydrateModelPreferences();
|
|
53
55
|
sendJson(200, { agents: getAllAgentsWithBuiltins() });
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -112,8 +114,11 @@ async function handleUpdate(body, sendJson) {
|
|
|
112
114
|
const existing = resolveAgent(agent_id);
|
|
113
115
|
if (!existing) return sendJson(404, { error: `Agent not found: ${agent_id}` });
|
|
114
116
|
|
|
117
|
+
const targetHarnessId = updates.baseHarnessId || existing.baseHarnessId;
|
|
115
118
|
if (updates.authMethod !== undefined) {
|
|
116
|
-
updates.authMethod = coerceAuthMethodForHarness(
|
|
119
|
+
updates.authMethod = coerceAuthMethodForHarness(targetHarnessId, updates.authMethod);
|
|
120
|
+
} else if (updates.baseHarnessId !== undefined) {
|
|
121
|
+
updates.authMethod = coerceAuthMethodForHarness(targetHarnessId, existing.authMethod);
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
let agent;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Agents storage — SQLite-backed local agent registry + conversation logs.
|
|
3
3
|
*
|
|
4
4
|
* The registry is the Local Live Store source of truth for agent capabilities.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* shape.
|
|
5
|
+
* Default harness agents are seeded as normal rows. User-created and default
|
|
6
|
+
* agents use the same editable/deletable shape.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
const crypto = require('crypto');
|
|
10
|
+
const fs = require('fs');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const {
|
|
@@ -25,10 +25,13 @@ const {
|
|
|
25
25
|
} = require('../lib/storage');
|
|
26
26
|
const { openLocalDb } = require('../state/db');
|
|
27
27
|
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
28
|
+
const { getPreferredAuthMethod } = require('../lib/prefs');
|
|
29
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
28
30
|
|
|
29
31
|
const DEFAULT_AGENT_STATUS = 'unknown';
|
|
30
32
|
const CUSTOM_AGENT_STATUS = 'ready';
|
|
31
33
|
const BUILTIN_AGENT_IDS = ['claude_code', 'codex', 'opencode', 'pi'];
|
|
34
|
+
const DEFAULT_AGENTS_SEEDED_FILE = path.join(STORAGE_DIR, '.default-agents-seeded');
|
|
32
35
|
const AMALGM_COMPUTER_ID = process.env.AMALGM_COMPUTER_ID || '';
|
|
33
36
|
|
|
34
37
|
const BUILTIN_AGENT_BLUEPRINTS = [
|
|
@@ -76,9 +79,9 @@ const BUILTIN_AGENTS = BUILTIN_AGENT_BLUEPRINTS.map((agent) => ({
|
|
|
76
79
|
nativeMcps: [],
|
|
77
80
|
tools: { mode: 'all', selectedToolIds: [] },
|
|
78
81
|
mcp: { inheritAll: true, customServers: [], appIds: [], nativeMcps: [] },
|
|
79
|
-
builtin:
|
|
80
|
-
deletable:
|
|
81
|
-
editable:
|
|
82
|
+
builtin: false,
|
|
83
|
+
deletable: true,
|
|
84
|
+
editable: true,
|
|
82
85
|
status: DEFAULT_AGENT_STATUS,
|
|
83
86
|
}));
|
|
84
87
|
|
|
@@ -133,6 +136,26 @@ function computerName() {
|
|
|
133
136
|
|| 'This computer';
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
function persistedAuthMethod(baseHarnessId) {
|
|
140
|
+
const localFallback = typeof credentialAdapter.getPersistedAuthMode === 'function'
|
|
141
|
+
? credentialAdapter.getPersistedAuthMode(baseHarnessId)
|
|
142
|
+
: 'amalgm';
|
|
143
|
+
if (typeof getPreferredAuthMethod === 'function') {
|
|
144
|
+
return getPreferredAuthMethod(baseHarnessId, localFallback);
|
|
145
|
+
}
|
|
146
|
+
return localFallback;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function defaultAgentAuthMethod(baseHarnessId, existing) {
|
|
150
|
+
const persisted = persistedAuthMethod(baseHarnessId);
|
|
151
|
+
const existingAuth = nonEmptyString(existing?.authMethod);
|
|
152
|
+
if (!existingAuth) return persisted;
|
|
153
|
+
if (existingAuth === 'amalgm' && persisted && persisted !== 'amalgm') {
|
|
154
|
+
return persisted;
|
|
155
|
+
}
|
|
156
|
+
return existingAuth;
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
function normalizeLocation(inputLocation, fallback = {}) {
|
|
137
160
|
const id = nonEmptyString(fallback.ownerComputerId) || computerId();
|
|
138
161
|
const name = nonEmptyString(fallback.locationName) || computerName();
|
|
@@ -195,7 +218,7 @@ function normalizeToolConfig(tools, legacyMcp) {
|
|
|
195
218
|
function normalizeAgent(input, existing) {
|
|
196
219
|
if (!isObject(input)) throw new Error('agent must be an object');
|
|
197
220
|
const id = nonEmptyString(input.id) || `custom-${crypto.randomUUID()}`;
|
|
198
|
-
const builtin = input.builtin === true
|
|
221
|
+
const builtin = input.builtin === true;
|
|
199
222
|
const baseHarnessId = nonEmptyString(input.baseHarnessId || input.base_harness_id) || id;
|
|
200
223
|
const adapter = nonEmptyString(input.adapter) || baseHarnessId;
|
|
201
224
|
const mcpConfig = normalizeMcpConfig(input.mcp, input.mcpAppIds, input.nativeMcps);
|
|
@@ -227,7 +250,7 @@ function normalizeAgent(input, existing) {
|
|
|
227
250
|
...mcpConfig,
|
|
228
251
|
inheritAll: toolConfig.mode !== 'selected',
|
|
229
252
|
},
|
|
230
|
-
authMethod: input.authMethod || existing?.authMethod ||
|
|
253
|
+
authMethod: input.authMethod || existing?.authMethod || persistedAuthMethod(baseHarnessId),
|
|
231
254
|
location,
|
|
232
255
|
locationName: locationLabel(location),
|
|
233
256
|
ownerComputerId,
|
|
@@ -238,8 +261,8 @@ function normalizeAgent(input, existing) {
|
|
|
238
261
|
? { ...defaultCapabilities({ tools: toolConfig }), ...input.capabilities }
|
|
239
262
|
: defaultCapabilities({ tools: toolConfig }),
|
|
240
263
|
builtin,
|
|
241
|
-
deletable: input.deletable === false
|
|
242
|
-
editable: input.editable === false
|
|
264
|
+
deletable: input.deletable === false ? false : true,
|
|
265
|
+
editable: input.editable === false ? false : true,
|
|
243
266
|
createdAt,
|
|
244
267
|
updatedAt,
|
|
245
268
|
...(input.configDir ? { configDir: input.configDir } : existing?.configDir ? { configDir: existing.configDir } : {}),
|
|
@@ -351,7 +374,6 @@ function writeAgentsJson(customAgents) {
|
|
|
351
374
|
function readCustomAgentsFromDb(db = openLocalDb()) {
|
|
352
375
|
return db.prepare(`
|
|
353
376
|
SELECT * FROM agents
|
|
354
|
-
WHERE builtin = 0
|
|
355
377
|
ORDER BY updated_at DESC, id ASC
|
|
356
378
|
`).all()
|
|
357
379
|
.map(rowToAgent)
|
|
@@ -372,29 +394,36 @@ function customAgentRowCount(db = openLocalDb()) {
|
|
|
372
394
|
|
|
373
395
|
function seedBuiltinAgents(options = {}) {
|
|
374
396
|
const db = openLocalDb();
|
|
397
|
+
const alreadySeeded = fs.existsSync(DEFAULT_AGENTS_SEEDED_FILE);
|
|
375
398
|
const events = db.transaction(() => {
|
|
376
399
|
const inserted = [];
|
|
377
400
|
for (const blueprint of BUILTIN_AGENT_BLUEPRINTS) {
|
|
378
401
|
const existing = readAgentRow(db, blueprint.id);
|
|
402
|
+
if (!existing && alreadySeeded) continue;
|
|
379
403
|
const next = normalizeAgent({
|
|
380
404
|
...blueprint,
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
405
|
+
name: existing?.name || blueprint.name,
|
|
406
|
+
description: existing?.description ?? blueprint.description,
|
|
407
|
+
adapter: existing?.adapter || blueprint.adapter,
|
|
408
|
+
baseHarnessId: existing?.baseHarnessId || blueprint.baseHarnessId,
|
|
409
|
+
baseModelId: existing?.baseModelId || blueprint.baseModelId,
|
|
410
|
+
systemPrompt: existing?.systemPrompt || '',
|
|
411
|
+
files: existing?.files || [],
|
|
412
|
+
skills: existing?.skills || [],
|
|
413
|
+
mcpAppIds: existing?.mcpAppIds || [],
|
|
414
|
+
nativeMcps: existing?.nativeMcps || [],
|
|
415
|
+
tools: existing?.tools || { mode: 'all', selectedToolIds: [] },
|
|
416
|
+
mcp: existing?.mcp || { inheritAll: true, customServers: [], appIds: [], nativeMcps: [] },
|
|
417
|
+
builtin: false,
|
|
418
|
+
deletable: true,
|
|
419
|
+
editable: true,
|
|
391
420
|
status: existing?.status || DEFAULT_AGENT_STATUS,
|
|
392
421
|
installStatus: existing?.installStatus || 'unknown',
|
|
393
422
|
authStatus: existing?.authStatus || 'unknown',
|
|
394
423
|
authDetails: existing?.authDetails,
|
|
395
424
|
configDir: existing?.configDir,
|
|
396
425
|
createdAt: existing?.createdAt,
|
|
397
|
-
authMethod: existing?.
|
|
426
|
+
authMethod: defaultAgentAuthMethod(existing?.baseHarnessId || blueprint.baseHarnessId, existing),
|
|
398
427
|
}, existing);
|
|
399
428
|
if (existing && agentsEqualForWrite(existing, next)) continue;
|
|
400
429
|
upsertAgentRow(db, next);
|
|
@@ -406,13 +435,17 @@ function seedBuiltinAgents(options = {}) {
|
|
|
406
435
|
}
|
|
407
436
|
return inserted;
|
|
408
437
|
})();
|
|
438
|
+
try {
|
|
439
|
+
ensureDir(STORAGE_DIR);
|
|
440
|
+
fs.writeFileSync(DEFAULT_AGENTS_SEEDED_FILE, nowIso());
|
|
441
|
+
} catch {
|
|
442
|
+
// Best-effort marker; default seeding still works without it.
|
|
443
|
+
}
|
|
409
444
|
publishEvents(events);
|
|
410
445
|
}
|
|
411
446
|
|
|
412
447
|
function migrateLegacyAgentsJsonIfNeeded() {
|
|
413
448
|
const db = openLocalDb();
|
|
414
|
-
if (customAgentRowCount(db) > 0) return;
|
|
415
|
-
|
|
416
449
|
const legacy = readJson(AGENTS_FILE, { version: 1, agents: [] });
|
|
417
450
|
const legacyAgents = Array.isArray(legacy?.agents) ? legacy.agents : [];
|
|
418
451
|
if (legacyAgents.length === 0) return;
|
|
@@ -421,6 +454,7 @@ function migrateLegacyAgentsJsonIfNeeded() {
|
|
|
421
454
|
for (const agent of legacyAgents) {
|
|
422
455
|
const normalized = normalizeAgent(agent, null);
|
|
423
456
|
if (normalized.builtin) continue;
|
|
457
|
+
if (readAgentRow(db, normalized.id)) continue;
|
|
424
458
|
upsertAgentRow(db, normalized);
|
|
425
459
|
}
|
|
426
460
|
})();
|
|
@@ -523,10 +557,6 @@ function updateAgent(agentId, updates, options = {}) {
|
|
|
523
557
|
const db = openLocalDb();
|
|
524
558
|
const existing = readAgentRow(db, agentId);
|
|
525
559
|
if (!existing) throw new Error(`Agent not found: ${agentId}`);
|
|
526
|
-
if (existing.builtin && options.allowBuiltinUpdate !== true) {
|
|
527
|
-
throw new Error(`Built-in agent "${existing.name}" cannot be edited`);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
560
|
const requestedName = nonEmptyString(updates?.name);
|
|
531
561
|
if (requestedName) {
|
|
532
562
|
const duplicate = getAllAgentsWithBuiltins()
|
|
@@ -569,12 +599,12 @@ function deleteAgent(agentId, options = {}) {
|
|
|
569
599
|
const db = openLocalDb();
|
|
570
600
|
const existing = readAgentRow(db, agentId);
|
|
571
601
|
if (!existing) throw new Error(`Agent not found: ${agentId}`);
|
|
572
|
-
if (existing.
|
|
573
|
-
throw new Error(`
|
|
602
|
+
if (existing.deletable === false) {
|
|
603
|
+
throw new Error(`Agent "${existing.name}" cannot be deleted`);
|
|
574
604
|
}
|
|
575
605
|
|
|
576
606
|
const event = db.transaction(() => {
|
|
577
|
-
db.prepare('DELETE FROM agents WHERE id = ?
|
|
607
|
+
db.prepare('DELETE FROM agents WHERE id = ?').run(agentId);
|
|
578
608
|
return insertAgentEvent(db, 'delete', existing, {
|
|
579
609
|
source: options.source || 'agents:delete',
|
|
580
610
|
});
|
|
@@ -14,7 +14,7 @@ const crypto = require('crypto');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
|
|
17
|
-
const {
|
|
17
|
+
const { AMALGM_USER_ID, DEFAULT_CWD, STORAGE_DIR } = require('../config');
|
|
18
18
|
const { runThroughChatServer } = require('../lib/chat-runner');
|
|
19
19
|
const {
|
|
20
20
|
hasSupabase,
|
|
@@ -31,11 +31,7 @@ const {
|
|
|
31
31
|
} = require('../lib/prefs');
|
|
32
32
|
const { textResult, errorResult } = require('../lib/tool-result');
|
|
33
33
|
const { ensureDir } = require('../lib/storage');
|
|
34
|
-
const {
|
|
35
|
-
appendAgentConvoLog,
|
|
36
|
-
normalizeConversationId,
|
|
37
|
-
resolveAgentByNameOrId,
|
|
38
|
-
} = require('./store');
|
|
34
|
+
const { appendAgentConvoLog, normalizeConversationId, resolveAgentByNameOrId } = require('./store');
|
|
39
35
|
const {
|
|
40
36
|
chatInputToLegacyFields,
|
|
41
37
|
getChatInputText,
|
|
@@ -465,8 +461,6 @@ async function deliverResponseToCallerSession({
|
|
|
465
461
|
callerResponseDelivered: true,
|
|
466
462
|
callerResponseDeliveredAt: deliveredAt,
|
|
467
463
|
callerResponseTarget: targetSessionId,
|
|
468
|
-
callerResponseText: messageText,
|
|
469
|
-
callerResponseDescription: description || null,
|
|
470
464
|
});
|
|
471
465
|
appendStatusLog(sourceSessionId, 'caller_response_delivered', {
|
|
472
466
|
targetConversationId: targetSessionId,
|
|
@@ -537,6 +531,7 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
537
531
|
`Agent not found: ${agentLookup}. Use agents_list to see available agents.`,
|
|
538
532
|
);
|
|
539
533
|
}
|
|
534
|
+
|
|
540
535
|
const isNewConversation = !normalizedConversationId;
|
|
541
536
|
const sessionId = normalizedConversationId || crypto.randomUUID();
|
|
542
537
|
const userMessageId = crypto.randomUUID();
|
|
@@ -552,10 +547,13 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
552
547
|
const persistedAuthMethod = credentialAdapter.VALID_HARNESS_IDS.includes(agent.baseHarnessId)
|
|
553
548
|
? credentialAdapter.getPersistedAuthMode(agent.baseHarnessId)
|
|
554
549
|
: 'amalgm';
|
|
550
|
+
const preferredAuthMethod = getPreferredAuthMethod(agent.baseHarnessId, persistedAuthMethod);
|
|
551
|
+
const isDefaultAgentClass = agent.id === agent.baseHarnessId;
|
|
555
552
|
const defaultAuthMethod = coerceAuthMethodForHarness(
|
|
556
553
|
agent.baseHarnessId,
|
|
557
|
-
agent.authMethod
|
|
558
|
-
|
|
554
|
+
isDefaultAgentClass && agent.authMethod === 'amalgm' && preferredAuthMethod !== 'amalgm'
|
|
555
|
+
? preferredAuthMethod
|
|
556
|
+
: agent.authMethod || preferredAuthMethod,
|
|
559
557
|
);
|
|
560
558
|
const agentFiles = normalizeStringList(agent.files);
|
|
561
559
|
const agentSkills = normalizeStringList(agent.skills);
|
|
@@ -707,7 +705,6 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
707
705
|
await supabaseInsert('sessions', {
|
|
708
706
|
id: sessionId,
|
|
709
707
|
user_id: AMALGM_USER_ID,
|
|
710
|
-
computer_id: AMALGM_COMPUTER_ID,
|
|
711
708
|
container_id: os.hostname() || 'agent-conversation',
|
|
712
709
|
harness: agent.id,
|
|
713
710
|
title: taskDescription || 'New Chat',
|
|
@@ -779,7 +776,6 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
779
776
|
chatInput: normalizedChatInput,
|
|
780
777
|
prompt: legacyChatFields.prompt,
|
|
781
778
|
userId: AMALGM_USER_ID,
|
|
782
|
-
computerId: AMALGM_COMPUTER_ID,
|
|
783
779
|
codeSessionId: sessionId,
|
|
784
780
|
assistantMessageId,
|
|
785
781
|
userMessageId,
|
|
@@ -912,11 +908,8 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
912
908
|
},
|
|
913
909
|
},
|
|
914
910
|
);
|
|
915
|
-
const activeAfterTurn = summarizeActiveConversation(sessionId);
|
|
916
|
-
const deliveredResponseText = String(activeAfterTurn?.callerResponseText || '').trim();
|
|
917
|
-
const finalOutputText = String(outputText || '').trim() ? outputText : deliveredResponseText;
|
|
918
911
|
const warnings = [];
|
|
919
|
-
if (!String(
|
|
912
|
+
if (!String(outputText || '').trim()) {
|
|
920
913
|
warnings.push(
|
|
921
914
|
metrics?.toolEvents > 0
|
|
922
915
|
? 'Agent completed without visible text after tool activity; check the session log or workspace diff before retrying.'
|
|
@@ -933,7 +926,7 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
933
926
|
agentName: agent.name,
|
|
934
927
|
runId,
|
|
935
928
|
description: taskDescription,
|
|
936
|
-
message:
|
|
929
|
+
message: outputText,
|
|
937
930
|
timestamp: new Date().toISOString(),
|
|
938
931
|
stopReason,
|
|
939
932
|
usage,
|
|
@@ -954,7 +947,7 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
954
947
|
});
|
|
955
948
|
|
|
956
949
|
console.log(
|
|
957
|
-
`[AmalgmMCP:Agent] ${agent.name} responded (${
|
|
950
|
+
`[AmalgmMCP:Agent] ${agent.name} responded (${outputText.length} chars, session: ${sessionId})`,
|
|
958
951
|
);
|
|
959
952
|
|
|
960
953
|
if (
|
|
@@ -982,7 +975,7 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
982
975
|
status: 'completed',
|
|
983
976
|
isNewConversation,
|
|
984
977
|
longMessageContext,
|
|
985
|
-
outputText
|
|
978
|
+
outputText,
|
|
986
979
|
stopReason,
|
|
987
980
|
usage,
|
|
988
981
|
metrics,
|
|
@@ -990,9 +983,6 @@ async function handleTalkToAgent(args, context = {}) {
|
|
|
990
983
|
recovery:
|
|
991
984
|
'Use agents_get_conversation with this conversation_id if the caller times out or needs the local transcript.',
|
|
992
985
|
includeMetrics,
|
|
993
|
-
extra: deliveredResponseText && !String(outputText || '').trim()
|
|
994
|
-
? { response_source: 'return_channel' }
|
|
995
|
-
: {},
|
|
996
986
|
}),
|
|
997
987
|
null,
|
|
998
988
|
2,
|
|
@@ -26,20 +26,17 @@ module.exports = [
|
|
|
26
26
|
{
|
|
27
27
|
name: 'agents_list',
|
|
28
28
|
description:
|
|
29
|
-
'List all available
|
|
29
|
+
'List all available agent classes. Use this to discover which agents you can talk to via talk_to_agent.',
|
|
30
30
|
inputSchema: { type: 'object', properties: {} },
|
|
31
31
|
async handler() {
|
|
32
32
|
const all = getAllAgentsWithBuiltins();
|
|
33
33
|
if (all.length === 0) return textResult('No agents available.');
|
|
34
34
|
const summary = all
|
|
35
35
|
.map((a) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
getSelectedModel(a.baseHarnessId) ||
|
|
41
|
-
DEFAULT_SELECTED_MODELS[a.baseHarnessId];
|
|
42
|
-
return `- ${a.name} (${a.id}) ${tag}\n ${a.description || 'No description'}\n adapter: ${a.adapter || a.baseHarnessId} | harness: ${a.baseHarnessId} | model: ${model}\n location: ${a.locationName || a.location?.name || 'this computer'} | status: ${a.status || 'unknown'}`;
|
|
36
|
+
const model = a.baseModelId ||
|
|
37
|
+
getSelectedModel(a.baseHarnessId) ||
|
|
38
|
+
DEFAULT_SELECTED_MODELS[a.baseHarnessId];
|
|
39
|
+
return `- ${a.name} (${a.id})\n ${a.description || 'No description'}\n adapter: ${a.adapter || a.baseHarnessId} | harness: ${a.baseHarnessId} | model: ${model}\n location: ${a.locationName || a.location?.name || 'this computer'} | status: ${a.status || 'unknown'}`;
|
|
43
40
|
})
|
|
44
41
|
.join('\n\n');
|
|
45
42
|
return textResult(`Available agents:\n\n${summary}`);
|
|
@@ -63,11 +60,9 @@ module.exports = [
|
|
|
63
60
|
description: agent.description,
|
|
64
61
|
adapter: agent.adapter || agent.baseHarnessId,
|
|
65
62
|
baseHarnessId: agent.baseHarnessId,
|
|
66
|
-
baseModelId: agent.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
getSelectedModel(agent.baseHarnessId) ||
|
|
70
|
-
DEFAULT_SELECTED_MODELS[agent.baseHarnessId],
|
|
63
|
+
baseModelId: agent.baseModelId ||
|
|
64
|
+
getSelectedModel(agent.baseHarnessId) ||
|
|
65
|
+
DEFAULT_SELECTED_MODELS[agent.baseHarnessId],
|
|
71
66
|
systemPrompt: agent.systemPrompt ? '(configured)' : '(none)',
|
|
72
67
|
mcp: {
|
|
73
68
|
inheritAll: agent.mcp?.inheritAll ?? true,
|
|
@@ -2,27 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { currentSeq } = require('./events');
|
|
4
4
|
|
|
5
|
-
const DEFAULT_RESOURCES = [
|
|
6
|
-
'tasks',
|
|
7
|
-
'event_triggers',
|
|
8
|
-
'agents',
|
|
9
|
-
'artifacts',
|
|
10
|
-
'toolbox',
|
|
11
|
-
'tools',
|
|
12
|
-
'tool_actions',
|
|
13
|
-
'projects',
|
|
14
|
-
'workspaces',
|
|
15
|
-
'memories',
|
|
16
|
-
];
|
|
5
|
+
const DEFAULT_RESOURCES = ['tasks', 'event_triggers', 'agents', 'artifacts', 'toolbox', 'tools', 'tool_actions', 'hooks', 'projects', 'workspaces', 'memories'];
|
|
17
6
|
|
|
18
7
|
function normalizeResources(resources) {
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
? resources
|
|
22
|
-
: String(resources).split(',');
|
|
23
|
-
const clean = values
|
|
24
|
-
.map((value) => String(value || '').trim())
|
|
25
|
-
.filter(Boolean);
|
|
8
|
+
const values = resources ? String(resources).split(',') : DEFAULT_RESOURCES;
|
|
9
|
+
const clean = values.map((value) => String(value || '').trim()).filter(Boolean);
|
|
26
10
|
return clean.length > 0 ? Array.from(new Set(clean)) : DEFAULT_RESOURCES;
|
|
27
11
|
}
|
|
28
12
|
|
|
@@ -36,18 +20,17 @@ function readResource(resource, cache) {
|
|
|
36
20
|
return require('../agents/store').getAllAgentsWithBuiltins();
|
|
37
21
|
case 'artifacts':
|
|
38
22
|
return require('../artifacts/store').loadArtifacts().artifacts;
|
|
39
|
-
case 'toolbox':
|
|
23
|
+
case 'toolbox':
|
|
40
24
|
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
41
25
|
return cache.toolbox;
|
|
42
|
-
|
|
43
|
-
case 'tools': {
|
|
26
|
+
case 'tools':
|
|
44
27
|
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
45
28
|
return cache.toolbox.tools;
|
|
46
|
-
|
|
47
|
-
case 'tool_actions': {
|
|
29
|
+
case 'tool_actions':
|
|
48
30
|
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
49
31
|
return cache.toolbox.toolActions;
|
|
50
|
-
|
|
32
|
+
case 'hooks':
|
|
33
|
+
return require('../agents/hooks').collectNativeHooks();
|
|
51
34
|
case 'projects':
|
|
52
35
|
case 'workspaces': {
|
|
53
36
|
const workspaceStore = require('../workspace/store');
|
|
@@ -68,37 +51,18 @@ function buildSnapshot(resourcesInput) {
|
|
|
68
51
|
const beforeSeq = currentSeq();
|
|
69
52
|
const cache = {};
|
|
70
53
|
const data = {};
|
|
71
|
-
|
|
72
54
|
for (const resource of resources) {
|
|
73
55
|
const value = readResource(resource, cache);
|
|
74
56
|
if (value !== undefined) data[resource] = value;
|
|
75
57
|
}
|
|
76
|
-
|
|
77
58
|
const afterSeq = currentSeq();
|
|
78
59
|
if (beforeSeq === afterSeq) {
|
|
79
|
-
return {
|
|
80
|
-
seq: afterSeq,
|
|
81
|
-
stable: true,
|
|
82
|
-
resources: data,
|
|
83
|
-
};
|
|
60
|
+
return { seq: afterSeq, stable: true, resources: data };
|
|
84
61
|
}
|
|
85
|
-
|
|
86
|
-
lastUnstable = {
|
|
87
|
-
seq: beforeSeq,
|
|
88
|
-
stable: false,
|
|
89
|
-
resources: data,
|
|
90
|
-
};
|
|
62
|
+
lastUnstable = { seq: beforeSeq, stable: false, resources: data };
|
|
91
63
|
}
|
|
92
64
|
|
|
93
|
-
return lastUnstable || {
|
|
94
|
-
seq: currentSeq(),
|
|
95
|
-
stable: true,
|
|
96
|
-
resources: {},
|
|
97
|
-
};
|
|
65
|
+
return lastUnstable || { seq: currentSeq(), stable: true, resources: {} };
|
|
98
66
|
}
|
|
99
67
|
|
|
100
|
-
module.exports = {
|
|
101
|
-
DEFAULT_RESOURCES,
|
|
102
|
-
buildSnapshot,
|
|
103
|
-
normalizeResources,
|
|
104
|
-
};
|
|
68
|
+
module.exports = { DEFAULT_RESOURCES, buildSnapshot, normalizeResources };
|
|
@@ -8,6 +8,7 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
|
|
|
8
8
|
const { recordNativeEvent } = require('../recorder');
|
|
9
9
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
10
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
+
const { syncNativeHarnessConfig } = require('../tooling/native-config');
|
|
11
12
|
const { importPackage } = require('../tooling/package-import');
|
|
12
13
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
13
14
|
|
|
@@ -31,6 +32,7 @@ class ClaudeAdapter {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
options(contract, extra = {}) {
|
|
35
|
+
syncNativeHarnessConfig(contract);
|
|
34
36
|
const systemPrompt = composeSystemPrompt(contract);
|
|
35
37
|
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
36
38
|
return {
|
|
@@ -47,7 +49,7 @@ class ClaudeAdapter {
|
|
|
47
49
|
...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
|
|
48
50
|
? { debug: true, debugFile: path.join(contract.auth.runtimeHome || process.cwd(), 'claude-debug.log') }
|
|
49
51
|
: {}),
|
|
50
|
-
settingSources: [],
|
|
52
|
+
settingSources: ['user', 'project', 'local'],
|
|
51
53
|
strictMcpConfig: false,
|
|
52
54
|
...extra,
|
|
53
55
|
};
|
|
@@ -9,8 +9,9 @@ const { promptText } = require('../input');
|
|
|
9
9
|
const { runtimeEnv } = require('../auth');
|
|
10
10
|
const { codexErrorMessage, normalizeCodexNotification } = require('../normalizers/codex');
|
|
11
11
|
const { recordNativeEvent } = require('../recorder');
|
|
12
|
-
const { toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
12
|
+
const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
|
|
13
13
|
const { bundledCodexBinary, bundledCodexPathDirs, executableExists } = require('../tooling/native-binaries');
|
|
14
|
+
const { syncCodexNativeConfig } = require('../tooling/native-config');
|
|
14
15
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
15
16
|
|
|
16
17
|
class JsonLineRpc {
|
|
@@ -176,11 +177,167 @@ function isCompactCommand(text) {
|
|
|
176
177
|
return String(text || '').trim().toLowerCase() === '/compact';
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function readTextFile(file) {
|
|
181
|
+
try {
|
|
182
|
+
return fs.readFileSync(file, 'utf8');
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function tomlSectionName(line) {
|
|
189
|
+
const match = String(line || '').match(/^\s*\[([^\]]+)\]\s*$/);
|
|
190
|
+
return match ? match[1].trim() : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function removeTopLevelKeys(toml, keys) {
|
|
194
|
+
const remove = new Set(keys);
|
|
195
|
+
let inTopLevel = true;
|
|
196
|
+
return String(toml || '')
|
|
197
|
+
.split(/\r?\n/)
|
|
198
|
+
.filter((line) => {
|
|
199
|
+
if (tomlSectionName(line)) inTopLevel = false;
|
|
200
|
+
if (!inTopLevel) return true;
|
|
201
|
+
const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*=/);
|
|
202
|
+
return !match || !remove.has(match[1]);
|
|
203
|
+
})
|
|
204
|
+
.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function removeTomlSections(toml, sectionNames) {
|
|
208
|
+
const remove = new Set(sectionNames);
|
|
209
|
+
let skipping = false;
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const line of String(toml || '').split(/\r?\n/)) {
|
|
212
|
+
const section = tomlSectionName(line);
|
|
213
|
+
if (section) {
|
|
214
|
+
skipping = [...remove].some((name) => section === name || section.startsWith(`${name}.`));
|
|
215
|
+
}
|
|
216
|
+
if (!skipping) out.push(line);
|
|
217
|
+
}
|
|
218
|
+
return out.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ensureFeatureBoolean(toml, key, value) {
|
|
222
|
+
const lines = String(toml || '').split(/\r?\n/);
|
|
223
|
+
const sectionIndex = lines.findIndex((line) => tomlSectionName(line) === 'features');
|
|
224
|
+
const rendered = `${key} = ${value ? 'true' : 'false'}`;
|
|
225
|
+
if (sectionIndex === -1) {
|
|
226
|
+
return `${String(toml || '').trimEnd()}\n\n[features]\n${rendered}\n`;
|
|
227
|
+
}
|
|
228
|
+
let insertAt = lines.length;
|
|
229
|
+
for (let i = sectionIndex + 1; i < lines.length; i += 1) {
|
|
230
|
+
if (tomlSectionName(lines[i])) {
|
|
231
|
+
insertAt = i;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (new RegExp(`^\\s*${key}\\s*=`).test(lines[i])) {
|
|
235
|
+
lines[i] = rendered;
|
|
236
|
+
return lines.join('\n');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
lines.splice(insertAt, 0, rendered);
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function tomlQuoted(value) {
|
|
244
|
+
return JSON.stringify(String(value || ''));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function hookTrustBlocks(toml) {
|
|
248
|
+
const lines = String(toml || '').split(/\r?\n/);
|
|
249
|
+
const blocks = [];
|
|
250
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
251
|
+
const section = tomlSectionName(lines[i]);
|
|
252
|
+
const match = section?.match(/^hooks\.state\."([^"]+)"$/);
|
|
253
|
+
if (!match) continue;
|
|
254
|
+
let end = lines.length;
|
|
255
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
256
|
+
if (tomlSectionName(lines[j])) {
|
|
257
|
+
end = j;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
blocks.push({ key: match[1], lines: lines.slice(i, end) });
|
|
262
|
+
}
|
|
263
|
+
return blocks;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
|
|
267
|
+
if (!sourceDir || !runtimeHome) return toml;
|
|
268
|
+
const sourceHookPrefix = `${path.join(sourceDir, 'hooks.json')}:`;
|
|
269
|
+
const runtimePrefixes = [
|
|
270
|
+
`${path.join(runtimeHome, 'hooks.json')}:`,
|
|
271
|
+
`${path.join(runtimeHome, '.codex', 'hooks.json')}:`,
|
|
272
|
+
];
|
|
273
|
+
const blocks = hookTrustBlocks(toml);
|
|
274
|
+
if (blocks.length === 0) return toml;
|
|
275
|
+
const existing = new Set(blocks.map((block) => block.key));
|
|
276
|
+
const additions = [];
|
|
277
|
+
for (const block of blocks) {
|
|
278
|
+
if (!block.key.startsWith(sourceHookPrefix)) continue;
|
|
279
|
+
const suffix = block.key.slice(sourceHookPrefix.length);
|
|
280
|
+
for (const prefix of runtimePrefixes) {
|
|
281
|
+
const nextKey = `${prefix}${suffix}`;
|
|
282
|
+
if (existing.has(nextKey)) continue;
|
|
283
|
+
existing.add(nextKey);
|
|
284
|
+
const lines = [...block.lines];
|
|
285
|
+
lines[0] = `[hooks.state.${tomlQuoted(nextKey)}]`;
|
|
286
|
+
additions.push(lines.join('\n'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (additions.length === 0) return toml;
|
|
290
|
+
const parent = /\n\s*\[hooks\.state\]\s*(?:\n|$)/.test(`\n${toml}`) ? '' : '\n[hooks.state]\n';
|
|
291
|
+
return `${String(toml || '').trimEnd()}${parent}\n${additions.join('\n\n')}\n`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function generatedMcpSectionNames(contract) {
|
|
295
|
+
return relayedMcpServers(contract).map((server) => `mcp_servers.${server.name}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
299
|
+
const mcpToml = toCodexMcpToml(contract);
|
|
300
|
+
const topLevelKeys = [
|
|
301
|
+
...(contract.authMethod === 'provider_auth' ? [] : ['model_provider']),
|
|
302
|
+
...(contract.authMethod === 'amalgm' ? ['model_context_window', 'model_auto_compact_token_limit'] : []),
|
|
303
|
+
];
|
|
304
|
+
let config = removeTopLevelKeys(existingConfig, topLevelKeys);
|
|
305
|
+
config = removeTomlSections(config, [
|
|
306
|
+
...(contract.authMethod === 'amalgm' ? ['model_providers.amalgm'] : []),
|
|
307
|
+
...generatedMcpSectionNames(contract),
|
|
308
|
+
]);
|
|
309
|
+
config = mirrorCodexHookTrust(config, syncInfo?.sourceDir, contract.auth.runtimeHome);
|
|
310
|
+
if (fs.existsSync(path.join(contract.auth.runtimeHome, 'hooks.json'))) {
|
|
311
|
+
config = ensureFeatureBoolean(config, 'codex_hooks', true);
|
|
312
|
+
}
|
|
313
|
+
const generated = contract.authMethod === 'amalgm'
|
|
314
|
+
? [
|
|
315
|
+
'model_provider = "amalgm"',
|
|
316
|
+
...modelWindowConfigLines(contract),
|
|
317
|
+
'',
|
|
318
|
+
'[model_providers.amalgm]',
|
|
319
|
+
'name = "amalgm"',
|
|
320
|
+
`base_url = "${contract.auth.baseUrl}"`,
|
|
321
|
+
'env_key = "OPENAI_API_KEY"',
|
|
322
|
+
'wire_api = "responses"',
|
|
323
|
+
'requires_openai_auth = false',
|
|
324
|
+
].join('\n')
|
|
325
|
+
: contract.authMethod === 'byok'
|
|
326
|
+
? 'model_provider = "openai"'
|
|
327
|
+
: '';
|
|
328
|
+
return [
|
|
329
|
+
generated.trimEnd(),
|
|
330
|
+
String(config || '').trim(),
|
|
331
|
+
String(mcpToml || '').trim(),
|
|
332
|
+
].filter(Boolean).join('\n\n') + '\n';
|
|
333
|
+
}
|
|
334
|
+
|
|
179
335
|
function writeConfig(contract) {
|
|
180
336
|
const home = contract.auth.runtimeHome;
|
|
181
337
|
if (!home) return;
|
|
182
338
|
fs.mkdirSync(home, { recursive: true });
|
|
183
|
-
const
|
|
339
|
+
const syncInfo = syncCodexNativeConfig(home);
|
|
340
|
+
const configPath = path.join(home, 'config.toml');
|
|
184
341
|
if (contract.authMethod === 'provider_auth') {
|
|
185
342
|
const sourceAuth = path.join(os.homedir(), '.codex', 'auth.json');
|
|
186
343
|
const targetAuth = path.join(home, 'auth.json');
|
|
@@ -188,34 +345,10 @@ function writeConfig(contract) {
|
|
|
188
345
|
fs.copyFileSync(sourceAuth, targetAuth);
|
|
189
346
|
fs.chmodSync(targetAuth, 0o600);
|
|
190
347
|
}
|
|
191
|
-
fs.writeFileSync(
|
|
192
|
-
'model_provider = "openai"',
|
|
193
|
-
'',
|
|
194
|
-
mcpToml,
|
|
195
|
-
].join('\n'), { mode: 0o600 });
|
|
348
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
|
|
196
349
|
return;
|
|
197
350
|
}
|
|
198
|
-
|
|
199
|
-
fs.writeFileSync(path.join(home, 'config.toml'), [
|
|
200
|
-
'model_provider = "amalgm"',
|
|
201
|
-
...modelWindowConfigLines(contract),
|
|
202
|
-
'',
|
|
203
|
-
'[model_providers.amalgm]',
|
|
204
|
-
'name = "amalgm"',
|
|
205
|
-
`base_url = "${contract.auth.baseUrl}"`,
|
|
206
|
-
'env_key = "OPENAI_API_KEY"',
|
|
207
|
-
'wire_api = "responses"',
|
|
208
|
-
'requires_openai_auth = false',
|
|
209
|
-
'',
|
|
210
|
-
mcpToml,
|
|
211
|
-
].join('\n'), { mode: 0o600 });
|
|
212
|
-
} else {
|
|
213
|
-
fs.writeFileSync(path.join(home, 'config.toml'), [
|
|
214
|
-
'model_provider = "openai"',
|
|
215
|
-
'',
|
|
216
|
-
mcpToml,
|
|
217
|
-
].join('\n'), { mode: 0o600 });
|
|
218
|
-
}
|
|
351
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
|
|
219
352
|
fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
|
|
220
353
|
auth_mode: 'apikey',
|
|
221
354
|
OPENAI_API_KEY: contract.auth.tokenRef,
|
|
@@ -350,4 +483,15 @@ class CodexAdapter {
|
|
|
350
483
|
}
|
|
351
484
|
}
|
|
352
485
|
|
|
353
|
-
module.exports = {
|
|
486
|
+
module.exports = {
|
|
487
|
+
CodexAdapter,
|
|
488
|
+
__private: {
|
|
489
|
+
buildCodexConfig,
|
|
490
|
+
ensureFeatureBoolean,
|
|
491
|
+
mirrorCodexHookTrust,
|
|
492
|
+
modelWindowConfigLines,
|
|
493
|
+
removeTomlSections,
|
|
494
|
+
removeTopLevelKeys,
|
|
495
|
+
writeConfig,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
@@ -70,6 +70,8 @@ function coerceAuth(harness, requested) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
|
|
73
|
+
// Claude Code provider auth is tied to the user's real home/keychain context.
|
|
74
|
+
// Isolating HOME makes the bundled Claude binary look logged out on macOS.
|
|
73
75
|
if (authMethod === 'provider_auth' && harness !== 'codex') return null;
|
|
74
76
|
const suffix = tokenFingerprint || 'default';
|
|
75
77
|
return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const { authEnvelope, runtimeEnv } = require('../auth');
|
|
6
|
+
|
|
7
|
+
test('claude provider auth keeps the user home environment', () => {
|
|
8
|
+
const envelope = authEnvelope({
|
|
9
|
+
harness: 'claude_code',
|
|
10
|
+
authMethod: 'provider_auth',
|
|
11
|
+
sessionId: 'session-test',
|
|
12
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.equal(envelope.runtimeHome, null);
|
|
16
|
+
|
|
17
|
+
const env = runtimeEnv({
|
|
18
|
+
harness: 'claude_code',
|
|
19
|
+
authMethod: 'provider_auth',
|
|
20
|
+
auth: envelope,
|
|
21
|
+
}, {
|
|
22
|
+
HOME: '/Users/example',
|
|
23
|
+
PATH: '/bin',
|
|
24
|
+
CLAUDE_CONFIG_DIR: '/Users/example/.claude',
|
|
25
|
+
ANTHROPIC_API_KEY: 'should-not-leak',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
assert.equal(env.HOME, '/Users/example');
|
|
29
|
+
assert.equal(env.CLAUDE_CONFIG_DIR, '/Users/example/.claude');
|
|
30
|
+
assert.equal(env.ANTHROPIC_API_KEY, undefined);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('codex provider auth still receives an isolated runtime home', () => {
|
|
34
|
+
const envelope = authEnvelope({
|
|
35
|
+
harness: 'codex',
|
|
36
|
+
authMethod: 'provider_auth',
|
|
37
|
+
sessionId: 'session-test',
|
|
38
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/runtime\/session-test\/codex\/[^/]+$/);
|
|
42
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const EXCLUDED_DIR_NAMES = new Set([
|
|
8
|
+
'.cache',
|
|
9
|
+
'.npm',
|
|
10
|
+
'.tmp',
|
|
11
|
+
'cache',
|
|
12
|
+
'log',
|
|
13
|
+
'logs',
|
|
14
|
+
'projects',
|
|
15
|
+
'sessions',
|
|
16
|
+
'shell_snapshots',
|
|
17
|
+
'tmp',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const EXCLUDED_FILE_PATTERNS = [
|
|
21
|
+
/^logs?_\d+\.sqlite(?:-.+)?$/i,
|
|
22
|
+
/^state_\d+\.sqlite(?:-.+)?$/i,
|
|
23
|
+
/^.*\.db-(?:shm|wal)$/i,
|
|
24
|
+
/^.*\.sqlite-(?:shm|wal)$/i,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function exists(p) {
|
|
28
|
+
try {
|
|
29
|
+
fs.accessSync(p);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldCopyConfigPath(root, source) {
|
|
37
|
+
const relative = path.relative(root, source);
|
|
38
|
+
if (!relative || relative === '.') return true;
|
|
39
|
+
const parts = relative.split(path.sep);
|
|
40
|
+
if (parts.some((part) => EXCLUDED_DIR_NAMES.has(part))) return false;
|
|
41
|
+
const basename = path.basename(source);
|
|
42
|
+
return !EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(basename));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyConfigTree(sourceDir, targetDir) {
|
|
46
|
+
if (!sourceDir || !targetDir || !exists(sourceDir)) return false;
|
|
47
|
+
if (path.resolve(sourceDir) === path.resolve(targetDir)) return true;
|
|
48
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
49
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
50
|
+
recursive: true,
|
|
51
|
+
force: true,
|
|
52
|
+
errorOnExist: false,
|
|
53
|
+
dereference: false,
|
|
54
|
+
filter: (source) => shouldCopyConfigPath(sourceDir, source),
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function copyFileIfPresent(source, target) {
|
|
60
|
+
if (!exists(source)) return false;
|
|
61
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
62
|
+
fs.copyFileSync(source, target);
|
|
63
|
+
try {
|
|
64
|
+
fs.chmodSync(target, fs.statSync(source).mode & 0o777);
|
|
65
|
+
} catch {}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
|
|
70
|
+
if (!runtimeHome || !dotDirName || !sourceDir || !exists(sourceDir)) return;
|
|
71
|
+
const alias = path.join(runtimeHome, dotDirName);
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.lstatSync(alias);
|
|
74
|
+
if (stat.isSymbolicLink()) {
|
|
75
|
+
const target = fs.readlinkSync(alias);
|
|
76
|
+
if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
|
|
77
|
+
fs.rmSync(alias, { recursive: true, force: true });
|
|
78
|
+
} else if (stat.isDirectory()) {
|
|
79
|
+
copyConfigTree(sourceDir, alias);
|
|
80
|
+
return;
|
|
81
|
+
} else {
|
|
82
|
+
fs.rmSync(alias, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
try {
|
|
86
|
+
fs.symlinkSync('.', alias, 'dir');
|
|
87
|
+
} catch {
|
|
88
|
+
copyConfigTree(sourceDir, alias);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function syncCodexNativeConfig(runtimeHome) {
|
|
93
|
+
if (!runtimeHome) return null;
|
|
94
|
+
const nativeHome = os.homedir();
|
|
95
|
+
const sourceDir = path.join(nativeHome, '.codex');
|
|
96
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
97
|
+
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
98
|
+
copyIntoHomeAlias(runtimeHome, '.codex', sourceDir);
|
|
99
|
+
copyConfigTree(path.join(nativeHome, '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
|
|
100
|
+
copyFileIfPresent(path.join(nativeHome, '.codex-supermemory.log'), path.join(runtimeHome, '.codex-supermemory.log'));
|
|
101
|
+
return copied ? { sourceDir, runtimeHome } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function syncClaudeNativeConfig(runtimeHome) {
|
|
105
|
+
if (!runtimeHome) return null;
|
|
106
|
+
const nativeHome = os.homedir();
|
|
107
|
+
const sourceDir = path.join(nativeHome, '.claude');
|
|
108
|
+
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
109
|
+
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
110
|
+
copyIntoHomeAlias(runtimeHome, '.claude', sourceDir);
|
|
111
|
+
copyFileIfPresent(path.join(nativeHome, '.claude.json'), path.join(runtimeHome, '.claude.json'));
|
|
112
|
+
copyConfigTree(path.join(nativeHome, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
|
|
113
|
+
return copied ? { sourceDir, runtimeHome } : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function syncNativeHarnessConfig(contract) {
|
|
117
|
+
const runtimeHome = contract?.auth?.runtimeHome;
|
|
118
|
+
if (!runtimeHome) return null;
|
|
119
|
+
if (contract.harness === 'codex') return syncCodexNativeConfig(runtimeHome);
|
|
120
|
+
if (contract.harness === 'claude_code') return syncClaudeNativeConfig(runtimeHome);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
__private: {
|
|
126
|
+
copyConfigTree,
|
|
127
|
+
copyFileIfPresent,
|
|
128
|
+
shouldCopyConfigPath,
|
|
129
|
+
},
|
|
130
|
+
syncClaudeNativeConfig,
|
|
131
|
+
syncCodexNativeConfig,
|
|
132
|
+
syncNativeHarnessConfig,
|
|
133
|
+
};
|