@amalgm/agents 0.1.2 → 0.1.3
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/PURPOSE.md +8 -2
- package/README.md +17 -0
- package/dist/bundles/create.d.ts +13 -0
- package/dist/bundles/create.js +125 -0
- package/dist/bundles/graph-records.d.ts +10 -0
- package/dist/bundles/graph-records.js +61 -0
- package/dist/bundles/graph.d.ts +6 -0
- package/dist/bundles/graph.js +121 -0
- package/dist/bundles/install.d.ts +6 -0
- package/dist/bundles/install.js +64 -0
- package/dist/bundles/types.d.ts +97 -0
- package/dist/bundles/types.js +1 -0
- package/dist/bundles/util.d.ts +14 -0
- package/dist/bundles/util.js +56 -0
- package/dist/bundles/validate.d.ts +2 -0
- package/dist/bundles/validate.js +92 -0
- package/dist/http/bundle-routes.d.ts +2 -0
- package/dist/http/bundle-routes.js +67 -0
- package/dist/http/server.js +3 -1
- package/dist/http-types.d.ts +3 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/mcp/server.js +1 -1
- package/docs/ENGINE_INTEGRATION.md +3 -3
- package/docs/REST.md +12 -0
- package/package.json +1 -1
package/PURPOSE.md
CHANGED
|
@@ -15,6 +15,8 @@ It also discovers installed agent skills across the canonical project and
|
|
|
15
15
|
harness roots so every transport presents the same available skill catalog.
|
|
16
16
|
Rich harness configuration is stored inside the same immutable definition and
|
|
17
17
|
projected onto its executable fields; it is not a second agent registry.
|
|
18
|
+
Portable bundle-v2 documents close an agent's subagent, skill, and tool graph;
|
|
19
|
+
hosts supply explicit ports for resources owned by other products.
|
|
18
20
|
|
|
19
21
|
## Primitives
|
|
20
22
|
|
|
@@ -41,11 +43,15 @@ projected onto its executable fields; it is not a second agent registry.
|
|
|
41
43
|
a new immutable revision, even when its content matches an earlier revision.
|
|
42
44
|
8. Agent configuration and its executable projection change atomically in one
|
|
43
45
|
immutable revision; native imports never ingest credentials or MCP config.
|
|
46
|
+
9. A portable bundle is accepted only when its flat graph is derivable from
|
|
47
|
+
its canonical records and every element is reachable from a declared head.
|
|
44
48
|
|
|
45
49
|
## Consequences
|
|
46
50
|
|
|
47
51
|
The package needs no cloud command inbox, scheduler, trigger relay, Chat
|
|
48
52
|
record, or general event-routing system. Engine and other hosts may call the
|
|
49
53
|
SDK whenever they need to route work, but routing is outside the Agents
|
|
50
|
-
product.
|
|
51
|
-
|
|
54
|
+
product. Bundle ports call Apps, Automations, and Toolbox public APIs; Agents
|
|
55
|
+
never imports their stores or duplicates their lifecycle behavior. Internal
|
|
56
|
+
revisions, ordered events, idempotency, and cancellation exist only to make
|
|
57
|
+
the behaviors above durable and predictable.
|
package/README.md
CHANGED
|
@@ -100,6 +100,23 @@ The REST adapter also serves `/agent-config`, `/agent-config/get`,
|
|
|
100
100
|
only instructions, skills, command hooks, and agent markdown from an injected
|
|
101
101
|
home; credentials and MCP configuration never enter Agents.
|
|
102
102
|
|
|
103
|
+
Portable bundle-v2 documents use the same SDK law as the REST surface:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const { bundle, preview } = await createAgentBundle(
|
|
107
|
+
agents,
|
|
108
|
+
{ agentIds: ['reviewer'] },
|
|
109
|
+
{ port: hostResourcePort },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await installAgentBundle(agents, bundle, { port: hostResourcePort });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Agents closes recursive subagents, skills, tools, and the canonical flat
|
|
116
|
+
graph. The injected port exports and installs Apps, Automations, and Toolbox
|
|
117
|
+
records through their owning SDKs. A missing port or graph mismatch fails
|
|
118
|
+
explicitly; dependencies are never omitted to make a bundle look portable.
|
|
119
|
+
|
|
103
120
|
## CLI
|
|
104
121
|
|
|
105
122
|
```bash
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { InstalledSkill } from '../skills/scanner.js';
|
|
3
|
+
import type { AgentBundle, AgentBundlePort, BundleCreateInput } from './types.js';
|
|
4
|
+
export interface CreateBundleOptions {
|
|
5
|
+
port?: AgentBundlePort;
|
|
6
|
+
installedSkills?: InstalledSkill[];
|
|
7
|
+
bundleId?: string;
|
|
8
|
+
now?: () => string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createAgentBundle(agents: Agents, input: BundleCreateInput, options?: CreateBundleOptions): Promise<{
|
|
11
|
+
bundle: AgentBundle;
|
|
12
|
+
preview: Record<string, unknown>;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { configFromAgent } from '../config/store.js';
|
|
2
|
+
import { buildBundleGraph } from './graph.js';
|
|
3
|
+
import { collectSkillRecords } from './graph-records.js';
|
|
4
|
+
import { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION, addUnique, bundleId, emptyRequirements, mergeRequirements, scrubSecrets, uniqueStrings, } from './util.js';
|
|
5
|
+
function hydratedSkill(skill, installed) {
|
|
6
|
+
if (skill.content)
|
|
7
|
+
return skill;
|
|
8
|
+
const sourcePath = typeof skill.source.path === 'string' ? skill.source.path : '';
|
|
9
|
+
const match = installed.find((item) => item.id === skill.id || item.name === skill.name
|
|
10
|
+
|| (sourcePath && item.source.path === sourcePath));
|
|
11
|
+
return match ? {
|
|
12
|
+
...skill,
|
|
13
|
+
name: match.name,
|
|
14
|
+
description: skill.description || match.description,
|
|
15
|
+
content: match.content || '',
|
|
16
|
+
source: match.source,
|
|
17
|
+
} : skill;
|
|
18
|
+
}
|
|
19
|
+
function collectAgents(agents, roots, installed) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const entries = [];
|
|
22
|
+
const visit = (id) => {
|
|
23
|
+
if (!id || seen.has(id))
|
|
24
|
+
return;
|
|
25
|
+
seen.add(id);
|
|
26
|
+
const record = agents.getAgent(id);
|
|
27
|
+
if (!record)
|
|
28
|
+
throw new Error(`Agent not found: ${id}`);
|
|
29
|
+
const config = configFromAgent(record);
|
|
30
|
+
config.skills = config.skills.map((skill) => hydratedSkill(skill, installed));
|
|
31
|
+
entries.push({
|
|
32
|
+
sourceAgentId: record.id,
|
|
33
|
+
agent: {
|
|
34
|
+
name: record.definition.name,
|
|
35
|
+
description: record.definition.description,
|
|
36
|
+
driverId: record.definition.driver.id,
|
|
37
|
+
modelId: record.definition.model?.id || '',
|
|
38
|
+
modelSettings: record.definition.model?.settings || null,
|
|
39
|
+
},
|
|
40
|
+
config,
|
|
41
|
+
});
|
|
42
|
+
config.subagents.forEach((subagent) => visit(subagent.agentId));
|
|
43
|
+
};
|
|
44
|
+
roots.forEach(visit);
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
function title(entries, external) {
|
|
48
|
+
const agent = entries[0]?.agent;
|
|
49
|
+
if (agent)
|
|
50
|
+
return { title: agent.name || 'Shared agent', description: agent.description || '' };
|
|
51
|
+
for (const records of [external.automations, external.apps, external.tools]) {
|
|
52
|
+
const first = records?.[0];
|
|
53
|
+
const summary = first && typeof first === 'object'
|
|
54
|
+
? (first.automation || first.app || first) : null;
|
|
55
|
+
if (summary)
|
|
56
|
+
return {
|
|
57
|
+
title: typeof summary.name === 'string' ? summary.name : 'Shared bundle',
|
|
58
|
+
description: typeof summary.description === 'string' ? summary.description : '',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { title: 'Shared bundle', description: '' };
|
|
62
|
+
}
|
|
63
|
+
// Engine reference: amalgm-mcp/agent-bundles/export.js createBundle.
|
|
64
|
+
export async function createAgentBundle(agents, input, options = {}) {
|
|
65
|
+
const agentIds = uniqueStrings(input.agentIds);
|
|
66
|
+
const automationIds = uniqueStrings(input.automationIds);
|
|
67
|
+
const appIds = uniqueStrings(input.appIds);
|
|
68
|
+
const requestedToolIds = uniqueStrings(input.toolIds);
|
|
69
|
+
if (![agentIds, automationIds, appIds, requestedToolIds].some((ids) => ids.length)) {
|
|
70
|
+
throw new Error('At least one agent, automation, app, or tool is required');
|
|
71
|
+
}
|
|
72
|
+
const bundleAgents = collectAgents(agents, agentIds, options.installedSkills || []);
|
|
73
|
+
const agentToolIds = bundleAgents.flatMap((entry) => entry.config.loadout.toolIds);
|
|
74
|
+
const needsPort = automationIds.length || appIds.length || requestedToolIds.length || agentToolIds.length;
|
|
75
|
+
if (needsPort && !options.port)
|
|
76
|
+
throw new Error('A bundle resource port is required for tools, apps, or automations');
|
|
77
|
+
const external = options.port ? await options.port.exportResources({
|
|
78
|
+
automationIds, appIds, toolIds: uniqueStrings([...requestedToolIds, ...agentToolIds]),
|
|
79
|
+
}) : {};
|
|
80
|
+
const requires = mergeRequirements(emptyRequirements(), external.requires);
|
|
81
|
+
for (const entry of bundleAgents) {
|
|
82
|
+
addUnique(requires.runtimes, entry.agent.driverId);
|
|
83
|
+
addUnique(requires.auth, entry.agent.driverId);
|
|
84
|
+
}
|
|
85
|
+
const tools = (external.tools || []).map((tool) => {
|
|
86
|
+
if (tool.origin === 'system')
|
|
87
|
+
addUnique(requires.systemTools, String(tool.id || ''));
|
|
88
|
+
return scrubSecrets(tool, `tools.${String(tool.id || '')}`, requires);
|
|
89
|
+
});
|
|
90
|
+
const toolActions = (external.toolActions || []).map((action) => scrubSecrets(action, `toolActions.${String(action.id || '')}`, requires));
|
|
91
|
+
if (requires.missingTools.length)
|
|
92
|
+
throw new Error(`Tool not found: ${requires.missingTools.join(', ')}`);
|
|
93
|
+
const skills = collectSkillRecords(bundleAgents);
|
|
94
|
+
const hooks = bundleAgents.reduce((count, entry) => count + entry.config.hooks.length, 0);
|
|
95
|
+
const warnings = [
|
|
96
|
+
'Instructions, skill text, tool descriptions, and hook commands are shared as plain text.',
|
|
97
|
+
'Secrets pasted into free-text instructions or files are not automatically detected.',
|
|
98
|
+
...(hooks ? ['This bundle includes executable hook definitions. Review commands before importing.'] : []),
|
|
99
|
+
...(tools.length ? ['Tools can call external services or local commands. Reconnect credentials locally.'] : []),
|
|
100
|
+
...(external.warnings || []),
|
|
101
|
+
];
|
|
102
|
+
const named = title(bundleAgents, external);
|
|
103
|
+
const headToolIds = external.headToolIds || requestedToolIds;
|
|
104
|
+
const heads = [
|
|
105
|
+
...agentIds.map((id) => ({ type: 'agent', id })),
|
|
106
|
+
...automationIds.map((id) => ({ type: 'automation', id })),
|
|
107
|
+
...appIds.map((id) => ({ type: 'app', id })),
|
|
108
|
+
...headToolIds.map((id) => ({ type: 'tool', id })),
|
|
109
|
+
];
|
|
110
|
+
const summary = {
|
|
111
|
+
agents: bundleAgents.length, automations: external.automations?.length || 0,
|
|
112
|
+
apps: external.apps?.length || 0, tools: tools.length, toolActions: toolActions.length,
|
|
113
|
+
skills: skills.length, hooks, assets: 0,
|
|
114
|
+
};
|
|
115
|
+
const bundle = {
|
|
116
|
+
kind: BUNDLE_KIND, schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
117
|
+
bundleId: options.bundleId || bundleId(), createdAt: options.now?.() || new Date().toISOString(),
|
|
118
|
+
rootAgentId: agentIds[0] || null, heads, headIds: [], ...named,
|
|
119
|
+
agents: bundleAgents, automations: external.automations || [], apps: external.apps || [],
|
|
120
|
+
tools, toolActions, skills, assets: [], requires, warnings, summary, elements: [], edges: [],
|
|
121
|
+
};
|
|
122
|
+
Object.assign(bundle, buildBundleGraph(bundle));
|
|
123
|
+
const bytes = Buffer.byteLength(JSON.stringify(bundle));
|
|
124
|
+
return { bundle, preview: { bundleId: bundle.bundleId, ...named, summary, requires, warnings, bytes } };
|
|
125
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AgentBundle, BundleAgentEntry } from './types.js';
|
|
2
|
+
export declare function agentEntryId(entry: unknown): string;
|
|
3
|
+
export declare function automationEntryId(entry: unknown): string;
|
|
4
|
+
export declare function appEntryId(entry: unknown): string;
|
|
5
|
+
export declare function toolEntryId(entry: unknown): string;
|
|
6
|
+
export declare function actionEntryId(entry: unknown): string;
|
|
7
|
+
export declare function skillFingerprint(skill: unknown): string;
|
|
8
|
+
export declare function skillElementId(skill: unknown): string;
|
|
9
|
+
export declare function skillLabel(skill: unknown): string;
|
|
10
|
+
export declare function collectSkillRecords(agents: BundleAgentEntry[]): AgentBundle['skills'];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { clean, digest, object } from './util.js';
|
|
2
|
+
export function agentEntryId(entry) {
|
|
3
|
+
const record = object(entry);
|
|
4
|
+
return clean(record?.sourceAgentId) || clean(object(record?.agent)?.sourceAgentId);
|
|
5
|
+
}
|
|
6
|
+
export function automationEntryId(entry) {
|
|
7
|
+
return clean(object(entry)?.sourceAutomationId);
|
|
8
|
+
}
|
|
9
|
+
export function appEntryId(entry) {
|
|
10
|
+
return clean(object(entry)?.sourceAppId);
|
|
11
|
+
}
|
|
12
|
+
export function toolEntryId(entry) {
|
|
13
|
+
return clean(object(entry)?.id);
|
|
14
|
+
}
|
|
15
|
+
export function actionEntryId(entry) {
|
|
16
|
+
return clean(object(entry)?.id);
|
|
17
|
+
}
|
|
18
|
+
export function skillFingerprint(skill) {
|
|
19
|
+
if (typeof skill === 'string')
|
|
20
|
+
return clean(skill) ? `string:${clean(skill)}` : '';
|
|
21
|
+
const record = object(skill);
|
|
22
|
+
if (!record)
|
|
23
|
+
return '';
|
|
24
|
+
if (clean(record.id))
|
|
25
|
+
return `id:${clean(record.id)}`;
|
|
26
|
+
if (clean(record.path))
|
|
27
|
+
return `path:${clean(record.path)}`;
|
|
28
|
+
const name = clean(record.name);
|
|
29
|
+
const content = clean(record.content);
|
|
30
|
+
if (name && content)
|
|
31
|
+
return `name-content:${name}:${content}`;
|
|
32
|
+
if (name)
|
|
33
|
+
return `name:${name}`;
|
|
34
|
+
if (content)
|
|
35
|
+
return `content:${content}`;
|
|
36
|
+
return `json:${JSON.stringify(skill)}`;
|
|
37
|
+
}
|
|
38
|
+
export function skillElementId(skill) {
|
|
39
|
+
const fingerprint = skillFingerprint(skill);
|
|
40
|
+
return fingerprint ? `skill:${digest(fingerprint)}` : '';
|
|
41
|
+
}
|
|
42
|
+
export function skillLabel(skill) {
|
|
43
|
+
if (typeof skill === 'string')
|
|
44
|
+
return clean(skill) || 'Skill';
|
|
45
|
+
const record = object(skill);
|
|
46
|
+
if (!record)
|
|
47
|
+
return 'Skill';
|
|
48
|
+
return clean(record.name) || clean(record.path).split('/').filter(Boolean).pop()
|
|
49
|
+
|| clean(record.id) || clean(record.content).slice(0, 60) || 'Skill';
|
|
50
|
+
}
|
|
51
|
+
export function collectSkillRecords(agents) {
|
|
52
|
+
const records = new Map();
|
|
53
|
+
for (const entry of agents) {
|
|
54
|
+
for (const skill of entry.config.skills) {
|
|
55
|
+
const key = skillFingerprint(skill);
|
|
56
|
+
if (key && !records.has(key))
|
|
57
|
+
records.set(key, JSON.parse(JSON.stringify(skill)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...records.values()];
|
|
61
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { actionEntryId, agentEntryId, appEntryId, automationEntryId, skillElementId, skillLabel, toolEntryId, } from './graph-records.js';
|
|
2
|
+
import { clean, object, uniqueStrings } from './util.js';
|
|
3
|
+
const ORDER = {
|
|
4
|
+
agent: 0, automation: 1, app: 2, tool: 3, tool_action: 4, skill: 5,
|
|
5
|
+
};
|
|
6
|
+
function elementId(type, id) {
|
|
7
|
+
return `${type}:${id}`;
|
|
8
|
+
}
|
|
9
|
+
function graphRecords(bundle) {
|
|
10
|
+
return {
|
|
11
|
+
agent: new Map(bundle.agents.map((entry) => [agentEntryId(entry), entry])),
|
|
12
|
+
automation: new Map(bundle.automations.map((entry) => [automationEntryId(entry), entry])),
|
|
13
|
+
app: new Map(bundle.apps.map((entry) => [appEntryId(entry), entry])),
|
|
14
|
+
tool: new Map(bundle.tools.map((entry) => [toolEntryId(entry), entry])),
|
|
15
|
+
tool_action: new Map(bundle.toolActions.map((entry) => [actionEntryId(entry), entry])),
|
|
16
|
+
skill: new Map(bundle.skills.map((entry) => [skillElementId(entry), entry])),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// Engine reference: amalgm-mcp/agent-bundles/graph.js buildFlatAgentBundleGraph.
|
|
20
|
+
export function buildBundleGraph(bundle) {
|
|
21
|
+
const records = graphRecords(bundle);
|
|
22
|
+
const elements = new Map();
|
|
23
|
+
const edges = new Map();
|
|
24
|
+
const addElement = (type, id, label) => {
|
|
25
|
+
const key = elementId(type, id);
|
|
26
|
+
elements.set(key, { id: key, type, label: label || id });
|
|
27
|
+
};
|
|
28
|
+
const addEdge = (fromType, fromId, toType, toId, kind) => {
|
|
29
|
+
const edge = { from: elementId(fromType, fromId), to: elementId(toType, toId), kind };
|
|
30
|
+
edges.set(`${edge.from}::${kind}::${edge.to}`, edge);
|
|
31
|
+
};
|
|
32
|
+
const requireRecord = (type, id, owner) => {
|
|
33
|
+
if (!records[type].has(id))
|
|
34
|
+
throw new Error(`${owner} references missing ${type.replace('_', ' ')}: ${id}`);
|
|
35
|
+
};
|
|
36
|
+
const addToolSelection = (ownerType, ownerId, selectedId) => {
|
|
37
|
+
if (records.tool_action.has(selectedId))
|
|
38
|
+
addEdge(ownerType, ownerId, 'tool_action', selectedId, 'uses-tool-action');
|
|
39
|
+
else {
|
|
40
|
+
requireRecord('tool', selectedId, `${ownerType} ${ownerId}`);
|
|
41
|
+
addEdge(ownerType, ownerId, 'tool', selectedId, 'uses-tool');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
for (const entry of bundle.agents)
|
|
45
|
+
addElement('agent', agentEntryId(entry), entry.agent.name);
|
|
46
|
+
for (const entry of bundle.automations) {
|
|
47
|
+
addElement('automation', automationEntryId(entry), clean(object(entry.automation)?.name));
|
|
48
|
+
}
|
|
49
|
+
for (const entry of bundle.apps)
|
|
50
|
+
addElement('app', appEntryId(entry), clean(object(entry.app)?.name));
|
|
51
|
+
for (const entry of bundle.tools)
|
|
52
|
+
addElement('tool', toolEntryId(entry), clean(entry.name));
|
|
53
|
+
for (const entry of bundle.toolActions)
|
|
54
|
+
addElement('tool_action', actionEntryId(entry), clean(entry.name));
|
|
55
|
+
for (const skill of bundle.skills) {
|
|
56
|
+
const id = skillElementId(skill);
|
|
57
|
+
elements.set(id, { id, type: 'skill', label: skillLabel(skill) });
|
|
58
|
+
}
|
|
59
|
+
for (const entry of bundle.agents) {
|
|
60
|
+
const id = agentEntryId(entry);
|
|
61
|
+
for (const subagent of entry.config.subagents) {
|
|
62
|
+
if (!subagent.agentId)
|
|
63
|
+
continue;
|
|
64
|
+
requireRecord('agent', subagent.agentId, `agent ${id}`);
|
|
65
|
+
addEdge('agent', id, 'agent', subagent.agentId, 'uses-subagent');
|
|
66
|
+
}
|
|
67
|
+
for (const selectedId of entry.config.loadout.toolIds)
|
|
68
|
+
addToolSelection('agent', id, selectedId);
|
|
69
|
+
for (const skill of entry.config.skills) {
|
|
70
|
+
const skillId = skillElementId(skill);
|
|
71
|
+
if (!records.skill.has(skillId))
|
|
72
|
+
throw new Error(`agent ${id} references missing skill: ${skillLabel(skill)}`);
|
|
73
|
+
addEdge('agent', id, 'skill', skillId.slice('skill:'.length), 'uses-skill');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const entry of bundle.automations) {
|
|
77
|
+
const id = automationEntryId(entry);
|
|
78
|
+
for (const selectedId of uniqueStrings(entry.toolIds))
|
|
79
|
+
addToolSelection('automation', id, selectedId);
|
|
80
|
+
}
|
|
81
|
+
for (const entry of bundle.apps) {
|
|
82
|
+
const id = appEntryId(entry);
|
|
83
|
+
for (const toolId of uniqueStrings(entry.toolIds)) {
|
|
84
|
+
requireRecord('tool', toolId, `app ${id}`);
|
|
85
|
+
addEdge('app', id, 'tool', toolId, 'provides-tool');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const tool of bundle.tools) {
|
|
89
|
+
const id = toolEntryId(tool);
|
|
90
|
+
const appId = clean(tool.appId);
|
|
91
|
+
if (appId) {
|
|
92
|
+
requireRecord('app', appId, `tool ${id}`);
|
|
93
|
+
addEdge('tool', id, 'app', appId, 'requires-app');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const action of bundle.toolActions) {
|
|
97
|
+
const id = actionEntryId(action);
|
|
98
|
+
const toolId = clean(action.toolId);
|
|
99
|
+
requireRecord('tool', toolId, `tool action ${id}`);
|
|
100
|
+
addEdge('tool', toolId, 'tool_action', id, 'has-action');
|
|
101
|
+
addEdge('tool_action', id, 'tool', toolId, 'action-of');
|
|
102
|
+
const appId = clean(action.appId);
|
|
103
|
+
if (appId) {
|
|
104
|
+
requireRecord('app', appId, `tool action ${id}`);
|
|
105
|
+
addEdge('tool_action', id, 'app', appId, 'requires-app');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const headIds = bundle.heads.map((head) => {
|
|
109
|
+
const id = elementId(head.type, head.id);
|
|
110
|
+
if (!elements.has(id))
|
|
111
|
+
throw new Error(`bundle head references missing element: ${id}`);
|
|
112
|
+
return id;
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
headIds,
|
|
116
|
+
elements: [...elements.values()].sort((left, right) => (ORDER[left.type] ?? 99) - (ORDER[right.type] ?? 99)
|
|
117
|
+
|| left.id.localeCompare(right.id)),
|
|
118
|
+
edges: [...edges.values()].sort((left, right) => left.from.localeCompare(right.from)
|
|
119
|
+
|| left.kind.localeCompare(right.kind) || left.to.localeCompare(right.to)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Agents } from '../agents.js';
|
|
2
|
+
import type { AgentBundlePort, BundleInstallResult } from './types.js';
|
|
3
|
+
export declare function installAgentBundle(agents: Agents, input: unknown, options?: {
|
|
4
|
+
port?: AgentBundlePort;
|
|
5
|
+
authRef?: string;
|
|
6
|
+
}): Promise<BundleInstallResult>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { normalizeAgentConfig } from '../config/schema.js';
|
|
3
|
+
import { updateAgentConfig } from '../config/store.js';
|
|
4
|
+
import { validateAgentBundle } from './validate.js';
|
|
5
|
+
function uniqueName(agents, value) {
|
|
6
|
+
const base = value.trim() || 'Imported agent';
|
|
7
|
+
const names = new Set(agents.listAgents().map((item) => item.definition.name.toLowerCase()));
|
|
8
|
+
if (!names.has(base.toLowerCase()))
|
|
9
|
+
return base;
|
|
10
|
+
if (!names.has(`${base} (imported)`.toLowerCase()))
|
|
11
|
+
return `${base} (imported)`;
|
|
12
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
13
|
+
const candidate = `${base} (imported ${index})`;
|
|
14
|
+
if (!names.has(candidate.toLowerCase()))
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
return `${base} (${crypto.randomUUID().slice(0, 8)})`;
|
|
18
|
+
}
|
|
19
|
+
// Engine reference: amalgm-mcp/agent-bundles/install.js installBundleAgents.
|
|
20
|
+
export async function installAgentBundle(agents, input, options = {}) {
|
|
21
|
+
const bundle = validateAgentBundle(input);
|
|
22
|
+
const hasExternal = bundle.automations.length || bundle.apps.length || bundle.tools.length;
|
|
23
|
+
if (hasExternal && !options.port)
|
|
24
|
+
throw new Error('A bundle resource port is required to install external resources');
|
|
25
|
+
const external = options.port ? await options.port.installResources(bundle) : {};
|
|
26
|
+
const idMap = new Map();
|
|
27
|
+
const created = [];
|
|
28
|
+
for (const entry of bundle.agents) {
|
|
29
|
+
const agent = agents.createAgent({
|
|
30
|
+
id: `imported-${crypto.randomUUID()}`,
|
|
31
|
+
name: uniqueName(agents, entry.agent.name),
|
|
32
|
+
description: entry.agent.description,
|
|
33
|
+
driver: { id: entry.agent.driverId, config: {} },
|
|
34
|
+
model: entry.agent.modelId ? { id: entry.agent.modelId, settings: entry.agent.modelSettings || {} } : null,
|
|
35
|
+
authRef: options.authRef || null,
|
|
36
|
+
instructions: entry.config.instructions,
|
|
37
|
+
resources: { files: [], skills: [], subagents: [] },
|
|
38
|
+
toolbox: { toolIds: entry.config.loadout.toolIds, actionIds: [] },
|
|
39
|
+
metadata: { installedFromBundle: bundle.bundleId },
|
|
40
|
+
});
|
|
41
|
+
idMap.set(entry.sourceAgentId, agent.id);
|
|
42
|
+
created.push({ sourceId: entry.sourceAgentId, agent, config: entry.config });
|
|
43
|
+
}
|
|
44
|
+
const installedAgents = created.map(({ agent, config }) => {
|
|
45
|
+
const rewritten = normalizeAgentConfig({
|
|
46
|
+
...config,
|
|
47
|
+
agentId: agent.id,
|
|
48
|
+
subagents: config.subagents.map((subagent) => ({
|
|
49
|
+
...subagent,
|
|
50
|
+
agentId: idMap.get(subagent.agentId) || '',
|
|
51
|
+
})).filter((subagent) => subagent.agentId),
|
|
52
|
+
});
|
|
53
|
+
return updateAgentConfig(agents, agent.id, rewritten).agent;
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
bundleId: bundle.bundleId,
|
|
58
|
+
rootAgentId: bundle.rootAgentId ? idMap.get(bundle.rootAgentId) || null : installedAgents[0]?.id || null,
|
|
59
|
+
idMap: Object.fromEntries(idMap),
|
|
60
|
+
installedAgents,
|
|
61
|
+
...external,
|
|
62
|
+
warnings: [...(bundle.warnings || []), ...(external.warnings || [])],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AgentConfig } from '../config/types.js';
|
|
2
|
+
import type { AgentRecord, JsonObject } from '../types.js';
|
|
3
|
+
export type BundleHeadType = 'agent' | 'automation' | 'app' | 'tool';
|
|
4
|
+
export interface BundleHead {
|
|
5
|
+
type: BundleHeadType;
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BundleAgentEntry {
|
|
9
|
+
sourceAgentId: string;
|
|
10
|
+
agent: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
driverId: string;
|
|
14
|
+
modelId: string;
|
|
15
|
+
modelSettings: JsonObject | null;
|
|
16
|
+
};
|
|
17
|
+
config: AgentConfig;
|
|
18
|
+
}
|
|
19
|
+
export interface BundleGraphElement {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BundleGraphEdge {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
}
|
|
29
|
+
export interface BundleRequirements {
|
|
30
|
+
runtimes: string[];
|
|
31
|
+
auth: string[];
|
|
32
|
+
secrets: string[];
|
|
33
|
+
bindings: string[];
|
|
34
|
+
systemTools: string[];
|
|
35
|
+
systemActions: string[];
|
|
36
|
+
missingTools: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface AgentBundle {
|
|
39
|
+
kind: 'amalgm.bundle';
|
|
40
|
+
schemaVersion: 2;
|
|
41
|
+
bundleId: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
rootAgentId: string | null;
|
|
44
|
+
heads: BundleHead[];
|
|
45
|
+
headIds: string[];
|
|
46
|
+
title: string;
|
|
47
|
+
description: string;
|
|
48
|
+
agents: BundleAgentEntry[];
|
|
49
|
+
automations: JsonObject[];
|
|
50
|
+
apps: JsonObject[];
|
|
51
|
+
tools: JsonObject[];
|
|
52
|
+
toolActions: JsonObject[];
|
|
53
|
+
skills: JsonObject[];
|
|
54
|
+
assets: JsonObject[];
|
|
55
|
+
requires: BundleRequirements;
|
|
56
|
+
warnings: string[];
|
|
57
|
+
summary: JsonObject;
|
|
58
|
+
elements: BundleGraphElement[];
|
|
59
|
+
edges: BundleGraphEdge[];
|
|
60
|
+
}
|
|
61
|
+
export interface BundleExternalExport {
|
|
62
|
+
headToolIds?: string[];
|
|
63
|
+
automations?: JsonObject[];
|
|
64
|
+
apps?: JsonObject[];
|
|
65
|
+
tools?: JsonObject[];
|
|
66
|
+
toolActions?: JsonObject[];
|
|
67
|
+
requires?: Partial<BundleRequirements>;
|
|
68
|
+
warnings?: string[];
|
|
69
|
+
}
|
|
70
|
+
export interface BundleExternalInstall {
|
|
71
|
+
installedAutomations?: JsonObject[];
|
|
72
|
+
installedApps?: JsonObject[];
|
|
73
|
+
installedTools?: JsonObject[];
|
|
74
|
+
warnings?: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface AgentBundlePort {
|
|
77
|
+
exportResources(input: {
|
|
78
|
+
automationIds: string[];
|
|
79
|
+
appIds: string[];
|
|
80
|
+
toolIds: string[];
|
|
81
|
+
}): Promise<BundleExternalExport> | BundleExternalExport;
|
|
82
|
+
installResources(bundle: AgentBundle): Promise<BundleExternalInstall> | BundleExternalInstall;
|
|
83
|
+
}
|
|
84
|
+
export interface BundleCreateInput {
|
|
85
|
+
agentIds?: string[];
|
|
86
|
+
automationIds?: string[];
|
|
87
|
+
appIds?: string[];
|
|
88
|
+
toolIds?: string[];
|
|
89
|
+
}
|
|
90
|
+
export interface BundleInstallResult extends BundleExternalInstall {
|
|
91
|
+
ok: true;
|
|
92
|
+
bundleId: string;
|
|
93
|
+
rootAgentId: string | null;
|
|
94
|
+
idMap: Record<string, string>;
|
|
95
|
+
installedAgents: AgentRecord[];
|
|
96
|
+
warnings: string[];
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from '../types.js';
|
|
2
|
+
import type { BundleRequirements } from './types.js';
|
|
3
|
+
export declare const BUNDLE_KIND: "amalgm.bundle";
|
|
4
|
+
export declare const BUNDLE_SCHEMA_VERSION: 2;
|
|
5
|
+
export declare function clean(value: unknown): string;
|
|
6
|
+
export declare function object(value: unknown): JsonObject | null;
|
|
7
|
+
export declare function clone<T>(value: T): T;
|
|
8
|
+
export declare function uniqueStrings(value: unknown): string[];
|
|
9
|
+
export declare function addUnique(target: string[], value: string): void;
|
|
10
|
+
export declare function bundleId(): string;
|
|
11
|
+
export declare function digest(value: unknown): string;
|
|
12
|
+
export declare function emptyRequirements(): BundleRequirements;
|
|
13
|
+
export declare function mergeRequirements(target: BundleRequirements, incoming?: Partial<BundleRequirements>): BundleRequirements;
|
|
14
|
+
export declare function scrubSecrets(value: JsonValue, path: string, requires: BundleRequirements): JsonValue;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
export const BUNDLE_KIND = 'amalgm.bundle';
|
|
3
|
+
export const BUNDLE_SCHEMA_VERSION = 2;
|
|
4
|
+
export function clean(value) {
|
|
5
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
6
|
+
}
|
|
7
|
+
export function object(value) {
|
|
8
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
9
|
+
}
|
|
10
|
+
export function clone(value) {
|
|
11
|
+
return JSON.parse(JSON.stringify(value));
|
|
12
|
+
}
|
|
13
|
+
export function uniqueStrings(value) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return [];
|
|
16
|
+
return [...new Set(value.map(clean).filter(Boolean))];
|
|
17
|
+
}
|
|
18
|
+
export function addUnique(target, value) {
|
|
19
|
+
if (value && !target.includes(value))
|
|
20
|
+
target.push(value);
|
|
21
|
+
}
|
|
22
|
+
export function bundleId() {
|
|
23
|
+
return `agent-bundle-${crypto.randomUUID()}`;
|
|
24
|
+
}
|
|
25
|
+
export function digest(value) {
|
|
26
|
+
return crypto.createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value))
|
|
27
|
+
.digest('hex').slice(0, 16);
|
|
28
|
+
}
|
|
29
|
+
export function emptyRequirements() {
|
|
30
|
+
return { runtimes: [], auth: [], secrets: [], bindings: [], systemTools: [],
|
|
31
|
+
systemActions: [], missingTools: [] };
|
|
32
|
+
}
|
|
33
|
+
export function mergeRequirements(target, incoming = {}) {
|
|
34
|
+
for (const key of Object.keys(target)) {
|
|
35
|
+
for (const value of incoming[key] || [])
|
|
36
|
+
addUnique(target[key], value);
|
|
37
|
+
}
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
const SECRET = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
|
|
41
|
+
export function scrubSecrets(value, path, requires) {
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.map((item, index) => scrubSecrets(item, `${path}[${index}]`, requires));
|
|
44
|
+
const record = object(value);
|
|
45
|
+
if (!record)
|
|
46
|
+
return value;
|
|
47
|
+
const output = {};
|
|
48
|
+
for (const [key, child] of Object.entries(record)) {
|
|
49
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
50
|
+
if (SECRET.test(key))
|
|
51
|
+
addUnique(requires.secrets, childPath);
|
|
52
|
+
else
|
|
53
|
+
output[key] = scrubSecrets(child, childPath, requires);
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { actionEntryId, agentEntryId, appEntryId, automationEntryId, skillFingerprint, toolEntryId } from './graph-records.js';
|
|
2
|
+
import { buildBundleGraph } from './graph.js';
|
|
3
|
+
import { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION, clean, object } from './util.js';
|
|
4
|
+
function unique(label, records, identify) {
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
for (const record of records) {
|
|
7
|
+
const id = identify(record);
|
|
8
|
+
if (!id)
|
|
9
|
+
throw new Error(`${label} entries must include ids`);
|
|
10
|
+
if (seen.has(id))
|
|
11
|
+
throw new Error(`Bundle contains duplicate ${label} id: ${id}`);
|
|
12
|
+
seen.add(id);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertClosed(bundle) {
|
|
16
|
+
const elements = new Set();
|
|
17
|
+
for (const element of bundle.elements) {
|
|
18
|
+
if (!clean(element.id) || !clean(element.type))
|
|
19
|
+
throw new Error('bundle graph elements must include id and type');
|
|
20
|
+
if (object(element)?.data !== undefined)
|
|
21
|
+
throw new Error(`bundle graph element must not copy data: ${element.id}`);
|
|
22
|
+
if (elements.has(element.id))
|
|
23
|
+
throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
|
|
24
|
+
elements.add(element.id);
|
|
25
|
+
}
|
|
26
|
+
const outgoing = new Map();
|
|
27
|
+
const edgeKeys = new Set();
|
|
28
|
+
for (const edge of bundle.edges) {
|
|
29
|
+
if (!elements.has(edge.from) || !elements.has(edge.to))
|
|
30
|
+
throw new Error('bundle graph edge references missing element');
|
|
31
|
+
const key = `${edge.from}::${edge.kind}::${edge.to}`;
|
|
32
|
+
if (edgeKeys.has(key))
|
|
33
|
+
throw new Error(`bundle graph contains duplicate edge: ${key}`);
|
|
34
|
+
edgeKeys.add(key);
|
|
35
|
+
outgoing.set(edge.from, [...(outgoing.get(edge.from) || []), edge.to]);
|
|
36
|
+
}
|
|
37
|
+
const reachable = new Set();
|
|
38
|
+
const pending = [...bundle.headIds];
|
|
39
|
+
while (pending.length) {
|
|
40
|
+
const id = pending.pop();
|
|
41
|
+
if (reachable.has(id))
|
|
42
|
+
continue;
|
|
43
|
+
reachable.add(id);
|
|
44
|
+
pending.push(...(outgoing.get(id) || []));
|
|
45
|
+
}
|
|
46
|
+
const unreachable = [...elements].filter((id) => !reachable.has(id));
|
|
47
|
+
if (unreachable.length)
|
|
48
|
+
throw new Error(`bundle graph contains unreachable elements: ${unreachable.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
// Engine reference: amalgm-mcp/agent-bundles/install.js validateBundle.
|
|
51
|
+
export function validateAgentBundle(input) {
|
|
52
|
+
const raw = object(input);
|
|
53
|
+
if (!raw)
|
|
54
|
+
throw new Error('bundle must be an object');
|
|
55
|
+
if (raw.kind !== BUNDLE_KIND)
|
|
56
|
+
throw new Error(`Unsupported bundle kind: ${clean(raw.kind) || 'unknown'}`);
|
|
57
|
+
if (raw.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
|
|
58
|
+
throw new Error(`Unsupported bundle schema version: ${String(raw.schemaVersion || 'unknown')}`);
|
|
59
|
+
}
|
|
60
|
+
const bundle = input;
|
|
61
|
+
const arrays = ['agents', 'automations', 'apps', 'tools', 'toolActions', 'skills', 'heads'];
|
|
62
|
+
if (arrays.some((key) => !Array.isArray(bundle[key])))
|
|
63
|
+
throw new Error('bundle canonical records must be arrays');
|
|
64
|
+
if (!bundle.heads.length)
|
|
65
|
+
throw new Error('bundle must include at least one head');
|
|
66
|
+
unique('agent', bundle.agents, agentEntryId);
|
|
67
|
+
unique('automation', bundle.automations, automationEntryId);
|
|
68
|
+
unique('app', bundle.apps, appEntryId);
|
|
69
|
+
unique('tool', bundle.tools, toolEntryId);
|
|
70
|
+
unique('tool action', bundle.toolActions, actionEntryId);
|
|
71
|
+
unique('skill', bundle.skills, skillFingerprint);
|
|
72
|
+
unique('head', bundle.heads, (value) => {
|
|
73
|
+
const head = object(value);
|
|
74
|
+
return ['agent', 'automation', 'app', 'tool'].includes(clean(head?.type))
|
|
75
|
+
? `${clean(head?.type)}:${clean(head?.id)}` : '';
|
|
76
|
+
});
|
|
77
|
+
if (bundle.requires?.missingTools?.length) {
|
|
78
|
+
throw new Error(`Bundle is missing tools: ${bundle.requires.missingTools.join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
const expected = buildBundleGraph(bundle);
|
|
81
|
+
const expectedRoot = bundle.heads.find((head) => head.type === 'agent')?.id || null;
|
|
82
|
+
if (bundle.rootAgentId !== expectedRoot) {
|
|
83
|
+
throw new Error('bundle rootAgentId does not match its canonical records and connections');
|
|
84
|
+
}
|
|
85
|
+
for (const key of ['headIds', 'elements', 'edges']) {
|
|
86
|
+
if (JSON.stringify(bundle[key]) !== JSON.stringify(expected[key])) {
|
|
87
|
+
throw new Error(`bundle ${key} does not match its canonical records and connections`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
assertClosed(bundle);
|
|
91
|
+
return bundle;
|
|
92
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createAgentBundle } from '../bundles/create.js';
|
|
2
|
+
import { installAgentBundle } from '../bundles/install.js';
|
|
3
|
+
import { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION } from '../bundles/util.js';
|
|
4
|
+
import { scanInstalledSkills } from '../skills/scanner.js';
|
|
5
|
+
function ids(...values) {
|
|
6
|
+
const result = [];
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
if (typeof value === 'string' && value.trim())
|
|
9
|
+
result.push(value.trim());
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
value.forEach((item) => {
|
|
12
|
+
if (typeof item === 'string' && item.trim())
|
|
13
|
+
result.push(item.trim());
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return [...new Set(result)];
|
|
17
|
+
}
|
|
18
|
+
export async function routeAgentBundles(context) {
|
|
19
|
+
const [resource, action, child] = context.path;
|
|
20
|
+
if (resource !== 'agent-bundles')
|
|
21
|
+
return false;
|
|
22
|
+
if (!action && context.method === 'GET') {
|
|
23
|
+
context.json(200, {
|
|
24
|
+
kind: BUNDLE_KIND,
|
|
25
|
+
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
26
|
+
available: { agents: context.agents.listAgents().map((agent) => ({
|
|
27
|
+
id: agent.id, name: agent.definition.name, description: agent.definition.description,
|
|
28
|
+
})) },
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (child || context.method !== 'POST')
|
|
33
|
+
return false;
|
|
34
|
+
const body = await context.body();
|
|
35
|
+
try {
|
|
36
|
+
if (action === 'preview') {
|
|
37
|
+
const installedSkills = scanInstalledSkills({ ...context.skillRoots, includeContent: true }).skills;
|
|
38
|
+
const result = await createAgentBundle(context.agents, {
|
|
39
|
+
agentIds: ids(body.agent_id, body.agent_ids),
|
|
40
|
+
automationIds: ids(body.automation_id, body.automation_ids),
|
|
41
|
+
appIds: ids(body.app_id, body.app_ids),
|
|
42
|
+
toolIds: ids(body.tool_id, body.tool_ids),
|
|
43
|
+
}, { ...(context.bundlePort ? { port: context.bundlePort } : {}), installedSkills });
|
|
44
|
+
context.json(200, { ok: true, ...result });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (action === 'install') {
|
|
48
|
+
if (!body.bundle) {
|
|
49
|
+
context.json(400, { error: 'bundle is required' });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const authRef = typeof body.auth_ref === 'string' ? body.auth_ref
|
|
53
|
+
: typeof body.authMethod === 'string' ? body.authMethod : undefined;
|
|
54
|
+
const result = await installAgentBundle(context.agents, body.bundle, {
|
|
55
|
+
...(context.bundlePort ? { port: context.bundlePort } : {}),
|
|
56
|
+
...(authRef ? { authRef } : {}),
|
|
57
|
+
});
|
|
58
|
+
context.json(200, result);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
context.json(400, { error: error instanceof Error ? error.message : 'Bundle operation failed' });
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/http/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import http from 'node:http';
|
|
|
2
2
|
import { Agents } from '../agents.js';
|
|
3
3
|
import { asAgentError } from '../errors.js';
|
|
4
4
|
import { routeAgentConfig } from './config-routes.js';
|
|
5
|
+
import { routeAgentBundles } from './bundle-routes.js';
|
|
5
6
|
import { routeAgents } from './agent-routes.js';
|
|
6
7
|
import { guardRequest, readJson, sendJson } from './request.js';
|
|
7
8
|
import { routeSessions } from './session-routes.js';
|
|
@@ -46,10 +47,11 @@ export function createRestServer(options = {}) {
|
|
|
46
47
|
url,
|
|
47
48
|
skillRoots: options.skillRoots || {},
|
|
48
49
|
...(options.nativeHomeDir ? { nativeHomeDir: options.nativeHomeDir } : {}),
|
|
50
|
+
...(options.bundlePort ? { bundlePort: options.bundlePort } : {}),
|
|
49
51
|
body: () => readJson(request, bodyLimit),
|
|
50
52
|
json: (status, value) => sendJson(response, status, value),
|
|
51
53
|
};
|
|
52
|
-
if (await routeAgentConfig(context) || await routeSkills(context))
|
|
54
|
+
if (await routeAgentBundles(context) || await routeAgentConfig(context) || await routeSkills(context))
|
|
53
55
|
return;
|
|
54
56
|
if (path.shift() !== 'v1') {
|
|
55
57
|
sendJson(response, 404, { error: { code: 'not_found', message: 'Route not found.' } });
|
package/dist/http-types.d.ts
CHANGED
|
@@ -3,12 +3,14 @@ import type { AddressInfo } from 'node:net';
|
|
|
3
3
|
import type { Agents } from './agents.js';
|
|
4
4
|
import type { AgentsOptions } from './types.js';
|
|
5
5
|
import type { SkillRootOptions } from './skills/roots.js';
|
|
6
|
+
import type { AgentBundlePort } from './bundles/types.js';
|
|
6
7
|
export interface RestServerOptions extends AgentsOptions {
|
|
7
8
|
agents?: Agents;
|
|
8
9
|
token?: string;
|
|
9
10
|
bodyLimitBytes?: number;
|
|
10
11
|
skillRoots?: SkillRootOptions;
|
|
11
12
|
nativeHomeDir?: string;
|
|
13
|
+
bundlePort?: AgentBundlePort;
|
|
12
14
|
}
|
|
13
15
|
export interface RestServer {
|
|
14
16
|
agents: Agents;
|
|
@@ -23,6 +25,7 @@ export interface RouteContext {
|
|
|
23
25
|
url: URL;
|
|
24
26
|
skillRoots: SkillRootOptions;
|
|
25
27
|
nativeHomeDir?: string;
|
|
28
|
+
bundlePort?: AgentBundlePort;
|
|
26
29
|
body(): Promise<Record<string, unknown>>;
|
|
27
30
|
json(status: number, value: unknown): void;
|
|
28
31
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,3 +13,10 @@ export { configFromAgent, listAgentConfigs, updateAgentConfig } from './config/s
|
|
|
13
13
|
export { importAgentConfig } from './config/import.js';
|
|
14
14
|
export { importNativeConfig } from './config/native.js';
|
|
15
15
|
export type * from './config/types.js';
|
|
16
|
+
export { createAgentBundle } from './bundles/create.js';
|
|
17
|
+
export type { CreateBundleOptions } from './bundles/create.js';
|
|
18
|
+
export { installAgentBundle } from './bundles/install.js';
|
|
19
|
+
export { buildBundleGraph } from './bundles/graph.js';
|
|
20
|
+
export { validateAgentBundle } from './bundles/validate.js';
|
|
21
|
+
export { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION } from './bundles/util.js';
|
|
22
|
+
export type * from './bundles/types.js';
|
package/dist/index.js
CHANGED
|
@@ -9,3 +9,8 @@ export { normalizeAgentConfig, stableConfigId } from './config/schema.js';
|
|
|
9
9
|
export { configFromAgent, listAgentConfigs, updateAgentConfig } from './config/store.js';
|
|
10
10
|
export { importAgentConfig } from './config/import.js';
|
|
11
11
|
export { importNativeConfig } from './config/native.js';
|
|
12
|
+
export { createAgentBundle } from './bundles/create.js';
|
|
13
|
+
export { installAgentBundle } from './bundles/install.js';
|
|
14
|
+
export { buildBundleGraph } from './bundles/graph.js';
|
|
15
|
+
export { validateAgentBundle } from './bundles/validate.js';
|
|
16
|
+
export { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION } from './bundles/util.js';
|
package/dist/mcp/server.js
CHANGED
|
@@ -16,7 +16,7 @@ export function createMcpServer(options = {}) {
|
|
|
16
16
|
return {
|
|
17
17
|
protocolVersion: message.params?.protocolVersion || '2024-11-05',
|
|
18
18
|
capabilities: { tools: { listChanged: false } },
|
|
19
|
-
serverInfo: { name: 'amalgm-agents', version: '0.1.
|
|
19
|
+
serverInfo: { name: 'amalgm-agents', version: '0.1.3' },
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
if (message.method === 'ping')
|
|
@@ -18,9 +18,9 @@ that domain.
|
|
|
18
18
|
| credential adapter | `authRef` resolver inside drivers |
|
|
19
19
|
| Supabase Chat session creation | Chat/cloud adapter observing Agents events |
|
|
20
20
|
|
|
21
|
-
Agent
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
Agent bundle-v2 graph and agent install laws live here. Engine supplies the
|
|
22
|
+
`AgentBundlePort` that exports and installs apps, automations, and tools through
|
|
23
|
+
their public SDKs; neither side imports another product's store.
|
|
24
24
|
|
|
25
25
|
## Required driver adapter
|
|
26
26
|
|
package/docs/REST.md
CHANGED
|
@@ -23,6 +23,18 @@ Set `nativeHomeDir` when embedding the server or `--native-home`/
|
|
|
23
23
|
`AMALGM_NATIVE_HOME` with the REST binary. Native import intentionally ignores
|
|
24
24
|
auth files, secrets, and MCP server configuration.
|
|
25
25
|
|
|
26
|
+
## Agent bundles
|
|
27
|
+
|
|
28
|
+
| Method | Path | Result |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `GET` | `/agent-bundles` | Read bundle-v2 capability and available agent heads |
|
|
31
|
+
| `POST` | `/agent-bundles/preview` | Export requested agent/app/automation/tool heads |
|
|
32
|
+
| `POST` | `/agent-bundles/install` | Validate and install `{ bundle, auth_ref? }` |
|
|
33
|
+
|
|
34
|
+
Embedding hosts inject `bundlePort` for resources owned by Apps, Automations,
|
|
35
|
+
and Toolbox. Agent-only bundles need no port. Bundles with external resources
|
|
36
|
+
fail explicitly when the owner port is absent.
|
|
37
|
+
|
|
26
38
|
## Agents
|
|
27
39
|
|
|
28
40
|
| Method | Path | Result |
|