amalgm 0.1.75 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/agent-config/importers/claude.js +52 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/codex.js +36 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/common.js +115 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/index.js +14 -0
- package/runtime/scripts/amalgm-mcp/agent-config/importers/opencode.js +59 -0
- package/runtime/scripts/amalgm-mcp/agent-config/renderers/hooks.js +55 -0
- package/runtime/scripts/amalgm-mcp/agent-config/renderers/instructions.js +54 -0
- package/runtime/scripts/amalgm-mcp/agent-config/rest.js +168 -0
- package/runtime/scripts/amalgm-mcp/agent-config/schema.js +248 -0
- package/runtime/scripts/amalgm-mcp/agent-config/store.js +123 -0
- package/runtime/scripts/amalgm-mcp/agents/hooks.js +2 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +45 -2
- package/runtime/scripts/amalgm-mcp/agents/talk.js +20 -4
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +52 -9
- package/runtime/scripts/amalgm-mcp/automations/tools.js +3 -3
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
- package/runtime/scripts/amalgm-mcp/lib/chat-payloads.js +101 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +10 -10
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/agents.js +13 -0
- package/runtime/scripts/amalgm-mcp/server/routes/chat-payloads.js +37 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +13 -3
- package/runtime/scripts/amalgm-mcp/tests/agent-config.test.js +80 -0
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +17 -5
- package/runtime/scripts/amalgm-mcp/tests/chat-payloads.test.js +71 -0
- package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +9 -9
- package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +29 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +21 -16
- package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +9 -7
- package/runtime/scripts/amalgm-mcp/toolbox/selection.js +21 -1
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +16 -1
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +8 -2
- package/runtime/scripts/chat-core/adapters/claude.js +2 -2
- package/runtime/scripts/chat-core/adapters/codex.js +82 -12
- package/runtime/scripts/chat-core/auth.js +13 -4
- package/runtime/scripts/chat-core/contract.js +18 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +27 -2
- package/runtime/scripts/chat-core/tests/native-config.test.js +65 -12
- package/runtime/scripts/chat-core/tooling/native-config.js +73 -0
- package/runtime/scripts/chat-core/tooling/runtime-home.js +2 -2
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -1
- package/runtime/scripts/local-gateway.js +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { openLocalDb } = require('../state/db');
|
|
4
|
+
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
5
|
+
|
|
6
|
+
const CHAT_PAYLOADS_META_KEY = 'chat_payloads';
|
|
7
|
+
const CHAT_PAYLOADS_RESOURCE = 'chat_payloads';
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanString(value) {
|
|
14
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseStoredPayloads(value) {
|
|
18
|
+
if (typeof value !== 'string' || !value) return {};
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(value);
|
|
21
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readPayloadMap() {
|
|
28
|
+
const row = openLocalDb()
|
|
29
|
+
.prepare('SELECT value FROM local_meta WHERE key = ?')
|
|
30
|
+
.get(CHAT_PAYLOADS_META_KEY);
|
|
31
|
+
return parseStoredPayloads(row?.value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeRecord(input, existing = null) {
|
|
35
|
+
const body = isPlainObject(input) ? input : {};
|
|
36
|
+
const id = cleanString(body.id || body.payloadId || existing?.id);
|
|
37
|
+
if (!id) throw new Error('chat payload id is required');
|
|
38
|
+
|
|
39
|
+
const payload = isPlainObject(body.payload)
|
|
40
|
+
? body.payload
|
|
41
|
+
: isPlainObject(existing?.payload)
|
|
42
|
+
? existing.payload
|
|
43
|
+
: null;
|
|
44
|
+
if (!payload) throw new Error('chat payload is required');
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
revision: cleanString(body.revision || body.payloadRevision || existing?.revision) || 'unknown',
|
|
49
|
+
payload,
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function listChatPayloads() {
|
|
55
|
+
return readPayloadMap();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getChatPayload(id) {
|
|
59
|
+
const payloadId = cleanString(id);
|
|
60
|
+
if (!payloadId) return null;
|
|
61
|
+
return readPayloadMap()[payloadId] || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeChatPayload(input, options = {}) {
|
|
65
|
+
const db = openLocalDb();
|
|
66
|
+
let record;
|
|
67
|
+
const event = db.transaction(() => {
|
|
68
|
+
const current = readPayloadMap();
|
|
69
|
+
record = normalizeRecord(input, current[cleanString(input?.id || input?.payloadId)] || null);
|
|
70
|
+
const next = {
|
|
71
|
+
...current,
|
|
72
|
+
[record.id]: record,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
db.prepare(`
|
|
76
|
+
INSERT INTO local_meta (key, value, updated_at)
|
|
77
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
78
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
79
|
+
value = excluded.value,
|
|
80
|
+
updated_at = excluded.updated_at
|
|
81
|
+
`).run(CHAT_PAYLOADS_META_KEY, JSON.stringify(next));
|
|
82
|
+
|
|
83
|
+
return insertStateEvent(db, {
|
|
84
|
+
resource: CHAT_PAYLOADS_RESOURCE,
|
|
85
|
+
op: 'update',
|
|
86
|
+
id: record.id,
|
|
87
|
+
value: record,
|
|
88
|
+
source: options.source || 'chat-payloads',
|
|
89
|
+
});
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
publishStateEvent(event);
|
|
93
|
+
return record;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
CHAT_PAYLOADS_RESOURCE,
|
|
98
|
+
getChatPayload,
|
|
99
|
+
listChatPayloads,
|
|
100
|
+
writeChatPayload,
|
|
101
|
+
};
|
|
@@ -28,32 +28,32 @@ function group(id, name, description, tools) {
|
|
|
28
28
|
|
|
29
29
|
const CORE_TOOL_GROUPS = [
|
|
30
30
|
group(
|
|
31
|
-
'
|
|
32
|
-
'
|
|
31
|
+
'automations',
|
|
32
|
+
'Automations',
|
|
33
33
|
'Create, inspect, and run scheduled or event-driven automations.',
|
|
34
34
|
require('../automations/tools'),
|
|
35
35
|
),
|
|
36
36
|
group(
|
|
37
|
-
'
|
|
38
|
-
'
|
|
37
|
+
'notifications',
|
|
38
|
+
'Notifications',
|
|
39
39
|
'Notify the user about important results, completions, and failures.',
|
|
40
40
|
require('../notify'),
|
|
41
41
|
),
|
|
42
42
|
group(
|
|
43
|
-
'
|
|
44
|
-
'
|
|
43
|
+
'agents',
|
|
44
|
+
'Agents',
|
|
45
45
|
'Discover and talk to other agents on the platform.',
|
|
46
46
|
require('../agents/tools'),
|
|
47
47
|
),
|
|
48
48
|
group(
|
|
49
|
-
'
|
|
50
|
-
'
|
|
49
|
+
'apps',
|
|
50
|
+
'Apps',
|
|
51
51
|
'Register, run, and route local-first Amalgm apps.',
|
|
52
52
|
require('../apps/tools'),
|
|
53
53
|
),
|
|
54
54
|
group(
|
|
55
|
-
'
|
|
56
|
-
'
|
|
55
|
+
'browser',
|
|
56
|
+
'Browser',
|
|
57
57
|
"Use Amalgm's visible browser session for navigation, interaction, and screenshots.",
|
|
58
58
|
require('../browser/tools'),
|
|
59
59
|
),
|
|
@@ -12,6 +12,7 @@ const { handleHealthRoutes } = require('./routes/health');
|
|
|
12
12
|
const { handleStateRoutes } = require('./routes/state');
|
|
13
13
|
const { handleBrowserRoutes } = require('./routes/browser');
|
|
14
14
|
const { handlePreferenceRoutes } = require('./routes/preferences');
|
|
15
|
+
const { handleChatPayloadRoutes } = require('./routes/chat-payloads');
|
|
15
16
|
const { handleAgentRoutes } = require('./routes/agents');
|
|
16
17
|
const { handleEventRoutes } = require('./routes/events');
|
|
17
18
|
const { handleAutomationRoutes } = require('./routes/automations');
|
|
@@ -28,6 +29,7 @@ const DEFAULT_ROUTE_GROUPS = [
|
|
|
28
29
|
handleStateRoutes,
|
|
29
30
|
handleBrowserRoutes,
|
|
30
31
|
handlePreferenceRoutes,
|
|
32
|
+
handleChatPayloadRoutes,
|
|
31
33
|
handleAgentRoutes,
|
|
32
34
|
handleEventRoutes,
|
|
33
35
|
handleAutomationRoutes,
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const agentsRest = require('../../agents/rest');
|
|
4
|
+
const agentConfigRest = require('../../agent-config/rest');
|
|
4
5
|
|
|
5
6
|
async function handleAgentRoutes(ctx) {
|
|
7
|
+
if (ctx.pathname === '/agent-config/get' && ctx.method === 'POST') {
|
|
8
|
+
agentConfigRest.handleGet(await ctx.readJsonBody(), ctx.sendJson);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (ctx.pathname === '/agent-config/update' && ctx.method === 'POST') {
|
|
12
|
+
agentConfigRest.handleUpdate(await ctx.readJsonBody(), ctx.sendJson);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (ctx.pathname === '/agent-config/import-native' && ctx.method === 'POST') {
|
|
16
|
+
agentConfigRest.handleImportNative(await ctx.readJsonBody(), ctx.sendJson);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
6
19
|
if (ctx.pathname === '/agents/list' && ctx.method === 'GET') {
|
|
7
20
|
agentsRest.handleList(ctx.sendJson);
|
|
8
21
|
return true;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chatPayloads = require('../../lib/chat-payloads');
|
|
4
|
+
|
|
5
|
+
async function handleChatPayloadRoutes(ctx) {
|
|
6
|
+
if (ctx.pathname === '/chat-payloads' && ctx.method === 'GET') {
|
|
7
|
+
const query = ctx.getQuery();
|
|
8
|
+
const payload = query.id ? chatPayloads.getChatPayload(query.id) : null;
|
|
9
|
+
ctx.sendJson(payload ? 200 : (query.id ? 404 : 200), {
|
|
10
|
+
...(query.id ? { payload } : {}),
|
|
11
|
+
payloads: chatPayloads.listChatPayloads(),
|
|
12
|
+
...(query.id && !payload ? { error: 'Chat payload not found' } : {}),
|
|
13
|
+
});
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (ctx.pathname === '/chat-payloads/get' && ctx.method === 'POST') {
|
|
18
|
+
const body = await ctx.readJsonBody();
|
|
19
|
+
const payload = chatPayloads.getChatPayload(body?.id || body?.payloadId);
|
|
20
|
+
if (!payload) {
|
|
21
|
+
ctx.sendJson(404, { error: 'Chat payload not found' });
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
ctx.sendJson(200, { payload });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (ctx.pathname === '/chat-payloads' && (ctx.method === 'PUT' || ctx.method === 'POST')) {
|
|
29
|
+
const payload = chatPayloads.writeChatPayload(await ctx.readJsonBody());
|
|
30
|
+
ctx.sendJson(200, { success: true, payload });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { handleChatPayloadRoutes };
|
|
@@ -296,6 +296,17 @@ function migrate(database = openLocalDb()) {
|
|
|
296
296
|
CREATE INDEX IF NOT EXISTS agents_builtin_idx
|
|
297
297
|
ON agents(builtin);
|
|
298
298
|
|
|
299
|
+
CREATE TABLE IF NOT EXISTS agent_configs (
|
|
300
|
+
agent_id TEXT PRIMARY KEY,
|
|
301
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
302
|
+
updated_at TEXT NOT NULL,
|
|
303
|
+
config_json TEXT NOT NULL,
|
|
304
|
+
FOREIGN KEY(agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
CREATE INDEX IF NOT EXISTS agent_configs_updated_idx
|
|
308
|
+
ON agent_configs(updated_at DESC);
|
|
309
|
+
|
|
299
310
|
CREATE TABLE IF NOT EXISTS workspaces (
|
|
300
311
|
id TEXT PRIMARY KEY,
|
|
301
312
|
name TEXT NOT NULL,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { currentSeq } = require('./events');
|
|
4
4
|
|
|
5
|
-
const DEFAULT_RESOURCES = ['automations', 'triggers', 'workflows', 'automation_runs', 'workflow_cell_runs', 'agents', 'apps', 'toolbox', 'tools', 'tool_actions', 'hooks', 'projects', 'workspaces', 'memories', 'mcp_connections', 'browser_profiles', 'browser_auth_bundles', 'browser_login_sessions'];
|
|
5
|
+
const DEFAULT_RESOURCES = ['automations', 'triggers', 'workflows', 'automation_runs', 'workflow_cell_runs', 'agents', 'agent_configs', 'apps', 'toolbox', 'tools', 'tool_actions', 'hooks', 'projects', 'workspaces', 'memories', 'user_preferences', 'chat_payloads', 'mcp_connections', 'browser_profiles', 'browser_auth_bundles', 'browser_login_sessions'];
|
|
6
6
|
|
|
7
7
|
function normalizeResources(resources) {
|
|
8
8
|
const values = resources ? String(resources).split(',') : DEFAULT_RESOURCES;
|
|
@@ -33,8 +33,14 @@ function readResource(resource, cache) {
|
|
|
33
33
|
case 'workflows':
|
|
34
34
|
cache.workflows ||= require('../automations/store').listWorkflows();
|
|
35
35
|
return cache.workflows;
|
|
36
|
-
case 'agents':
|
|
37
|
-
|
|
36
|
+
case 'agents': {
|
|
37
|
+
const { agentWithConfig } = require('../agent-config/store');
|
|
38
|
+
return require('../agents/store').getAllAgentsWithBuiltins()
|
|
39
|
+
.map((agent) => agentWithConfig(agent))
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
case 'agent_configs':
|
|
43
|
+
return require('../agent-config/store').listAgentConfigs();
|
|
38
44
|
case 'apps':
|
|
39
45
|
return require('../apps/store').loadApps().apps;
|
|
40
46
|
case 'toolbox':
|
|
@@ -55,6 +61,10 @@ function readResource(resource, cache) {
|
|
|
55
61
|
}
|
|
56
62
|
case 'memories':
|
|
57
63
|
return require('../../chat-core/tooling/active-memory').collectActiveMemoryFiles();
|
|
64
|
+
case 'user_preferences':
|
|
65
|
+
return require('../lib/prefs').readUserPreferences();
|
|
66
|
+
case 'chat_payloads':
|
|
67
|
+
return require('../lib/chat-payloads').listChatPayloads();
|
|
58
68
|
case 'uploads':
|
|
59
69
|
return require('../fs/rest')._private.buildUploadsResource();
|
|
60
70
|
case 'mcp_connections':
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const { importCodexConfig } = require('../agent-config/importers/codex');
|
|
9
|
+
const { normalizeAgentConfig } = require('../agent-config/schema');
|
|
10
|
+
const { toCodexHooksJson } = require('../agent-config/renderers/hooks');
|
|
11
|
+
|
|
12
|
+
function withNativeHome(fn) {
|
|
13
|
+
const previousNativeHome = process.env.AMALGM_NATIVE_HOME;
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-agent-config-'));
|
|
15
|
+
process.env.AMALGM_NATIVE_HOME = root;
|
|
16
|
+
try {
|
|
17
|
+
return fn(root);
|
|
18
|
+
} finally {
|
|
19
|
+
if (previousNativeHome === undefined) delete process.env.AMALGM_NATIVE_HOME;
|
|
20
|
+
else process.env.AMALGM_NATIVE_HOME = previousNativeHome;
|
|
21
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('codex native import ports command hooks without inheriting MCP or auth config', () => {
|
|
26
|
+
withNativeHome((home) => {
|
|
27
|
+
const codexHome = path.join(home, '.codex');
|
|
28
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(codexHome, 'AGENTS.md'), 'Use project memory when it helps.\n');
|
|
30
|
+
fs.writeFileSync(path.join(codexHome, 'auth.json'), '{"auth_mode":"chatgpt","secret":"do-not-import"}');
|
|
31
|
+
fs.writeFileSync(path.join(codexHome, 'config.toml'), [
|
|
32
|
+
'[mcp_servers.supermemory]',
|
|
33
|
+
'url = "https://example.test/mcp"',
|
|
34
|
+
'',
|
|
35
|
+
'[mcp_servers.supermemory.http_headers]',
|
|
36
|
+
'Authorization = "Bearer do-not-import"',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n'));
|
|
39
|
+
fs.writeFileSync(path.join(codexHome, 'hooks.json'), JSON.stringify({
|
|
40
|
+
UserPromptSubmit: [{
|
|
41
|
+
hooks: [{
|
|
42
|
+
type: 'command',
|
|
43
|
+
command: 'node ~/.codex/supermemory/recall.js',
|
|
44
|
+
timeout: 90,
|
|
45
|
+
statusMessage: 'Searching memories...',
|
|
46
|
+
}],
|
|
47
|
+
}],
|
|
48
|
+
Stop: [{
|
|
49
|
+
hooks: [{
|
|
50
|
+
type: 'command',
|
|
51
|
+
command: 'node ~/.codex/supermemory/save.js',
|
|
52
|
+
timeout: 60,
|
|
53
|
+
statusMessage: 'Saving to memory...',
|
|
54
|
+
}],
|
|
55
|
+
}],
|
|
56
|
+
}, null, 2));
|
|
57
|
+
|
|
58
|
+
const imported = importCodexConfig();
|
|
59
|
+
const config = normalizeAgentConfig({ agentId: 'agent-memory', ...imported.config });
|
|
60
|
+
|
|
61
|
+
assert.equal(config.instructions, 'Use project memory when it helps.\n');
|
|
62
|
+
assert.equal(config.hooks.length, 2);
|
|
63
|
+
assert.deepEqual(config.hooks.map((hook) => hook.event), ['UserPromptSubmit', 'Stop']);
|
|
64
|
+
assert.equal(config.hooks[0].command, 'node ~/.codex/supermemory/recall.js');
|
|
65
|
+
assert.equal(config.hooks[0].timeout, 90);
|
|
66
|
+
assert.equal(config.hooks[0].statusMessage, 'Searching memories...');
|
|
67
|
+
assert.equal(config.hooks[0].source.provider, 'codex');
|
|
68
|
+
assert.equal(config.tools.mode, 'all');
|
|
69
|
+
|
|
70
|
+
const serialized = JSON.stringify(config);
|
|
71
|
+
assert.equal(serialized.includes('mcp_servers'), false);
|
|
72
|
+
assert.equal(serialized.includes('Authorization'), false);
|
|
73
|
+
assert.equal(serialized.includes('do-not-import'), false);
|
|
74
|
+
|
|
75
|
+
const hooksJson = toCodexHooksJson(config);
|
|
76
|
+
assert.match(hooksJson, /Searching memories/);
|
|
77
|
+
assert.match(hooksJson, /Saving to memory/);
|
|
78
|
+
assert.match(hooksJson, /node ~\/\.codex\/supermemory\/recall\.js/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -158,18 +158,30 @@ test('workflow compiler accepts compact tool and agent helpers', () => {
|
|
|
158
158
|
trigger: event("github.push"),
|
|
159
159
|
cells: [
|
|
160
160
|
code("greet", async () => ({ message: "hello" })),
|
|
161
|
-
tool("notify", "
|
|
161
|
+
tool("notify", "notifications.notify_user", ({ cells }) => ({ message: cells.greet.output.message })),
|
|
162
162
|
agent("ask_codex", "Codex", { prompt: "Review this", run_in_background: true })
|
|
163
163
|
]
|
|
164
164
|
})`);
|
|
165
165
|
|
|
166
|
-
assert.equal(ir.cells[1].toolId, '
|
|
166
|
+
assert.equal(ir.cells[1].toolId, 'notifications');
|
|
167
167
|
assert.equal(ir.cells[1].actionName, 'notify_user');
|
|
168
|
-
assert.equal(ir.cells[2].toolId, '
|
|
168
|
+
assert.equal(ir.cells[2].toolId, 'agents');
|
|
169
169
|
assert.equal(ir.cells[2].actionName, 'talk_to_agent');
|
|
170
170
|
assert.equal(ir.cells[2].args.agent, 'Codex');
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
test('workflow tool resolver accepts legacy first-party action refs', async () => {
|
|
174
|
+
const valid = await validateWorkflowText(`export default workflow({
|
|
175
|
+
trigger: event("test.legacy"),
|
|
176
|
+
cells: [
|
|
177
|
+
tool("notify", "amalgm.notify_user", { message: "hello" }),
|
|
178
|
+
tool("notify2", "amalgm.notifications.notify_user", { message: "hello again" })
|
|
179
|
+
]
|
|
180
|
+
})`);
|
|
181
|
+
|
|
182
|
+
assert.equal(valid.ok, true);
|
|
183
|
+
});
|
|
184
|
+
|
|
173
185
|
test('workflow compiler splits dotted toolbox action refs on the last dot', () => {
|
|
174
186
|
const ir = compileWorkflowText(`export default workflow({
|
|
175
187
|
trigger: event("test.dotted"),
|
|
@@ -187,7 +199,7 @@ test('automation validator catches dry-run cell reference failures without persi
|
|
|
187
199
|
trigger: event("test.validate"),
|
|
188
200
|
cells: [
|
|
189
201
|
code("greet", async () => ({ message: "hello" })),
|
|
190
|
-
tool("notify", "
|
|
202
|
+
tool("notify", "notifications.notify_user", ({ cells }) => ({ message: cells.greet.output.message }))
|
|
191
203
|
]
|
|
192
204
|
})`);
|
|
193
205
|
|
|
@@ -198,7 +210,7 @@ test('automation validator catches dry-run cell reference failures without persi
|
|
|
198
210
|
trigger: event("test.validate"),
|
|
199
211
|
cells: [
|
|
200
212
|
code("greet", async () => ({ message: "hello" })),
|
|
201
|
-
tool("notify", "
|
|
213
|
+
tool("notify", "notifications.notify_user", ({ cells }) => ({ message: cells.missing.output.message }))
|
|
202
214
|
]
|
|
203
215
|
})`);
|
|
204
216
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-chat-payloads-test-'));
|
|
10
|
+
process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
|
|
11
|
+
|
|
12
|
+
const { closeLocalDb, openLocalDb } = require('../state/db');
|
|
13
|
+
const {
|
|
14
|
+
getChatPayload,
|
|
15
|
+
listChatPayloads,
|
|
16
|
+
writeChatPayload,
|
|
17
|
+
} = require('../lib/chat-payloads');
|
|
18
|
+
const { buildSnapshot } = require('../state/snapshot');
|
|
19
|
+
|
|
20
|
+
test.after(() => {
|
|
21
|
+
closeLocalDb();
|
|
22
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('chat payloads persist in local SQLite and snapshot by id', () => {
|
|
26
|
+
const payload = {
|
|
27
|
+
machineId: 'machine-a',
|
|
28
|
+
harness: 'codex',
|
|
29
|
+
resolvedHarnessId: 'codex',
|
|
30
|
+
modelId: 'openai/gpt-5.5',
|
|
31
|
+
modelSettings: { effort: 'high', fastMode: true },
|
|
32
|
+
authMethod: 'amalgm',
|
|
33
|
+
cwd: '/tmp/project',
|
|
34
|
+
customAgentId: null,
|
|
35
|
+
systemPrompt: null,
|
|
36
|
+
mcpAppIds: ['github'],
|
|
37
|
+
tools: { mode: 'selected', selectedToolIds: ['tool.search'] },
|
|
38
|
+
pendingWorktree: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const record = writeChatPayload({
|
|
42
|
+
id: 'chat-view:test',
|
|
43
|
+
revision: 'rev-1',
|
|
44
|
+
payload,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(record.id, 'chat-view:test');
|
|
48
|
+
assert.equal(record.revision, 'rev-1');
|
|
49
|
+
assert.deepEqual(getChatPayload('chat-view:test').payload, payload);
|
|
50
|
+
assert.deepEqual(listChatPayloads()['chat-view:test'].payload, payload);
|
|
51
|
+
|
|
52
|
+
const snapshot = buildSnapshot('chat_payloads,user_preferences');
|
|
53
|
+
assert.equal(snapshot.resources.chat_payloads['chat-view:test'].revision, 'rev-1');
|
|
54
|
+
assert.ok(snapshot.resources.user_preferences);
|
|
55
|
+
|
|
56
|
+
const rows = openLocalDb()
|
|
57
|
+
.prepare('SELECT resource, id FROM event_log WHERE resource = ?')
|
|
58
|
+
.all('chat_payloads');
|
|
59
|
+
assert.deepEqual(rows.map((row) => row.id), ['chat-view:test']);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('chat payload revision updates can reuse the existing payload body', () => {
|
|
63
|
+
const updated = writeChatPayload({
|
|
64
|
+
id: 'chat-view:test',
|
|
65
|
+
revision: 'rev-2',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
assert.equal(updated.revision, 'rev-2');
|
|
69
|
+
assert.equal(updated.payload.authMethod, 'amalgm');
|
|
70
|
+
assert.equal(getChatPayload('chat-view:test').revision, 'rev-2');
|
|
71
|
+
});
|
|
@@ -7,11 +7,11 @@ const { CORE_TOOL_GROUPS, CORE_TOOLS } = require('../server/core-tools');
|
|
|
7
7
|
|
|
8
8
|
test('core tools are grouped into first-party toolbox-style defaults', () => {
|
|
9
9
|
assert.deepEqual(CORE_TOOL_GROUPS.map((group) => group.id), [
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
10
|
+
'automations',
|
|
11
|
+
'notifications',
|
|
12
|
+
'agents',
|
|
13
|
+
'apps',
|
|
14
|
+
'browser',
|
|
15
15
|
]);
|
|
16
16
|
assert.equal(CORE_TOOL_GROUPS.every((group) => group.origin === 'system'), true);
|
|
17
17
|
assert.equal(CORE_TOOL_GROUPS.every((group) => group.owner === 'amalgm'), true);
|
|
@@ -30,8 +30,8 @@ test('core tool actions carry stable toolbox ids', () => {
|
|
|
30
30
|
const browserNavigate = CORE_TOOLS.find((tool) => tool.name === 'browser_navigate');
|
|
31
31
|
const notifyUser = CORE_TOOLS.find((tool) => tool.name === 'notify_user');
|
|
32
32
|
|
|
33
|
-
assert.equal(browserNavigate.toolboxToolId, '
|
|
34
|
-
assert.equal(browserNavigate.toolboxActionId, '
|
|
35
|
-
assert.equal(notifyUser.toolboxToolId, '
|
|
36
|
-
assert.equal(notifyUser.toolboxActionId, '
|
|
33
|
+
assert.equal(browserNavigate.toolboxToolId, 'browser');
|
|
34
|
+
assert.equal(browserNavigate.toolboxActionId, 'browser.browser_navigate');
|
|
35
|
+
assert.equal(notifyUser.toolboxToolId, 'notifications');
|
|
36
|
+
assert.equal(notifyUser.toolboxActionId, 'notifications.notify_user');
|
|
37
37
|
});
|
|
@@ -149,6 +149,35 @@ test('MCP surface filters static tools in selected-tool mode', async () => {
|
|
|
149
149
|
assert.equal(unselectedResult.content[0].text, 'Unknown tool: toolbox__amalgm_failures_static_fail');
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
test('MCP surface accepts legacy selected ids for first-party tools', async () => {
|
|
153
|
+
const surface = createToolboxMcpSurface({
|
|
154
|
+
staticTools: [{
|
|
155
|
+
name: 'browser_navigate',
|
|
156
|
+
description: 'Navigate.',
|
|
157
|
+
inputSchema: { type: 'object', properties: {} },
|
|
158
|
+
toolboxToolId: 'browser',
|
|
159
|
+
toolboxActionId: 'browser.browser_navigate',
|
|
160
|
+
async handler() {
|
|
161
|
+
return textResult('ok');
|
|
162
|
+
},
|
|
163
|
+
}],
|
|
164
|
+
async listDynamicTools() {
|
|
165
|
+
return [];
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const listed = await surface.listTools({
|
|
170
|
+
sessionMetadata: {
|
|
171
|
+
tools: {
|
|
172
|
+
mode: 'selected',
|
|
173
|
+
selectedToolIds: ['amalgm.browser'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.deepEqual(listed.map((tool) => tool.name), ['toolbox__browser_browser_navigate']);
|
|
179
|
+
});
|
|
180
|
+
|
|
152
181
|
test('MCP surface converts static handler errors into MCP errors', async () => {
|
|
153
182
|
const surface = createToolboxMcpSurface({ staticTools: staticTools() });
|
|
154
183
|
const result = await surface.callTool('toolbox__amalgm_failures_static_fail', {}, {});
|
|
@@ -12,20 +12,21 @@ const {
|
|
|
12
12
|
test('system toolbox catalog exposes first-party default tools', () => {
|
|
13
13
|
const catalog = buildSystemToolboxCatalog();
|
|
14
14
|
|
|
15
|
-
assert.equal(catalog.tools
|
|
16
|
-
assert.equal(catalog.tools
|
|
17
|
-
assert.equal(catalog.tools
|
|
15
|
+
assert.equal(catalog.tools.browser.name, 'Browser');
|
|
16
|
+
assert.equal(catalog.tools.browser.owner, 'amalgm');
|
|
17
|
+
assert.equal(catalog.tools.browser.origin, 'system');
|
|
18
|
+
assert.equal(catalog.tools.browser.policy.firstParty, true);
|
|
18
19
|
assert.equal(
|
|
19
|
-
catalog.toolActions['
|
|
20
|
-
'
|
|
20
|
+
catalog.toolActions['browser.browser_navigate'].sourceAction.mcpToolName,
|
|
21
|
+
'toolbox__browser_browser_navigate',
|
|
21
22
|
);
|
|
22
23
|
assert.equal(
|
|
23
|
-
catalog.toolActions['
|
|
24
|
+
catalog.toolActions['browser.browser_navigate'].sourceAction.handlerName,
|
|
24
25
|
'browser_navigate',
|
|
25
26
|
);
|
|
26
27
|
assert.equal(
|
|
27
|
-
catalog.toolActions['
|
|
28
|
-
'
|
|
28
|
+
catalog.toolActions['notifications.notify_user'].sourceAction.mcpToolName,
|
|
29
|
+
'toolbox__notifications_notify_user',
|
|
29
30
|
);
|
|
30
31
|
});
|
|
31
32
|
|
|
@@ -33,8 +34,8 @@ test('system toolbox catalog merge preserves persisted records', () => {
|
|
|
33
34
|
const merged = mergeSystemToolboxCatalog({
|
|
34
35
|
version: 1,
|
|
35
36
|
tools: {
|
|
36
|
-
|
|
37
|
-
id: '
|
|
37
|
+
browser: {
|
|
38
|
+
id: 'browser',
|
|
38
39
|
name: 'Persisted Browser',
|
|
39
40
|
origin: 'system',
|
|
40
41
|
},
|
|
@@ -45,37 +46,41 @@ test('system toolbox catalog merge preserves persisted records', () => {
|
|
|
45
46
|
},
|
|
46
47
|
},
|
|
47
48
|
toolActions: {
|
|
48
|
-
'
|
|
49
|
-
id: '
|
|
50
|
-
toolId: '
|
|
49
|
+
'browser.browser_navigate': {
|
|
50
|
+
id: 'browser.browser_navigate',
|
|
51
|
+
toolId: 'browser',
|
|
51
52
|
name: 'persisted_navigate',
|
|
52
53
|
},
|
|
53
54
|
},
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
assert.equal(merged.tools
|
|
57
|
+
assert.equal(merged.tools.browser.name, 'Persisted Browser');
|
|
57
58
|
assert.equal(merged.tools['custom.cli'].name, 'Custom CLI');
|
|
58
59
|
assert.equal(
|
|
59
|
-
merged.toolActions['
|
|
60
|
+
merged.toolActions['browser.browser_navigate'].name,
|
|
60
61
|
'persisted_navigate',
|
|
61
62
|
);
|
|
62
|
-
assert.equal(merged.toolActions['
|
|
63
|
+
assert.equal(merged.toolActions['apps.apps_list'].name, 'apps_list');
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
test('system toolbox catalog omits legacy aggregate amalgm records', () => {
|
|
66
67
|
const filtered = persistedCatalogWithoutLegacyAggregate({
|
|
67
68
|
tools: {
|
|
68
69
|
amalgm: { id: 'amalgm', origin: 'system' },
|
|
70
|
+
'amalgm.browser': { id: 'amalgm.browser', origin: 'system' },
|
|
69
71
|
'custom.cli': { id: 'custom.cli', origin: 'user' },
|
|
70
72
|
},
|
|
71
73
|
toolActions: {
|
|
72
74
|
'amalgm.notify_user': { id: 'amalgm.notify_user', toolId: 'amalgm' },
|
|
75
|
+
'amalgm.browser.browser_navigate': { id: 'amalgm.browser.browser_navigate', toolId: 'amalgm.browser' },
|
|
73
76
|
'custom.cli.run': { id: 'custom.cli.run', toolId: 'custom.cli' },
|
|
74
77
|
},
|
|
75
78
|
});
|
|
76
79
|
|
|
77
80
|
assert.equal(filtered.tools.amalgm, undefined);
|
|
81
|
+
assert.equal(filtered.tools['amalgm.browser'], undefined);
|
|
78
82
|
assert.equal(filtered.toolActions['amalgm.notify_user'], undefined);
|
|
83
|
+
assert.equal(filtered.toolActions['amalgm.browser.browser_navigate'], undefined);
|
|
79
84
|
assert.equal(filtered.tools['custom.cli'].id, 'custom.cli');
|
|
80
85
|
assert.equal(filtered.toolActions['custom.cli.run'].id, 'custom.cli.run');
|
|
81
86
|
});
|