amalgm 0.1.146 → 0.1.147

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.
@@ -1,1004 +1,11 @@
1
1
  'use strict';
2
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 { hydrateConfigSkills } = require('../skills/store');
9
- const { defaultToolboxService } = require('../toolbox/service');
10
- const { getApp } = require('../apps/store');
11
- const { exportAutomationEntry, installAutomationEntry } = require('./automation-entries');
12
- const { exportAppEntry, installAppEntry } = require('./app-entries');
13
- const { annotateAppBoundPaths, rewriteAppBoundPaths } = require('./tool-bindings');
14
-
15
- // Agent-only bundles keep the legacy kind so older runtimes can still install
16
- // them; bundles that carry automations or apps use the general kind so older
17
- // runtimes reject them outright instead of silently dropping primitives.
18
- const BUNDLE_KIND = 'amalgm.bundle';
19
- const LEGACY_AGENT_BUNDLE_KIND = 'amalgm.agent.bundle';
20
- const BUNDLE_SCHEMA_VERSION = 1;
21
- const GRAPH_ELEMENT_ORDER = {
22
- agent: 0,
23
- automation: 1,
24
- app: 2,
25
- tool: 3,
26
- tool_action: 4,
27
- skill: 5,
28
- };
29
-
30
- const SECRET_KEY_PATTERN = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
31
-
32
- function nowIso() {
33
- return new Date().toISOString();
34
- }
35
-
36
- function isObject(value) {
37
- return !!value && typeof value === 'object' && !Array.isArray(value);
38
- }
39
-
40
- function cloneJson(value) {
41
- if (value == null) return value;
42
- return JSON.parse(JSON.stringify(value));
43
- }
44
-
45
- function uniqueStrings(values) {
46
- if (!Array.isArray(values)) return [];
47
- return [...new Set(values
48
- .filter((value) => typeof value === 'string')
49
- .map((value) => value.trim())
50
- .filter(Boolean))];
51
- }
52
-
53
- function addUnique(target, value) {
54
- if (!value) return;
55
- if (!target.includes(value)) target.push(value);
56
- }
57
-
58
- function bundleId() {
59
- return `agent-bundle-${crypto.randomUUID()}`;
60
- }
61
-
62
- function stableDigest(value) {
63
- return crypto.createHash('sha256')
64
- .update(typeof value === 'string' ? value : JSON.stringify(value))
65
- .digest('hex')
66
- .slice(0, 16);
67
- }
68
-
69
- function graphElementId(type, id) {
70
- return `${type}:${id}`;
71
- }
72
-
73
- function skillFingerprint(skill) {
74
- if (typeof skill === 'string') {
75
- const value = skill.trim();
76
- return value ? `string:${value}` : '';
77
- }
78
- if (!isObject(skill)) return '';
79
- const id = typeof skill.id === 'string' ? skill.id.trim() : '';
80
- if (id) return `id:${id}`;
81
- const path = typeof skill.path === 'string' ? skill.path.trim() : '';
82
- if (path) return `path:${path}`;
83
- const name = typeof skill.name === 'string' ? skill.name.trim() : '';
84
- const content = typeof skill.content === 'string' ? skill.content.trim() : '';
85
- if (name && content) return `name-content:${name}:${content}`;
86
- if (name) return `name:${name}`;
87
- if (content) return `content:${content}`;
88
- return `json:${JSON.stringify(skill)}`;
89
- }
90
-
91
- function skillElementId(skill) {
92
- const fingerprint = skillFingerprint(skill);
93
- return fingerprint ? graphElementId('skill', stableDigest(fingerprint)) : '';
94
- }
95
-
96
- function skillLabel(skill) {
97
- if (typeof skill === 'string') return skill.trim() || 'Skill';
98
- if (!isObject(skill)) return 'Skill';
99
- const id = typeof skill.id === 'string' ? skill.id.trim() : '';
100
- const name = typeof skill.name === 'string' ? skill.name.trim() : '';
101
- const path = typeof skill.path === 'string' ? skill.path.trim() : '';
102
- if (name) return name;
103
- if (path) return path.split('/').filter(Boolean).pop() || path;
104
- if (id) return id;
105
- const content = typeof skill.content === 'string' ? skill.content.trim() : '';
106
- return content.slice(0, 60) || 'Skill';
107
- }
108
-
109
- function compareGraphElements(left, right) {
110
- const order = (GRAPH_ELEMENT_ORDER[left.type] || 99) - (GRAPH_ELEMENT_ORDER[right.type] || 99);
111
- if (order !== 0) return order;
112
- return String(left.id).localeCompare(String(right.id));
113
- }
114
-
115
- function compareGraphEdges(left, right) {
116
- return String(left.from).localeCompare(String(right.from))
117
- || String(left.kind).localeCompare(String(right.kind))
118
- || String(left.to).localeCompare(String(right.to));
119
- }
120
-
121
- function pushGraphElement(elements, element) {
122
- if (!element?.id) return;
123
- if (!elements.has(element.id)) elements.set(element.id, element);
124
- }
125
-
126
- function pushGraphEdge(edges, edge) {
127
- if (!edge?.from || !edge?.to || !edge?.kind) return;
128
- const key = `${edge.from}::${edge.kind}::${edge.to}`;
129
- if (!edges.has(key)) edges.set(key, edge);
130
- }
131
-
132
- function portableAgent(agent) {
133
- return {
134
- sourceAgentId: agent.id,
135
- name: agent.name || agent.id,
136
- description: agent.description || '',
137
- baseHarnessId: agent.baseHarnessId || agent.adapter || 'codex',
138
- baseModelId: agent.baseModelId || '',
139
- modelSettings: isObject(agent.modelSettings) ? cloneJson(agent.modelSettings) : null,
140
- authMethod: agent.authMethod || 'amalgm',
141
- };
142
- }
143
-
144
- function scrubSecrets(value, path, requires) {
145
- if (Array.isArray(value)) {
146
- return value.map((entry, index) => scrubSecrets(entry, `${path}[${index}]`, requires));
147
- }
148
- if (!isObject(value)) return value;
149
-
150
- const out = {};
151
- for (const [key, child] of Object.entries(value)) {
152
- const childPath = path ? `${path}.${key}` : key;
153
- if (SECRET_KEY_PATTERN.test(key)) {
154
- addUnique(requires.secrets, childPath);
155
- continue;
156
- }
157
- out[key] = scrubSecrets(child, childPath, requires);
158
- }
159
- return out;
160
- }
161
-
162
- function portableTool(tool, requires) {
163
- return scrubSecrets(cloneJson(tool), `tools.${tool.id}`, requires);
164
- }
165
-
166
- function portableAction(action, requires) {
167
- return scrubSecrets(cloneJson(action), `toolActions.${action.id}`, requires);
168
- }
169
-
170
- function countHooks(bundleAgents) {
171
- return bundleAgents.reduce((count, entry) => (
172
- count + (Array.isArray(entry.config?.hooks) ? entry.config.hooks.length : 0)
173
- ), 0);
174
- }
175
-
176
- function collectSkillRecords(bundleAgents) {
177
- const byKey = new Map();
178
- for (const entry of bundleAgents) {
179
- for (const skill of entry.config.skills || []) {
180
- const key = skillFingerprint(skill);
181
- if (!key) continue;
182
- byKey.set(key, cloneJson(skill));
183
- }
184
- }
185
- return [...byKey.values()];
186
- }
187
-
188
- function automationEntryId(entry) {
189
- return isObject(entry) ? cleanEntryId(entry.sourceAutomationId) : '';
190
- }
191
-
192
- function appEntryId(entry) {
193
- return isObject(entry) ? cleanEntryId(entry.sourceAppId) : '';
194
- }
195
-
196
- function cleanEntryId(value) {
197
- return typeof value === 'string' ? value.trim() : '';
198
- }
199
-
200
- /**
201
- * Tools shared as top-level primitives (not just dependencies of agents or
202
- * automations). The bundle's explicit heads are the single source of truth.
203
- */
204
- function standaloneToolIds(bundle) {
205
- const heads = Array.isArray(bundle?.heads) ? bundle.heads : [];
206
- return uniqueStrings(heads
207
- .filter((head) => isObject(head) && cleanEntryId(head.type) === 'tool')
208
- .map((head) => cleanEntryId(head.id)));
209
- }
210
-
211
- /**
212
- * App elements carry the portable record and a file manifest; file contents
213
- * stay in bundle.apps[] so the graph remains cheap to render.
214
- */
215
- function appElementData(entry) {
216
- return {
217
- sourceAppId: appEntryId(entry),
218
- app: cloneJson(entry.app || {}),
219
- files: (Array.isArray(entry.files) ? entry.files : []).map((file) => ({
220
- path: file.path,
221
- size: file.size,
222
- encoding: file.encoding,
223
- ...(file.executable ? { executable: true } : {}),
224
- })),
225
- totalBytes: entry.totalBytes || 0,
226
- skipped: cloneJson(entry.skipped || []),
227
- };
228
- }
229
-
230
- function buildFlatAgentBundleGraph(bundle) {
231
- const elements = new Map();
232
- const edges = new Map();
233
- const toolMap = new Map((Array.isArray(bundle.tools) ? bundle.tools : []).map((tool) => [tool.id, tool]));
234
- const toolActionMap = new Map((Array.isArray(bundle.toolActions) ? bundle.toolActions : []).map((action) => [action.id, action]));
235
- const systemToolIds = new Set(uniqueStrings(bundle.requires?.systemTools));
236
- const missingToolIds = new Set(uniqueStrings(bundle.requires?.missingTools));
237
- const skills = collectSkillRecords(bundle.agents || []);
238
-
239
- for (const entry of bundle.agents || []) {
240
- const currentAgentId = agentEntryId(entry);
241
- if (!currentAgentId) continue;
242
- pushGraphElement(elements, {
243
- id: graphElementId('agent', currentAgentId),
244
- type: 'agent',
245
- label: entry.agent?.name || currentAgentId,
246
- data: {
247
- sourceAgentId: currentAgentId,
248
- agent: cloneJson(entry.agent || {}),
249
- config: cloneJson(normalizeAgentConfig(entry.config || {})),
250
- },
251
- });
252
- }
253
-
254
- for (const entry of bundle.automations || []) {
255
- const currentAutomationId = automationEntryId(entry);
256
- if (!currentAutomationId) continue;
257
- pushGraphElement(elements, {
258
- id: graphElementId('automation', currentAutomationId),
259
- type: 'automation',
260
- label: entry.automation?.name || currentAutomationId,
261
- data: cloneJson(entry),
262
- });
263
- }
264
-
265
- for (const entry of bundle.apps || []) {
266
- const currentAppId = appEntryId(entry);
267
- if (!currentAppId) continue;
268
- pushGraphElement(elements, {
269
- id: graphElementId('app', currentAppId),
270
- type: 'app',
271
- label: entry.app?.name || currentAppId,
272
- data: appElementData(entry),
273
- });
274
- }
275
-
276
- for (const skill of skills) {
277
- const elementId = skillElementId(skill);
278
- if (!elementId) continue;
279
- pushGraphElement(elements, {
280
- id: elementId,
281
- type: 'skill',
282
- label: skillLabel(skill),
283
- data: typeof skill === 'string' ? { value: skill } : cloneJson(skill),
284
- });
285
- }
286
-
287
- const includedToolIds = new Set();
288
- const includedActionIds = new Set();
289
-
290
- for (const entry of bundle.automations || []) {
291
- const currentAutomationId = automationEntryId(entry);
292
- if (!currentAutomationId) continue;
293
- for (const toolId of uniqueStrings(entry.toolIds)) {
294
- includedToolIds.add(toolId);
295
- pushGraphEdge(edges, {
296
- from: graphElementId('automation', currentAutomationId),
297
- to: graphElementId('tool', toolId),
298
- kind: 'uses-tool',
299
- });
300
- }
301
- }
302
- // Tools that belong to an app render nested under it in the share sidebar;
303
- // the canonical tool record still appears exactly once.
304
- for (const entry of bundle.apps || []) {
305
- const currentAppId = appEntryId(entry);
306
- if (!currentAppId) continue;
307
- for (const toolId of uniqueStrings(entry.toolIds)) {
308
- includedToolIds.add(toolId);
309
- pushGraphEdge(edges, {
310
- from: graphElementId('app', currentAppId),
311
- to: graphElementId('tool', toolId),
312
- kind: 'uses-tool',
313
- });
314
- }
315
- }
316
- for (const entry of bundle.agents || []) {
317
- const currentAgentId = agentEntryId(entry);
318
- if (!currentAgentId) continue;
319
- const currentConfig = normalizeAgentConfig(entry.config || {});
320
-
321
- for (const subagent of currentConfig.subagents || []) {
322
- if (!subagent?.agentId) continue;
323
- pushGraphEdge(edges, {
324
- from: graphElementId('agent', currentAgentId),
325
- to: graphElementId('agent', subagent.agentId),
326
- kind: 'uses-subagent',
327
- });
328
- }
329
-
330
- for (const selectedId of uniqueStrings(currentConfig.loadout?.toolIds)) {
331
- const selectedAction = toolActionMap.get(selectedId);
332
- if (selectedAction) {
333
- includedActionIds.add(selectedAction.id);
334
- includedToolIds.add(selectedAction.toolId);
335
- pushGraphEdge(edges, {
336
- from: graphElementId('agent', currentAgentId),
337
- to: graphElementId('tool_action', selectedAction.id),
338
- kind: 'uses-tool-action',
339
- });
340
- continue;
341
- }
342
-
343
- includedToolIds.add(selectedId);
344
- pushGraphEdge(edges, {
345
- from: graphElementId('agent', currentAgentId),
346
- to: graphElementId('tool', selectedId),
347
- kind: 'uses-tool',
348
- });
349
- }
350
-
351
- for (const skill of currentConfig.skills || []) {
352
- const targetId = skillElementId(skill);
353
- if (!targetId) continue;
354
- pushGraphEdge(edges, {
355
- from: graphElementId('agent', currentAgentId),
356
- to: targetId,
357
- kind: 'uses-skill',
358
- });
359
- }
360
- }
361
-
362
- // Every tool in bundle.tools gets an element — standalone tool heads are
363
- // not referenced by any agent or automation edge but must still resolve.
364
- const toolElementIds = new Set([...includedToolIds, ...toolMap.keys()]);
365
- for (const toolId of toolElementIds) {
366
- const tool = toolMap.get(toolId);
367
- pushGraphElement(elements, {
368
- id: graphElementId('tool', toolId),
369
- type: 'tool',
370
- label: tool?.name || toolId,
371
- data: tool
372
- ? cloneJson(tool)
373
- : {
374
- id: toolId,
375
- name: toolId,
376
- origin: systemToolIds.has(toolId) ? 'system' : missingToolIds.has(toolId) ? 'missing' : 'unknown',
377
- },
378
- });
379
- }
380
-
381
- for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
382
- if (!toolElementIds.has(action.toolId)) continue;
383
- includedActionIds.add(action.id);
384
- pushGraphElement(elements, {
385
- id: graphElementId('tool_action', action.id),
386
- type: 'tool_action',
387
- label: action.name || action.id,
388
- data: cloneJson(action),
389
- });
390
- pushGraphEdge(edges, {
391
- from: graphElementId('tool_action', action.id),
392
- to: graphElementId('tool', action.toolId),
393
- kind: 'action-of',
394
- });
395
- }
396
-
397
- return {
398
- headIds: bundleHeadIds(bundle, elements),
399
- elements: [...elements.values()].sort(compareGraphElements),
400
- edges: [...edges.values()].sort(compareGraphEdges),
401
- };
402
- }
403
-
404
- /**
405
- * Heads are the user-facing roots of the bundle. Explicit bundle.heads win;
406
- * otherwise fall back to the legacy single agent root plus every automation
407
- * and app entry (those are always top-level).
408
- */
409
- function bundleHeadIds(bundle, elements) {
410
- const headIds = [];
411
- const explicitHeads = Array.isArray(bundle.heads) ? bundle.heads : [];
412
- for (const head of explicitHeads) {
413
- const type = cleanEntryId(head?.type);
414
- const id = cleanEntryId(head?.id);
415
- if (!type || !id) continue;
416
- const elementId = graphElementId(type, id);
417
- if (elements.has(elementId) && !headIds.includes(elementId)) headIds.push(elementId);
418
- }
419
- if (headIds.length > 0) return headIds;
420
-
421
- const firstAgentId = agentEntryId(bundle.agents?.[0]);
422
- const rootAgentId = cleanEntryId(bundle.rootAgentId) || firstAgentId;
423
- if (rootAgentId && elements.has(graphElementId('agent', rootAgentId))) {
424
- headIds.push(graphElementId('agent', rootAgentId));
425
- }
426
- for (const entry of bundle.automations || []) {
427
- const id = automationEntryId(entry);
428
- if (id) headIds.push(graphElementId('automation', id));
429
- }
430
- for (const entry of bundle.apps || []) {
431
- const id = appEntryId(entry);
432
- if (id) headIds.push(graphElementId('app', id));
433
- }
434
- return [...new Set(headIds)];
435
- }
436
-
437
- function assertClosedFlatBundleGraph(bundle) {
438
- if (!Array.isArray(bundle.headIds) || bundle.headIds.length === 0) {
439
- throw new Error('bundle graph must include at least one head');
440
- }
441
-
442
- const elementIds = new Set();
443
- for (const element of Array.isArray(bundle.elements) ? bundle.elements : []) {
444
- if (!isObject(element) || typeof element.id !== 'string' || !element.id.trim()) {
445
- throw new Error('bundle graph elements must include non-empty ids');
446
- }
447
- if (elementIds.has(element.id)) {
448
- throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
449
- }
450
- elementIds.add(element.id);
451
- }
452
-
453
- for (const headId of bundle.headIds) {
454
- if (typeof headId !== 'string' || !elementIds.has(headId)) {
455
- throw new Error(`bundle graph head is missing element: ${headId}`);
456
- }
457
- }
458
-
459
- for (const edge of Array.isArray(bundle.edges) ? bundle.edges : []) {
460
- if (!isObject(edge) || typeof edge.from !== 'string' || typeof edge.to !== 'string' || typeof edge.kind !== 'string') {
461
- throw new Error('bundle graph edges must include from, to, and kind');
462
- }
463
- if (!elementIds.has(edge.from)) throw new Error(`bundle graph edge source is missing element: ${edge.from}`);
464
- if (!elementIds.has(edge.to)) throw new Error(`bundle graph edge target is missing element: ${edge.to}`);
465
- }
466
- }
467
-
468
- function withBundleGraph(bundle) {
469
- const graph = buildFlatAgentBundleGraph(bundle);
470
- const nextBundle = {
471
- ...bundle,
472
- headIds: graph.headIds,
473
- elements: graph.elements,
474
- edges: graph.edges,
475
- };
476
- assertClosedFlatBundleGraph(nextBundle);
477
- return nextBundle;
478
- }
479
-
480
- function collectToolRecords(toolIds, requires) {
481
- const catalog = defaultToolboxService.readCatalog();
482
- const tools = new Map();
483
- const actions = new Map();
484
- const selected = uniqueStrings(toolIds);
485
-
486
- for (const selectedId of selected) {
487
- const selectedTool = catalog.tools?.[selectedId];
488
- const selectedAction = catalog.toolActions?.[selectedId];
489
- const tool = selectedTool || (selectedAction ? catalog.tools?.[selectedAction.toolId] : null);
490
-
491
- if (!tool) {
492
- addUnique(requires.missingTools, selectedId);
493
- continue;
494
- }
495
-
496
- if (tool.origin === 'system') {
497
- addUnique(requires.systemTools, tool.id);
498
- }
499
-
500
- if (!tools.has(tool.id)) tools.set(tool.id, portableTool(tool, requires));
501
-
502
- if (selectedAction) {
503
- actions.set(selectedAction.id, portableAction(selectedAction, requires));
504
- }
505
-
506
- for (const action of Object.values(catalog.toolActions || {})) {
507
- if (action?.toolId === tool.id) actions.set(action.id, portableAction(action, requires));
508
- }
509
- }
510
-
511
- return {
512
- tools: [...tools.values()],
513
- toolActions: [...actions.values()],
514
- };
515
- }
516
-
517
- function collectAgentGraph(rootAgentId) {
518
- const seen = new Set();
519
- const ordered = [];
520
-
521
- function visit(agentId) {
522
- if (!agentId || seen.has(agentId)) return;
523
- seen.add(agentId);
524
- const agent = resolveAgent(agentId);
525
- if (!agent) throw new Error(`Agent not found: ${agentId}`);
526
- const config = hydrateConfigSkills(getAgentConfig(agent));
527
- ordered.push({
528
- sourceAgentId: agent.id,
529
- agent: portableAgent(agent),
530
- config: normalizeAgentConfig(config),
531
- });
532
- for (const subagent of config.subagents || []) {
533
- if (subagent?.agentId) visit(subagent.agentId);
534
- }
535
- }
536
-
537
- visit(rootAgentId);
538
- return ordered;
539
- }
540
-
541
- function bundleTitleAndDescription(agents, automations, apps, sharedTools) {
542
- const firstAgent = agents[0]?.agent;
543
- if (firstAgent) {
544
- return { title: firstAgent.name || 'Shared agent', description: firstAgent.description || '' };
545
- }
546
- const firstAutomation = automations[0]?.automation;
547
- if (firstAutomation) {
548
- return { title: firstAutomation.name || 'Shared automation', description: firstAutomation.description || '' };
549
- }
550
- const firstApp = apps[0]?.app;
551
- if (firstApp) {
552
- return { title: firstApp.name || 'Shared app', description: firstApp.description || '' };
553
- }
554
- const firstTool = sharedTools?.[0];
555
- if (firstTool) {
556
- return { title: firstTool.name || 'Shared tool', description: firstTool.description || '' };
557
- }
558
- return { title: 'Shared bundle', description: '' };
559
- }
560
-
561
- function bundleWarnings({ hooks, tools, automations, apps, skippedAppFiles }) {
562
- return [
563
- 'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
564
- 'Secrets pasted into free-text instructions or files are not automatically detected.',
565
- ...(hooks > 0 ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
566
- ...(tools > 0 ? ['Tools can call external services or local commands. Recipients should reconnect credentials on their own machine.'] : []),
567
- ...(automations > 0 ? ['This bundle includes automations. Their workflows run code and call tools on the importing machine — review workflow scripts before importing.'] : []),
568
- ...(apps > 0 ? ['This bundle includes apps. Their build and start commands execute on install — review commands before importing.'] : []),
569
- ...(skippedAppFiles > 0 ? [`${skippedAppFiles} app file${skippedAppFiles === 1 ? ' was' : 's were'} excluded from the bundle (dependencies, build outputs, env files, or size limits). Builds re-run on install.`] : []),
570
- ];
571
- }
572
-
573
- /**
574
- * Build a portable bundle from any mix of top-level primitives. Each requested
575
- * id becomes a head; agents pull in their full subagent graph, and agents and
576
- * automations pull in the toolbox tools they reference.
577
- */
578
- function createBundle(input = {}, options = {}) {
579
- const agentIds = uniqueStrings(input.agentIds);
580
- const automationIds = uniqueStrings(input.automationIds);
581
- const appIds = uniqueStrings(input.appIds);
582
- const toolIds = uniqueStrings(input.toolIds);
583
- if (agentIds.length === 0 && automationIds.length === 0 && appIds.length === 0 && toolIds.length === 0) {
584
- throw new Error('At least one agent, automation, app, or tool is required');
585
- }
586
-
587
- const requires = {
588
- runtimes: [],
589
- auth: [],
590
- secrets: [],
591
- bindings: [],
592
- systemTools: [],
593
- systemActions: [],
594
- missingTools: [],
595
- };
596
-
597
- const agents = [];
598
- const seenAgentIds = new Set();
599
- for (const agentId of agentIds) {
600
- for (const entry of collectAgentGraph(agentId)) {
601
- if (seenAgentIds.has(entry.sourceAgentId)) continue;
602
- seenAgentIds.add(entry.sourceAgentId);
603
- agents.push(entry);
604
- }
605
- }
606
-
607
- const loadoutToolIds = [];
608
- for (const entry of agents) {
609
- addUnique(requires.runtimes, entry.agent.baseHarnessId);
610
- addUnique(requires.auth, `${entry.agent.baseHarnessId}:${entry.agent.authMethod || 'amalgm'}`);
611
- loadoutToolIds.push(...(entry.config.loadout?.toolIds || []));
612
- }
613
-
614
- const automations = [];
615
- for (const automationId of automationIds) {
616
- const exported = exportAutomationEntry(automationId);
617
- automations.push(exported.entry);
618
- loadoutToolIds.push(...exported.toolboxToolIds);
619
- for (const ref of exported.systemRefs) addUnique(requires.systemActions, ref);
620
- for (const ref of exported.missingRefs) addUnique(requires.missingTools, ref);
621
- }
622
-
623
- // Tool → app closure: an app-owned tool only works next to its app, so any
624
- // requested tool whose record points at an app pulls that app in as a
625
- // dependency (not a head). Stale pointers degrade to a warning.
626
- const appLinkWarnings = [];
627
- const dependencyAppIds = [];
628
- const catalog = defaultToolboxService.readCatalog();
629
- for (const selectedId of uniqueStrings([...loadoutToolIds, ...toolIds])) {
630
- const selectedAction = catalog.toolActions?.[selectedId];
631
- const tool = catalog.tools?.[selectedId]
632
- || (selectedAction ? catalog.tools?.[selectedAction.toolId] : null);
633
- const owningAppId = typeof tool?.appId === 'string' ? tool.appId.trim() : '';
634
- if (!owningAppId || appIds.includes(owningAppId) || dependencyAppIds.includes(owningAppId)) continue;
635
- if (!getApp(owningAppId)) {
636
- appLinkWarnings.push(`Tool ${tool.id} belongs to app ${owningAppId}, which no longer exists; the app was not bundled.`);
637
- continue;
638
- }
639
- dependencyAppIds.push(owningAppId);
640
- }
641
-
642
- const apps = [];
643
- const appCwdBySourceId = new Map();
644
- let skippedAppFiles = 0;
645
- const appToolIds = [];
646
- for (const appId of [...appIds, ...dependencyAppIds]) {
647
- const exported = exportAppEntry(appId);
648
- apps.push(exported.entry);
649
- appCwdBySourceId.set(exported.entry.sourceAppId, exported.sourceCwd);
650
- skippedAppFiles += exported.skipped.length;
651
- // App → tool closure: tools that belong to a bundled app travel with it.
652
- appToolIds.push(...(exported.entry.toolIds || []));
653
- }
654
-
655
- const { tools, toolActions } = collectToolRecords([...loadoutToolIds, ...appToolIds, ...toolIds], requires);
656
-
657
- // Tool files that live inside a shared app travel with that app: record
658
- // app-relative bindings so install rewrites them to the recipient's copy.
659
- // Absolute paths no shared app covers stay machine-bound requirements.
660
- for (const record of [...tools, ...toolActions]) {
661
- if (record.origin === 'system') continue;
662
- const kind = record.toolId ? 'toolAction' : 'tool';
663
- const { unbound } = annotateAppBoundPaths(record, appCwdBySourceId);
664
- for (const label of unbound) addUnique(requires.bindings, `${kind}:${record.id}:${label}`);
665
- }
666
- // Standalone tool heads must resolve to a real catalog tool — a dependency
667
- // tool that is missing degrades to a placeholder, but a head cannot.
668
- const sharedToolIds = toolIds.filter((toolId) => tools.some((tool) => tool.id === toolId));
669
- const missingSharedToolIds = toolIds.filter((toolId) => !sharedToolIds.includes(toolId));
670
- if (missingSharedToolIds.length > 0) {
671
- throw new Error(`Tool not found: ${missingSharedToolIds.join(', ')}`);
672
- }
673
- const sharedTools = sharedToolIds.map((toolId) => tools.find((tool) => tool.id === toolId));
674
- const skills = collectSkillRecords(agents);
675
- const hooks = countHooks(agents);
676
- const warnings = [
677
- ...bundleWarnings({
678
- hooks,
679
- tools: tools.length,
680
- automations: automations.length,
681
- apps: apps.length,
682
- skippedAppFiles,
683
- }),
684
- ...appLinkWarnings,
685
- ];
686
- // Title comes from what the user deliberately shared: dependency apps
687
- // pulled in by an app-owned tool must not name the bundle.
688
- const headApps = apps.filter((entry) => appIds.includes(entry.sourceAppId));
689
- const { title, description } = bundleTitleAndDescription(agents, automations, headApps, sharedTools);
690
- const heads = [
691
- ...agentIds.map((id) => ({ type: 'agent', id })),
692
- ...automationIds.map((id) => ({ type: 'automation', id })),
693
- ...appIds.map((id) => ({ type: 'app', id })),
694
- ...sharedToolIds.map((id) => ({ type: 'tool', id })),
695
- ];
696
-
697
- const bundle = withBundleGraph({
698
- kind: automations.length > 0 || apps.length > 0 || sharedToolIds.length > 0
699
- ? BUNDLE_KIND
700
- : LEGACY_AGENT_BUNDLE_KIND,
701
- schemaVersion: BUNDLE_SCHEMA_VERSION,
702
- bundleId: options.bundleId || bundleId(),
703
- createdAt: nowIso(),
704
- rootAgentId: agents[0]?.sourceAgentId || null,
705
- heads,
706
- title,
707
- description,
708
- agents,
709
- automations,
710
- apps,
711
- tools,
712
- toolActions,
713
- skills,
714
- assets: [],
715
- requires,
716
- warnings,
717
- summary: {
718
- agents: agents.length,
719
- automations: automations.length,
720
- apps: apps.length,
721
- tools: tools.length,
722
- toolActions: toolActions.length,
723
- skills: skills.length,
724
- hooks,
725
- assets: 0,
726
- },
727
- });
728
-
729
- const bytes = Buffer.byteLength(JSON.stringify(bundle));
730
- return {
731
- bundle,
732
- preview: {
733
- bundleId: bundle.bundleId,
734
- title: bundle.title,
735
- description: bundle.description,
736
- summary: bundle.summary,
737
- requires,
738
- warnings,
739
- agents: agents.map((entry) => ({
740
- sourceAgentId: entry.sourceAgentId,
741
- name: entry.agent.name,
742
- description: entry.agent.description,
743
- baseHarnessId: entry.agent.baseHarnessId,
744
- baseModelId: entry.agent.baseModelId,
745
- tools: entry.config.loadout?.toolIds?.length || 0,
746
- skills: entry.config.skills?.length || 0,
747
- subagents: entry.config.subagents?.length || 0,
748
- hooks: entry.config.hooks?.length || 0,
749
- })),
750
- automations: automations.map((entry) => ({
751
- sourceAutomationId: entry.sourceAutomationId,
752
- name: entry.automation.name,
753
- description: entry.automation.description,
754
- triggers: entry.triggers.length,
755
- workflows: entry.workflows.length,
756
- tools: entry.toolIds.length,
757
- })),
758
- apps: apps.map((entry) => ({
759
- sourceAppId: entry.sourceAppId,
760
- name: entry.app.name,
761
- description: entry.app.description,
762
- files: entry.files.length,
763
- totalBytes: entry.totalBytes,
764
- skippedFiles: entry.skipped.length,
765
- })),
766
- tools: tools.map((tool) => ({
767
- id: tool.id,
768
- name: tool.name,
769
- type: tool.type,
770
- origin: tool.origin,
771
- actionCount: toolActions.filter((action) => action.toolId === tool.id).length,
772
- })),
773
- sizeBytes: bytes,
774
- },
775
- };
776
- }
777
-
778
- function createAgentBundle(agentId, options = {}) {
779
- if (!agentId) throw new Error('agent_id is required');
780
- return createBundle({ agentIds: [agentId] }, options);
781
- }
782
-
783
- function uniqueAgentName(name) {
784
- const base = String(name || 'Imported agent').trim() || 'Imported agent';
785
- const existing = new Set(
786
- getAllAgentsWithBuiltins().map((agent) => String(agent.name || '').trim().toLowerCase()),
787
- );
788
- if (!existing.has(base.toLowerCase())) return base;
789
- const imported = `${base} (imported)`;
790
- if (!existing.has(imported.toLowerCase())) return imported;
791
- for (let index = 2; index < 1000; index += 1) {
792
- const candidate = `${base} (imported ${index})`;
793
- if (!existing.has(candidate.toLowerCase())) return candidate;
794
- }
795
- return `${base} (${crypto.randomUUID().slice(0, 8)})`;
796
- }
797
-
798
- function externalMcpToolIdsForLoadout(toolIds, tools) {
799
- const toolMap = new Map((Array.isArray(tools) ? tools : []).map((tool) => [tool.id, tool]));
800
- return uniqueStrings(toolIds)
801
- .map((toolId) => toolMap.get(toolId))
802
- .filter((tool) => tool?.type === 'mcp' && tool?.origin !== 'system')
803
- .map((tool) => tool.id);
804
- }
805
-
806
- function agentEntryId(entry) {
807
- if (!isObject(entry)) return '';
808
- const sourceAgentId = typeof entry.sourceAgentId === 'string' ? entry.sourceAgentId.trim() : '';
809
- if (sourceAgentId) return sourceAgentId;
810
- return typeof entry.agent?.sourceAgentId === 'string' ? entry.agent.sourceAgentId.trim() : '';
811
- }
812
-
813
- function referencedSubagentIds(entry) {
814
- if (!isObject(entry)) return [];
815
- const subagents = Array.isArray(entry.config?.subagents) ? entry.config.subagents : [];
816
- return subagents
817
- .map((subagent) => (typeof subagent?.agentId === 'string' ? subagent.agentId.trim() : ''))
818
- .filter(Boolean);
819
- }
820
-
821
- function assertClosedAgentBundleGraph(bundle) {
822
- const agentIds = new Set(bundle.agents.map(agentEntryId).filter(Boolean));
823
- const missing = new Set();
824
- for (const entry of bundle.agents) {
825
- for (const agentId of referencedSubagentIds(entry)) {
826
- if (!agentIds.has(agentId)) missing.add(agentId);
827
- }
828
- }
829
-
830
- if (missing.size > 0) {
831
- throw new Error(`Bundle is missing shared subagents: ${Array.from(missing).join(', ')}`);
832
- }
833
- }
834
-
835
- function validateBundle(bundle) {
836
- if (!isObject(bundle)) throw new Error('bundle must be an object');
837
- if (bundle.kind !== BUNDLE_KIND && bundle.kind !== LEGACY_AGENT_BUNDLE_KIND) {
838
- throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
839
- }
840
- if (bundle.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
841
- throw new Error(`Unsupported bundle schema version: ${bundle.schemaVersion || 'unknown'}`);
842
- }
843
- const agents = Array.isArray(bundle.agents) ? bundle.agents : [];
844
- const automations = Array.isArray(bundle.automations) ? bundle.automations : [];
845
- const apps = Array.isArray(bundle.apps) ? bundle.apps : [];
846
- const toolHeads = standaloneToolIds(bundle);
847
- if (agents.length === 0 && automations.length === 0 && apps.length === 0 && toolHeads.length === 0) {
848
- throw new Error('bundle must include at least one agent, automation, app, or tool');
849
- }
850
- if (agents.length > 0) assertClosedAgentBundleGraph(bundle);
851
- return withBundleGraph(bundle);
852
- }
853
-
854
- function installTools(bundle, appDirBySourceId = new Map(), appIdBySourceId = new Map()) {
855
- const warnings = [];
856
- const rewrite = (record) => {
857
- const result = rewriteAppBoundPaths(record, appDirBySourceId);
858
- warnings.push(...result.warnings);
859
- return result.record;
860
- };
861
-
862
- const toolActionsByToolId = new Map();
863
- for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
864
- if (!action?.toolId) continue;
865
- const existing = toolActionsByToolId.get(action.toolId) || [];
866
- existing.push(rewrite(action));
867
- toolActionsByToolId.set(action.toolId, existing);
868
- }
869
-
870
- const installed = [];
871
- for (const tool of Array.isArray(bundle.tools) ? bundle.tools : []) {
872
- if (!tool?.id || tool.origin === 'system') continue;
873
- const record = rewrite(tool);
874
- // The app link travels by source app id; point it at the recipient's
875
- // installed copy, or drop it if that app was not part of the install.
876
- if (typeof record.appId === 'string' && record.appId) {
877
- const installedAppId = appIdBySourceId.get(record.appId);
878
- if (installedAppId) {
879
- record.appId = installedAppId;
880
- } else {
881
- delete record.appId;
882
- warnings.push(`Tool ${tool.id} belongs to an app that was not installed with this bundle; the app link was removed.`);
883
- }
884
- }
885
- const result = defaultToolboxService.registerTool({
886
- ...record,
887
- origin: tool.origin === 'catalog' ? 'catalog' : 'user',
888
- actions: toolActionsByToolId.get(tool.id) || [],
889
- });
890
- installed.push(result.tool);
891
- }
892
- return { installed, warnings };
893
- }
894
-
895
- function installBundleAgents(bundle, options = {}) {
896
- const idMap = new Map();
897
- const createdAgents = [];
898
- const createdAt = nowIso();
899
-
900
- for (const entry of Array.isArray(bundle.agents) ? bundle.agents : []) {
901
- const config = normalizeAgentConfig(entry.config || {});
902
- const fields = agentFieldsFromConfig(config);
903
- const agent = createAgent({
904
- name: uniqueAgentName(entry.agent?.name),
905
- description: entry.agent?.description || '',
906
- baseHarnessId: entry.agent?.baseHarnessId || 'codex',
907
- baseModelId: entry.agent?.baseModelId || '',
908
- modelSettings: isObject(entry.agent?.modelSettings) ? entry.agent.modelSettings : null,
909
- systemPrompt: fields.systemPrompt,
910
- skills: fields.skills,
911
- loadout: config.loadout || { toolIds: [] },
912
- mcpAppIds: externalMcpToolIdsForLoadout(config.loadout?.toolIds || [], bundle.tools),
913
- authMethod: options.authMethod || 'amalgm',
914
- status: 'ready',
915
- installStatus: 'external',
916
- createdAt,
917
- }, { source: 'agent-bundles:install' });
918
- idMap.set(entry.sourceAgentId || entry.agent?.sourceAgentId, agent.id);
919
- createdAgents.push({ sourceAgentId: entry.sourceAgentId, agent, config });
920
- }
921
-
922
- const installedAgents = [];
923
- for (const created of createdAgents) {
924
- const rewrittenConfig = normalizeAgentConfig({
925
- ...created.config,
926
- agentId: created.agent.id,
927
- runtimeHomeLayout: 'per-agent',
928
- subagents: (created.config.subagents || [])
929
- .map((subagent) => {
930
- const mappedAgentId = idMap.get(subagent.agentId);
931
- if (!mappedAgentId) return null;
932
- return {
933
- ...subagent,
934
- id: `subagent-${mappedAgentId}`,
935
- agentId: mappedAgentId,
936
- };
937
- })
938
- .filter(Boolean),
939
- });
940
- const savedConfig = upsertAgentConfig(created.agent.id, rewrittenConfig, {
941
- source: 'agent-bundles:install',
942
- });
943
- installedAgents.push({
944
- ...created.agent,
945
- ...agentFieldsFromConfig(savedConfig),
946
- config: savedConfig,
947
- });
948
- }
949
-
950
- return { idMap, installedAgents };
951
- }
952
-
953
- function defaultAppsInstallRoot() {
954
- // Installed apps are Amalgm-owned state: they live under the user's apps
955
- // primitive root (<user dir>/apps), not in the projects workspace.
956
- return require('../config').APPS_DIR;
957
- }
958
-
959
- /**
960
- * Install every primitive in a validated bundle. Apps first — they
961
- * materialize into the user's apps root, build, and start, and their
962
- * installed directories anchor the app-bound tool path rewrites. Then tools
963
- * (agents and automations reference them), agents, and automations.
964
- */
965
- async function installBundle(input, options = {}) {
966
- const bundle = validateBundle(input);
967
-
968
- const installedApps = [];
969
- const appDirBySourceId = new Map();
970
- const appIdBySourceId = new Map();
971
- const appEntries = Array.isArray(bundle.apps) ? bundle.apps : [];
972
- if (appEntries.length > 0) {
973
- const rootDir = options.appsRootDir || defaultAppsInstallRoot();
974
- for (const entry of appEntries) {
975
- const installed = await installAppEntry(entry, { rootDir, register: options.registerApp });
976
- appDirBySourceId.set(appEntryId(entry), installed.cwd);
977
- if (installed.id) appIdBySourceId.set(appEntryId(entry), installed.id);
978
- installedApps.push(installed);
979
- }
980
- }
981
-
982
- const { installed: installedTools, warnings: bindingWarnings } = installTools(bundle, appDirBySourceId, appIdBySourceId);
983
- const { idMap, installedAgents } = installBundleAgents(bundle, options);
984
-
985
- const installedAutomations = [];
986
- for (const entry of Array.isArray(bundle.automations) ? bundle.automations : []) {
987
- installedAutomations.push(installAutomationEntry(entry, { source: 'agent-bundles:install' }));
988
- }
989
-
990
- return {
991
- ok: true,
992
- bundleId: bundle.bundleId,
993
- rootAgentId: idMap.get(bundle.rootAgentId) || installedAgents[0]?.id || null,
994
- idMap: Object.fromEntries(idMap.entries()),
995
- installedAgents,
996
- installedTools,
997
- installedAutomations,
998
- installedApps,
999
- warnings: [...(bundle.warnings || []), ...bindingWarnings],
1000
- };
1001
- }
3
+ // Facade over the agent-bundle modules so existing require paths keep working.
4
+ // The implementation lives in graph.js (flat share graph), export.js
5
+ // (createBundle and its collectors), and install.js (validate + install).
6
+ const { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION, LEGACY_AGENT_BUNDLE_KIND } = require('./graph');
7
+ const { createAgentBundle, createBundle } = require('./export');
8
+ const { installBundle, validateBundle } = require('./install');
1002
9
 
1003
10
  module.exports = {
1004
11
  BUNDLE_KIND,