@remcp/runtime 0.2.163 → 0.2.165
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.165 — 2026-10-18
|
|
4
|
+
|
|
5
|
+
**Crash-session recovery without losing browser automation when the local snapshot budget fills.**
|
|
6
|
+
|
|
7
|
+
- When the dedicated Chromium profile already has 12 interrupted-session backups, preserve its tabs and all evidence and start a single, owner-only isolated browser profile instead of failing to launch.
|
|
8
|
+
- Reuse the isolated profile across subsequent starts; never copy credentials or account state. Emit a local warning that sign-in state is not shared.
|
|
9
|
+
- Still fail closed on unsafe symlinks, oversized evidence, unowned recovery directories or exhausted isolated-profile recovery capacity. No automatic deletion of backups.
|
|
10
|
+
- Keep 83 runtime operations, the ten hosted MCP definitions, URL/DNS/SSRF checks and authorization boundaries unchanged. Add regression tests for full-capacity and unsafe-profile cases.
|
|
11
|
+
|
|
3
12
|
## 0.2.154 — 2026-10-17
|
|
4
13
|
|
|
5
14
|
**ReMCP Gray v2 brand parity release.** The runtime tool contract remains at 83 operations with unchanged permissions and I/O semantics. The coordinated public client and OpenAI submission assets receive the owner-provided gray identity.
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ discovery for new clients.
|
|
|
64
64
|
|
|
65
65
|
For browser typing, `browser_text` is the visible DOM locator while `text` is the Unicode payload. `browser_action(action="scroll")` accepts page/element deltas or direction/wheel steps and scrolls the page or nearest scrollable ancestor; `scroll_into_view` remains the separate semantic reveal operation.
|
|
66
66
|
|
|
67
|
-
When ReMCP auto-launches Chrome/Edge for local CDP, it uses a dedicated ReMCP browser profile. If that browser was interrupted (for example during an agent upgrade), and Chromium has saved tabs in that profile, the next launch requests Chromium's own last-session restoration instead of opening a new `about:blank` tab. A clean browser shutdown never triggers automatic reopening. This inspects only local session metadata, never reads or transmits tab URLs to ReMCP's relay. The browser itself controls which tabs can actually be recovered. On an interrupted profile, ReMCP now saves a private, local-only snapshot of `Session_*` and `Tabs_*` files before Chromium can rotate them on startup, even when only tab files remain. Recovery snapshots are adjacent to the dedicated browser profile in a `browser-cdp-recovery/` directory (owner-only on Unix); existing snapshots are never deleted automatically. Copy limits are 16 regular files, 32 MiB per file, 64 MiB per snapshot and 12 distinct snapshots. If evidence
|
|
67
|
+
When ReMCP auto-launches Chrome/Edge for local CDP, it uses a dedicated ReMCP browser profile. If that browser was interrupted (for example during an agent upgrade), and Chromium has saved tabs in that profile, the next launch requests Chromium's own last-session restoration instead of opening a new `about:blank` tab. A clean browser shutdown never triggers automatic reopening. This inspects only local session metadata, never reads or transmits tab URLs to ReMCP's relay. The browser itself controls which tabs can actually be recovered. On an interrupted profile, ReMCP now saves a private, local-only snapshot of `Session_*` and `Tabs_*` files before Chromium can rotate them on startup, even when only tab files remain. Recovery snapshots are adjacent to the dedicated browser profile in a `browser-cdp-recovery/` directory (owner-only on Unix); existing snapshots are never deleted automatically. Copy limits are 16 regular files, 32 MiB per file, 64 MiB per snapshot and 12 distinct snapshots. If the primary profile reaches the 12-snapshot limit, ReMCP switches to one separate owner-only isolated browser profile rather than overwriting prior recovery evidence. The original profile, its tabs and backups remain on disk; credentials and sign-in state are **not** copied into the isolated profile, and a warning is logged. Subsequent launches reuse that isolated profile. If either profile has unsafe evidence (for example symlinks or oversized files) or the isolated profile also reaches its recovery limit, browser auto-launch fails closed. No session URLs are parsed, returned through tools, or uploaded. You can review and manage those local snapshots yourself.
|
|
68
68
|
|
|
69
69
|
For frontend QA, use semantic data to operate the page and rendered pixels to verify it: `browser_action(action="set_viewport")` sets an exact responsive-test viewport, `browser_wait` waits for the real application state, and `browser_snapshot(include_screenshot=true, selector=...)` recenters the component, returns its bounds, and attaches the real CDP-rendered viewport PNG. This intentionally treats screenshots as the visual oracle without making screenshot-driven coordinate guessing the primary control path.
|
|
70
70
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Coalesce *only overlapping* read-only CDP /json/list transport requests.
|
|
2
|
+
// Raw tab metadata is held only until request completion; each caller must
|
|
3
|
+
// re-validate all target URLs/DNS independently before returning any tabs.
|
|
4
|
+
const active = new Map();
|
|
5
|
+
const MAX_ENDPOINTS = 8;
|
|
6
|
+
|
|
7
|
+
export async function shareBrowserDiscovery(origin, load) {
|
|
8
|
+
const pending = active.get(origin);
|
|
9
|
+
if (pending) return pending;
|
|
10
|
+
if (active.size >= MAX_ENDPOINTS) return load();
|
|
11
|
+
const work = Promise.resolve().then(load);
|
|
12
|
+
active.set(origin, work);
|
|
13
|
+
try { return await work; }
|
|
14
|
+
finally {
|
|
15
|
+
if (active.get(origin) === work) active.delete(origin);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -5,7 +5,8 @@ import { isIP } from 'node:net';
|
|
|
5
5
|
import { lookup } from 'node:dns/promises';
|
|
6
6
|
import { closeSync, constants as fsConstants, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readdirSync } from 'node:fs';
|
|
7
7
|
import { resolveSafePath } from '../util.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { selectSafeBrowserLaunchProfile } from './browser-session-backup.mjs';
|
|
9
|
+
import { shareBrowserDiscovery } from './browser-discovery-flight.mjs';
|
|
9
10
|
import { clamp, commandExists, jsonResult, optionalString, requireEnum, spawnDetached, unavailable } from './common.mjs';
|
|
10
11
|
|
|
11
12
|
|
|
@@ -153,8 +154,9 @@ export async function ensureBrowserEndpoint(endpoint) {
|
|
|
153
154
|
mkdirSync(profile, { recursive:true, mode:0o700 });
|
|
154
155
|
// Chrome rotates its Sessions/ files on startup. Protect existing local
|
|
155
156
|
// evidence before a restart, including profiles with only Tabs_* left.
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const selected = selectSafeBrowserLaunchProfile(profile);
|
|
158
|
+
if (selected.isolated) console.warn('ReMCP browser: using an isolated recovery profile; existing session and backups are preserved, and sign-in state is not copied.');
|
|
159
|
+
spawnDetached(executable, browserAutoLaunchArgs(selected.profile));
|
|
158
160
|
if (!await waitForBrowserEndpoint(5000)) throw new Error('ReMCP launched a browser but its local CDP endpoint did not become ready');
|
|
159
161
|
})().finally(() => { browserLaunchPromise = null; });
|
|
160
162
|
}
|
|
@@ -436,14 +438,16 @@ export async function filterBrowserTargets(list, { validate = assertResolvedPage
|
|
|
436
438
|
return targets;
|
|
437
439
|
}
|
|
438
440
|
|
|
439
|
-
export async function listBrowserTargets(endpoint, { targetId = null } = {}) {
|
|
441
|
+
export async function listBrowserTargets(endpoint, { targetId = null, readOnlySingleFlight = false, validate = assertResolvedPageUrl } = {}) {
|
|
440
442
|
assertBrowserRemoteEnabled();
|
|
441
443
|
// Reading tabs and page semantics must not launch a separate Chromium/profile.
|
|
442
444
|
// Only an explicit browser_navigate(action=new_tab) can bootstrap an isolated browser.
|
|
443
445
|
const base = endpointUrl(endpoint); // keeps the CDP address loopback-only
|
|
444
446
|
let list;
|
|
445
447
|
try {
|
|
446
|
-
|
|
448
|
+
const load = () => requestJson('/json/list', endpoint, { timeout_ms: 1500 });
|
|
449
|
+
list = readOnlySingleFlight && !base.username && !base.password
|
|
450
|
+
? await shareBrowserDiscovery(base.origin, load) : await load();
|
|
447
451
|
} catch (error) {
|
|
448
452
|
const message = String(error?.message || '');
|
|
449
453
|
// Keep HTTP/policy/oversize/malformed-response errors distinct from an unavailable CDP.
|
|
@@ -453,7 +457,7 @@ export async function listBrowserTargets(endpoint, { targetId = null } = {}) {
|
|
|
453
457
|
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.`);
|
|
454
458
|
}
|
|
455
459
|
// Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
|
|
456
|
-
return filterBrowserTargets(list, { targetId });
|
|
460
|
+
return filterBrowserTargets(list, { targetId, validate });
|
|
457
461
|
}
|
|
458
462
|
|
|
459
463
|
export function validatedTargetWebSocketUrl(target, endpoint) {
|
|
@@ -14,7 +14,7 @@ import { assertResolvedBrowserUrl, assertResolvedPageUrl, boundedText, endpointU
|
|
|
14
14
|
import { createGuardedNewTab, evaluate, withTarget } from './browser-session.mjs';
|
|
15
15
|
|
|
16
16
|
export async function browserTabs(args = {}) {
|
|
17
|
-
const targets = await listBrowserTargets(args.endpoint);
|
|
17
|
+
const targets = await listBrowserTargets(args.endpoint, { readOnlySingleFlight:true });
|
|
18
18
|
const pages = targets.filter(item => item.type === 'page').slice(0, 500).map(item => ({
|
|
19
19
|
id: boundedText(item.id, 256),
|
|
20
20
|
title: boundedText(item.title, 1000),
|
|
@@ -42,7 +42,8 @@ function safeDirectory(dir) {
|
|
|
42
42
|
if (process.platform === 'win32' && lstatSync(dir).isSymbolicLink()) throw new Error('Unsafe recovery directory');
|
|
43
43
|
const fd = openSync(dir, constants.O_RDONLY | DIRECTORY | NOFOLLOW);
|
|
44
44
|
try {
|
|
45
|
-
|
|
45
|
+
const stat = fstatSync(fd);
|
|
46
|
+
if (!stat.isDirectory() || (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid())) throw new Error('Unsafe recovery directory');
|
|
46
47
|
if (process.platform !== 'win32') fchmodSync(fd, 0o700);
|
|
47
48
|
} finally { closeSync(fd); }
|
|
48
49
|
}
|
|
@@ -133,3 +134,28 @@ export function preserveInterruptedBrowserSession(profile) {
|
|
|
133
134
|
}
|
|
134
135
|
} finally { if (dirFd !== undefined) closeSync(dirFd); }
|
|
135
136
|
}
|
|
137
|
+
|
|
138
|
+
// Never overwrite an interrupted Chromium profile when its recovery evidence
|
|
139
|
+
// is at capacity. One sibling profile keeps automation available while
|
|
140
|
+
// retaining the original profile and all snapshot files untouched.
|
|
141
|
+
export function selectSafeBrowserLaunchProfile(profile) {
|
|
142
|
+
const isolated = profile + '-isolated';
|
|
143
|
+
let prior = false;
|
|
144
|
+
try { lstatSync(isolated); prior = true; }
|
|
145
|
+
catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
146
|
+
if (prior) {
|
|
147
|
+
safeDirectory(isolated);
|
|
148
|
+
preserveInterruptedBrowserSession(isolated);
|
|
149
|
+
return { profile: isolated, isolated: true };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
preserveInterruptedBrowserSession(profile);
|
|
153
|
+
return { profile, isolated: false };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error?.message !== 'Session recovery snapshot limit reached') throw error;
|
|
156
|
+
// Never prune user-owned session recovery evidence.
|
|
157
|
+
safeDirectory(isolated);
|
|
158
|
+
preserveInterruptedBrowserSession(isolated);
|
|
159
|
+
return { profile: isolated, isolated: true };
|
|
160
|
+
}
|
|
161
|
+
}
|