@remcp/runtime 0.2.161 → 0.2.162

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.161",
3
+ "version": "0.2.162",
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",
@@ -2,7 +2,7 @@
2
2
  // A workflow is explicitly requested by the caller after inspecting the page.
3
3
  // No arbitrary JavaScript, cross-target access, file uploads or implicit submits.
4
4
  import { assertResolvedPageUrl } from './browser-endpoint.mjs';
5
- import { callPageFunction, evaluate } from './browser-session.mjs';
5
+ import { evaluate } from './browser-session.mjs';
6
6
 
7
7
  const ACTIONS = new Set(['fill_form','set_value','select','focus','click','wait_for']);
8
8
  const CONDITIONS = new Set(['selector','text','url_contains','load']);
@@ -45,7 +45,28 @@ export function validateBrowserWorkflow(steps, { validateFormFields }) {
45
45
  });
46
46
  }
47
47
 
48
- async function waitForStep(session, step, started) {
48
+ // A workflow is a single-page plan. Redirects across origins require a new
49
+ // snapshot and a separate deliberate action, even if both URLs are public.
50
+ export function assertWorkflowOrigin(expectedOrigin, currentUrl) {
51
+ let currentOrigin;
52
+ try { currentOrigin = new URL(currentUrl).origin; }
53
+ catch { throw new Error('Browser workflow page origin could not be verified'); }
54
+ if (expectedOrigin !== null && currentOrigin !== expectedOrigin) {
55
+ throw new Error('Browser workflow origin changed');
56
+ }
57
+ return currentOrigin;
58
+ }
59
+
60
+ // Enforce the origin in the exact same page JS evaluation that writes a field.
61
+ // Caller-supplied selectors/values stay JSON data, not arbitrary JavaScript.
62
+ async function guardedPageFunction(session, trustedFunction, args, origin) {
63
+ const expression = '(() => { if (location.origin !== ' + JSON.stringify(origin) +
64
+ ') throw new Error("workflow_origin_changed"); return (' + trustedFunction +
65
+ ')(...' + JSON.stringify(args) + '); })()';
66
+ return evaluate(session, expression);
67
+ }
68
+
69
+ async function waitForStep(session, step, started, expectedOrigin) {
49
70
  const deadline = Math.min(started + 12000, Date.now() + step.timeout_ms);
50
71
  const expression = step.condition === 'selector' ?
51
72
  'Boolean(document.querySelector('+JSON.stringify(step.criterion)+'))'
@@ -67,7 +88,8 @@ async function waitForStep(session, step, started) {
67
88
  await new Promise(resolve=>setTimeout(resolve, 75));
68
89
  continue;
69
90
  }
70
- // Never accept a match on a private or unsafe page.
91
+ // Never accept a match on an unsafe page or an unexpected origin.
92
+ assertWorkflowOrigin(expectedOrigin, state?.url);
71
93
  await assertResolvedPageUrl(state?.url);
72
94
  if (state?.matched) return;
73
95
  await new Promise(resolve=>setTimeout(resolve, 75));
@@ -80,25 +102,28 @@ export async function executeBrowserWorkflow(session, steps, {
80
102
  }) {
81
103
  const started = Date.now();
82
104
  const completed = [];
105
+ let boundOrigin = null;
83
106
  for (let index = 0; index < steps.length; index++) {
84
107
  const step = steps[index];
85
108
  if (Date.now() - started > 12000) throw new Error('Workflow exceeded 12 second budget after '+completed.length+' steps');
86
109
  try {
87
110
  const currentUrl = await evaluate(session, 'location.href');
111
+ const currentOrigin = assertWorkflowOrigin(boundOrigin, currentUrl);
88
112
  await assertResolvedPageUrl(currentUrl);
113
+ if (boundOrigin === null) boundOrigin = currentOrigin;
89
114
  if (step.action === 'fill_form') {
90
- await callPageFunction(session,pageFillForm,[step.fields]);
115
+ await guardedPageFunction(session,pageFillForm,[step.fields],boundOrigin);
91
116
  } else if (step.action === 'wait_for') {
92
- await waitForStep(session,step,started);
117
+ await waitForStep(session,step,started,boundOrigin);
93
118
  } else if (step.action === 'click') {
94
- const bounds=await callPageFunction(session,pageElementAction,[step.selector,null,'click','']);
119
+ const bounds=await guardedPageFunction(session,pageElementAction,[step.selector,null,'click',''],boundOrigin);
95
120
  const x=Number(bounds.x)+Number(bounds.width)/2;
96
121
  const y=Number(bounds.y)+Number(bounds.height)/2;
97
122
  await session.send('Input.dispatchMouseEvent',{type:'mouseMoved',x,y,button:'none'});
98
123
  await session.send('Input.dispatchMouseEvent',{type:'mousePressed',x,y,button:'left',clickCount:1});
99
124
  await session.send('Input.dispatchMouseEvent',{type:'mouseReleased',x,y,button:'left',clickCount:1});
100
125
  } else {
101
- await callPageFunction(session,pageElementAction,[step.selector,null,step.action,step.value??'']);
126
+ await guardedPageFunction(session,pageElementAction,[step.selector,null,step.action,step.value??''],boundOrigin);
102
127
  }
103
128
  completed.push({index:index+1,action:step.action,ok:true});
104
129
  } catch (error) {