amalgm 0.1.120 → 0.1.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const { getAgentConfig, upsertAgentConfig, agentFieldsFromConfig } = require('../agent-config/store');
|
|
6
|
+
const { normalizeAgentConfig } = require('../agent-config/schema');
|
|
7
|
+
const { createAgent, getAllAgentsWithBuiltins, resolveAgent } = require('../agents/store');
|
|
8
|
+
const { defaultToolboxService } = require('../toolbox/service');
|
|
9
|
+
|
|
10
|
+
const BUNDLE_KIND = 'amalgm.agent.bundle';
|
|
11
|
+
const BUNDLE_SCHEMA_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
const SECRET_KEY_PATTERN = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
|
|
14
|
+
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isObject(value) {
|
|
20
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cloneJson(value) {
|
|
24
|
+
if (value == null) return value;
|
|
25
|
+
return JSON.parse(JSON.stringify(value));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function uniqueStrings(values) {
|
|
29
|
+
if (!Array.isArray(values)) return [];
|
|
30
|
+
return [...new Set(values
|
|
31
|
+
.filter((value) => typeof value === 'string')
|
|
32
|
+
.map((value) => value.trim())
|
|
33
|
+
.filter(Boolean))];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addUnique(target, value) {
|
|
37
|
+
if (!value) return;
|
|
38
|
+
if (!target.includes(value)) target.push(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bundleId() {
|
|
42
|
+
return `agent-bundle-${crypto.randomUUID()}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function portableAgent(agent) {
|
|
46
|
+
return {
|
|
47
|
+
sourceAgentId: agent.id,
|
|
48
|
+
name: agent.name || agent.id,
|
|
49
|
+
description: agent.description || '',
|
|
50
|
+
baseHarnessId: agent.baseHarnessId || agent.adapter || 'codex',
|
|
51
|
+
baseModelId: agent.baseModelId || '',
|
|
52
|
+
modelSettings: isObject(agent.modelSettings) ? cloneJson(agent.modelSettings) : null,
|
|
53
|
+
authMethod: agent.authMethod || 'amalgm',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function scrubSecrets(value, path, requires) {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map((entry, index) => scrubSecrets(entry, `${path}[${index}]`, requires));
|
|
60
|
+
}
|
|
61
|
+
if (!isObject(value)) return value;
|
|
62
|
+
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const [key, child] of Object.entries(value)) {
|
|
65
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
66
|
+
if (SECRET_KEY_PATTERN.test(key)) {
|
|
67
|
+
addUnique(requires.secrets, childPath);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out[key] = scrubSecrets(child, childPath, requires);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sourceBindingRequirements(tool, requires) {
|
|
76
|
+
const source = isObject(tool?.source) ? tool.source : {};
|
|
77
|
+
if (typeof source.cwd === 'string' && source.cwd.trim()) {
|
|
78
|
+
addUnique(requires.bindings, `tool:${tool.id}:cwd`);
|
|
79
|
+
}
|
|
80
|
+
for (const key of ['command', 'url', 'baseUrl', 'specUrl', 'specPath']) {
|
|
81
|
+
const value = source[key];
|
|
82
|
+
if (typeof value === 'string' && value.startsWith('/')) {
|
|
83
|
+
addUnique(requires.bindings, `tool:${tool.id}:source.${key}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(source.args)) {
|
|
87
|
+
source.args.forEach((arg, index) => {
|
|
88
|
+
if (typeof arg === 'string' && arg.startsWith('/')) {
|
|
89
|
+
addUnique(requires.bindings, `tool:${tool.id}:source.args[${index}]`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function portableTool(tool, requires) {
|
|
96
|
+
sourceBindingRequirements(tool, requires);
|
|
97
|
+
return scrubSecrets(cloneJson(tool), `tools.${tool.id}`, requires);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function portableAction(action, requires) {
|
|
101
|
+
return scrubSecrets(cloneJson(action), `toolActions.${action.id}`, requires);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countHooks(bundleAgents) {
|
|
105
|
+
return bundleAgents.reduce((count, entry) => (
|
|
106
|
+
count + (Array.isArray(entry.config?.hooks) ? entry.config.hooks.length : 0)
|
|
107
|
+
), 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function collectSkillRecords(bundleAgents) {
|
|
111
|
+
const byId = new Map();
|
|
112
|
+
for (const entry of bundleAgents) {
|
|
113
|
+
for (const skill of entry.config.skills || []) {
|
|
114
|
+
if (!skill?.id) continue;
|
|
115
|
+
byId.set(skill.id, cloneJson(skill));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...byId.values()];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectToolRecords(toolIds, requires) {
|
|
122
|
+
const catalog = defaultToolboxService.readCatalog();
|
|
123
|
+
const tools = new Map();
|
|
124
|
+
const actions = new Map();
|
|
125
|
+
const selected = uniqueStrings(toolIds);
|
|
126
|
+
|
|
127
|
+
for (const selectedId of selected) {
|
|
128
|
+
const selectedTool = catalog.tools?.[selectedId];
|
|
129
|
+
const selectedAction = catalog.toolActions?.[selectedId];
|
|
130
|
+
const tool = selectedTool || (selectedAction ? catalog.tools?.[selectedAction.toolId] : null);
|
|
131
|
+
|
|
132
|
+
if (!tool) {
|
|
133
|
+
addUnique(requires.missingTools, selectedId);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (tool.origin === 'system') {
|
|
138
|
+
addUnique(requires.systemTools, tool.id);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!tools.has(tool.id)) tools.set(tool.id, portableTool(tool, requires));
|
|
143
|
+
|
|
144
|
+
if (selectedAction) {
|
|
145
|
+
actions.set(selectedAction.id, portableAction(selectedAction, requires));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const action of Object.values(catalog.toolActions || {})) {
|
|
150
|
+
if (action?.toolId === tool.id) actions.set(action.id, portableAction(action, requires));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
tools: [...tools.values()],
|
|
156
|
+
toolActions: [...actions.values()],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function collectAgentGraph(rootAgentId) {
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
const ordered = [];
|
|
163
|
+
|
|
164
|
+
function visit(agentId) {
|
|
165
|
+
if (!agentId || seen.has(agentId)) return;
|
|
166
|
+
seen.add(agentId);
|
|
167
|
+
const agent = resolveAgent(agentId);
|
|
168
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
169
|
+
const config = getAgentConfig(agent);
|
|
170
|
+
ordered.push({
|
|
171
|
+
sourceAgentId: agent.id,
|
|
172
|
+
agent: portableAgent(agent),
|
|
173
|
+
config: normalizeAgentConfig(config),
|
|
174
|
+
});
|
|
175
|
+
for (const subagent of config.subagents || []) {
|
|
176
|
+
if (subagent?.agentId) visit(subagent.agentId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
visit(rootAgentId);
|
|
181
|
+
return ordered;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createAgentBundle(agentId, options = {}) {
|
|
185
|
+
if (!agentId) throw new Error('agent_id is required');
|
|
186
|
+
const agents = collectAgentGraph(agentId);
|
|
187
|
+
const requires = {
|
|
188
|
+
runtimes: [],
|
|
189
|
+
auth: [],
|
|
190
|
+
secrets: [],
|
|
191
|
+
bindings: [],
|
|
192
|
+
systemTools: [],
|
|
193
|
+
missingTools: [],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const loadoutToolIds = [];
|
|
197
|
+
for (const entry of agents) {
|
|
198
|
+
addUnique(requires.runtimes, entry.agent.baseHarnessId);
|
|
199
|
+
addUnique(requires.auth, `${entry.agent.baseHarnessId}:${entry.agent.authMethod || 'amalgm'}`);
|
|
200
|
+
loadoutToolIds.push(...(entry.config.loadout?.toolIds || []));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { tools, toolActions } = collectToolRecords(loadoutToolIds, requires);
|
|
204
|
+
const skills = collectSkillRecords(agents);
|
|
205
|
+
const hooks = countHooks(agents);
|
|
206
|
+
const warnings = [
|
|
207
|
+
'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
|
|
208
|
+
'Secrets pasted into free-text instructions or files are not automatically detected.',
|
|
209
|
+
...(hooks > 0 ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
|
|
210
|
+
...(tools.length > 0 ? ['Tools can call external services or local commands. Recipients should reconnect credentials on their own machine.'] : []),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const bundle = {
|
|
214
|
+
kind: BUNDLE_KIND,
|
|
215
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
216
|
+
bundleId: options.bundleId || bundleId(),
|
|
217
|
+
createdAt: nowIso(),
|
|
218
|
+
rootAgentId: agents[0]?.sourceAgentId || agentId,
|
|
219
|
+
title: agents[0]?.agent?.name || 'Shared agent',
|
|
220
|
+
description: agents[0]?.agent?.description || '',
|
|
221
|
+
agents,
|
|
222
|
+
tools,
|
|
223
|
+
toolActions,
|
|
224
|
+
skills,
|
|
225
|
+
assets: [],
|
|
226
|
+
requires,
|
|
227
|
+
warnings,
|
|
228
|
+
summary: {
|
|
229
|
+
agents: agents.length,
|
|
230
|
+
tools: tools.length,
|
|
231
|
+
toolActions: toolActions.length,
|
|
232
|
+
skills: skills.length,
|
|
233
|
+
hooks,
|
|
234
|
+
assets: 0,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const bytes = Buffer.byteLength(JSON.stringify(bundle));
|
|
239
|
+
return {
|
|
240
|
+
bundle,
|
|
241
|
+
preview: {
|
|
242
|
+
bundleId: bundle.bundleId,
|
|
243
|
+
title: bundle.title,
|
|
244
|
+
description: bundle.description,
|
|
245
|
+
summary: bundle.summary,
|
|
246
|
+
requires,
|
|
247
|
+
warnings,
|
|
248
|
+
agents: agents.map((entry) => ({
|
|
249
|
+
sourceAgentId: entry.sourceAgentId,
|
|
250
|
+
name: entry.agent.name,
|
|
251
|
+
description: entry.agent.description,
|
|
252
|
+
baseHarnessId: entry.agent.baseHarnessId,
|
|
253
|
+
baseModelId: entry.agent.baseModelId,
|
|
254
|
+
tools: entry.config.loadout?.toolIds?.length || 0,
|
|
255
|
+
skills: entry.config.skills?.length || 0,
|
|
256
|
+
subagents: entry.config.subagents?.length || 0,
|
|
257
|
+
hooks: entry.config.hooks?.length || 0,
|
|
258
|
+
})),
|
|
259
|
+
tools: tools.map((tool) => ({
|
|
260
|
+
id: tool.id,
|
|
261
|
+
name: tool.name,
|
|
262
|
+
type: tool.type,
|
|
263
|
+
origin: tool.origin,
|
|
264
|
+
actionCount: toolActions.filter((action) => action.toolId === tool.id).length,
|
|
265
|
+
})),
|
|
266
|
+
sizeBytes: bytes,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function uniqueAgentName(name) {
|
|
272
|
+
const base = String(name || 'Imported agent').trim() || 'Imported agent';
|
|
273
|
+
const existing = new Set(
|
|
274
|
+
getAllAgentsWithBuiltins().map((agent) => String(agent.name || '').trim().toLowerCase()),
|
|
275
|
+
);
|
|
276
|
+
if (!existing.has(base.toLowerCase())) return base;
|
|
277
|
+
const imported = `${base} (imported)`;
|
|
278
|
+
if (!existing.has(imported.toLowerCase())) return imported;
|
|
279
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
280
|
+
const candidate = `${base} (imported ${index})`;
|
|
281
|
+
if (!existing.has(candidate.toLowerCase())) return candidate;
|
|
282
|
+
}
|
|
283
|
+
return `${base} (${crypto.randomUUID().slice(0, 8)})`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function externalMcpToolIdsForLoadout(toolIds, tools) {
|
|
287
|
+
const toolMap = new Map((Array.isArray(tools) ? tools : []).map((tool) => [tool.id, tool]));
|
|
288
|
+
return uniqueStrings(toolIds)
|
|
289
|
+
.map((toolId) => toolMap.get(toolId))
|
|
290
|
+
.filter((tool) => tool?.type === 'mcp' && tool?.origin !== 'system')
|
|
291
|
+
.map((tool) => tool.id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function validateBundle(bundle) {
|
|
295
|
+
if (!isObject(bundle)) throw new Error('bundle must be an object');
|
|
296
|
+
if (bundle.kind !== BUNDLE_KIND) throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
|
|
297
|
+
if (bundle.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
|
|
298
|
+
throw new Error(`Unsupported bundle schema version: ${bundle.schemaVersion || 'unknown'}`);
|
|
299
|
+
}
|
|
300
|
+
if (!Array.isArray(bundle.agents) || bundle.agents.length === 0) {
|
|
301
|
+
throw new Error('bundle must include at least one agent');
|
|
302
|
+
}
|
|
303
|
+
return bundle;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function installTools(bundle) {
|
|
307
|
+
const toolActionsByToolId = new Map();
|
|
308
|
+
for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
|
|
309
|
+
if (!action?.toolId) continue;
|
|
310
|
+
const existing = toolActionsByToolId.get(action.toolId) || [];
|
|
311
|
+
existing.push(action);
|
|
312
|
+
toolActionsByToolId.set(action.toolId, existing);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const installed = [];
|
|
316
|
+
for (const tool of Array.isArray(bundle.tools) ? bundle.tools : []) {
|
|
317
|
+
if (!tool?.id || tool.origin === 'system') continue;
|
|
318
|
+
const result = defaultToolboxService.registerTool({
|
|
319
|
+
...tool,
|
|
320
|
+
origin: tool.origin === 'catalog' ? 'catalog' : 'user',
|
|
321
|
+
actions: toolActionsByToolId.get(tool.id) || [],
|
|
322
|
+
});
|
|
323
|
+
installed.push(result.tool);
|
|
324
|
+
}
|
|
325
|
+
return installed;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function installAgentBundle(input, options = {}) {
|
|
329
|
+
const bundle = validateBundle(input);
|
|
330
|
+
const installedTools = installTools(bundle);
|
|
331
|
+
const idMap = new Map();
|
|
332
|
+
const createdAgents = [];
|
|
333
|
+
const createdAt = nowIso();
|
|
334
|
+
|
|
335
|
+
for (const entry of bundle.agents) {
|
|
336
|
+
const config = normalizeAgentConfig(entry.config || {});
|
|
337
|
+
const fields = agentFieldsFromConfig(config);
|
|
338
|
+
const agent = createAgent({
|
|
339
|
+
name: uniqueAgentName(entry.agent?.name),
|
|
340
|
+
description: entry.agent?.description || '',
|
|
341
|
+
baseHarnessId: entry.agent?.baseHarnessId || 'codex',
|
|
342
|
+
baseModelId: entry.agent?.baseModelId || '',
|
|
343
|
+
modelSettings: isObject(entry.agent?.modelSettings) ? entry.agent.modelSettings : null,
|
|
344
|
+
systemPrompt: fields.systemPrompt,
|
|
345
|
+
skills: fields.skills,
|
|
346
|
+
loadout: config.loadout || { toolIds: [] },
|
|
347
|
+
mcpAppIds: externalMcpToolIdsForLoadout(config.loadout?.toolIds || [], bundle.tools),
|
|
348
|
+
authMethod: options.authMethod || 'amalgm',
|
|
349
|
+
status: 'ready',
|
|
350
|
+
installStatus: 'external',
|
|
351
|
+
createdAt,
|
|
352
|
+
}, { source: 'agent-bundles:install' });
|
|
353
|
+
idMap.set(entry.sourceAgentId || entry.agent?.sourceAgentId, agent.id);
|
|
354
|
+
createdAgents.push({ sourceAgentId: entry.sourceAgentId, agent, config });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const installedAgents = [];
|
|
358
|
+
for (const created of createdAgents) {
|
|
359
|
+
const rewrittenConfig = normalizeAgentConfig({
|
|
360
|
+
...created.config,
|
|
361
|
+
agentId: created.agent.id,
|
|
362
|
+
runtimeHomeLayout: 'per-agent',
|
|
363
|
+
subagents: (created.config.subagents || [])
|
|
364
|
+
.map((subagent) => {
|
|
365
|
+
const mappedAgentId = idMap.get(subagent.agentId);
|
|
366
|
+
if (!mappedAgentId) return null;
|
|
367
|
+
return {
|
|
368
|
+
...subagent,
|
|
369
|
+
id: `subagent-${mappedAgentId}`,
|
|
370
|
+
agentId: mappedAgentId,
|
|
371
|
+
};
|
|
372
|
+
})
|
|
373
|
+
.filter(Boolean),
|
|
374
|
+
});
|
|
375
|
+
const savedConfig = upsertAgentConfig(created.agent.id, rewrittenConfig, {
|
|
376
|
+
source: 'agent-bundles:install',
|
|
377
|
+
});
|
|
378
|
+
installedAgents.push({
|
|
379
|
+
...created.agent,
|
|
380
|
+
...agentFieldsFromConfig(savedConfig),
|
|
381
|
+
config: savedConfig,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
bundleId: bundle.bundleId,
|
|
388
|
+
rootAgentId: idMap.get(bundle.rootAgentId) || installedAgents[0]?.id || null,
|
|
389
|
+
idMap: Object.fromEntries(idMap.entries()),
|
|
390
|
+
installedAgents,
|
|
391
|
+
installedTools,
|
|
392
|
+
warnings: bundle.warnings || [],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
BUNDLE_KIND,
|
|
398
|
+
BUNDLE_SCHEMA_VERSION,
|
|
399
|
+
createAgentBundle,
|
|
400
|
+
installAgentBundle,
|
|
401
|
+
validateBundle,
|
|
402
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createAgentBundle,
|
|
5
|
+
installAgentBundle,
|
|
6
|
+
} = require('./bundles');
|
|
7
|
+
|
|
8
|
+
async function handlePreview(body, sendJson) {
|
|
9
|
+
const { agent_id: agentId } = body || {};
|
|
10
|
+
if (!agentId) return sendJson(400, { error: 'agent_id is required' });
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const result = createAgentBundle(agentId);
|
|
14
|
+
return sendJson(200, { ok: true, ...result });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return sendJson(400, { error: error.message || 'Failed to create agent bundle' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function handleInstall(body, sendJson) {
|
|
21
|
+
const { bundle, authMethod } = body || {};
|
|
22
|
+
if (!bundle) return sendJson(400, { error: 'bundle is required' });
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = installAgentBundle(bundle, { authMethod });
|
|
26
|
+
return sendJson(200, result);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error.message || 'Failed to install agent bundle';
|
|
29
|
+
return sendJson(message.includes('Unsupported bundle') ? 400 : 500, { error: message });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
handleInstall,
|
|
35
|
+
handlePreview,
|
|
36
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const agentsRest = require('../../agents/rest');
|
|
4
4
|
const agentConfigRest = require('../../agent-config/rest');
|
|
5
|
+
const agentBundlesRest = require('../../agent-bundles/rest');
|
|
5
6
|
|
|
6
7
|
async function handleAgentRoutes(ctx) {
|
|
7
8
|
if (ctx.pathname === '/agent-config/get' && ctx.method === 'POST') {
|
|
@@ -16,6 +17,14 @@ async function handleAgentRoutes(ctx) {
|
|
|
16
17
|
agentConfigRest.handleImportNative(await ctx.readJsonBody(), ctx.sendJson);
|
|
17
18
|
return true;
|
|
18
19
|
}
|
|
20
|
+
if (ctx.pathname === '/agent-bundles/preview' && ctx.method === 'POST') {
|
|
21
|
+
agentBundlesRest.handlePreview(await ctx.readJsonBody(), ctx.sendJson);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (ctx.pathname === '/agent-bundles/install' && ctx.method === 'POST') {
|
|
25
|
+
agentBundlesRest.handleInstall(await ctx.readJsonBody(), ctx.sendJson);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
19
28
|
if (ctx.pathname === '/agents/list' && ctx.method === 'GET') {
|
|
20
29
|
agentsRest.handleList(ctx.sendJson);
|
|
21
30
|
return true;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
process.env.AMALGM_DIR = require('node:fs').mkdtempSync(
|
|
4
|
+
require('node:path').join(require('node:os').tmpdir(), 'amalgm-agent-bundles-'),
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
const assert = require('node:assert/strict');
|
|
8
|
+
const test = require('node:test');
|
|
9
|
+
|
|
10
|
+
const { createAgent, resolveAgent } = require('../agents/store');
|
|
11
|
+
const { upsertAgentConfig, getAgentConfig } = require('../agent-config/store');
|
|
12
|
+
const { defaultToolboxService } = require('../toolbox/service');
|
|
13
|
+
const { createAgentBundle, installAgentBundle } = require('../agent-bundles/bundles');
|
|
14
|
+
|
|
15
|
+
test('agent bundle includes recursive subagents, selected user tools, and warnings', () => {
|
|
16
|
+
defaultToolboxService.registerTool({
|
|
17
|
+
id: 'acme.cli',
|
|
18
|
+
name: 'Acme CLI',
|
|
19
|
+
type: 'cli',
|
|
20
|
+
origin: 'user',
|
|
21
|
+
source: {
|
|
22
|
+
command: '/Users/example/acme/run.js',
|
|
23
|
+
cwd: '/Users/example/acme',
|
|
24
|
+
inputMode: 'json-stdin',
|
|
25
|
+
outputMode: 'json',
|
|
26
|
+
},
|
|
27
|
+
actions: [{
|
|
28
|
+
id: 'acme.cli.run',
|
|
29
|
+
name: 'run',
|
|
30
|
+
description: 'Run Acme.',
|
|
31
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
|
|
32
|
+
}],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const child = createAgent({
|
|
36
|
+
id: 'bundle-child',
|
|
37
|
+
name: 'Bundle Child',
|
|
38
|
+
baseHarnessId: 'codex',
|
|
39
|
+
baseModelId: 'openai/gpt-5.5',
|
|
40
|
+
systemPrompt: 'Child instructions',
|
|
41
|
+
loadout: { toolIds: ['browser'] },
|
|
42
|
+
authMethod: 'amalgm',
|
|
43
|
+
});
|
|
44
|
+
upsertAgentConfig(child.id, {
|
|
45
|
+
instructions: 'Child instructions',
|
|
46
|
+
loadout: { toolIds: ['browser'] },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const root = createAgent({
|
|
50
|
+
id: 'bundle-root',
|
|
51
|
+
name: 'Bundle Root',
|
|
52
|
+
baseHarnessId: 'codex',
|
|
53
|
+
baseModelId: 'openai/gpt-5.5',
|
|
54
|
+
systemPrompt: 'Root instructions',
|
|
55
|
+
loadout: { toolIds: ['acme.cli'] },
|
|
56
|
+
authMethod: 'amalgm',
|
|
57
|
+
});
|
|
58
|
+
upsertAgentConfig(root.id, {
|
|
59
|
+
instructions: 'Root instructions',
|
|
60
|
+
skills: [{ id: 'skill-review', name: 'Review', content: 'Review carefully.', enabled: true }],
|
|
61
|
+
loadout: { toolIds: ['acme.cli'] },
|
|
62
|
+
hooks: [{
|
|
63
|
+
id: 'hook-root',
|
|
64
|
+
name: 'Root hook',
|
|
65
|
+
event: 'UserPromptSubmit',
|
|
66
|
+
phase: 'UserPromptSubmit',
|
|
67
|
+
type: 'command',
|
|
68
|
+
command: 'node hooks/root.js',
|
|
69
|
+
enabled: true,
|
|
70
|
+
}],
|
|
71
|
+
subagents: [{
|
|
72
|
+
id: 'subagent-child',
|
|
73
|
+
agentId: child.id,
|
|
74
|
+
name: child.name,
|
|
75
|
+
enabled: true,
|
|
76
|
+
}],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { bundle, preview } = createAgentBundle(root.id, { bundleId: 'agent-bundle-test' });
|
|
80
|
+
|
|
81
|
+
assert.equal(bundle.kind, 'amalgm.agent.bundle');
|
|
82
|
+
assert.equal(bundle.rootAgentId, root.id);
|
|
83
|
+
assert.deepEqual(bundle.agents.map((entry) => entry.sourceAgentId), [root.id, child.id]);
|
|
84
|
+
assert.deepEqual(bundle.tools.map((tool) => tool.id), ['acme.cli']);
|
|
85
|
+
assert.deepEqual(bundle.toolActions.map((action) => action.id), ['acme.cli.run']);
|
|
86
|
+
assert.equal(bundle.skills.length, 1);
|
|
87
|
+
assert.equal(bundle.summary.hooks, 1);
|
|
88
|
+
assert.equal(bundle.requires.systemTools.includes('browser'), true);
|
|
89
|
+
assert.equal(bundle.requires.bindings.includes('tool:acme.cli:cwd'), true);
|
|
90
|
+
assert.equal(bundle.requires.bindings.includes('tool:acme.cli:source.command'), true);
|
|
91
|
+
assert.equal(preview.summary.agents, 2);
|
|
92
|
+
assert.equal(preview.summary.tools, 1);
|
|
93
|
+
assert.equal(preview.warnings.some((warning) => warning.includes('free-text')), true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('installing an agent bundle creates local copies and rewrites subagent refs', () => {
|
|
97
|
+
const source = createAgent({
|
|
98
|
+
id: 'install-source-root',
|
|
99
|
+
name: 'Install Source Root',
|
|
100
|
+
baseHarnessId: 'codex',
|
|
101
|
+
baseModelId: 'openai/gpt-5.5',
|
|
102
|
+
systemPrompt: 'Install source root',
|
|
103
|
+
loadout: { toolIds: [] },
|
|
104
|
+
authMethod: 'amalgm',
|
|
105
|
+
});
|
|
106
|
+
const child = createAgent({
|
|
107
|
+
id: 'install-source-child',
|
|
108
|
+
name: 'Install Source Child',
|
|
109
|
+
baseHarnessId: 'codex',
|
|
110
|
+
baseModelId: 'openai/gpt-5.5',
|
|
111
|
+
systemPrompt: 'Install source child',
|
|
112
|
+
loadout: { toolIds: [] },
|
|
113
|
+
authMethod: 'amalgm',
|
|
114
|
+
});
|
|
115
|
+
upsertAgentConfig(child.id, {
|
|
116
|
+
instructions: 'Install source child',
|
|
117
|
+
loadout: { toolIds: [] },
|
|
118
|
+
});
|
|
119
|
+
upsertAgentConfig(source.id, {
|
|
120
|
+
instructions: 'Install source root',
|
|
121
|
+
loadout: { toolIds: [] },
|
|
122
|
+
subagents: [{ id: 'source-child', agentId: child.id, name: child.name, enabled: true }],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { bundle } = createAgentBundle(source.id, { bundleId: 'agent-bundle-install-test' });
|
|
126
|
+
const result = installAgentBundle(bundle);
|
|
127
|
+
|
|
128
|
+
assert.equal(result.ok, true);
|
|
129
|
+
assert.equal(result.installedAgents.length, 2);
|
|
130
|
+
assert.notEqual(result.rootAgentId, source.id);
|
|
131
|
+
assert.equal(Boolean(resolveAgent(result.rootAgentId)), true);
|
|
132
|
+
|
|
133
|
+
const installedRootConfig = getAgentConfig(result.rootAgentId);
|
|
134
|
+
assert.equal(installedRootConfig.subagents.length, 1);
|
|
135
|
+
assert.notEqual(installedRootConfig.subagents[0].agentId, child.id);
|
|
136
|
+
assert.equal(Boolean(resolveAgent(installedRootConfig.subagents[0].agentId)), true);
|
|
137
|
+
});
|