@remcp/runtime 0.2.156 → 0.2.157

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remcp/runtime",
3
- "version": "0.2.156",
3
+ "version": "0.2.157",
4
4
  "description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -98,6 +98,57 @@ export const PAGE_ELEMENT_ACTION = `function(selector, needle, action, value) {
98
98
  };
99
99
  }`;
100
100
 
101
+ // Bounded form mutation: validate every target before changing any field.
102
+ // Values are intentionally excluded from the return object and tool logs.
103
+ export const PAGE_FILL_FORM = `function(fields) {
104
+ const targets = fields.map(field => {
105
+ const el = document.querySelector(field.selector);
106
+ if (!el || !el.getClientRects().length || el.disabled || el.readOnly ||
107
+ el.matches('input[type="file"],input[type="submit"],input[type="reset"],button')) {
108
+ throw new Error('Form field unavailable or not editable: ' + field.selector);
109
+ }
110
+ const tag = el.tagName.toLowerCase();
111
+ if (!['input','textarea','select'].includes(tag)) throw new Error('Unsupported form field: ' + field.selector);
112
+ if (tag === 'select' && !Array.from(el.options).some(option => option.value === String(field.value))) {
113
+ throw new Error('No matching select option: ' + field.selector);
114
+ }
115
+ return {el,tag,value:field.value};
116
+ });
117
+ const summary = [];
118
+ for (const {el,tag,value} of targets) {
119
+ const checkbox = tag === 'input' && ['checkbox','radio'].includes(el.type);
120
+ const prop = checkbox ? 'checked' : 'value';
121
+ let prototype = Object.getPrototypeOf(el);
122
+ let setter;
123
+ while (prototype && !setter) {
124
+ setter = Object.getOwnPropertyDescriptor(prototype, prop)?.set;
125
+ prototype = Object.getPrototypeOf(prototype);
126
+ }
127
+ const next = checkbox ? Boolean(value) : String(value);
128
+ if (setter) setter.call(el, next); else el[prop] = next;
129
+ el.dispatchEvent(new Event('input', {bubbles:true}));
130
+ el.dispatchEvent(new Event('change', {bubbles:true}));
131
+ summary.push({tag, changed:true});
132
+ }
133
+ return {filled:summary.length, fields:summary};
134
+ }`;
135
+
136
+ export function validateFormFields(fields) {
137
+ if (!Array.isArray(fields) || fields.length < 1 || fields.length > 20) {
138
+ throw new Error('fill_form requires 1 to 20 fields');
139
+ }
140
+ return fields.map(field => {
141
+ const selector = typeof field?.selector === 'string' ? field.selector.trim() : '';
142
+ if (!selector || selector.length > 512 || !Object.hasOwn(field,'value') ||
143
+ !['string','boolean','number'].includes(typeof field.value) ||
144
+ (typeof field.value === 'number' && !Number.isFinite(field.value))) {
145
+ throw new Error('Each form field requires a CSS selector and a string/boolean/number value');
146
+ }
147
+ if (String(field.value).length > 16384) throw new Error('Form field value exceeds 16384 characters');
148
+ return {selector, value:field.value};
149
+ });
150
+ }
151
+
101
152
  export function browserActionLocatorText(args = {}, action = '') {
102
153
  const explicit = optionalString(args.browser_text || args.match_text);
103
154
  if (explicit) return explicit;
@@ -137,8 +188,13 @@ export function browserScrollDeltas(args = {}) {
137
188
  }
138
189
 
139
190
  export async function browserAction(args) {
140
- const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport']);
191
+ const action = requireEnum(args.action, 'action', ['click', 'focus', 'type', 'set_value', 'select', 'scroll', 'scroll_into_view', 'upload', 'press', 'set_viewport', 'fill_form']);
192
+ const formFields = action === 'fill_form' ? validateFormFields(args.fields) : null;
141
193
  return withTarget(args, async (session, target) => {
194
+ if (action === 'fill_form') {
195
+ const result = await callPageFunction(session, PAGE_FILL_FORM, [formFields]);
196
+ return jsonResult({target_id:target.id, action, result});
197
+ }
142
198
  if (action === 'set_viewport') {
143
199
  const width = clamp(args.width, 1280, 200, 8192);
144
200
  const height = clamp(args.height, 720, 200, 8192);
@@ -386,11 +386,12 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
386
386
  // Validating every tab sequentially can require hundreds of serial DNS lookups,
387
387
  // making browser_tabs exceed the relay's deadline on machines with many tabs.
388
388
  // Keep DNS and security validation, but bound both the work and its concurrency.
389
- export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl } = {}) {
389
+ export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null } = {}) {
390
390
  if (!Array.isArray(list)) return [];
391
391
  const candidates = [];
392
392
  for (const item of list) {
393
393
  if (!item || item.type !== 'page' || !item.webSocketDebuggerUrl) continue;
394
+ if (targetId && item.id !== targetId) continue;
394
395
  candidates.push(item);
395
396
  if (candidates.length >= 1000) break;
396
397
  }
@@ -414,7 +415,7 @@ export async function filterBrowserTargets(list, { validate = assertResolvedPage
414
415
  return targets;
415
416
  }
416
417
 
417
- export async function listBrowserTargets(endpoint) {
418
+ export async function listBrowserTargets(endpoint, { targetId = null } = {}) {
418
419
  assertBrowserRemoteEnabled();
419
420
  // Reading tabs and page semantics must not launch a separate Chromium/profile.
420
421
  // Only an explicit browser_navigate(action=new_tab) can bootstrap an isolated browser.
@@ -431,7 +432,7 @@ export async function listBrowserTargets(endpoint) {
431
432
  throw new Error(`${label} is unavailable at ${base.origin}. Read-only browser_tabs and browser_snapshot do not launch a browser. Use ui_snapshot and ui_find for the existing browser window; do not retry this CDP endpoint unchanged. Only browser_navigate(action=new_tab), when explicitly requested, may start a local isolated browser.`);
432
433
  }
433
434
  // Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
434
- return filterBrowserTargets(list);
435
+ return filterBrowserTargets(list, { targetId });
435
436
  }
436
437
 
437
438
  export function validatedTargetWebSocketUrl(target, endpoint) {
@@ -449,8 +450,10 @@ export function validatedTargetWebSocketUrl(target, endpoint) {
449
450
  }
450
451
 
451
452
  export async function chooseTarget(args = {}) {
452
- const targets = (await listBrowserTargets(args.endpoint)).filter(item => item.type === 'page' && item.webSocketDebuggerUrl);
453
453
  const id = optionalString(args.target_id || args.targetId);
454
+ // Explicit target IDs are returned by browser_tabs. Never DNS-resolve every
455
+ // unrelated tab merely to act on one target: validate only its actual URL.
456
+ const targets = await listBrowserTargets(args.endpoint, { targetId:id });
454
457
  const title = optionalString(args.title);
455
458
  const urlContains = optionalString(args.url_contains || args.urlContains);
456
459
  const matched = targets.find(item =>
@@ -390,24 +390,9 @@ export async function evaluate(session, expression, awaitPromise = true) {
390
390
  }
391
391
 
392
392
  export async function callPageFunction(session, functionDeclaration, args = [], awaitPromise = true) {
393
- const global = await session.send('Runtime.evaluate', {
394
- expression: 'globalThis',
395
- returnByValue: false,
396
- awaitPromise: false,
397
- });
398
- if (global.exceptionDetails || !global.result?.objectId) {
399
- throw new Error(global.exceptionDetails?.exception?.description || global.exceptionDetails?.text || 'Could not access the page execution context');
400
- }
401
- const result = await session.send('Runtime.callFunctionOn', {
402
- objectId: global.result.objectId,
403
- functionDeclaration,
404
- arguments: args.map(value => ({ value })),
405
- returnByValue: true,
406
- awaitPromise,
407
- userGesture: true,
408
- });
409
- if (result.exceptionDetails) {
410
- throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'JavaScript function call failed');
411
- }
412
- return result.result?.value;
393
+ // These declarations are trusted internal browser actions. JSON arguments
394
+ // stay data, never raw page code. One CDP Runtime.evaluate call replaces
395
+ // Runtime.evaluate(globalThis) + Runtime.callFunctionOn (two serial RTTs).
396
+ const expression = '(' + functionDeclaration + ')(...' + JSON.stringify(args) + ')';
397
+ return evaluate(session, expression, awaitPromise);
413
398
  }
@@ -38,7 +38,7 @@ export const AI_TOOL_DESCRIPTIONS = Object.freeze({
38
38
  browser_navigate: 'Use this to open a URL in a new debuggable tab or navigate a known Chromium page by URL, back, forward or reload. Prefer it over typing into the address bar; new_tab also bootstraps browser automation when no page target exists yet.',
39
39
  browser_snapshot: 'Use this to inspect a web page structurally through CDP accessibility. Prefer it over desktop ui_snapshot for page content; add include_screenshot=true only when final pixel/layout verification is needed. A selector may be temporarily centered for capture, but the original page scroll position is restored before the tool returns.',
40
40
  browser_find: 'Use this to locate visible web elements by CSS selector, text or ARIA role and obtain reusable selectors/bounds. It requires an element criterion; use browser_snapshot, not an empty browser_find, to enumerate page structure. Prefer this before browser_action rather than guessing selectors or coordinates.',
41
- browser_action: 'Use this for deterministic DOM/CDP interaction with a known web element: click/focus/type/value/select/upload/key/scroll or viewport emulation. Prefer it over desktop pointer/ui_action for page content; use browser_evaluate only when the supported actions cannot express the task.',
41
+ browser_action: 'Use this for deterministic DOM/CDP interaction with a known web element: click/focus/type/value/select/upload/key/scroll or viewport emulation. Prefer it over desktop pointer/ui_action for page content; use action=fill_form with CSS selectors to edit up to 20 visible fields in one tool call without submitting. Use browser_evaluate only when supported actions cannot express the task.',
42
42
  browser_wait: 'Use this after browser navigation/action to wait for selector/text/URL/load/navigation/network-idle instead of fixed sleeps or screenshot polling. Use browser_evaluate separately for JavaScript predicates.',
43
43
  browser_evaluate: 'Use this as the powerful browser escape hatch only when browser_snapshot/find/action/wait cannot express the required page operation or inspection. JavaScript executes in page context and page content is untrusted.',
44
44
  service: 'Use this for operating-system service inventory/status/start/stop/restart. Prefer status before a mutation when service state matters; use start_process for ordinary commands that are not service-manager operations.',
@@ -165,7 +165,7 @@ export const extendedToolDefinitions = [
165
165
  define('browser_navigate', 'Browser navigate', 'Open a new debuggable tab or navigate a selected browser page by URL, history back/forward, or reload through Chrome DevTools Protocol.', o({ ...browserTarget, action:e(['url','new_tab','back','forward','reload'], 'Default url when url is provided, otherwise reload. new_tab creates the first page target when needed.'), url:s('Destination URL for action=url or action=new_tab.'), wait:b('Wait for the page load event; default true.'), ignore_cache:b('Reload without cache when action=reload.') }), openMutatingNonDestructive, browserHandlers.browser_navigate, ['browser_cdp']),
166
166
  define('browser_snapshot', 'Browser snapshot', 'Return the selected browser page accessibility tree through Chrome DevTools Protocol and, when requested, attach a real rendered viewport PNG for visual verification. A selector is temporarily recentered for capture, its exact viewport bounds are returned, and the prior scroll position is restored before return.', o({ ...browserTarget, max_nodes:n('Maximum AX nodes; default 1500.'), include_screenshot:b('Attach a real CDP-rendered viewport PNG screenshot; default false.'), selector:s('When include_screenshot=true, center this CSS-selected element before capture and report its viewport bounds.') }), readOnlyOpen, browserHandlers.browser_snapshot, ['browser_cdp']),
167
167
  define('browser_find', 'Find browser element', 'Find visible DOM elements by CSS selector, text, or ARIA role and return reusable selectors plus text, value, and bounding boxes.', o({ ...browserTarget, selector:s('CSS selector.'), text:s('Visible text substring.'), role:s('ARIA role.'), limit:n() }), readOnlyOpen, browserHandlers.browser_find, ['browser_cdp']),
168
- define('browser_action', 'Browser element action', 'Click, focus, type/set a value, select, scroll a page or scrollable element, scroll an element into view, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll','scroll_into_view','upload','press','set_viewport']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), browser_text:s('Visible text used only to locate a DOM element; use this instead of overloading text for action=type.'), text:s('For action=type, Unicode text to insert. For non-type element actions without selector/browser_text, visible text may identify the target.'), clear:b('For action=type, clear the current value before inserting text.'), value:s('Value for action=set_value; also accepted as a legacy type payload when text is used as the locator.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), delta_x:n('Horizontal scroll delta; positive means right.'), delta_y:n('Vertical scroll delta; positive means down.'), delta:n('Alias for delta_y.'), direction:e(['up','down','left','right']), wheel_times:n('Number of 120-unit wheel steps for direction; 1..50.'), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
168
+ define('browser_action', 'Browser element action', 'Click, focus, type/set a value, fill up to 20 form fields in one guarded call, select, scroll a page or scrollable element, scroll an element into view, upload files, press a key, or set an exact responsive-test viewport in a debuggable browser page using DOM/CDP semantics.', o({ ...browserTarget, action:e(['click','focus','type','set_value','select','scroll','scroll_into_view','upload','press','set_viewport','fill_form']), selector:s('CSS selector for the target element; prefer the reusable selector returned by browser_find.'), fields:{type:'array',minItems:1,maxItems:20,items:{type:'object',additionalProperties:false,properties:{selector:s('CSS selector.'),value:{type:['string','boolean','number'],description:'New field value; not returned in results.'}},required:['selector','value']},description:'fill_form: 1–20 CSS form fields in one CDP call. Does not submit.'}, browser_text:s('Visible text used only to locate a DOM element; use this instead of overloading text for action=type.'), text:s('For action=type, Unicode text to insert. For non-type element actions without selector/browser_text, visible text may identify the target.'), clear:b('For action=type, clear the current value before inserting text.'), value:s('Value for action=set_value; also accepted as a legacy type payload when text is used as the locator.'), text_value:s('Compatibility alias for the type/set_value text value.'), option:s(), delta_x:n('Horizontal scroll delta; positive means right.'), delta_y:n('Vertical scroll delta; positive means down.'), delta:n('Alias for delta_y.'), direction:e(['up','down','left','right']), wheel_times:n('Number of 120-unit wheel steps for direction; 1..50.'), path:s(), paths:{type:'array',items:{type:'string'}}, key:s(), width:n('CSS viewport width for set_viewport.'), height:n('CSS viewport height for set_viewport.'), device_scale_factor:n('Device scale factor for set_viewport; default 1.'), mobile:b('Enable mobile emulation for set_viewport.') }, ['action']), openMutating, browserHandlers.browser_action, ['browser_cdp']),
169
169
  define('browser_wait', 'Wait for browser', 'Wait for DOM state, page text, URL, completed loading, a navigation away from the current URL, or a short network-idle period in a debuggable browser page. Use browser_evaluate separately for JavaScript predicates or page-specific inspection.', o({ ...browserTarget, condition:e(['selector','text','url_contains','load','navigation','network_idle']), selector:s(), text:s(), value:s(), poll_ms:n(), idle_ms:n('Required zero-in-flight network quiet window for network_idle; default 500 ms.') }), readOnlyOpen, browserHandlers.browser_wait, ['browser_cdp']),
170
170
  define('browser_evaluate', 'Evaluate browser JavaScript', 'Evaluate JavaScript in a selected debuggable browser page and return its serializable value. This is a powerful escape hatch for page-specific automation.', o({ ...browserTarget, expression:s('JavaScript expression.'), await_promise:b('Await a returned Promise; default true.') }, ['expression']), openMutating, browserHandlers.browser_evaluate, ['browser_evaluate']),
171
171