@remcp/runtime 0.2.215 → 0.2.217

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.215",
3
+ "version": "0.2.217",
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",
@@ -16,19 +16,30 @@ import { findOcrText, ocrImage } from './ocr.mjs';
16
16
  import { boundedComputerSnapshot, clipboard, cursorPosition, displayInventory, listWindows, objectRecord, resultValue, screenshotRegion, snapshotError, suggestedUiAction, toolResultMessage, uiSnapshot } from './desktop-core.mjs';
17
17
  import { activeWindowCaptureBounds, browserTitleHint, computerScreenshotTask, hasExplicitComputerScreenshotTarget, ocrScreenshotResult } from './desktop-vision.mjs';
18
18
 
19
- // Several simultaneous Windows PowerShell starts can stall until their shared
20
- // timeout on low-core hosts, which made the whole snapshot report every probe
21
- // as unavailable. Queue the probes on Windows; other platforms stay parallel.
22
- export async function settleComputerSnapshotProbes(tasks, { serial = process.platform === 'win32' } = {}) {
23
- if (!serial) return Promise.allSettled(tasks.map((task) => task()));
24
- const results = [];
25
- for (const task of tasks) {
26
- try {
27
- results.push({ status: 'fulfilled', value: await task() });
28
- } catch (reason) {
29
- results.push({ status: 'rejected', reason });
19
+ // Four simultaneous Windows PowerShell starts can stall until their shared
20
+ // timeout on low-core hosts, while two finish normally. Keep a small pool on
21
+ // Windows so the snapshot stays under the relay timeout; other platforms stay
22
+ // fully parallel.
23
+ export async function settleComputerSnapshotProbes(tasks, {
24
+ concurrency = process.platform === 'win32' ? 2 : Math.max(tasks.length, 1),
25
+ } = {}) {
26
+ const limit = Math.max(1, Math.min(Number(concurrency) || 1, tasks.length || 1));
27
+ if (limit >= tasks.length) return Promise.allSettled(tasks.map((task) => task()));
28
+ const results = new Array(tasks.length);
29
+ let next = 0;
30
+ const worker = async () => {
31
+ for (;;) {
32
+ const index = next;
33
+ next += 1;
34
+ if (index >= tasks.length) return;
35
+ try {
36
+ results[index] = { status: 'fulfilled', value: await tasks[index]() };
37
+ } catch (reason) {
38
+ results[index] = { status: 'rejected', reason };
39
+ }
30
40
  }
31
- }
41
+ };
42
+ await Promise.all(Array.from({ length: limit }, () => worker()));
32
43
  return results;
33
44
  }
34
45
 
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
5
  import { constants, createReadStream } from 'node:fs';
6
- import { access, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, rename, rm, stat, unlink } from 'node:fs/promises';
6
+ import { access, chmod, chown, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, unlink } from 'node:fs/promises';
7
7
  import { pipeline } from 'node:stream/promises';
8
8
  import { liveConfig, runtimeConfig } from '../config.mjs';
9
9
  import { documentKind, readDocxText, readPdfText } from '../documents.mjs';
@@ -316,14 +316,43 @@ export async function openRegularFile(absolute, flags = READ_NOFOLLOW) {
316
316
  return openRegularFileAtPath(absolute, flags, absolute);
317
317
  }
318
318
 
319
+ async function openLinuxDirectoryAnchor(absolute, options = {}) {
320
+ try {
321
+ return await openDirectoryPath(absolute, options);
322
+ } catch (error) {
323
+ if (runtimeConfig.allowedRoots.length) throw error;
324
+ // Unrestricted mode may traverse trusted links such as macOS-style aliases
325
+ // or user-created symlinked directories. Resolve only the anchor; returned
326
+ // tool paths stay lexical so the caller still sees the path it supplied.
327
+ if (options.create) await mkdir(absolute, { recursive:true }).catch(() => {});
328
+ const resolved = await realpath(absolute).catch(() => null);
329
+ if (!resolved) throw error;
330
+ return openDirectoryPath(resolved, options);
331
+ }
332
+ }
333
+
334
+ async function trustedNonLinuxDirectory(absolute, { create = false } = {}) {
335
+ if (create) await mkdir(absolute, { recursive: true });
336
+ const info = await lstat(absolute);
337
+ if (info.isSymbolicLink()) {
338
+ // With configured roots the path is already canonicalized; a remaining
339
+ // symlink component could be a confinement escape. Only unrestricted mode
340
+ // may follow trusted system links such as macOS /tmp -> /private/tmp.
341
+ if (runtimeConfig.allowedRoots.length) fail('Filesystem path component is not a directory');
342
+ const resolved = await realpath(absolute);
343
+ const target = await stat(resolved);
344
+ if (!target.isDirectory()) fail('Filesystem path component is not a directory');
345
+ return resolved;
346
+ }
347
+ if (!info.isDirectory()) fail('Filesystem path component is not a directory');
348
+ return absolute;
349
+ }
350
+
319
351
  export async function openWritableParent(absolute, options = {}) {
320
352
  if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
321
- return openDirectoryPath(absolute, { create:true, ...options });
353
+ return openLinuxDirectoryAnchor(absolute, { create:true, ...options });
322
354
  }
323
- await mkdir(absolute, { recursive:true });
324
- const info = await lstat(absolute);
325
- if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
326
- return { anchor:absolute, close:async() => {} };
355
+ return { anchor: await trustedNonLinuxDirectory(absolute, { create:true }), close:async() => {} };
327
356
  }
328
357
 
329
358
  export async function writeResolvedFile(absolute, content, { append = false } = {}) {
@@ -352,11 +381,9 @@ export async function withWritableParent(absolute, callback) {
352
381
 
353
382
  export async function openExistingParent(absolute) {
354
383
  if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
355
- return openDirectoryPath(absolute);
384
+ return openLinuxDirectoryAnchor(absolute);
356
385
  }
357
- const info = await lstat(absolute);
358
- if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
359
- return { anchor:absolute, close:async() => {} };
386
+ return { anchor: await trustedNonLinuxDirectory(absolute), close:async() => {} };
360
387
  }
361
388
 
362
389
  export async function withExistingParent(absolute, callback) {
@@ -370,13 +397,11 @@ export async function withExistingParent(absolute, callback) {
370
397
 
371
398
  export async function ensureDirectoryPath(absolute, options = {}) {
372
399
  if (process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY) {
373
- const directory = await openDirectoryPath(absolute, { create:true, ...options });
400
+ const directory = await openLinuxDirectoryAnchor(absolute, { create:true, ...options });
374
401
  await directory.close();
375
402
  return;
376
403
  }
377
- await mkdir(absolute, { recursive:true });
378
- const info = await lstat(absolute);
379
- if (info.isSymbolicLink() || !info.isDirectory()) fail('Filesystem path component is not a directory');
404
+ await trustedNonLinuxDirectory(absolute, { create:true });
380
405
  }
381
406
 
382
407
  export async function readRegularBuffer(absolute, maxBytes = Infinity) {
@@ -111,7 +111,7 @@ export async function listProcessesTool(args) {
111
111
  }
112
112
  parsed.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
113
113
  }
114
- rows.push('pid,ppid,cpu%,mem%,elapsed,command');
114
+ rows.push(process.platform === 'win32' ? 'name,pid,session,mem' : 'pid,ppid,cpu%,mem%,elapsed,command');
115
115
  rows.push(...parsed.slice(0, limit).map(entry => entry.row));
116
116
  if (parsed.length > limit) rows.push(`… ${parsed.length - limit} more processes hidden; call list_processes with a higher limit to see them`);
117
117
  return text(rows.join('\n'));