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.
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const { normalizeAgentConfig } = require('../agent-config/schema');
6
+
7
+ // Agent-only bundles keep the legacy kind so older runtimes can still install
8
+ // them; bundles that carry automations or apps use the general kind so older
9
+ // runtimes reject them outright instead of silently dropping primitives.
10
+ const BUNDLE_KIND = 'amalgm.bundle';
11
+ const LEGACY_AGENT_BUNDLE_KIND = 'amalgm.agent.bundle';
12
+ const BUNDLE_SCHEMA_VERSION = 1;
13
+ const GRAPH_ELEMENT_ORDER = {
14
+ agent: 0,
15
+ automation: 1,
16
+ app: 2,
17
+ tool: 3,
18
+ tool_action: 4,
19
+ skill: 5,
20
+ };
21
+
22
+ function nowIso() {
23
+ return new Date().toISOString();
24
+ }
25
+
26
+ function isObject(value) {
27
+ return !!value && typeof value === 'object' && !Array.isArray(value);
28
+ }
29
+
30
+ function cloneJson(value) {
31
+ if (value == null) return value;
32
+ return JSON.parse(JSON.stringify(value));
33
+ }
34
+
35
+ function uniqueStrings(values) {
36
+ if (!Array.isArray(values)) return [];
37
+ return [...new Set(values
38
+ .filter((value) => typeof value === 'string')
39
+ .map((value) => value.trim())
40
+ .filter(Boolean))];
41
+ }
42
+
43
+ function addUnique(target, value) {
44
+ if (!value) return;
45
+ if (!target.includes(value)) target.push(value);
46
+ }
47
+
48
+ function stableDigest(value) {
49
+ return crypto.createHash('sha256')
50
+ .update(typeof value === 'string' ? value : JSON.stringify(value))
51
+ .digest('hex')
52
+ .slice(0, 16);
53
+ }
54
+
55
+ function graphElementId(type, id) {
56
+ return `${type}:${id}`;
57
+ }
58
+
59
+ function skillFingerprint(skill) {
60
+ if (typeof skill === 'string') {
61
+ const value = skill.trim();
62
+ return value ? `string:${value}` : '';
63
+ }
64
+ if (!isObject(skill)) return '';
65
+ const id = typeof skill.id === 'string' ? skill.id.trim() : '';
66
+ if (id) return `id:${id}`;
67
+ const path = typeof skill.path === 'string' ? skill.path.trim() : '';
68
+ if (path) return `path:${path}`;
69
+ const name = typeof skill.name === 'string' ? skill.name.trim() : '';
70
+ const content = typeof skill.content === 'string' ? skill.content.trim() : '';
71
+ if (name && content) return `name-content:${name}:${content}`;
72
+ if (name) return `name:${name}`;
73
+ if (content) return `content:${content}`;
74
+ return `json:${JSON.stringify(skill)}`;
75
+ }
76
+
77
+ function skillElementId(skill) {
78
+ const fingerprint = skillFingerprint(skill);
79
+ return fingerprint ? graphElementId('skill', stableDigest(fingerprint)) : '';
80
+ }
81
+
82
+ function skillLabel(skill) {
83
+ if (typeof skill === 'string') return skill.trim() || 'Skill';
84
+ if (!isObject(skill)) return 'Skill';
85
+ const id = typeof skill.id === 'string' ? skill.id.trim() : '';
86
+ const name = typeof skill.name === 'string' ? skill.name.trim() : '';
87
+ const path = typeof skill.path === 'string' ? skill.path.trim() : '';
88
+ if (name) return name;
89
+ if (path) return path.split('/').filter(Boolean).pop() || path;
90
+ if (id) return id;
91
+ const content = typeof skill.content === 'string' ? skill.content.trim() : '';
92
+ return content.slice(0, 60) || 'Skill';
93
+ }
94
+
95
+ function compareGraphElements(left, right) {
96
+ const order = (GRAPH_ELEMENT_ORDER[left.type] || 99) - (GRAPH_ELEMENT_ORDER[right.type] || 99);
97
+ if (order !== 0) return order;
98
+ return String(left.id).localeCompare(String(right.id));
99
+ }
100
+
101
+ function compareGraphEdges(left, right) {
102
+ return String(left.from).localeCompare(String(right.from))
103
+ || String(left.kind).localeCompare(String(right.kind))
104
+ || String(left.to).localeCompare(String(right.to));
105
+ }
106
+
107
+ function pushGraphElement(elements, element) {
108
+ if (!element?.id) return;
109
+ if (!elements.has(element.id)) elements.set(element.id, element);
110
+ }
111
+
112
+ function pushGraphEdge(edges, edge) {
113
+ if (!edge?.from || !edge?.to || !edge?.kind) return;
114
+ const key = `${edge.from}::${edge.kind}::${edge.to}`;
115
+ if (!edges.has(key)) edges.set(key, edge);
116
+ }
117
+
118
+ function collectSkillRecords(bundleAgents) {
119
+ const byKey = new Map();
120
+ for (const entry of bundleAgents) {
121
+ for (const skill of entry.config.skills || []) {
122
+ const key = skillFingerprint(skill);
123
+ if (!key) continue;
124
+ byKey.set(key, cloneJson(skill));
125
+ }
126
+ }
127
+ return [...byKey.values()];
128
+ }
129
+
130
+ function cleanEntryId(value) {
131
+ return typeof value === 'string' ? value.trim() : '';
132
+ }
133
+
134
+ function agentEntryId(entry) {
135
+ if (!isObject(entry)) return '';
136
+ const sourceAgentId = typeof entry.sourceAgentId === 'string' ? entry.sourceAgentId.trim() : '';
137
+ if (sourceAgentId) return sourceAgentId;
138
+ return typeof entry.agent?.sourceAgentId === 'string' ? entry.agent.sourceAgentId.trim() : '';
139
+ }
140
+
141
+ function automationEntryId(entry) {
142
+ return isObject(entry) ? cleanEntryId(entry.sourceAutomationId) : '';
143
+ }
144
+
145
+ function appEntryId(entry) {
146
+ return isObject(entry) ? cleanEntryId(entry.sourceAppId) : '';
147
+ }
148
+
149
+ /**
150
+ * Tools shared as top-level primitives (not just dependencies of agents or
151
+ * automations). The bundle's explicit heads are the single source of truth.
152
+ */
153
+ function standaloneToolIds(bundle) {
154
+ const heads = Array.isArray(bundle?.heads) ? bundle.heads : [];
155
+ return uniqueStrings(heads
156
+ .filter((head) => isObject(head) && cleanEntryId(head.type) === 'tool')
157
+ .map((head) => cleanEntryId(head.id)));
158
+ }
159
+
160
+ /**
161
+ * App elements carry the portable record and a file manifest; file contents
162
+ * stay in bundle.apps[] so the graph remains cheap to render.
163
+ */
164
+ function appElementData(entry) {
165
+ return {
166
+ sourceAppId: appEntryId(entry),
167
+ app: cloneJson(entry.app || {}),
168
+ files: (Array.isArray(entry.files) ? entry.files : []).map((file) => ({
169
+ path: file.path,
170
+ size: file.size,
171
+ encoding: file.encoding,
172
+ ...(file.executable ? { executable: true } : {}),
173
+ })),
174
+ totalBytes: entry.totalBytes || 0,
175
+ skipped: cloneJson(entry.skipped || []),
176
+ };
177
+ }
178
+
179
+ function buildFlatAgentBundleGraph(bundle) {
180
+ const elements = new Map();
181
+ const edges = new Map();
182
+ const toolMap = new Map((Array.isArray(bundle.tools) ? bundle.tools : []).map((tool) => [tool.id, tool]));
183
+ const toolActionMap = new Map((Array.isArray(bundle.toolActions) ? bundle.toolActions : []).map((action) => [action.id, action]));
184
+ const systemToolIds = new Set(uniqueStrings(bundle.requires?.systemTools));
185
+ const missingToolIds = new Set(uniqueStrings(bundle.requires?.missingTools));
186
+ const skills = collectSkillRecords(bundle.agents || []);
187
+
188
+ for (const entry of bundle.agents || []) {
189
+ const currentAgentId = agentEntryId(entry);
190
+ if (!currentAgentId) continue;
191
+ pushGraphElement(elements, {
192
+ id: graphElementId('agent', currentAgentId),
193
+ type: 'agent',
194
+ label: entry.agent?.name || currentAgentId,
195
+ data: {
196
+ sourceAgentId: currentAgentId,
197
+ agent: cloneJson(entry.agent || {}),
198
+ config: cloneJson(normalizeAgentConfig(entry.config || {})),
199
+ },
200
+ });
201
+ }
202
+
203
+ for (const entry of bundle.automations || []) {
204
+ const currentAutomationId = automationEntryId(entry);
205
+ if (!currentAutomationId) continue;
206
+ pushGraphElement(elements, {
207
+ id: graphElementId('automation', currentAutomationId),
208
+ type: 'automation',
209
+ label: entry.automation?.name || currentAutomationId,
210
+ data: cloneJson(entry),
211
+ });
212
+ }
213
+
214
+ for (const entry of bundle.apps || []) {
215
+ const currentAppId = appEntryId(entry);
216
+ if (!currentAppId) continue;
217
+ pushGraphElement(elements, {
218
+ id: graphElementId('app', currentAppId),
219
+ type: 'app',
220
+ label: entry.app?.name || currentAppId,
221
+ data: appElementData(entry),
222
+ });
223
+ }
224
+
225
+ for (const skill of skills) {
226
+ const elementId = skillElementId(skill);
227
+ if (!elementId) continue;
228
+ pushGraphElement(elements, {
229
+ id: elementId,
230
+ type: 'skill',
231
+ label: skillLabel(skill),
232
+ data: typeof skill === 'string' ? { value: skill } : cloneJson(skill),
233
+ });
234
+ }
235
+
236
+ const includedToolIds = new Set();
237
+ const includedActionIds = new Set();
238
+
239
+ for (const entry of bundle.automations || []) {
240
+ const currentAutomationId = automationEntryId(entry);
241
+ if (!currentAutomationId) continue;
242
+ for (const toolId of uniqueStrings(entry.toolIds)) {
243
+ includedToolIds.add(toolId);
244
+ pushGraphEdge(edges, {
245
+ from: graphElementId('automation', currentAutomationId),
246
+ to: graphElementId('tool', toolId),
247
+ kind: 'uses-tool',
248
+ });
249
+ }
250
+ }
251
+ // Tools that belong to an app render nested under it in the share sidebar;
252
+ // the canonical tool record still appears exactly once.
253
+ for (const entry of bundle.apps || []) {
254
+ const currentAppId = appEntryId(entry);
255
+ if (!currentAppId) continue;
256
+ for (const toolId of uniqueStrings(entry.toolIds)) {
257
+ includedToolIds.add(toolId);
258
+ pushGraphEdge(edges, {
259
+ from: graphElementId('app', currentAppId),
260
+ to: graphElementId('tool', toolId),
261
+ kind: 'uses-tool',
262
+ });
263
+ }
264
+ }
265
+ for (const entry of bundle.agents || []) {
266
+ const currentAgentId = agentEntryId(entry);
267
+ if (!currentAgentId) continue;
268
+ const currentConfig = normalizeAgentConfig(entry.config || {});
269
+
270
+ for (const subagent of currentConfig.subagents || []) {
271
+ if (!subagent?.agentId) continue;
272
+ pushGraphEdge(edges, {
273
+ from: graphElementId('agent', currentAgentId),
274
+ to: graphElementId('agent', subagent.agentId),
275
+ kind: 'uses-subagent',
276
+ });
277
+ }
278
+
279
+ for (const selectedId of uniqueStrings(currentConfig.loadout?.toolIds)) {
280
+ const selectedAction = toolActionMap.get(selectedId);
281
+ if (selectedAction) {
282
+ includedActionIds.add(selectedAction.id);
283
+ includedToolIds.add(selectedAction.toolId);
284
+ pushGraphEdge(edges, {
285
+ from: graphElementId('agent', currentAgentId),
286
+ to: graphElementId('tool_action', selectedAction.id),
287
+ kind: 'uses-tool-action',
288
+ });
289
+ continue;
290
+ }
291
+
292
+ includedToolIds.add(selectedId);
293
+ pushGraphEdge(edges, {
294
+ from: graphElementId('agent', currentAgentId),
295
+ to: graphElementId('tool', selectedId),
296
+ kind: 'uses-tool',
297
+ });
298
+ }
299
+
300
+ for (const skill of currentConfig.skills || []) {
301
+ const targetId = skillElementId(skill);
302
+ if (!targetId) continue;
303
+ pushGraphEdge(edges, {
304
+ from: graphElementId('agent', currentAgentId),
305
+ to: targetId,
306
+ kind: 'uses-skill',
307
+ });
308
+ }
309
+ }
310
+
311
+ // Every tool in bundle.tools gets an element — standalone tool heads are
312
+ // not referenced by any agent or automation edge but must still resolve.
313
+ const toolElementIds = new Set([...includedToolIds, ...toolMap.keys()]);
314
+ for (const toolId of toolElementIds) {
315
+ const tool = toolMap.get(toolId);
316
+ pushGraphElement(elements, {
317
+ id: graphElementId('tool', toolId),
318
+ type: 'tool',
319
+ label: tool?.name || toolId,
320
+ data: tool
321
+ ? cloneJson(tool)
322
+ : {
323
+ id: toolId,
324
+ name: toolId,
325
+ origin: systemToolIds.has(toolId) ? 'system' : missingToolIds.has(toolId) ? 'missing' : 'unknown',
326
+ },
327
+ });
328
+ }
329
+
330
+ for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
331
+ if (!toolElementIds.has(action.toolId)) continue;
332
+ includedActionIds.add(action.id);
333
+ pushGraphElement(elements, {
334
+ id: graphElementId('tool_action', action.id),
335
+ type: 'tool_action',
336
+ label: action.name || action.id,
337
+ data: cloneJson(action),
338
+ });
339
+ pushGraphEdge(edges, {
340
+ from: graphElementId('tool_action', action.id),
341
+ to: graphElementId('tool', action.toolId),
342
+ kind: 'action-of',
343
+ });
344
+ }
345
+
346
+ return {
347
+ headIds: bundleHeadIds(bundle, elements),
348
+ elements: [...elements.values()].sort(compareGraphElements),
349
+ edges: [...edges.values()].sort(compareGraphEdges),
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Heads are the user-facing roots of the bundle. Explicit bundle.heads win;
355
+ * otherwise fall back to the legacy single agent root plus every automation
356
+ * and app entry (those are always top-level).
357
+ */
358
+ function bundleHeadIds(bundle, elements) {
359
+ const headIds = [];
360
+ const explicitHeads = Array.isArray(bundle.heads) ? bundle.heads : [];
361
+ for (const head of explicitHeads) {
362
+ const type = cleanEntryId(head?.type);
363
+ const id = cleanEntryId(head?.id);
364
+ if (!type || !id) continue;
365
+ const elementId = graphElementId(type, id);
366
+ if (elements.has(elementId) && !headIds.includes(elementId)) headIds.push(elementId);
367
+ }
368
+ if (headIds.length > 0) return headIds;
369
+
370
+ const firstAgentId = agentEntryId(bundle.agents?.[0]);
371
+ const rootAgentId = cleanEntryId(bundle.rootAgentId) || firstAgentId;
372
+ if (rootAgentId && elements.has(graphElementId('agent', rootAgentId))) {
373
+ headIds.push(graphElementId('agent', rootAgentId));
374
+ }
375
+ for (const entry of bundle.automations || []) {
376
+ const id = automationEntryId(entry);
377
+ if (id) headIds.push(graphElementId('automation', id));
378
+ }
379
+ for (const entry of bundle.apps || []) {
380
+ const id = appEntryId(entry);
381
+ if (id) headIds.push(graphElementId('app', id));
382
+ }
383
+ return [...new Set(headIds)];
384
+ }
385
+
386
+ function assertClosedFlatBundleGraph(bundle) {
387
+ if (!Array.isArray(bundle.headIds) || bundle.headIds.length === 0) {
388
+ throw new Error('bundle graph must include at least one head');
389
+ }
390
+
391
+ const elementIds = new Set();
392
+ for (const element of Array.isArray(bundle.elements) ? bundle.elements : []) {
393
+ if (!isObject(element) || typeof element.id !== 'string' || !element.id.trim()) {
394
+ throw new Error('bundle graph elements must include non-empty ids');
395
+ }
396
+ if (elementIds.has(element.id)) {
397
+ throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
398
+ }
399
+ elementIds.add(element.id);
400
+ }
401
+
402
+ for (const headId of bundle.headIds) {
403
+ if (typeof headId !== 'string' || !elementIds.has(headId)) {
404
+ throw new Error(`bundle graph head is missing element: ${headId}`);
405
+ }
406
+ }
407
+
408
+ for (const edge of Array.isArray(bundle.edges) ? bundle.edges : []) {
409
+ if (!isObject(edge) || typeof edge.from !== 'string' || typeof edge.to !== 'string' || typeof edge.kind !== 'string') {
410
+ throw new Error('bundle graph edges must include from, to, and kind');
411
+ }
412
+ if (!elementIds.has(edge.from)) throw new Error(`bundle graph edge source is missing element: ${edge.from}`);
413
+ if (!elementIds.has(edge.to)) throw new Error(`bundle graph edge target is missing element: ${edge.to}`);
414
+ }
415
+ }
416
+
417
+ function withBundleGraph(bundle) {
418
+ const graph = buildFlatAgentBundleGraph(bundle);
419
+ const nextBundle = {
420
+ ...bundle,
421
+ headIds: graph.headIds,
422
+ elements: graph.elements,
423
+ edges: graph.edges,
424
+ };
425
+ assertClosedFlatBundleGraph(nextBundle);
426
+ return nextBundle;
427
+ }
428
+
429
+ module.exports = {
430
+ BUNDLE_KIND,
431
+ BUNDLE_SCHEMA_VERSION,
432
+ LEGACY_AGENT_BUNDLE_KIND,
433
+ addUnique,
434
+ agentEntryId,
435
+ appEntryId,
436
+ assertClosedFlatBundleGraph,
437
+ automationEntryId,
438
+ buildFlatAgentBundleGraph,
439
+ cleanEntryId,
440
+ cloneJson,
441
+ collectSkillRecords,
442
+ isObject,
443
+ nowIso,
444
+ standaloneToolIds,
445
+ uniqueStrings,
446
+ withBundleGraph,
447
+ };
@@ -0,0 +1,241 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const { upsertAgentConfig, agentFieldsFromConfig } = require('../agent-config/store');
6
+ const { normalizeAgentConfig } = require('../agent-config/schema');
7
+ const { createAgent, getAllAgentsWithBuiltins } = require('../agents/store');
8
+ const { defaultToolboxService } = require('../toolbox/service');
9
+ const { installAutomationEntry } = require('./automation-entries');
10
+ const { installAppEntry } = require('./app-entries');
11
+ const { rewriteAppBoundPaths } = require('./tool-bindings');
12
+ const {
13
+ BUNDLE_KIND,
14
+ BUNDLE_SCHEMA_VERSION,
15
+ LEGACY_AGENT_BUNDLE_KIND,
16
+ agentEntryId,
17
+ appEntryId,
18
+ isObject,
19
+ nowIso,
20
+ standaloneToolIds,
21
+ uniqueStrings,
22
+ withBundleGraph,
23
+ } = require('./graph');
24
+
25
+ function uniqueAgentName(name) {
26
+ const base = String(name || 'Imported agent').trim() || 'Imported agent';
27
+ const existing = new Set(
28
+ getAllAgentsWithBuiltins().map((agent) => String(agent.name || '').trim().toLowerCase()),
29
+ );
30
+ if (!existing.has(base.toLowerCase())) return base;
31
+ const imported = `${base} (imported)`;
32
+ if (!existing.has(imported.toLowerCase())) return imported;
33
+ for (let index = 2; index < 1000; index += 1) {
34
+ const candidate = `${base} (imported ${index})`;
35
+ if (!existing.has(candidate.toLowerCase())) return candidate;
36
+ }
37
+ return `${base} (${crypto.randomUUID().slice(0, 8)})`;
38
+ }
39
+
40
+ function externalMcpToolIdsForLoadout(toolIds, tools) {
41
+ const toolMap = new Map((Array.isArray(tools) ? tools : []).map((tool) => [tool.id, tool]));
42
+ return uniqueStrings(toolIds)
43
+ .map((toolId) => toolMap.get(toolId))
44
+ .filter((tool) => tool?.type === 'mcp' && tool?.origin !== 'system')
45
+ .map((tool) => tool.id);
46
+ }
47
+
48
+ function referencedSubagentIds(entry) {
49
+ if (!isObject(entry)) return [];
50
+ const subagents = Array.isArray(entry.config?.subagents) ? entry.config.subagents : [];
51
+ return subagents
52
+ .map((subagent) => (typeof subagent?.agentId === 'string' ? subagent.agentId.trim() : ''))
53
+ .filter(Boolean);
54
+ }
55
+
56
+ function assertClosedAgentBundleGraph(bundle) {
57
+ const agentIds = new Set(bundle.agents.map(agentEntryId).filter(Boolean));
58
+ const missing = new Set();
59
+ for (const entry of bundle.agents) {
60
+ for (const agentId of referencedSubagentIds(entry)) {
61
+ if (!agentIds.has(agentId)) missing.add(agentId);
62
+ }
63
+ }
64
+
65
+ if (missing.size > 0) {
66
+ throw new Error(`Bundle is missing shared subagents: ${Array.from(missing).join(', ')}`);
67
+ }
68
+ }
69
+
70
+ function validateBundle(bundle) {
71
+ if (!isObject(bundle)) throw new Error('bundle must be an object');
72
+ if (bundle.kind !== BUNDLE_KIND && bundle.kind !== LEGACY_AGENT_BUNDLE_KIND) {
73
+ throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
74
+ }
75
+ if (bundle.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
76
+ throw new Error(`Unsupported bundle schema version: ${bundle.schemaVersion || 'unknown'}`);
77
+ }
78
+ const agents = Array.isArray(bundle.agents) ? bundle.agents : [];
79
+ const automations = Array.isArray(bundle.automations) ? bundle.automations : [];
80
+ const apps = Array.isArray(bundle.apps) ? bundle.apps : [];
81
+ const toolHeads = standaloneToolIds(bundle);
82
+ if (agents.length === 0 && automations.length === 0 && apps.length === 0 && toolHeads.length === 0) {
83
+ throw new Error('bundle must include at least one agent, automation, app, or tool');
84
+ }
85
+ if (agents.length > 0) assertClosedAgentBundleGraph(bundle);
86
+ return withBundleGraph(bundle);
87
+ }
88
+
89
+ function installTools(bundle, appDirBySourceId = new Map(), appIdBySourceId = new Map()) {
90
+ const warnings = [];
91
+ const rewrite = (record) => {
92
+ const result = rewriteAppBoundPaths(record, appDirBySourceId);
93
+ warnings.push(...result.warnings);
94
+ return result.record;
95
+ };
96
+
97
+ const toolActionsByToolId = new Map();
98
+ for (const action of Array.isArray(bundle.toolActions) ? bundle.toolActions : []) {
99
+ if (!action?.toolId) continue;
100
+ const existing = toolActionsByToolId.get(action.toolId) || [];
101
+ existing.push(rewrite(action));
102
+ toolActionsByToolId.set(action.toolId, existing);
103
+ }
104
+
105
+ const installed = [];
106
+ for (const tool of Array.isArray(bundle.tools) ? bundle.tools : []) {
107
+ if (!tool?.id || tool.origin === 'system') continue;
108
+ const record = rewrite(tool);
109
+ // The app link travels by source app id; point it at the recipient's
110
+ // installed copy, or drop it if that app was not part of the install.
111
+ if (typeof record.appId === 'string' && record.appId) {
112
+ const installedAppId = appIdBySourceId.get(record.appId);
113
+ if (installedAppId) {
114
+ record.appId = installedAppId;
115
+ } else {
116
+ delete record.appId;
117
+ warnings.push(`Tool ${tool.id} belongs to an app that was not installed with this bundle; the app link was removed.`);
118
+ }
119
+ }
120
+ const result = defaultToolboxService.registerTool({
121
+ ...record,
122
+ origin: tool.origin === 'catalog' ? 'catalog' : 'user',
123
+ actions: toolActionsByToolId.get(tool.id) || [],
124
+ });
125
+ installed.push(result.tool);
126
+ }
127
+ return { installed, warnings };
128
+ }
129
+
130
+ function installBundleAgents(bundle, options = {}) {
131
+ const idMap = new Map();
132
+ const createdAgents = [];
133
+ const createdAt = nowIso();
134
+
135
+ for (const entry of Array.isArray(bundle.agents) ? bundle.agents : []) {
136
+ const config = normalizeAgentConfig(entry.config || {});
137
+ const fields = agentFieldsFromConfig(config);
138
+ const agent = createAgent({
139
+ name: uniqueAgentName(entry.agent?.name),
140
+ description: entry.agent?.description || '',
141
+ baseHarnessId: entry.agent?.baseHarnessId || 'codex',
142
+ baseModelId: entry.agent?.baseModelId || '',
143
+ modelSettings: isObject(entry.agent?.modelSettings) ? entry.agent.modelSettings : null,
144
+ systemPrompt: fields.systemPrompt,
145
+ skills: fields.skills,
146
+ loadout: config.loadout || { toolIds: [] },
147
+ mcpAppIds: externalMcpToolIdsForLoadout(config.loadout?.toolIds || [], bundle.tools),
148
+ authMethod: options.authMethod || 'amalgm',
149
+ status: 'ready',
150
+ installStatus: 'external',
151
+ createdAt,
152
+ }, { source: 'agent-bundles:install' });
153
+ idMap.set(entry.sourceAgentId || entry.agent?.sourceAgentId, agent.id);
154
+ createdAgents.push({ sourceAgentId: entry.sourceAgentId, agent, config });
155
+ }
156
+
157
+ const installedAgents = [];
158
+ for (const created of createdAgents) {
159
+ const rewrittenConfig = normalizeAgentConfig({
160
+ ...created.config,
161
+ agentId: created.agent.id,
162
+ runtimeHomeLayout: 'per-agent',
163
+ subagents: (created.config.subagents || [])
164
+ .map((subagent) => {
165
+ const mappedAgentId = idMap.get(subagent.agentId);
166
+ if (!mappedAgentId) return null;
167
+ return {
168
+ ...subagent,
169
+ id: `subagent-${mappedAgentId}`,
170
+ agentId: mappedAgentId,
171
+ };
172
+ })
173
+ .filter(Boolean),
174
+ });
175
+ const savedConfig = upsertAgentConfig(created.agent.id, rewrittenConfig, {
176
+ source: 'agent-bundles:install',
177
+ });
178
+ installedAgents.push({
179
+ ...created.agent,
180
+ ...agentFieldsFromConfig(savedConfig),
181
+ config: savedConfig,
182
+ });
183
+ }
184
+
185
+ return { idMap, installedAgents };
186
+ }
187
+
188
+ function defaultAppsInstallRoot() {
189
+ // Installed apps are Amalgm-owned state: they live under the user's apps
190
+ // primitive root (<user dir>/apps), not in the projects workspace.
191
+ return require('../config').APPS_DIR;
192
+ }
193
+
194
+ /**
195
+ * Install every primitive in a validated bundle. Apps first — they
196
+ * materialize into the user's apps root, build, and start, and their
197
+ * installed directories anchor the app-bound tool path rewrites. Then tools
198
+ * (agents and automations reference them), agents, and automations.
199
+ */
200
+ async function installBundle(input, options = {}) {
201
+ const bundle = validateBundle(input);
202
+
203
+ const installedApps = [];
204
+ const appDirBySourceId = new Map();
205
+ const appIdBySourceId = new Map();
206
+ const appEntries = Array.isArray(bundle.apps) ? bundle.apps : [];
207
+ if (appEntries.length > 0) {
208
+ const rootDir = options.appsRootDir || defaultAppsInstallRoot();
209
+ for (const entry of appEntries) {
210
+ const installed = await installAppEntry(entry, { rootDir, register: options.registerApp });
211
+ appDirBySourceId.set(appEntryId(entry), installed.cwd);
212
+ if (installed.id) appIdBySourceId.set(appEntryId(entry), installed.id);
213
+ installedApps.push(installed);
214
+ }
215
+ }
216
+
217
+ const { installed: installedTools, warnings: bindingWarnings } = installTools(bundle, appDirBySourceId, appIdBySourceId);
218
+ const { idMap, installedAgents } = installBundleAgents(bundle, options);
219
+
220
+ const installedAutomations = [];
221
+ for (const entry of Array.isArray(bundle.automations) ? bundle.automations : []) {
222
+ installedAutomations.push(installAutomationEntry(entry, { source: 'agent-bundles:install' }));
223
+ }
224
+
225
+ return {
226
+ ok: true,
227
+ bundleId: bundle.bundleId,
228
+ rootAgentId: idMap.get(bundle.rootAgentId) || installedAgents[0]?.id || null,
229
+ idMap: Object.fromEntries(idMap.entries()),
230
+ installedAgents,
231
+ installedTools,
232
+ installedAutomations,
233
+ installedApps,
234
+ warnings: [...(bundle.warnings || []), ...bindingWarnings],
235
+ };
236
+ }
237
+
238
+ module.exports = {
239
+ installBundle,
240
+ validateBundle,
241
+ };
@@ -163,6 +163,7 @@ function getConnectedRoutes() {
163
163
  && app.desiredState === 'running'
164
164
  && app.status !== 'stopped'
165
165
  && app.status !== 'error'
166
+ && app.status !== 'degraded'
166
167
  && app.port
167
168
  && app.appRef,
168
169
  )