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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/runtime/scripts/amalgm-mcp/agent-config/importers/claude.js +52 -0
  3. package/runtime/scripts/amalgm-mcp/agent-config/importers/codex.js +36 -0
  4. package/runtime/scripts/amalgm-mcp/agent-config/importers/common.js +115 -0
  5. package/runtime/scripts/amalgm-mcp/agent-config/importers/index.js +14 -0
  6. package/runtime/scripts/amalgm-mcp/agent-config/importers/opencode.js +59 -0
  7. package/runtime/scripts/amalgm-mcp/agent-config/renderers/hooks.js +55 -0
  8. package/runtime/scripts/amalgm-mcp/agent-config/renderers/instructions.js +54 -0
  9. package/runtime/scripts/amalgm-mcp/agent-config/rest.js +168 -0
  10. package/runtime/scripts/amalgm-mcp/agent-config/schema.js +248 -0
  11. package/runtime/scripts/amalgm-mcp/agent-config/store.js +123 -0
  12. package/runtime/scripts/amalgm-mcp/agents/hooks.js +2 -0
  13. package/runtime/scripts/amalgm-mcp/agents/rest.js +45 -2
  14. package/runtime/scripts/amalgm-mcp/agents/talk.js +20 -4
  15. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +52 -9
  16. package/runtime/scripts/amalgm-mcp/automations/tools.js +3 -3
  17. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
  18. package/runtime/scripts/amalgm-mcp/lib/chat-payloads.js +101 -0
  19. package/runtime/scripts/amalgm-mcp/server/core-tools.js +10 -10
  20. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  21. package/runtime/scripts/amalgm-mcp/server/routes/agents.js +13 -0
  22. package/runtime/scripts/amalgm-mcp/server/routes/chat-payloads.js +37 -0
  23. package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
  24. package/runtime/scripts/amalgm-mcp/state/snapshot.js +13 -3
  25. package/runtime/scripts/amalgm-mcp/tests/agent-config.test.js +80 -0
  26. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +17 -5
  27. package/runtime/scripts/amalgm-mcp/tests/chat-payloads.test.js +71 -0
  28. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +9 -9
  29. package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +29 -0
  30. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +21 -16
  31. package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +9 -7
  32. package/runtime/scripts/amalgm-mcp/toolbox/selection.js +21 -1
  33. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +16 -1
  34. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/workflows/runner.js +8 -2
  36. package/runtime/scripts/chat-core/adapters/claude.js +2 -2
  37. package/runtime/scripts/chat-core/adapters/codex.js +82 -12
  38. package/runtime/scripts/chat-core/auth.js +13 -4
  39. package/runtime/scripts/chat-core/contract.js +18 -0
  40. package/runtime/scripts/chat-core/tests/auth.test.js +27 -2
  41. package/runtime/scripts/chat-core/tests/native-config.test.js +65 -12
  42. package/runtime/scripts/chat-core/tooling/native-config.js +73 -0
  43. package/runtime/scripts/chat-core/tooling/runtime-home.js +2 -2
  44. package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -1
  45. 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
- 'amalgm.automations',
32
- 'Amalgm Automations',
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
- 'amalgm.notifications',
38
- 'Amalgm Notifications',
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
- 'amalgm.agents',
44
- 'Amalgm Agents',
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
- 'amalgm.apps',
50
- 'Amalgm Apps',
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
- 'amalgm.browser',
56
- 'Amalgm Browser',
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
- return require('../agents/store').getAllAgentsWithBuiltins();
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", "amalgm.notifications.notify_user", ({ cells }) => ({ message: cells.greet.output.message })),
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, 'amalgm.notifications');
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, 'amalgm.agents');
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", "amalgm.notifications.notify_user", ({ cells }) => ({ message: cells.greet.output.message }))
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", "amalgm.notifications.notify_user", ({ cells }) => ({ message: cells.missing.output.message }))
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
- 'amalgm.automations',
11
- 'amalgm.notifications',
12
- 'amalgm.agents',
13
- 'amalgm.apps',
14
- 'amalgm.browser',
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, 'amalgm.browser');
34
- assert.equal(browserNavigate.toolboxActionId, 'amalgm.browser.browser_navigate');
35
- assert.equal(notifyUser.toolboxToolId, 'amalgm.notifications');
36
- assert.equal(notifyUser.toolboxActionId, 'amalgm.notifications.notify_user');
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['amalgm.browser'].name, 'Amalgm Browser');
16
- assert.equal(catalog.tools['amalgm.browser'].origin, 'system');
17
- assert.equal(catalog.tools['amalgm.browser'].policy.firstParty, true);
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['amalgm.browser.browser_navigate'].sourceAction.mcpToolName,
20
- 'toolbox__amalgm_browser_browser_navigate',
20
+ catalog.toolActions['browser.browser_navigate'].sourceAction.mcpToolName,
21
+ 'toolbox__browser_browser_navigate',
21
22
  );
22
23
  assert.equal(
23
- catalog.toolActions['amalgm.browser.browser_navigate'].sourceAction.handlerName,
24
+ catalog.toolActions['browser.browser_navigate'].sourceAction.handlerName,
24
25
  'browser_navigate',
25
26
  );
26
27
  assert.equal(
27
- catalog.toolActions['amalgm.notifications.notify_user'].sourceAction.mcpToolName,
28
- 'toolbox__amalgm_notifications_notify_user',
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
- 'amalgm.browser': {
37
- id: 'amalgm.browser',
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
- 'amalgm.browser.browser_navigate': {
49
- id: 'amalgm.browser.browser_navigate',
50
- toolId: 'amalgm.browser',
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['amalgm.browser'].name, 'Persisted Browser');
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['amalgm.browser.browser_navigate'].name,
60
+ merged.toolActions['browser.browser_navigate'].name,
60
61
  'persisted_navigate',
61
62
  );
62
- assert.equal(merged.toolActions['amalgm.apps.apps_list'].name, 'apps_list');
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
  });