amalgm 0.1.257 → 0.1.259
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/lib/cli.js +207 -3
- package/lib/layout.js +30 -9
- package/lib/shared-realtime-tunnel.js +12 -14
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/lib/layout.js +32 -9
- package/runtime/scripts/amalgm-mcp/observer/index.js +65 -4
- package/runtime/scripts/amalgm-mcp/observer/scan.js +60 -1
- package/runtime/scripts/amalgm-mcp/observer/store.js +43 -8
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +186 -32
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +33 -6
- package/runtime/scripts/amalgm-mcp/registration/index.js +21 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +117 -11
- package/runtime/scripts/amalgm-mcp/registry/index.js +1 -1
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +16 -8
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +6 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +67 -8
- package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +85 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +95 -3
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +21 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +3 -6
- package/runtime/scripts/amalgm-mcp/tests/toolbox-artifacts.test.js +88 -0
- package/runtime/scripts/amalgm-mcp/tests/toolbox-cloud-registration.test.js +91 -0
- package/runtime/scripts/amalgm-mcp/toolbox/artifacts.js +225 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +59 -10
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +14 -5
|
@@ -35,7 +35,7 @@ test('system toolbox catalog exposes first-party default tools', () => {
|
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
test('system toolbox catalog merge preserves
|
|
38
|
+
test('system toolbox catalog merge preserves user records but keeps built-ins authoritative', () => {
|
|
39
39
|
const merged = mergeSystemToolboxCatalog({
|
|
40
40
|
version: 1,
|
|
41
41
|
tools: {
|
|
@@ -59,12 +59,9 @@ test('system toolbox catalog merge preserves persisted records', () => {
|
|
|
59
59
|
},
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
assert.equal(merged.tools.browser.name, '
|
|
62
|
+
assert.equal(merged.tools.browser.name, 'Browser');
|
|
63
63
|
assert.equal(merged.tools['custom.cli'].name, 'Custom CLI');
|
|
64
|
-
assert.equal(
|
|
65
|
-
merged.toolActions['browser.browser_open'].name,
|
|
66
|
-
'persisted_navigate',
|
|
67
|
-
);
|
|
64
|
+
assert.equal(merged.toolActions['browser.browser_open'], undefined);
|
|
68
65
|
assert.equal(merged.toolActions['apps.apps_list'].name, 'apps_list');
|
|
69
66
|
});
|
|
70
67
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
SYSTEM_TOOL_ARTIFACTS,
|
|
11
|
+
artifactPathForTool,
|
|
12
|
+
isSystemToolId,
|
|
13
|
+
materializeSystemToolArtifacts,
|
|
14
|
+
migrateLegacyToolboxCatalog,
|
|
15
|
+
readToolArtifacts,
|
|
16
|
+
writeToolArtifact,
|
|
17
|
+
} = require('../toolbox/artifacts');
|
|
18
|
+
|
|
19
|
+
function temporaryToolbox() {
|
|
20
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-toolbox-artifacts-'));
|
|
21
|
+
const toolboxDir = path.join(home, 'toolbox');
|
|
22
|
+
fs.mkdirSync(toolboxDir);
|
|
23
|
+
return { home, toolboxDir };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('each built-in tool materializes as one distinct managed artifact', () => {
|
|
27
|
+
const { home, toolboxDir } = temporaryToolbox();
|
|
28
|
+
try {
|
|
29
|
+
const first = materializeSystemToolArtifacts({ toolboxDir });
|
|
30
|
+
assert.deepEqual(first.changedRelativePaths, SYSTEM_TOOL_ARTIFACTS.map((entry) => entry.relativePath));
|
|
31
|
+
const catalog = readToolArtifacts({ toolboxDir });
|
|
32
|
+
assert.deepEqual(Object.keys(catalog.tools).sort(), SYSTEM_TOOL_ARTIFACTS.map((entry) => entry.toolId).sort());
|
|
33
|
+
for (const entry of SYSTEM_TOOL_ARTIFACTS) {
|
|
34
|
+
const tool = catalog.tools[entry.toolId];
|
|
35
|
+
assert.equal(tool.origin, 'system');
|
|
36
|
+
assert.equal(tool.owner, 'amalgm');
|
|
37
|
+
assert.deepEqual(tool.source, { transport: 'amalgm-runtime', service: 'mcp' },
|
|
38
|
+
'the cloud artifact has no machine-local port');
|
|
39
|
+
assert.equal(path.basename(artifactPathForTool(tool, { toolboxDir })), path.basename(entry.relativePath));
|
|
40
|
+
assert.equal(isSystemToolId(tool.id), true);
|
|
41
|
+
}
|
|
42
|
+
assert.deepEqual(materializeSystemToolArtifacts({ toolboxDir }).changedRelativePaths, [],
|
|
43
|
+
'rerunning login does not rewrite an unchanged built-in tool');
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a user tool is a sibling artifact and remains independent of built-in ownership', () => {
|
|
50
|
+
const { home, toolboxDir } = temporaryToolbox();
|
|
51
|
+
try {
|
|
52
|
+
materializeSystemToolArtifacts({ toolboxDir });
|
|
53
|
+
const tool = { id: 'aayush.stock-price', name: 'Stock Price', origin: 'user', owner: 'aayush' };
|
|
54
|
+
const actions = [{ id: 'aayush.stock-price.lookup', toolId: tool.id, name: 'lookup' }];
|
|
55
|
+
const target = writeToolArtifact({ tool, actions }, { toolboxDir });
|
|
56
|
+
assert.equal(path.basename(target), 'tool-aayush.stock-price.json');
|
|
57
|
+
const catalog = readToolArtifacts({ toolboxDir });
|
|
58
|
+
assert.equal(catalog.tools[tool.id].origin, 'user');
|
|
59
|
+
assert.equal(catalog.toolActions[actions[0].id].toolId, tool.id);
|
|
60
|
+
assert.equal(isSystemToolId(tool.id), false);
|
|
61
|
+
} finally {
|
|
62
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a legacy aggregate catalog becomes user tool artifacts before leaving the registered toolbox tree', () => {
|
|
67
|
+
const { home, toolboxDir } = temporaryToolbox();
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(path.join(toolboxDir, 'toolbox.json'), JSON.stringify({
|
|
70
|
+
version: 1,
|
|
71
|
+
tools: {
|
|
72
|
+
'user.example': { id: 'user.example', name: 'Example', origin: 'user', owner: 'user' },
|
|
73
|
+
},
|
|
74
|
+
toolActions: {
|
|
75
|
+
'user.example.run': { id: 'user.example.run', toolId: 'user.example', name: 'run' },
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
const migrated = migrateLegacyToolboxCatalog({ toolboxDir });
|
|
79
|
+
assert.equal(migrated.migrated, true);
|
|
80
|
+
assert.equal(fs.existsSync(path.join(toolboxDir, 'toolbox.json')), false);
|
|
81
|
+
assert.equal(fs.existsSync(path.join(home, 'toolbox.legacy.json')), true);
|
|
82
|
+
const catalog = readToolArtifacts({ toolboxDir });
|
|
83
|
+
assert.equal(catalog.tools['user.example'].name, 'Example');
|
|
84
|
+
assert.equal(catalog.toolActions['user.example.run'].name, 'run');
|
|
85
|
+
} finally {
|
|
86
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The full automatic rail for a user-created tool:
|
|
5
|
+
*
|
|
6
|
+
* toolbox API write → one sibling artifact → existing toolbox watcher
|
|
7
|
+
* → UUID registry → durable entity-cloud outbox.
|
|
8
|
+
*
|
|
9
|
+
* This must run in its own test file because AMALGM_DIR is intentionally read
|
|
10
|
+
* once by the runtime composition root.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const Database = require('better-sqlite3');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const test = require('node:test');
|
|
19
|
+
|
|
20
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-toolbox-cloud-'));
|
|
21
|
+
process.env.AMALGM_DIR = home;
|
|
22
|
+
|
|
23
|
+
const { createRegistrationService } = require('../registration/service');
|
|
24
|
+
const { deleteTool, updateTool, upsertTool } = require('../toolbox/store');
|
|
25
|
+
|
|
26
|
+
let service = null;
|
|
27
|
+
|
|
28
|
+
test.after(() => {
|
|
29
|
+
service?.close();
|
|
30
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
async function waitFor(read, message) {
|
|
34
|
+
const deadline = Date.now() + 10_000;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
const value = read();
|
|
37
|
+
if (value) return value;
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
39
|
+
}
|
|
40
|
+
assert.fail(message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('a user tool becomes a cloud-backed entity through the existing toolbox watcher', async () => {
|
|
44
|
+
service = createRegistrationService({
|
|
45
|
+
dir: path.join(home, 'entities'),
|
|
46
|
+
homeDir: home,
|
|
47
|
+
cloudUserId: '11111111-1111-4111-8111-111111111111',
|
|
48
|
+
cloudDeviceId: 'toolbox-cloud-proof-device',
|
|
49
|
+
});
|
|
50
|
+
const userHome = await service.bootstrapUserHome();
|
|
51
|
+
const bootstrapRecords = await service.exactUserHomeCloudRecords();
|
|
52
|
+
const bootstrap = service.entityCloud.bootstrap(bootstrapRecords);
|
|
53
|
+
service.entityCloud.install({
|
|
54
|
+
registry: service.registration.registry,
|
|
55
|
+
resourceId: bootstrap.resourceId,
|
|
56
|
+
authorityEpoch: 1,
|
|
57
|
+
headVersion: 0,
|
|
58
|
+
snapshotBase64: bootstrap.snapshotBase64,
|
|
59
|
+
snapshotChecksum: bootstrap.snapshotChecksum,
|
|
60
|
+
bootstrapRecords,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.throws(() => updateTool({ id: 'browser', name: 'Changed Browser' }),
|
|
64
|
+
/System tools cannot be modified/);
|
|
65
|
+
assert.throws(() => deleteTool('browser'), /System tools cannot be deleted/);
|
|
66
|
+
|
|
67
|
+
upsertTool({
|
|
68
|
+
id: 'proof.stock',
|
|
69
|
+
name: 'Proof Stock',
|
|
70
|
+
type: 'cli',
|
|
71
|
+
origin: 'user',
|
|
72
|
+
owner: 'user',
|
|
73
|
+
source: { command: 'echo' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const toolbox = userHome.roots.find((entry) => entry.key === 'toolbox').record;
|
|
77
|
+
const artifact = await waitFor(
|
|
78
|
+
() => service.registration.registry.activeAt(toolbox.uuid, 'tool-proof.stock.json'),
|
|
79
|
+
'the watcher did not register the user tool artifact',
|
|
80
|
+
);
|
|
81
|
+
assert.ok(fs.existsSync(path.join(home, 'toolbox', 'tool-proof.stock.json')));
|
|
82
|
+
|
|
83
|
+
const db = new Database(path.join(home, 'entities', 'identity.db'), { readonly: true });
|
|
84
|
+
try {
|
|
85
|
+
const rows = db.prepare('SELECT operation_json FROM entity_cloud_mutations').all();
|
|
86
|
+
assert.equal(rows.some((row) => JSON.parse(row.operation_json).record?.uuid === artifact.uuid), true,
|
|
87
|
+
'registration wrote one durable cloud delivery intent for the tool entity');
|
|
88
|
+
} finally {
|
|
89
|
+
db.close();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Toolbox filesystem artifacts.
|
|
5
|
+
*
|
|
6
|
+
* Purpose: one tool must have one cloud-addressable file, so a user-created
|
|
7
|
+
* tool can travel between machines without ever granting it the authority of
|
|
8
|
+
* a built-in Amalgm tool.
|
|
9
|
+
*
|
|
10
|
+
* Axioms:
|
|
11
|
+
* - A tool definition and every action belonging to it live in one artifact.
|
|
12
|
+
* - Built-in artifact paths are the exact managed nodes declared by layout.
|
|
13
|
+
* - User artifacts are siblings, never children of a managed tool path.
|
|
14
|
+
* - Portable artifacts never carry a machine-local address or watcher fact.
|
|
15
|
+
* - The SQLite toolbox tables and toolbox.index.json are local derived caches;
|
|
16
|
+
* artifact files are the portable representation.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const { AMALGM_DIR } = require('../config');
|
|
23
|
+
const { SYSTEM_TOOL_ARTIFACTS } = require('../lib/layout');
|
|
24
|
+
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
25
|
+
|
|
26
|
+
const TOOL_ARTIFACT_SCHEMA_VERSION = 1;
|
|
27
|
+
const TOOLBOX_DIR = path.join(AMALGM_DIR, 'toolbox');
|
|
28
|
+
const TOOLBOX_INDEX_FILE = path.join(AMALGM_DIR, 'toolbox.index.json');
|
|
29
|
+
const LEGACY_TOOLBOX_FILE_NAME = 'toolbox.json';
|
|
30
|
+
|
|
31
|
+
function safeToolId(value) {
|
|
32
|
+
return String(value || '')
|
|
33
|
+
.trim()
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9_.-]+/g, '.')
|
|
36
|
+
.replace(/\.+/g, '.')
|
|
37
|
+
.replace(/^[._-]+|[._-]+$/g, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function systemArtifactForTool(toolId) {
|
|
41
|
+
return SYSTEM_TOOL_ARTIFACTS.find((entry) => entry.toolId === toolId) || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isSystemToolId(toolId) {
|
|
45
|
+
return !!systemArtifactForTool(toolId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function userArtifactFileName(toolId) {
|
|
49
|
+
const id = safeToolId(toolId);
|
|
50
|
+
if (!id) throw new Error('tool artifact requires a stable tool id');
|
|
51
|
+
return `tool-${id}.json`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toolboxDirFor(options = {}) {
|
|
55
|
+
return path.resolve(options.toolboxDir || TOOLBOX_DIR);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function indexFileFor(toolboxDir) {
|
|
59
|
+
return path.join(path.dirname(toolboxDir), 'toolbox.index.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function artifactPathForTool(tool, options = {}) {
|
|
63
|
+
const toolboxDir = toolboxDirFor(options);
|
|
64
|
+
const toolId = safeToolId(tool?.id || tool);
|
|
65
|
+
const system = systemArtifactForTool(toolId);
|
|
66
|
+
if (system) return path.join(toolboxDir, path.basename(system.relativePath));
|
|
67
|
+
return path.join(toolboxDir, userArtifactFileName(toolId));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function artifactDocument({ tool, actions = [] }) {
|
|
71
|
+
if (!tool || typeof tool !== 'object' || !safeToolId(tool.id)) {
|
|
72
|
+
throw new Error('tool artifact requires a tool with an id');
|
|
73
|
+
}
|
|
74
|
+
const toolId = safeToolId(tool.id);
|
|
75
|
+
const normalizedActions = Array.isArray(actions)
|
|
76
|
+
? actions.filter((action) => action && action.toolId === toolId)
|
|
77
|
+
: [];
|
|
78
|
+
const portableTool = isSystemToolId(toolId)
|
|
79
|
+
? {
|
|
80
|
+
...tool,
|
|
81
|
+
id: toolId,
|
|
82
|
+
// `127.0.0.1:<port>` is a device binding, not tool identity. The
|
|
83
|
+
// system catalog resolves the current runtime endpoint when serving
|
|
84
|
+
// the local tool surface.
|
|
85
|
+
source: { transport: 'amalgm-runtime', service: 'mcp' },
|
|
86
|
+
}
|
|
87
|
+
: { ...tool, id: toolId };
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: TOOL_ARTIFACT_SCHEMA_VERSION,
|
|
90
|
+
kind: 'amalgm-tool',
|
|
91
|
+
tool: portableTool,
|
|
92
|
+
actions: normalizedActions
|
|
93
|
+
.map((action) => ({ ...action, toolId }))
|
|
94
|
+
.sort((left, right) => String(left.id).localeCompare(String(right.id))),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function validDocument(value) {
|
|
99
|
+
return !!value
|
|
100
|
+
&& value.schemaVersion === TOOL_ARTIFACT_SCHEMA_VERSION
|
|
101
|
+
&& value.kind === 'amalgm-tool'
|
|
102
|
+
&& value.tool
|
|
103
|
+
&& typeof value.tool === 'object'
|
|
104
|
+
&& !!safeToolId(value.tool.id)
|
|
105
|
+
&& Array.isArray(value.actions)
|
|
106
|
+
&& value.actions.every((action) => action && action.toolId === safeToolId(value.tool.id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeToolArtifact({ tool, actions }, options = {}) {
|
|
110
|
+
const target = artifactPathForTool(tool, options);
|
|
111
|
+
ensureDir(path.dirname(target));
|
|
112
|
+
const document = artifactDocument({ tool, actions });
|
|
113
|
+
const current = readJson(target, null);
|
|
114
|
+
if (JSON.stringify(current) !== JSON.stringify(document)) writeJsonAtomic(target, document);
|
|
115
|
+
return target;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function removeUserToolArtifact(toolId, options = {}) {
|
|
119
|
+
if (isSystemToolId(safeToolId(toolId))) {
|
|
120
|
+
throw new Error('System tool artifacts cannot be removed');
|
|
121
|
+
}
|
|
122
|
+
const target = artifactPathForTool(toolId, options);
|
|
123
|
+
try { fs.unlinkSync(target); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
124
|
+
return target;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readToolArtifacts(options = {}) {
|
|
128
|
+
const toolboxDir = toolboxDirFor(options);
|
|
129
|
+
const tools = {};
|
|
130
|
+
const toolActions = {};
|
|
131
|
+
let entries = [];
|
|
132
|
+
try { entries = fs.readdirSync(toolboxDir, { withFileTypes: true }); } catch { return { tools, toolActions }; }
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!entry.isFile() || !entry.name.endsWith('.json') || entry.name === LEGACY_TOOLBOX_FILE_NAME) continue;
|
|
135
|
+
const document = readJson(path.join(toolboxDir, entry.name), null);
|
|
136
|
+
if (!validDocument(document)) continue;
|
|
137
|
+
const toolId = safeToolId(document.tool.id);
|
|
138
|
+
const expected = artifactPathForTool(toolId, { toolboxDir });
|
|
139
|
+
if (path.resolve(expected) !== path.resolve(path.join(toolboxDir, entry.name))) continue;
|
|
140
|
+
tools[toolId] = { ...document.tool, id: toolId };
|
|
141
|
+
for (const action of document.actions) toolActions[action.id] = { ...action, toolId };
|
|
142
|
+
}
|
|
143
|
+
return { tools, toolActions };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function writeToolboxIndex(toolbox, options = {}) {
|
|
147
|
+
const toolboxDir = toolboxDirFor(options);
|
|
148
|
+
const target = options.indexFile || indexFileFor(toolboxDir);
|
|
149
|
+
const document = {
|
|
150
|
+
version: 1,
|
|
151
|
+
tools: toolbox?.tools || {},
|
|
152
|
+
toolActions: toolbox?.toolActions || {},
|
|
153
|
+
};
|
|
154
|
+
const current = readJson(target, null);
|
|
155
|
+
if (JSON.stringify(current) !== JSON.stringify(document)) writeJsonAtomic(target, document);
|
|
156
|
+
return target;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function materializeSystemToolArtifacts(options = {}) {
|
|
160
|
+
const toolboxDir = toolboxDirFor(options);
|
|
161
|
+
const { buildSystemToolboxCatalog } = require('./system-catalog');
|
|
162
|
+
const catalog = buildSystemToolboxCatalog();
|
|
163
|
+
const changedRelativePaths = [];
|
|
164
|
+
for (const system of SYSTEM_TOOL_ARTIFACTS) {
|
|
165
|
+
const tool = catalog.tools[system.toolId];
|
|
166
|
+
if (!tool) throw new Error(`built-in toolbox definition missing for ${system.toolId}`);
|
|
167
|
+
const target = artifactPathForTool(tool, { toolboxDir });
|
|
168
|
+
const document = artifactDocument({
|
|
169
|
+
tool,
|
|
170
|
+
actions: Object.values(catalog.toolActions).filter((action) => action.toolId === tool.id),
|
|
171
|
+
});
|
|
172
|
+
if (JSON.stringify(readJson(target, null)) !== JSON.stringify(document)) {
|
|
173
|
+
ensureDir(path.dirname(target));
|
|
174
|
+
writeJsonAtomic(target, document);
|
|
175
|
+
changedRelativePaths.push(system.relativePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { catalog, changedRelativePaths };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Move the old aggregate catalog out of the registered toolbox tree only
|
|
182
|
+
* after all its user records have a per-tool replacement. The new index is a
|
|
183
|
+
* local cache next to, not inside, the cloud-backed toolbox folder. */
|
|
184
|
+
function migrateLegacyToolboxCatalog(options = {}) {
|
|
185
|
+
const toolboxDir = toolboxDirFor(options);
|
|
186
|
+
const legacy = path.join(toolboxDir, LEGACY_TOOLBOX_FILE_NAME);
|
|
187
|
+
if (!fs.existsSync(legacy)) return { migrated: false, artifactPaths: [] };
|
|
188
|
+
const parsed = readJson(legacy, null);
|
|
189
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.tools || typeof parsed.tools !== 'object') {
|
|
190
|
+
return { migrated: false, artifactPaths: [] };
|
|
191
|
+
}
|
|
192
|
+
const artifactPaths = [];
|
|
193
|
+
for (const [rawId, rawTool] of Object.entries(parsed.tools)) {
|
|
194
|
+
const toolId = safeToolId(rawTool?.id || rawId);
|
|
195
|
+
if (!toolId || isSystemToolId(toolId) || !rawTool || typeof rawTool !== 'object') continue;
|
|
196
|
+
const actions = Object.values(parsed.toolActions || {})
|
|
197
|
+
.filter((action) => action && action.toolId === toolId);
|
|
198
|
+
artifactPaths.push(writeToolArtifact({ tool: { ...rawTool, id: toolId }, actions }, { toolboxDir }));
|
|
199
|
+
}
|
|
200
|
+
writeToolboxIndex({ tools: parsed.tools || {}, toolActions: parsed.toolActions || {} }, { toolboxDir });
|
|
201
|
+
let migrated = path.join(path.dirname(toolboxDir), 'toolbox.legacy.json');
|
|
202
|
+
if (fs.existsSync(migrated)) migrated = path.join(path.dirname(toolboxDir), `toolbox.legacy-${Date.now()}.json`);
|
|
203
|
+
fs.renameSync(legacy, migrated);
|
|
204
|
+
return { migrated: true, artifactPaths };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
LEGACY_TOOLBOX_FILE_NAME,
|
|
209
|
+
SYSTEM_TOOL_ARTIFACTS,
|
|
210
|
+
TOOL_ARTIFACT_SCHEMA_VERSION,
|
|
211
|
+
TOOLBOX_DIR,
|
|
212
|
+
TOOLBOX_INDEX_FILE,
|
|
213
|
+
artifactDocument,
|
|
214
|
+
artifactPathForTool,
|
|
215
|
+
indexFileFor,
|
|
216
|
+
isSystemToolId,
|
|
217
|
+
materializeSystemToolArtifacts,
|
|
218
|
+
migrateLegacyToolboxCatalog,
|
|
219
|
+
readToolArtifacts,
|
|
220
|
+
removeUserToolArtifact,
|
|
221
|
+
safeToolId,
|
|
222
|
+
userArtifactFileName,
|
|
223
|
+
writeToolArtifact,
|
|
224
|
+
writeToolboxIndex,
|
|
225
|
+
};
|
|
@@ -12,14 +12,25 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
|
-
const {
|
|
16
|
-
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
15
|
+
const { readJson } = require('../lib/storage');
|
|
17
16
|
const { openLocalDb } = require('../state/db');
|
|
18
17
|
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
18
|
+
const {
|
|
19
|
+
TOOLBOX_DIR,
|
|
20
|
+
TOOLBOX_INDEX_FILE,
|
|
21
|
+
isSystemToolId,
|
|
22
|
+
readToolArtifacts,
|
|
23
|
+
removeUserToolArtifact,
|
|
24
|
+
writeToolArtifact,
|
|
25
|
+
writeToolboxIndex,
|
|
26
|
+
} = require('./artifacts');
|
|
19
27
|
|
|
20
28
|
const TOOLBOX_VERSION = 1;
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
// `toolbox.index.json` is a local cache, deliberately outside the registered
|
|
30
|
+
// toolbox folder. Individual `toolbox/*.json` artifacts are the portable
|
|
31
|
+
// source of truth; `toolbox/toolbox.json` remains only a legacy import path.
|
|
32
|
+
const TOOLBOX_FILE = TOOLBOX_INDEX_FILE;
|
|
33
|
+
const LEGACY_TOOLBOX_FILE = path.join(TOOLBOX_DIR, 'toolbox.json');
|
|
23
34
|
|
|
24
35
|
const TOOL_TYPES = new Set(['mcp', 'api', 'cli']);
|
|
25
36
|
const TOOL_ORIGINS = new Set(['system', 'user', 'catalog']);
|
|
@@ -528,19 +539,32 @@ function writeToolboxToDb(toolbox, options = {}) {
|
|
|
528
539
|
}
|
|
529
540
|
|
|
530
541
|
function exportToolboxJson(toolbox) {
|
|
531
|
-
|
|
532
|
-
writeJsonAtomic(TOOLBOX_FILE, normalizeToolbox(toolbox));
|
|
542
|
+
writeToolboxIndex(normalizeToolbox(toolbox), { toolboxDir: TOOLBOX_DIR });
|
|
533
543
|
}
|
|
534
544
|
|
|
535
545
|
function migrateLegacyToolboxFileIfNeeded() {
|
|
536
546
|
const db = openLocalDb();
|
|
537
547
|
if (toolboxRowCount(db) > 0) return;
|
|
538
548
|
|
|
539
|
-
const legacyToolbox = dropInheritedHarnessTools(normalizeToolbox(readJson(
|
|
549
|
+
const legacyToolbox = dropInheritedHarnessTools(normalizeToolbox(readJson(LEGACY_TOOLBOX_FILE, emptyToolbox())));
|
|
540
550
|
if (Object.keys(legacyToolbox.tools).length === 0 && Object.keys(legacyToolbox.toolActions).length === 0) return;
|
|
541
551
|
writeToolboxToDb(legacyToolbox, { replace: true });
|
|
542
552
|
}
|
|
543
553
|
|
|
554
|
+
function writeUserToolArtifacts(toolbox) {
|
|
555
|
+
for (const tool of Object.values(toolbox.tools || {})) {
|
|
556
|
+
if (!tool || isSystemToolId(tool.id) || tool.origin === 'system') continue;
|
|
557
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function cacheToolboxFromArtifacts(toolbox) {
|
|
562
|
+
const normalized = normalizeToolbox(toolbox);
|
|
563
|
+
writeToolboxToDb(normalized, { replace: true });
|
|
564
|
+
exportToolboxJson(normalized);
|
|
565
|
+
return normalized;
|
|
566
|
+
}
|
|
567
|
+
|
|
544
568
|
function publishCommittedEvents(events) {
|
|
545
569
|
for (const event of events) publishStateEvent(event);
|
|
546
570
|
}
|
|
@@ -569,14 +593,18 @@ function insertToolActionEvent(db, op, action, options = {}) {
|
|
|
569
593
|
|
|
570
594
|
function readToolbox() {
|
|
571
595
|
migrateLegacyToolboxFileIfNeeded();
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
596
|
+
// An existing device may still have user records only in its local cache.
|
|
597
|
+
// Give each one a portable artifact before treating artifacts as canonical.
|
|
598
|
+
const cached = readToolboxFromDb();
|
|
599
|
+
dropLegacyAggregateAmalgmTool(cached);
|
|
600
|
+
writeUserToolArtifacts(cached);
|
|
601
|
+
const toolbox = cacheToolboxFromArtifacts(readToolArtifacts({ toolboxDir: TOOLBOX_DIR }));
|
|
575
602
|
return toolbox;
|
|
576
603
|
}
|
|
577
604
|
|
|
578
605
|
function writeToolbox(toolbox) {
|
|
579
606
|
const normalized = writeToolboxToDb(toolbox, { replace: true });
|
|
607
|
+
writeUserToolArtifacts(normalized);
|
|
580
608
|
exportToolboxJson(normalized);
|
|
581
609
|
return normalized;
|
|
582
610
|
}
|
|
@@ -614,6 +642,9 @@ function upsertTool(input) {
|
|
|
614
642
|
const existing = input?.id ? toolbox.tools[sanitizeId(input.id)] : null;
|
|
615
643
|
const existingActionIds = new Set(Object.keys(toolbox.toolActions));
|
|
616
644
|
const tool = normalizeTool(input, existing);
|
|
645
|
+
if (isSystemToolId(tool.id) || tool.origin === 'system') {
|
|
646
|
+
throw new Error('System tools are managed by Amalgm and cannot be created or replaced here');
|
|
647
|
+
}
|
|
617
648
|
toolbox.tools[tool.id] = tool;
|
|
618
649
|
|
|
619
650
|
const actions = [];
|
|
@@ -659,6 +690,8 @@ function upsertTool(input) {
|
|
|
659
690
|
}
|
|
660
691
|
return inserted;
|
|
661
692
|
})();
|
|
693
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
694
|
+
exportToolboxJson(toolbox);
|
|
662
695
|
publishCommittedEvents(events);
|
|
663
696
|
return { tool, actions };
|
|
664
697
|
}
|
|
@@ -735,6 +768,8 @@ function updateTool(input) {
|
|
|
735
768
|
source: 'toolbox:update-tool',
|
|
736
769
|
})];
|
|
737
770
|
})();
|
|
771
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
772
|
+
exportToolboxJson(toolbox);
|
|
738
773
|
publishCommittedEvents(events);
|
|
739
774
|
return { tool, actions: toolActionsForTool(toolbox, tool.id) };
|
|
740
775
|
}
|
|
@@ -744,6 +779,7 @@ function upsertToolAction(input) {
|
|
|
744
779
|
const toolId = sanitizeId(input?.toolId || input?.providerId || input?.tool);
|
|
745
780
|
const tool = toolbox.tools[toolId];
|
|
746
781
|
if (!tool) throw new Error(`Unknown tool: ${toolId}`);
|
|
782
|
+
if (tool.origin === 'system' || isSystemToolId(tool.id)) throw new Error('System tool actions cannot be modified');
|
|
747
783
|
const existing = input?.id ? toolbox.toolActions[sanitizeId(input.id)] : null;
|
|
748
784
|
const action = normalizeAction(input, tool, existing);
|
|
749
785
|
toolbox.toolActions[action.id] = action;
|
|
@@ -755,6 +791,8 @@ function upsertToolAction(input) {
|
|
|
755
791
|
source: 'toolbox:upsert-action',
|
|
756
792
|
})];
|
|
757
793
|
})();
|
|
794
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
795
|
+
exportToolboxJson(toolbox);
|
|
758
796
|
publishCommittedEvents(events);
|
|
759
797
|
return action;
|
|
760
798
|
}
|
|
@@ -805,6 +843,8 @@ function updateToolAction(input) {
|
|
|
805
843
|
source: 'toolbox:update-action',
|
|
806
844
|
})];
|
|
807
845
|
})();
|
|
846
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
847
|
+
exportToolboxJson(toolbox);
|
|
808
848
|
publishCommittedEvents(events);
|
|
809
849
|
return action;
|
|
810
850
|
}
|
|
@@ -830,6 +870,12 @@ function deleteTool(id, options = {}) {
|
|
|
830
870
|
}));
|
|
831
871
|
return inserted;
|
|
832
872
|
})();
|
|
873
|
+
removeUserToolArtifact(toolId, { toolboxDir: TOOLBOX_DIR });
|
|
874
|
+
exportToolboxJson({
|
|
875
|
+
...toolbox,
|
|
876
|
+
tools: Object.fromEntries(Object.entries(toolbox.tools).filter(([id]) => id !== toolId)),
|
|
877
|
+
toolActions: Object.fromEntries(Object.entries(toolbox.toolActions).filter(([, action]) => action.toolId !== toolId)),
|
|
878
|
+
});
|
|
833
879
|
publishCommittedEvents(events);
|
|
834
880
|
scrubToolFromApps(toolId);
|
|
835
881
|
return { ok: true, deleted: true };
|
|
@@ -867,6 +913,9 @@ function deleteToolAction(id, options = {}) {
|
|
|
867
913
|
source: 'toolbox:delete-action',
|
|
868
914
|
})];
|
|
869
915
|
})();
|
|
916
|
+
delete toolbox.toolActions[actionId];
|
|
917
|
+
writeToolArtifact({ tool, actions: toolActionsForTool(toolbox, tool.id) }, { toolboxDir: TOOLBOX_DIR });
|
|
918
|
+
exportToolboxJson(toolbox);
|
|
870
919
|
publishCommittedEvents(events);
|
|
871
920
|
return { ok: true, deleted: true };
|
|
872
921
|
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Virtual first-party Toolbox catalog entries.
|
|
5
5
|
*
|
|
6
|
-
* These records
|
|
7
|
-
*
|
|
8
|
-
* MCP
|
|
6
|
+
* These records are the canonical definitions materialized as one exact
|
|
7
|
+
* `toolbox/amalgm-*.json` artifact per built-in tool. The overlay keeps the
|
|
8
|
+
* legacy MCP surface stable and refuses a stale local cache from overriding a
|
|
9
|
+
* product-managed definition.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { PORT } = require('../config');
|
|
@@ -29,6 +30,10 @@ function coreToolGroups() {
|
|
|
29
30
|
return require('../server/core-tools').CORE_TOOL_GROUPS || [];
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function systemToolIds() {
|
|
34
|
+
return new Set(coreToolGroups().map((group) => group.id));
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
function systemToolForGroup(group) {
|
|
33
38
|
return {
|
|
34
39
|
id: group.id,
|
|
@@ -95,12 +100,15 @@ function buildSystemToolboxCatalog() {
|
|
|
95
100
|
function persistedCatalogWithoutLegacyAggregate(toolbox) {
|
|
96
101
|
const tools = { ...(toolbox?.tools || {}) };
|
|
97
102
|
const toolActions = { ...(toolbox?.toolActions || {}) };
|
|
103
|
+
const systemIds = systemToolIds();
|
|
98
104
|
delete tools.amalgm;
|
|
105
|
+
for (const systemId of systemIds) delete tools[systemId];
|
|
99
106
|
for (const toolId of LEGACY_FIRST_PARTY_TOOL_IDS) delete tools[toolId];
|
|
100
107
|
for (const [actionId, action] of Object.entries(toolActions)) {
|
|
101
108
|
const legacyGroupId = actionId.split('.').slice(0, 2).join('.');
|
|
102
109
|
if (
|
|
103
110
|
action?.toolId === 'amalgm'
|
|
111
|
+
|| systemIds.has(action?.toolId)
|
|
104
112
|
|| LEGACY_FIRST_PARTY_TOOL_IDS.has(action?.toolId)
|
|
105
113
|
|| LEGACY_FIRST_PARTY_TOOL_IDS.has(legacyGroupId)
|
|
106
114
|
) {
|
|
@@ -116,18 +124,19 @@ function mergeSystemToolboxCatalog(toolbox) {
|
|
|
116
124
|
return {
|
|
117
125
|
...toolbox,
|
|
118
126
|
tools: {
|
|
119
|
-
...catalog.tools,
|
|
120
127
|
...persisted.tools,
|
|
128
|
+
...catalog.tools,
|
|
121
129
|
},
|
|
122
130
|
toolActions: {
|
|
123
|
-
...catalog.toolActions,
|
|
124
131
|
...persisted.toolActions,
|
|
132
|
+
...catalog.toolActions,
|
|
125
133
|
},
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
module.exports = {
|
|
130
138
|
buildSystemToolboxCatalog,
|
|
139
|
+
systemToolIds,
|
|
131
140
|
mergeSystemToolboxCatalog,
|
|
132
141
|
persistedCatalogWithoutLegacyAggregate,
|
|
133
142
|
};
|