amalgm 0.1.91 → 0.1.92

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.
@@ -4,6 +4,7 @@ const assert = require('node:assert/strict');
4
4
  const test = require('node:test');
5
5
 
6
6
  const { CORE_TOOL_GROUPS, CORE_TOOLS } = require('../server/core-tools');
7
+ const { toolboxMcpToolName } = require('../toolbox/names');
7
8
 
8
9
  test('core tools are grouped into first-party toolbox-style defaults', () => {
9
10
  assert.deepEqual(CORE_TOOL_GROUPS.map((group) => group.id), [
@@ -23,19 +24,19 @@ test('core tool grouping preserves handler definitions for MCP surface wrapping'
23
24
 
24
25
  assert.equal(CORE_TOOLS.length, groupedTools.length);
25
26
  assert.deepEqual(CORE_TOOLS.map((tool) => tool.name), groupedTools.map((tool) => tool.name));
26
- assert.equal(CORE_TOOLS.some((tool) => tool.name === 'browser_open'), true);
27
+ assert.equal(CORE_TOOLS.some((tool) => tool.name === 'open' && tool.toolboxToolId === 'browser'), true);
27
28
  assert.equal(CORE_TOOLS.some((tool) => tool.name === 'notify_user'), true);
28
29
  assert.equal(CORE_TOOLS.some((tool) => tool.name === 'append_change_log'), true);
29
30
  });
30
31
 
31
32
  test('core tool actions carry stable toolbox ids', () => {
32
- const browserOpen = CORE_TOOLS.find((tool) => tool.name === 'browser_open');
33
+ const browserOpen = CORE_TOOLS.find((tool) => tool.name === 'open');
33
34
  const notifyUser = CORE_TOOLS.find((tool) => tool.name === 'notify_user');
34
35
  const cuaClick = CORE_TOOLS.find((tool) => tool.name === 'cua_click');
35
36
  const recordStart = CORE_TOOLS.find((tool) => tool.name === 'record_start');
36
37
 
37
38
  assert.equal(browserOpen.toolboxToolId, 'browser');
38
- assert.equal(browserOpen.toolboxActionId, 'browser.browser_open');
39
+ assert.equal(browserOpen.toolboxActionId, 'browser.open');
39
40
  assert.equal(cuaClick.toolboxToolId, 'browser');
40
41
  assert.equal(cuaClick.toolboxActionId, 'browser.cua_click');
41
42
  assert.equal(recordStart.toolboxActionId, 'browser.record_start');
@@ -47,16 +48,16 @@ test('browser is a single toolbox entry holding every browser action', () => {
47
48
  const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
48
49
  assert.deepEqual(browser.tools.map((tool) => tool.name), [
49
50
  // core verbs
50
- 'browser_open',
51
- 'browser_snapshot',
52
- 'browser_screenshot',
53
- 'browser_click',
54
- 'browser_fill',
55
- 'browser_press',
56
- 'browser_eval',
57
- 'browser_wait',
58
- 'browser_cli',
59
- 'browser_close',
51
+ 'open',
52
+ 'snapshot',
53
+ 'screenshot',
54
+ 'click',
55
+ 'fill',
56
+ 'press',
57
+ 'eval',
58
+ 'wait',
59
+ 'cli',
60
+ 'close',
60
61
  // computer use
61
62
  'cua_screenshot',
62
63
  'cua_click',
@@ -71,12 +72,10 @@ test('browser is a single toolbox entry holding every browser action', () => {
71
72
  'record_stop',
72
73
  'record_list',
73
74
  // auth / profiles
74
- 'browser_profiles_list',
75
- 'browser_auth_bundles_list',
76
- 'browser_login_sessions_list',
77
- 'browser_auth_link_create',
78
- 'browser_auth_save',
79
- 'browser_auth_load',
75
+ 'auth_list',
76
+ 'auth_link_create',
77
+ 'auth_save',
78
+ 'auth_load',
80
79
  ]);
81
80
 
82
81
  // No other browser-ish groups exist.
@@ -86,6 +85,21 @@ test('browser is a single toolbox entry holding every browser action', () => {
86
85
  );
87
86
  });
88
87
 
88
+ test('browser MCP names never stutter', () => {
89
+ // Regression for the 2026-06-10 trap: action ids like browser.browser_open
90
+ // rendered as toolbox__browser_browser_open, and agents reasonably guessed
91
+ // toolbox__browser_open — which did not exist. Short action names make the
92
+ // guessable name the real one.
93
+ const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
94
+ for (const tool of browser.tools) {
95
+ const mcpName = toolboxMcpToolName({ id: tool.toolboxActionId });
96
+ assert.equal(mcpName.startsWith('toolbox__browser_'), true);
97
+ assert.equal(mcpName.includes('browser_browser'), false, `${mcpName} stutters`);
98
+ }
99
+ assert.equal(toolboxMcpToolName({ id: 'browser.open' }), 'toolbox__browser_open');
100
+ assert.equal(toolboxMcpToolName({ id: 'browser.record_start' }), 'toolbox__browser_record_start');
101
+ });
102
+
89
103
  test('browser actions carry capability tags so UIs can section one card', () => {
90
104
  const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
91
105
  const byCapability = (capability) => browser.tools
@@ -95,31 +109,47 @@ test('browser actions carry capability tags so UIs can section one card', () =>
95
109
  assert.equal(byCapability('core').length, 10);
96
110
  assert.equal(byCapability('computer-use').every((name) => name.startsWith('cua_')), true);
97
111
  assert.deepEqual(byCapability('recording'), ['record_start', 'record_stop', 'record_list']);
98
- assert.equal(byCapability('auth').length, 6);
112
+ assert.deepEqual(byCapability('auth'), ['auth_list', 'auth_link_create', 'auth_save', 'auth_load']);
99
113
  assert.equal(browser.tools.every((tool) => Boolean(tool.capability)), true);
100
114
  });
101
115
 
102
- test('legacy capability-group loadouts still grant the collapsed actions', () => {
116
+ test('loadouts from every browser era still grant the current actions', () => {
103
117
  const { loadoutContainsToolOrAction } = require('../toolbox/loadout-context');
104
118
 
105
- // Old group id grants exactly that group's actions.
119
+ // Era-1 group id grants exactly that group's actions.
106
120
  const cuaLoadout = new Set(['browser_cua']);
107
121
  assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.cua_click'), true);
108
122
  assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.record_start'), false);
109
- assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.browser_open'), false);
123
+ assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.open'), false);
110
124
 
111
- // Old fine-grained action ids map 1:1.
125
+ // Era-1 fine-grained action ids map 1:1.
112
126
  const actionLoadout = new Set(['browser_recording.record_start']);
113
127
  assert.equal(loadoutContainsToolOrAction(actionLoadout, 'browser', 'browser.record_start'), true);
114
128
  assert.equal(loadoutContainsToolOrAction(actionLoadout, 'browser', 'browser.record_stop'), false);
115
129
 
116
- // New single entry grants everything.
130
+ // Era-1 auth group covers the merged auth_list (and its old spellings).
131
+ const authLoadout = new Set(['browser_auth']);
132
+ assert.equal(loadoutContainsToolOrAction(authLoadout, 'browser', 'browser.auth_list'), true);
133
+ assert.equal(loadoutContainsToolOrAction(authLoadout, 'browser', 'browser.auth_save'), true);
134
+ assert.equal(loadoutContainsToolOrAction(authLoadout, 'browser', 'browser.open'), false);
135
+ const authActionLoadout = new Set(['browser_auth.browser_profiles_list']);
136
+ assert.equal(loadoutContainsToolOrAction(authActionLoadout, 'browser', 'browser.auth_list'), true);
137
+
138
+ // Era-2 fine-grained grants (browser.browser_open) cover the renamed verbs.
139
+ const renamedLoadout = new Set(['browser.browser_open', 'browser.browser_eval']);
140
+ assert.equal(loadoutContainsToolOrAction(renamedLoadout, 'browser', 'browser.open'), true);
141
+ assert.equal(loadoutContainsToolOrAction(renamedLoadout, 'browser', 'browser.eval'), true);
142
+ assert.equal(loadoutContainsToolOrAction(renamedLoadout, 'browser', 'browser.click'), false);
143
+ const renamedAuthLoadout = new Set(['browser.browser_auth_bundles_list']);
144
+ assert.equal(loadoutContainsToolOrAction(renamedAuthLoadout, 'browser', 'browser.auth_list'), true);
145
+
146
+ // The single entry grants everything.
117
147
  const browserLoadout = new Set(['browser']);
118
- assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.browser_open'), true);
148
+ assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.open'), true);
119
149
  assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.cua_click'), true);
120
- assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.browser_auth_save'), true);
150
+ assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.auth_save'), true);
121
151
 
122
- // Legacy amalgm.browser aggregate keeps granting browser actions.
152
+ // The ancient amalgm.browser aggregate keeps granting browser actions.
123
153
  const legacyAggregate = new Set(['amalgm.browser']);
124
- assert.equal(loadoutContainsToolOrAction(legacyAggregate, 'browser', 'browser.browser_open'), true);
154
+ assert.equal(loadoutContainsToolOrAction(legacyAggregate, 'browser', 'browser.open'), true);
125
155
  });
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const test = require('node:test');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ // The platform prompt is what every agent reads about the browser before its
9
+ // first call. It documented a deleted tool surface once (browser_navigate et
10
+ // al.) and agents spent whole sessions calling tools that did not exist —
11
+ // keep it pinned to the real action names.
12
+
13
+ const CONTEXT_PATH = path.join(__dirname, '..', '..', 'platform-context.txt');
14
+
15
+ test('platform context documents the live browser surface, not a deleted one', () => {
16
+ const text = fs.readFileSync(CONTEXT_PATH, 'utf8');
17
+
18
+ for (const ghost of [
19
+ 'browser_navigate',
20
+ 'browser_type',
21
+ 'browser_get_text',
22
+ 'browser_evaluate',
23
+ 'browser_start_video',
24
+ 'browser_stop_video',
25
+ ]) {
26
+ assert.equal(text.includes(ghost), false, `platform context still documents deleted tool ${ghost}`);
27
+ }
28
+
29
+ assert.equal(text.includes('toolbox__browser_open'), true, 'platform context should show the real MCP name shape');
30
+ for (const action of ['`open`', '`snapshot`', '`record_start`', '`auth_link_create`']) {
31
+ assert.equal(text.includes(action), true, `platform context should document ${action}`);
32
+ }
33
+ });
34
+
35
+ test('platform context browser names match the registered actions', () => {
36
+ const text = fs.readFileSync(CONTEXT_PATH, 'utf8');
37
+ const { CORE_TOOL_GROUPS } = require('../server/core-tools');
38
+ const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
39
+ // Every core verb the prompt teaches must exist under that exact name.
40
+ const documented = ['open', 'snapshot', 'click', 'fill', 'press', 'eval', 'wait', 'screenshot', 'cli', 'close'];
41
+ const registered = new Set(browser.tools.map((tool) => tool.name));
42
+ for (const name of documented) {
43
+ assert.equal(registered.has(name), true, `prompt documents "${name}" but the toolbox does not register it`);
44
+ assert.equal(text.includes(`\`${name}\``), true, `prompt should mention ${name}`);
45
+ }
46
+ });
@@ -17,12 +17,12 @@ test('system toolbox catalog exposes first-party default tools', () => {
17
17
  assert.equal(catalog.tools.browser.origin, 'system');
18
18
  assert.equal(catalog.tools.browser.policy.firstParty, true);
19
19
  assert.equal(
20
- catalog.toolActions['browser.browser_open'].sourceAction.mcpToolName,
21
- 'toolbox__browser_browser_open',
20
+ catalog.toolActions['browser.open'].sourceAction.mcpToolName,
21
+ 'toolbox__browser_open',
22
22
  );
23
23
  assert.equal(
24
- catalog.toolActions['browser.browser_open'].sourceAction.handlerName,
25
- 'browser_open',
24
+ catalog.toolActions['browser.open'].sourceAction.handlerName,
25
+ 'open',
26
26
  );
27
27
  assert.equal(
28
28
  catalog.toolActions['notifications.notify_user'].sourceAction.mcpToolName,
@@ -161,7 +161,7 @@ test('toolbox service overlays first-party system catalog entries', () => {
161
161
  const service = createToolboxService(createRepository());
162
162
  const catalog = service.readCatalog();
163
163
  const browser = service.getTool('browser', { includeActions: false });
164
- const navigate = service.getAction('browser.browser_open');
164
+ const navigate = service.getAction('browser.open');
165
165
 
166
166
  assert.equal(catalog.tools.amalgm, undefined);
167
167
  assert.equal(catalog.toolActions['amalgm.notify_user'], undefined);
@@ -170,7 +170,7 @@ test('toolbox service overlays first-party system catalog entries', () => {
170
170
  assert.equal(catalog.tools.browser.owner, 'amalgm');
171
171
  assert.equal(browser.tool.name, 'Browser');
172
172
  assert.equal(browser.actions, undefined);
173
- assert.equal(navigate.action.sourceAction.mcpToolName, 'toolbox__browser_browser_open');
173
+ assert.equal(navigate.action.sourceAction.mcpToolName, 'toolbox__browser_open');
174
174
  assert.equal(service.listTools({ origin: 'system' }).tools.some((tool) => tool.id === 'browser'), true);
175
175
  });
176
176
 
@@ -11,31 +11,54 @@ const FIRST_PARTY_TOOL_IDS = new Set([
11
11
  'browser',
12
12
  ]);
13
13
 
14
- // Browser used to ship as four toolbox entries; it is one (`browser`) now.
15
- // Loadouts saved against the old capability groups keep working: the old
16
- // group id grants exactly the actions that group contained, and old action
17
- // ids map 1:1 onto `browser.<action>`. Frozen sets — these never grow.
14
+ // Browser action names through history loadouts saved in any era keep
15
+ // granting the same capability. Frozen maps; these never grow.
16
+ // era 1: four toolbox entries (browser + browser_cua/_recording/_auth)
17
+ // era 2: one entry, prefixed action names (browser.browser_open)
18
+ // era 3 (now): one entry, short verbs (browser.open); the three auth list
19
+ // actions merged into auth_list.
20
+ const OLD_BROWSER_ACTION_NAMES = {
21
+ open: ['browser_open'],
22
+ snapshot: ['browser_snapshot'],
23
+ screenshot: ['browser_screenshot'],
24
+ click: ['browser_click'],
25
+ fill: ['browser_fill'],
26
+ press: ['browser_press'],
27
+ eval: ['browser_eval'],
28
+ wait: ['browser_wait'],
29
+ cli: ['browser_cli'],
30
+ close: ['browser_close'],
31
+ auth_list: ['browser_profiles_list', 'browser_auth_bundles_list', 'browser_login_sessions_list'],
32
+ auth_link_create: ['browser_auth_link_create'],
33
+ auth_save: ['browser_auth_save'],
34
+ auth_load: ['browser_auth_load'],
35
+ };
36
+
18
37
  const LEGACY_BROWSER_GROUP_ACTIONS = {
19
38
  browser_cua: new Set([
20
39
  'cua_screenshot', 'cua_click', 'cua_double_click', 'cua_move',
21
40
  'cua_scroll', 'cua_type', 'cua_keypress', 'cua_drag',
22
41
  ]),
23
42
  browser_recording: new Set(['record_start', 'record_stop', 'record_list']),
24
- browser_auth: new Set([
25
- 'browser_profiles_list', 'browser_auth_bundles_list',
26
- 'browser_login_sessions_list', 'browser_auth_link_create',
27
- 'browser_auth_save', 'browser_auth_load',
28
- ]),
43
+ browser_auth: new Set(['auth_list', 'auth_link_create', 'auth_save', 'auth_load']),
29
44
  };
30
45
 
31
46
  function legacyBrowserLoadoutHas(loadoutToolIds, actionId) {
32
47
  if (!actionId.startsWith('browser.')) return false;
33
48
  const action = actionId.slice('browser.'.length);
49
+ const oldNames = OLD_BROWSER_ACTION_NAMES[action] || [];
50
+ // Era-2 fine-grained grants: browser.browser_open ⇒ browser.open.
51
+ for (const oldName of oldNames) {
52
+ if (loadoutToolIds.has(`browser.${oldName}`)) return true;
53
+ }
54
+ // Era-1 group grants, including their fine-grained spellings.
34
55
  for (const [legacyId, actions] of Object.entries(LEGACY_BROWSER_GROUP_ACTIONS)) {
35
56
  if (!actions.has(action)) continue;
36
57
  if (loadoutToolIds.has(legacyId)) return true;
37
- if (loadoutToolIds.has(`${legacyId}.${action}`)) return true;
38
58
  if (loadoutToolIds.has(`amalgm.${legacyId}`)) return true;
59
+ for (const name of [action, ...oldNames]) {
60
+ if (loadoutToolIds.has(`${legacyId}.${name}`)) return true;
61
+ }
39
62
  }
40
63
  return false;
41
64
  }
@@ -75,18 +75,35 @@ function toolKind(name = '', input = {}, metadata = {}) {
75
75
  return 'unknown';
76
76
  }
77
77
 
78
+ /**
79
+ * Browser action out of a tool name, across naming eras: current
80
+ * toolbox__browser_open, era-2 toolbox__browser_browser_open / browser_open,
81
+ * and bare record_start/cua_click. Null for non-browser tools.
82
+ */
83
+ function browserAction(name) {
84
+ const match = String(name || '').match(/^(?:toolbox__browser_|browser_)(.+)$/);
85
+ if (match) return match[1].replace(/^browser_/, '');
86
+ if (/^(?:record_(?:start|stop|list)|cua_[a-z_]+)$/.test(name)) return name;
87
+ return null;
88
+ }
89
+
78
90
  function titleFromMcp(tool, input = {}) {
79
91
  const name = String(tool || '');
80
- const action = name.replace(/^browser_/, '');
81
- if (name === 'browser_open') return input.url ? `Opening ${hostLabel(input.url)}` : 'Opening browser page';
82
- if (name === 'browser_snapshot') return 'Reading browser page';
83
- if (name === 'browser_screenshot') return 'Taking browser screenshot';
84
- if (name === 'browser_fill') return input.text ? `Typing "${compactText(input.text, 60)}"` : 'Typing in browser';
85
- if (name === 'browser_click') return 'Clicking in browser';
86
- if (name === 'browser_wait') return 'Waiting for page';
87
- if (name === 'browser_cli') return Array.isArray(input.args) ? `Browser: ${compactText(input.args.join(' '), 60)}` : 'Running browser command';
88
- if (name === 'record_start') return 'Starting browser recording';
89
- if (name === 'record_stop') return 'Saving browser recording';
92
+ const action = browserAction(name);
93
+ if (action === 'open') return input.url ? `Opening ${hostLabel(input.url)}` : 'Opening browser page';
94
+ if (action === 'snapshot') return 'Reading browser page';
95
+ if (action === 'screenshot' || action === 'cua_screenshot') return 'Taking browser screenshot';
96
+ if (action === 'fill') return input.text ? `Typing "${compactText(input.text, 60)}"` : 'Typing in browser';
97
+ if (action === 'click') return 'Clicking in browser';
98
+ if (action === 'press') return input.key ? `Pressing ${compactText(input.key, 20)}` : 'Pressing key in browser';
99
+ if (action === 'eval') return 'Running JavaScript in browser';
100
+ if (action === 'wait') return 'Waiting for page';
101
+ if (action === 'cli') return Array.isArray(input.args) ? `Browser: ${compactText(input.args.join(' '), 60)}` : 'Running browser command';
102
+ if (action === 'close') return 'Closing browser session';
103
+ if (action === 'record_start') return 'Starting browser recording';
104
+ if (action === 'record_stop') return 'Saving browser recording';
105
+ if (action === 'record_list') return 'Listing browser recordings';
106
+ if (action === 'auth_list') return 'Listing browser logins';
90
107
  if (name === 'apps_register') return input.name ? `Registering app ${compactText(input.name, 60)}` : 'Registering app';
91
108
  if (name === 'apps_start') return 'Starting app';
92
109
  if (name === 'apps_stop') return 'Stopping app';
@@ -115,26 +115,27 @@ Discover and talk to other agents running on the platform.
115
115
 
116
116
  Agent communication is session-based. Pass the `session_id` on subsequent calls to continue the same conversation — the other agent retains full context. Use this for delegation, second opinions, or cross-model collaboration.
117
117
 
118
- ## Browser — Visible Electron Session
119
-
120
- You have a persistent browser surface for navigation, interaction, screenshots, and scraping.
121
-
122
- - In the desktop app, `browser_*` tools drive Amalgm's visible in-tab Electron browser artifact surface.
123
- - Browser sessions persist after tool use. Hide/show the artifact surface without closing the session.
124
- - Outside Electron, the tools can fall back to `agent-browser` for headless/remote browser automation.
125
-
126
- - `browser_navigate` — Go to a URL.
127
- - `browser_screenshot` — Screenshot the current page (base64 PNG).
128
- - `browser_snapshot` Token-efficient page snapshot with interactive refs such as `@e1`.
129
- - `browser_click` — Click an element by ref, CSS selector, text, role, label, placeholder, or test id.
130
- - `browser_type` — Type text into an input.
131
- - `browser_get_text` — Extract text content.
132
- - `browser_evaluate` — Run JavaScript in the page context.
133
- - `browser_start_video` — Start recording the browser session.
134
- - `browser_stop_video` Stop recording and save a WebM under `~/.amalgm/browser-sessions/`.
135
- - `browser_close` — Close and release resources.
136
-
137
- The browser persists across tool calls — navigate once, then interact without re-navigating. Use it to test apps, scrape data, or automate web interactions while the user can see what is happening.
118
+ ## Browser
119
+
120
+ One persistent browser per chat session. When the Amalgm desktop app is open, the user watches it live in a split view; everywhere else it runs headless in the background. Same actions either way — you never pick a mode.
121
+
122
+ Browser actions are MCP tools named `toolbox__browser_<action>` (your harness may add its own prefix, e.g. `mcp__amalgm__toolbox__browser_open`).
123
+
124
+ The loop: `open` a URL `snapshot` to see interactive elements as @refs → `click`/`fill`/`press` by @ref re-`snapshot` after the page changes.
125
+
126
+ - `open` — go to a URL (the session persists across calls; navigate once, then interact)
127
+ - `snapshot` — read the page: every interactive element gets a stable @ref
128
+ - `click` / `fill` / `press` act on @refs, CSS selectors, or `text=Label` targets
129
+ - `eval` — run JavaScript in the page and get the result
130
+ - `wait` — wait for a selector, URL, or delay
131
+ - `screenshot` — PNG when you need pixels (snapshot is cheaper for driving)
132
+ - `cli` — the full agent-browser command surface: scroll, hover, back, tabs, cookies, storage, network, traces, find-by-role, and more
133
+ - `close` — end the session
134
+ - `cua_screenshot` / `cua_click` / `cua_type` / coordinate-based control for vision loops
135
+ - `record_start` / `record_stop` / `record_list` capture the session to a local WebM video (QA evidence)
136
+ - `auth_list` / `auth_link_create` / `auth_save` / `auth_load` — reuse logins: mint a link for the user to sign in, then save/load encrypted auth bundles
137
+
138
+ Recordings and auth bundles stay on this computer. Use the browser to test apps, scrape data, or automate web interactions.
138
139
 
139
140
  ---
140
141