amalgm 0.1.154 → 0.1.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +1 -2
- package/runtime/scripts/amalgm-mcp/agent-bundles/export.js +46 -38
- package/runtime/scripts/amalgm-mcp/agent-bundles/graph.js +232 -261
- package/runtime/scripts/amalgm-mcp/agent-bundles/install.js +7 -12
- package/runtime/scripts/amalgm-mcp/browser/auth-bundles.js +12 -1
- package/runtime/scripts/amalgm-mcp/browser/auth-tools.js +8 -9
- package/runtime/scripts/amalgm-mcp/browser/cli.js +3 -2
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +364 -0
- package/runtime/scripts/amalgm-mcp/browser/engine.js +120 -42
- package/runtime/scripts/amalgm-mcp/browser/profiles.js +124 -0
- package/runtime/scripts/amalgm-mcp/browser/rest.js +47 -7
- package/runtime/scripts/amalgm-mcp/browser/sessions.js +10 -7
- package/runtime/scripts/amalgm-mcp/browser/store.js +1 -0
- package/runtime/scripts/amalgm-mcp/index.js +13 -0
- package/runtime/scripts/amalgm-mcp/server/routes/browser.js +10 -0
- package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +22 -12
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-jar.test.js +152 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-source.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-profile-lifecycle.test.js +78 -0
- package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +82 -15
|
@@ -4,12 +4,9 @@ const crypto = require('crypto');
|
|
|
4
4
|
|
|
5
5
|
const { normalizeAgentConfig } = require('../agent-config/schema');
|
|
6
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
7
|
const BUNDLE_KIND = 'amalgm.bundle';
|
|
11
|
-
const
|
|
12
|
-
const
|
|
8
|
+
const BUNDLE_SCHEMA_VERSION = 2;
|
|
9
|
+
const HEAD_TYPES = new Set(['agent', 'automation', 'app', 'tool']);
|
|
13
10
|
const GRAPH_ELEMENT_ORDER = {
|
|
14
11
|
agent: 0,
|
|
15
12
|
automation: 1,
|
|
@@ -41,8 +38,7 @@ function uniqueStrings(values) {
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
function addUnique(target, value) {
|
|
44
|
-
if (!value)
|
|
45
|
-
if (!target.includes(value)) target.push(value);
|
|
41
|
+
if (value && !target.includes(value)) target.push(value);
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
function stableDigest(value) {
|
|
@@ -62,12 +58,12 @@ function skillFingerprint(skill) {
|
|
|
62
58
|
return value ? `string:${value}` : '';
|
|
63
59
|
}
|
|
64
60
|
if (!isObject(skill)) return '';
|
|
65
|
-
const id =
|
|
61
|
+
const id = cleanEntryId(skill.id);
|
|
66
62
|
if (id) return `id:${id}`;
|
|
67
|
-
const path =
|
|
63
|
+
const path = cleanEntryId(skill.path);
|
|
68
64
|
if (path) return `path:${path}`;
|
|
69
|
-
const name =
|
|
70
|
-
const content =
|
|
65
|
+
const name = cleanEntryId(skill.name);
|
|
66
|
+
const content = cleanEntryId(skill.content);
|
|
71
67
|
if (name && content) return `name-content:${name}:${content}`;
|
|
72
68
|
if (name) return `name:${name}`;
|
|
73
69
|
if (content) return `content:${content}`;
|
|
@@ -82,20 +78,18 @@ function skillElementId(skill) {
|
|
|
82
78
|
function skillLabel(skill) {
|
|
83
79
|
if (typeof skill === 'string') return skill.trim() || 'Skill';
|
|
84
80
|
if (!isObject(skill)) return 'Skill';
|
|
85
|
-
const
|
|
86
|
-
const name = typeof skill.name === 'string' ? skill.name.trim() : '';
|
|
87
|
-
const path = typeof skill.path === 'string' ? skill.path.trim() : '';
|
|
81
|
+
const name = cleanEntryId(skill.name);
|
|
88
82
|
if (name) return name;
|
|
83
|
+
const path = cleanEntryId(skill.path);
|
|
89
84
|
if (path) return path.split('/').filter(Boolean).pop() || path;
|
|
85
|
+
const id = cleanEntryId(skill.id);
|
|
90
86
|
if (id) return id;
|
|
91
|
-
|
|
92
|
-
return content.slice(0, 60) || 'Skill';
|
|
87
|
+
return cleanEntryId(skill.content).slice(0, 60) || 'Skill';
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
function compareGraphElements(left, right) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return String(left.id).localeCompare(String(right.id));
|
|
91
|
+
return (GRAPH_ELEMENT_ORDER[left.type] ?? 99) - (GRAPH_ELEMENT_ORDER[right.type] ?? 99)
|
|
92
|
+
|| String(left.id).localeCompare(String(right.id));
|
|
99
93
|
}
|
|
100
94
|
|
|
101
95
|
function compareGraphEdges(left, right) {
|
|
@@ -104,38 +98,13 @@ function compareGraphEdges(left, right) {
|
|
|
104
98
|
|| String(left.to).localeCompare(String(right.to));
|
|
105
99
|
}
|
|
106
100
|
|
|
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
101
|
function cleanEntryId(value) {
|
|
131
102
|
return typeof value === 'string' ? value.trim() : '';
|
|
132
103
|
}
|
|
133
104
|
|
|
134
105
|
function agentEntryId(entry) {
|
|
135
106
|
if (!isObject(entry)) return '';
|
|
136
|
-
|
|
137
|
-
if (sourceAgentId) return sourceAgentId;
|
|
138
|
-
return typeof entry.agent?.sourceAgentId === 'string' ? entry.agent.sourceAgentId.trim() : '';
|
|
107
|
+
return cleanEntryId(entry.sourceAgentId) || cleanEntryId(entry.agent?.sourceAgentId);
|
|
139
108
|
}
|
|
140
109
|
|
|
141
110
|
function automationEntryId(entry) {
|
|
@@ -146,294 +115,294 @@ function appEntryId(entry) {
|
|
|
146
115
|
return isObject(entry) ? cleanEntryId(entry.sourceAppId) : '';
|
|
147
116
|
}
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
118
|
+
function toolEntryId(entry) {
|
|
119
|
+
return isObject(entry) ? cleanEntryId(entry.id) : '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function actionEntryId(entry) {
|
|
123
|
+
return isObject(entry) ? cleanEntryId(entry.id) : '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectSkillRecords(bundleAgents) {
|
|
127
|
+
const byKey = new Map();
|
|
128
|
+
for (const entry of bundleAgents) {
|
|
129
|
+
const config = normalizeAgentConfig(entry?.config || {});
|
|
130
|
+
for (const skill of config.skills || []) {
|
|
131
|
+
const key = skillFingerprint(skill);
|
|
132
|
+
if (key && !byKey.has(key)) byKey.set(key, cloneJson(skill));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...byKey.values()];
|
|
136
|
+
}
|
|
137
|
+
|
|
153
138
|
function standaloneToolIds(bundle) {
|
|
154
|
-
|
|
155
|
-
return uniqueStrings(heads
|
|
139
|
+
return uniqueStrings((Array.isArray(bundle?.heads) ? bundle.heads : [])
|
|
156
140
|
.filter((head) => isObject(head) && cleanEntryId(head.type) === 'tool')
|
|
157
141
|
.map((head) => cleanEntryId(head.id)));
|
|
158
142
|
}
|
|
159
143
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
144
|
+
function assertUniqueRecords(label, records, idForRecord) {
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (const record of records) {
|
|
147
|
+
const id = idForRecord(record);
|
|
148
|
+
if (!id) throw new Error(`${label} entries must include ids`);
|
|
149
|
+
if (seen.has(id)) throw new Error(`Bundle contains duplicate ${label} id: ${id}`);
|
|
150
|
+
seen.add(id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function assertCanonicalBundle(bundle) {
|
|
155
|
+
if (!isObject(bundle)) throw new Error('bundle must be an object');
|
|
156
|
+
if (bundle.kind !== BUNDLE_KIND) {
|
|
157
|
+
throw new Error(`Unsupported bundle kind: ${bundle.kind || 'unknown'}`);
|
|
158
|
+
}
|
|
159
|
+
if (bundle.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
|
|
160
|
+
throw new Error(`Unsupported bundle schema version: ${bundle.schemaVersion || 'unknown'}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const agents = Array.isArray(bundle.agents) ? bundle.agents : [];
|
|
164
|
+
const automations = Array.isArray(bundle.automations) ? bundle.automations : [];
|
|
165
|
+
const apps = Array.isArray(bundle.apps) ? bundle.apps : [];
|
|
166
|
+
const tools = Array.isArray(bundle.tools) ? bundle.tools : [];
|
|
167
|
+
const actions = Array.isArray(bundle.toolActions) ? bundle.toolActions : [];
|
|
168
|
+
const skills = Array.isArray(bundle.skills) ? bundle.skills : [];
|
|
169
|
+
const heads = Array.isArray(bundle.heads) ? bundle.heads : [];
|
|
170
|
+
|
|
171
|
+
assertUniqueRecords('agent', agents, agentEntryId);
|
|
172
|
+
assertUniqueRecords('automation', automations, automationEntryId);
|
|
173
|
+
assertUniqueRecords('app', apps, appEntryId);
|
|
174
|
+
assertUniqueRecords('tool', tools, toolEntryId);
|
|
175
|
+
assertUniqueRecords('tool action', actions, actionEntryId);
|
|
176
|
+
assertUniqueRecords('skill', skills, skillFingerprint);
|
|
177
|
+
assertUniqueRecords('head', heads, (head) => {
|
|
178
|
+
const type = cleanEntryId(head?.type);
|
|
179
|
+
const id = cleanEntryId(head?.id);
|
|
180
|
+
if (!HEAD_TYPES.has(type)) return '';
|
|
181
|
+
return `${type}:${id}`;
|
|
182
|
+
});
|
|
183
|
+
if (heads.length === 0) throw new Error('bundle must include at least one head');
|
|
184
|
+
|
|
185
|
+
const missingTools = uniqueStrings(bundle.requires?.missingTools);
|
|
186
|
+
if (missingTools.length > 0) {
|
|
187
|
+
throw new Error(`Bundle is missing tools: ${missingTools.join(', ')}`);
|
|
188
|
+
}
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
function buildFlatAgentBundleGraph(bundle) {
|
|
192
|
+
assertCanonicalBundle(bundle);
|
|
193
|
+
|
|
194
|
+
const agents = bundle.agents || [];
|
|
195
|
+
const automations = bundle.automations || [];
|
|
196
|
+
const apps = bundle.apps || [];
|
|
197
|
+
const tools = bundle.tools || [];
|
|
198
|
+
const actions = bundle.toolActions || [];
|
|
199
|
+
const skills = bundle.skills || [];
|
|
181
200
|
const elements = new Map();
|
|
182
201
|
const edges = new Map();
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (!currentAgentId) continue;
|
|
192
|
-
pushGraphElement(elements, {
|
|
193
|
-
id: graphElementId('agent', currentAgentId),
|
|
194
|
-
type: 'agent',
|
|
195
|
-
label: entry.agent?.name || currentAgentId,
|
|
196
|
-
data: {
|
|
197
|
-
sourceAgentId: currentAgentId,
|
|
198
|
-
agent: cloneJson(entry.agent || {}),
|
|
199
|
-
config: cloneJson(normalizeAgentConfig(entry.config || {})),
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
}
|
|
202
|
+
const records = {
|
|
203
|
+
agent: new Map(agents.map((entry) => [agentEntryId(entry), entry])),
|
|
204
|
+
automation: new Map(automations.map((entry) => [automationEntryId(entry), entry])),
|
|
205
|
+
app: new Map(apps.map((entry) => [appEntryId(entry), entry])),
|
|
206
|
+
tool: new Map(tools.map((entry) => [toolEntryId(entry), entry])),
|
|
207
|
+
tool_action: new Map(actions.map((entry) => [actionEntryId(entry), entry])),
|
|
208
|
+
skill: new Map(skills.map((entry) => [skillElementId(entry), entry])),
|
|
209
|
+
};
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
}
|
|
211
|
+
const addElement = (type, id, label) => {
|
|
212
|
+
const elementId = graphElementId(type, id);
|
|
213
|
+
elements.set(elementId, { id: elementId, type, label: label || id });
|
|
214
|
+
};
|
|
215
|
+
const addEdge = (fromType, fromId, toType, toId, kind) => {
|
|
216
|
+
const from = graphElementId(fromType, fromId);
|
|
217
|
+
const to = graphElementId(toType, toId);
|
|
218
|
+
const key = `${from}::${kind}::${to}`;
|
|
219
|
+
edges.set(key, { from, to, kind });
|
|
220
|
+
};
|
|
221
|
+
const requireRecord = (type, id, owner) => {
|
|
222
|
+
if (!records[type].has(id)) {
|
|
223
|
+
throw new Error(`${owner} references missing ${type.replace('_', ' ')}: ${id}`);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const addToolSelection = (ownerType, ownerId, selectedId) => {
|
|
227
|
+
if (records.tool_action.has(selectedId)) {
|
|
228
|
+
addEdge(ownerType, ownerId, 'tool_action', selectedId, 'uses-tool-action');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
requireRecord('tool', selectedId, `${ownerType} ${ownerId}`);
|
|
232
|
+
addEdge(ownerType, ownerId, 'tool', selectedId, 'uses-tool');
|
|
233
|
+
};
|
|
214
234
|
|
|
215
|
-
for (const entry of
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
pushGraphElement(elements, {
|
|
219
|
-
id: graphElementId('app', currentAppId),
|
|
220
|
-
type: 'app',
|
|
221
|
-
label: entry.app?.name || currentAppId,
|
|
222
|
-
data: appElementData(entry),
|
|
223
|
-
});
|
|
235
|
+
for (const entry of agents) {
|
|
236
|
+
const id = agentEntryId(entry);
|
|
237
|
+
addElement('agent', id, entry.agent?.name);
|
|
224
238
|
}
|
|
225
|
-
|
|
239
|
+
for (const entry of automations) {
|
|
240
|
+
const id = automationEntryId(entry);
|
|
241
|
+
addElement('automation', id, entry.automation?.name);
|
|
242
|
+
}
|
|
243
|
+
for (const entry of apps) {
|
|
244
|
+
const id = appEntryId(entry);
|
|
245
|
+
addElement('app', id, entry.app?.name);
|
|
246
|
+
}
|
|
247
|
+
for (const tool of tools) addElement('tool', toolEntryId(tool), tool.name);
|
|
248
|
+
for (const action of actions) addElement('tool_action', actionEntryId(action), action.name);
|
|
226
249
|
for (const skill of skills) {
|
|
227
250
|
const elementId = skillElementId(skill);
|
|
228
|
-
|
|
229
|
-
pushGraphElement(elements, {
|
|
230
|
-
id: elementId,
|
|
231
|
-
type: 'skill',
|
|
232
|
-
label: skillLabel(skill),
|
|
233
|
-
data: typeof skill === 'string' ? { value: skill } : cloneJson(skill),
|
|
234
|
-
});
|
|
251
|
+
elements.set(elementId, { id: elementId, type: 'skill', label: skillLabel(skill) });
|
|
235
252
|
}
|
|
236
253
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
pushGraphEdge(edges, {
|
|
246
|
-
from: graphElementId('automation', currentAutomationId),
|
|
247
|
-
to: graphElementId('tool', toolId),
|
|
248
|
-
kind: 'uses-tool',
|
|
249
|
-
});
|
|
254
|
+
for (const entry of agents) {
|
|
255
|
+
const id = agentEntryId(entry);
|
|
256
|
+
const config = normalizeAgentConfig(entry.config || {});
|
|
257
|
+
for (const subagent of config.subagents || []) {
|
|
258
|
+
const subagentId = cleanEntryId(subagent?.agentId);
|
|
259
|
+
if (!subagentId) continue;
|
|
260
|
+
requireRecord('agent', subagentId, `agent ${id}`);
|
|
261
|
+
addEdge('agent', id, 'agent', subagentId, 'uses-subagent');
|
|
250
262
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// the canonical tool record still appears exactly once.
|
|
254
|
-
for (const entry of bundle.apps || []) {
|
|
255
|
-
const currentAppId = appEntryId(entry);
|
|
256
|
-
if (!currentAppId) continue;
|
|
257
|
-
for (const toolId of uniqueStrings(entry.toolIds)) {
|
|
258
|
-
includedToolIds.add(toolId);
|
|
259
|
-
pushGraphEdge(edges, {
|
|
260
|
-
from: graphElementId('app', currentAppId),
|
|
261
|
-
to: graphElementId('tool', toolId),
|
|
262
|
-
kind: 'uses-tool',
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
for (const entry of bundle.agents || []) {
|
|
267
|
-
const currentAgentId = agentEntryId(entry);
|
|
268
|
-
if (!currentAgentId) continue;
|
|
269
|
-
const currentConfig = normalizeAgentConfig(entry.config || {});
|
|
270
|
-
|
|
271
|
-
for (const subagent of currentConfig.subagents || []) {
|
|
272
|
-
if (!subagent?.agentId) continue;
|
|
273
|
-
pushGraphEdge(edges, {
|
|
274
|
-
from: graphElementId('agent', currentAgentId),
|
|
275
|
-
to: graphElementId('agent', subagent.agentId),
|
|
276
|
-
kind: 'uses-subagent',
|
|
277
|
-
});
|
|
263
|
+
for (const selectedId of uniqueStrings(config.loadout?.toolIds)) {
|
|
264
|
+
addToolSelection('agent', id, selectedId);
|
|
278
265
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
includedActionIds.add(selectedAction.id);
|
|
284
|
-
includedToolIds.add(selectedAction.toolId);
|
|
285
|
-
pushGraphEdge(edges, {
|
|
286
|
-
from: graphElementId('agent', currentAgentId),
|
|
287
|
-
to: graphElementId('tool_action', selectedAction.id),
|
|
288
|
-
kind: 'uses-tool-action',
|
|
289
|
-
});
|
|
290
|
-
continue;
|
|
266
|
+
for (const skill of config.skills || []) {
|
|
267
|
+
const elementId = skillElementId(skill);
|
|
268
|
+
if (!records.skill.has(elementId)) {
|
|
269
|
+
throw new Error(`agent ${id} references missing skill: ${skillLabel(skill)}`);
|
|
291
270
|
}
|
|
271
|
+
const skillId = elementId.slice('skill:'.length);
|
|
272
|
+
addEdge('agent', id, 'skill', skillId, 'uses-skill');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
292
275
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
kind: 'uses-tool',
|
|
298
|
-
});
|
|
276
|
+
for (const entry of automations) {
|
|
277
|
+
const id = automationEntryId(entry);
|
|
278
|
+
for (const selectedId of uniqueStrings(entry.toolIds)) {
|
|
279
|
+
addToolSelection('automation', id, selectedId);
|
|
299
280
|
}
|
|
281
|
+
}
|
|
300
282
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
to: targetId,
|
|
307
|
-
kind: 'uses-skill',
|
|
308
|
-
});
|
|
283
|
+
for (const entry of apps) {
|
|
284
|
+
const id = appEntryId(entry);
|
|
285
|
+
for (const toolId of uniqueStrings(entry.toolIds)) {
|
|
286
|
+
requireRecord('tool', toolId, `app ${id}`);
|
|
287
|
+
addEdge('app', id, 'tool', toolId, 'provides-tool');
|
|
309
288
|
}
|
|
310
289
|
}
|
|
311
290
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
type: 'tool',
|
|
320
|
-
label: tool?.name || toolId,
|
|
321
|
-
data: tool
|
|
322
|
-
? cloneJson(tool)
|
|
323
|
-
: {
|
|
324
|
-
id: toolId,
|
|
325
|
-
name: toolId,
|
|
326
|
-
origin: systemToolIds.has(toolId) ? 'system' : missingToolIds.has(toolId) ? 'missing' : 'unknown',
|
|
327
|
-
},
|
|
328
|
-
});
|
|
291
|
+
for (const tool of tools) {
|
|
292
|
+
const toolId = toolEntryId(tool);
|
|
293
|
+
const appId = cleanEntryId(tool.appId);
|
|
294
|
+
if (appId) {
|
|
295
|
+
requireRecord('app', appId, `tool ${toolId}`);
|
|
296
|
+
addEdge('tool', toolId, 'app', appId, 'requires-app');
|
|
297
|
+
}
|
|
329
298
|
}
|
|
330
299
|
|
|
331
|
-
for (const action of
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
to: graphElementId('tool', action.toolId),
|
|
343
|
-
kind: 'action-of',
|
|
344
|
-
});
|
|
300
|
+
for (const action of actions) {
|
|
301
|
+
const actionId = actionEntryId(action);
|
|
302
|
+
const toolId = cleanEntryId(action.toolId);
|
|
303
|
+
requireRecord('tool', toolId, `tool action ${actionId}`);
|
|
304
|
+
addEdge('tool', toolId, 'tool_action', actionId, 'has-action');
|
|
305
|
+
addEdge('tool_action', actionId, 'tool', toolId, 'action-of');
|
|
306
|
+
const appId = cleanEntryId(action.appId);
|
|
307
|
+
if (appId) {
|
|
308
|
+
requireRecord('app', appId, `tool action ${actionId}`);
|
|
309
|
+
addEdge('tool_action', actionId, 'app', appId, 'requires-app');
|
|
310
|
+
}
|
|
345
311
|
}
|
|
346
312
|
|
|
313
|
+
const headIds = bundle.heads.map((head) => {
|
|
314
|
+
const elementId = graphElementId(cleanEntryId(head.type), cleanEntryId(head.id));
|
|
315
|
+
if (!elements.has(elementId)) throw new Error(`bundle head references missing element: ${elementId}`);
|
|
316
|
+
return elementId;
|
|
317
|
+
});
|
|
318
|
+
|
|
347
319
|
return {
|
|
348
|
-
headIds
|
|
320
|
+
headIds,
|
|
349
321
|
elements: [...elements.values()].sort(compareGraphElements),
|
|
350
322
|
edges: [...edges.values()].sort(compareGraphEdges),
|
|
351
323
|
};
|
|
352
324
|
}
|
|
353
325
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const headIds = [];
|
|
361
|
-
const explicitHeads = Array.isArray(bundle.heads) ? bundle.heads : [];
|
|
362
|
-
for (const head of explicitHeads) {
|
|
363
|
-
const type = cleanEntryId(head?.type);
|
|
364
|
-
const id = cleanEntryId(head?.id);
|
|
365
|
-
if (!type || !id) continue;
|
|
366
|
-
const elementId = graphElementId(type, id);
|
|
367
|
-
if (elements.has(elementId) && !headIds.includes(elementId)) headIds.push(elementId);
|
|
326
|
+
function reachableElementIds(bundle) {
|
|
327
|
+
const outgoing = new Map();
|
|
328
|
+
for (const edge of bundle.edges || []) {
|
|
329
|
+
const targets = outgoing.get(edge.from) || [];
|
|
330
|
+
targets.push(edge.to);
|
|
331
|
+
outgoing.set(edge.from, targets);
|
|
368
332
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
for (const entry of bundle.automations || []) {
|
|
377
|
-
const id = automationEntryId(entry);
|
|
378
|
-
if (id) headIds.push(graphElementId('automation', id));
|
|
379
|
-
}
|
|
380
|
-
for (const entry of bundle.apps || []) {
|
|
381
|
-
const id = appEntryId(entry);
|
|
382
|
-
if (id) headIds.push(graphElementId('app', id));
|
|
333
|
+
const reachable = new Set();
|
|
334
|
+
const pending = [...(bundle.headIds || [])];
|
|
335
|
+
while (pending.length > 0) {
|
|
336
|
+
const id = pending.pop();
|
|
337
|
+
if (reachable.has(id)) continue;
|
|
338
|
+
reachable.add(id);
|
|
339
|
+
pending.push(...(outgoing.get(id) || []));
|
|
383
340
|
}
|
|
384
|
-
return
|
|
341
|
+
return reachable;
|
|
385
342
|
}
|
|
386
343
|
|
|
387
344
|
function assertClosedFlatBundleGraph(bundle) {
|
|
388
345
|
if (!Array.isArray(bundle.headIds) || bundle.headIds.length === 0) {
|
|
389
346
|
throw new Error('bundle graph must include at least one head');
|
|
390
347
|
}
|
|
391
|
-
|
|
392
348
|
const elementIds = new Set();
|
|
393
349
|
for (const element of Array.isArray(bundle.elements) ? bundle.elements : []) {
|
|
394
|
-
if (!isObject(element) ||
|
|
395
|
-
throw new Error('bundle graph elements must include
|
|
350
|
+
if (!isObject(element) || !cleanEntryId(element.id) || !cleanEntryId(element.type)) {
|
|
351
|
+
throw new Error('bundle graph elements must include id and type');
|
|
396
352
|
}
|
|
397
|
-
if (
|
|
398
|
-
throw new Error(`bundle graph
|
|
353
|
+
if (Object.prototype.hasOwnProperty.call(element, 'data')) {
|
|
354
|
+
throw new Error(`bundle graph element must not copy canonical payload data: ${element.id}`);
|
|
399
355
|
}
|
|
356
|
+
if (elementIds.has(element.id)) throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
|
|
400
357
|
elementIds.add(element.id);
|
|
401
358
|
}
|
|
402
|
-
|
|
403
359
|
for (const headId of bundle.headIds) {
|
|
404
|
-
if (
|
|
405
|
-
throw new Error(`bundle graph head is missing element: ${headId}`);
|
|
406
|
-
}
|
|
360
|
+
if (!elementIds.has(headId)) throw new Error(`bundle graph head is missing element: ${headId}`);
|
|
407
361
|
}
|
|
408
|
-
|
|
362
|
+
const edgeKeys = new Set();
|
|
409
363
|
for (const edge of Array.isArray(bundle.edges) ? bundle.edges : []) {
|
|
410
|
-
if (!isObject(edge) ||
|
|
364
|
+
if (!isObject(edge) || !cleanEntryId(edge.from) || !cleanEntryId(edge.to) || !cleanEntryId(edge.kind)) {
|
|
411
365
|
throw new Error('bundle graph edges must include from, to, and kind');
|
|
412
366
|
}
|
|
413
367
|
if (!elementIds.has(edge.from)) throw new Error(`bundle graph edge source is missing element: ${edge.from}`);
|
|
414
368
|
if (!elementIds.has(edge.to)) throw new Error(`bundle graph edge target is missing element: ${edge.to}`);
|
|
369
|
+
const key = `${edge.from}::${edge.kind}::${edge.to}`;
|
|
370
|
+
if (edgeKeys.has(key)) throw new Error(`bundle graph contains duplicate edge: ${key}`);
|
|
371
|
+
edgeKeys.add(key);
|
|
372
|
+
}
|
|
373
|
+
const reachable = reachableElementIds(bundle);
|
|
374
|
+
const unreachable = [...elementIds].filter((id) => !reachable.has(id));
|
|
375
|
+
if (unreachable.length > 0) {
|
|
376
|
+
throw new Error(`bundle graph contains unreachable elements: ${unreachable.join(', ')}`);
|
|
415
377
|
}
|
|
416
378
|
}
|
|
417
379
|
|
|
418
380
|
function withBundleGraph(bundle) {
|
|
419
381
|
const graph = buildFlatAgentBundleGraph(bundle);
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
headIds: graph.headIds,
|
|
423
|
-
elements: graph.elements,
|
|
424
|
-
edges: graph.edges,
|
|
425
|
-
};
|
|
382
|
+
const rootAgentId = cleanEntryId(bundle.heads.find((head) => head?.type === 'agent')?.id) || null;
|
|
383
|
+
const nextBundle = { ...bundle, rootAgentId, ...graph };
|
|
426
384
|
assertClosedFlatBundleGraph(nextBundle);
|
|
427
385
|
return nextBundle;
|
|
428
386
|
}
|
|
429
387
|
|
|
388
|
+
function assertBundleGraphMatches(bundle) {
|
|
389
|
+
const expected = withBundleGraph({ ...bundle, headIds: undefined, elements: undefined, edges: undefined });
|
|
390
|
+
for (const field of ['rootAgentId', 'headIds', 'elements', 'edges']) {
|
|
391
|
+
if (JSON.stringify(bundle[field]) !== JSON.stringify(expected[field])) {
|
|
392
|
+
throw new Error(`bundle ${field} does not match its canonical records and connections`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return bundle;
|
|
396
|
+
}
|
|
397
|
+
|
|
430
398
|
module.exports = {
|
|
431
399
|
BUNDLE_KIND,
|
|
432
400
|
BUNDLE_SCHEMA_VERSION,
|
|
433
|
-
LEGACY_AGENT_BUNDLE_KIND,
|
|
434
401
|
addUnique,
|
|
435
402
|
agentEntryId,
|
|
436
403
|
appEntryId,
|
|
404
|
+
assertBundleGraphMatches,
|
|
405
|
+
assertCanonicalBundle,
|
|
437
406
|
assertClosedFlatBundleGraph,
|
|
438
407
|
automationEntryId,
|
|
439
408
|
buildFlatAgentBundleGraph,
|
|
@@ -442,6 +411,8 @@ module.exports = {
|
|
|
442
411
|
collectSkillRecords,
|
|
443
412
|
isObject,
|
|
444
413
|
nowIso,
|
|
414
|
+
reachableElementIds,
|
|
415
|
+
skillElementId,
|
|
445
416
|
standaloneToolIds,
|
|
446
417
|
uniqueStrings,
|
|
447
418
|
withBundleGraph,
|