@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 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. Internal revisions, ordered events, idempotency, and cancellation
51
- exist only to make the six behaviors above durable and predictable.
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,6 @@
1
+ import type { AgentBundle, BundleGraphEdge, BundleGraphElement } from './types.js';
2
+ export declare function buildBundleGraph(bundle: AgentBundle): {
3
+ headIds: string[];
4
+ elements: BundleGraphElement[];
5
+ edges: BundleGraphEdge[];
6
+ };
@@ -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,2 @@
1
+ import type { AgentBundle } from './types.js';
2
+ export declare function validateAgentBundle(input: unknown): AgentBundle;
@@ -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,2 @@
1
+ import type { RouteContext } from '../http-types.js';
2
+ export declare function routeAgentBundles(context: RouteContext): Promise<boolean>;
@@ -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
+ }
@@ -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.' } });
@@ -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';
@@ -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.2' },
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 bundles currently mix agents, apps, automations, and tool bindings. That
22
- cross-product packaging concern should remain outside this repository. A bundle
23
- installer may call each product's public apply method.
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first agent definitions, immutable revisions, and durable sessions.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,