@remcp/runtime 0.2.183 → 0.2.185
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 +14 -0
- package/package.json +1 -1
- package/src/extended/browser-endpoint.mjs +9 -3
- package/src/extended/browser-read.mjs +10 -2
- package/src/extended/browser-readiness.mjs +37 -21
- package/src/patch.mjs +6 -1
- package/src/tools/files-bulk-operations.mjs +6 -1
- package/src/tools/terminal.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.185 — 2026-10-26
|
|
4
|
+
|
|
5
|
+
**Make read-only browser discovery more focused and patch application fail safely.**
|
|
6
|
+
|
|
7
|
+
- Coalesce simultaneous read-only CDP readiness checks for the same validated endpoint, request implementation and timeout without sharing URL-policy decisions or mutating browser state.
|
|
8
|
+
- Honor browser tab ID, title and URL filters before expensive validation, while preserving per-target DNS, socket and SSRF checks.
|
|
9
|
+
- Reject incomplete multi-hunk patch writes to one file and out-of-range zero-context insertion positions instead of silently modifying the wrong text.
|
|
10
|
+
- Keep existing runtime tool schemas and authorization boundaries; validate across isolated Chromium, deterministic regressions and the full runtime suite.
|
|
11
|
+
|
|
12
|
+
## 0.2.184 — 2026-10-26
|
|
13
|
+
|
|
14
|
+
- Return initial output from long-running terminal sessions with a 45 ms exit-status grace instead of 175 ms, while preserving final exit codes for fast commands, silent-process timeout bounds and tool output schemas.
|
|
15
|
+
- Add regression coverage for first-output latency, structured exit state, incremental output and command cancellation.
|
|
16
|
+
|
|
3
17
|
## 0.2.181 — 2026-10-25
|
|
4
18
|
|
|
5
19
|
- Add bounded read-only `browser_devtools` Form Map and selector-scoped computed Styles/box-model inspection with no user-entered values, raw CSS, URL content or page text in diagnostic reports.
|
package/package.json
CHANGED
|
@@ -173,12 +173,18 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
|
|
|
173
173
|
// Validating every tab sequentially can require hundreds of serial DNS lookups,
|
|
174
174
|
// making browser_tabs exceed the relay's deadline on machines with many tabs.
|
|
175
175
|
// Keep DNS and security validation, but bound both the work and its concurrency.
|
|
176
|
-
export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null, endpoint = null } = {}) {
|
|
176
|
+
export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null, titleContains = null, urlContains = null, endpoint = null } = {}) {
|
|
177
177
|
if (!Array.isArray(list)) return [];
|
|
178
178
|
const candidates = [];
|
|
179
|
+
const titleNeedle = String(titleContains || '').toLowerCase();
|
|
180
|
+
const urlNeedle = String(urlContains || '').toLowerCase();
|
|
179
181
|
for (const item of list) {
|
|
180
182
|
if (!item || item.type !== 'page' || !item.webSocketDebuggerUrl) continue;
|
|
181
183
|
if (targetId && item.id !== targetId) continue;
|
|
184
|
+
// Filter untrusted CDP metadata before expensive URL/DNS validation, but
|
|
185
|
+
// still validate every matching URL before it can reach the caller.
|
|
186
|
+
if (titleNeedle && !String(item.title || '').toLowerCase().includes(titleNeedle)) continue;
|
|
187
|
+
if (urlNeedle && !String(item.url || '').toLowerCase().includes(urlNeedle)) continue;
|
|
182
188
|
// Reject fabricated/stale CDP socket metadata *before* DNS validation.
|
|
183
189
|
// browser_tabs must never advertise a target that cannot pass chooseTarget.
|
|
184
190
|
if (endpoint) {
|
|
@@ -229,7 +235,7 @@ export async function filterBrowserTargets(list, { validate = assertResolvedPage
|
|
|
229
235
|
return targets;
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
export async function listBrowserTargets(endpoint, { targetId = null, readOnlySingleFlight = false, validate = assertResolvedPageUrl } = {}) {
|
|
238
|
+
export async function listBrowserTargets(endpoint, { targetId = null, titleContains = null, urlContains = null, readOnlySingleFlight = false, validate = assertResolvedPageUrl } = {}) {
|
|
233
239
|
assertBrowserRemoteEnabled();
|
|
234
240
|
// Reading tabs and page semantics must not launch a separate Chromium/profile.
|
|
235
241
|
// Only an explicit browser_navigate(action=new_tab) can bootstrap an isolated browser.
|
|
@@ -248,7 +254,7 @@ export async function listBrowserTargets(endpoint, { targetId = null, readOnlySi
|
|
|
248
254
|
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.${browserLaunchExitHint({ configured })}`);
|
|
249
255
|
}
|
|
250
256
|
// Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
|
|
251
|
-
return filterBrowserTargets(list, { targetId, validate, endpoint:base });
|
|
257
|
+
return filterBrowserTargets(list, { targetId, titleContains, urlContains, validate, endpoint:base });
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
export function validatedTargetWebSocketUrl(target, endpoint) {
|
|
@@ -15,7 +15,12 @@ import { assertResolvedBrowserUrl, assertResolvedPageUrl, boundedText, endpointU
|
|
|
15
15
|
import { createGuardedNewTab, evaluate, withTarget } from './browser-session.mjs';
|
|
16
16
|
|
|
17
17
|
export async function browserTabs(args = {}) {
|
|
18
|
-
const
|
|
18
|
+
const targetId = optionalString(args.target_id || args.targetId);
|
|
19
|
+
const titleContains = optionalString(args.title);
|
|
20
|
+
const urlContains = optionalString(args.url_contains || args.urlContains);
|
|
21
|
+
const targets = await listBrowserTargets(args.endpoint, {
|
|
22
|
+
targetId, titleContains, urlContains, readOnlySingleFlight:true,
|
|
23
|
+
});
|
|
19
24
|
const pages = targets.filter(item => item.type === 'page').slice(0, 500).map(item => ({
|
|
20
25
|
id: boundedText(item.id, 256),
|
|
21
26
|
title: boundedText(item.title, 1000),
|
|
@@ -25,7 +30,10 @@ export async function browserTabs(args = {}) {
|
|
|
25
30
|
const result = jsonResult(pages);
|
|
26
31
|
// Keep the typed empty-array contract while providing a native fallback hint.
|
|
27
32
|
if (!pages.length) {
|
|
28
|
-
|
|
33
|
+
const scoped = Boolean(targetId || titleContains || urlContains);
|
|
34
|
+
result.content = [{ type: 'text', text: scoped
|
|
35
|
+
? 'CDP is connected, but no authorized browser page matches the requested selector. Check the selector or use ui_snapshot and ui_find for native browser accessibility.'
|
|
36
|
+
: 'CDP is connected, but no debuggable Chromium tabs are available. Use ui_snapshot and ui_find to inspect existing browser windows without CDP; only open a new tab when explicitly requested.' }];
|
|
29
37
|
}
|
|
30
38
|
return result;
|
|
31
39
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { browserRemoteEnabled, endpointUrl, requestJson, validatedTargetWebSocketUrl } from './browser-endpoint.mjs';
|
|
5
5
|
|
|
6
6
|
const records = new Map();
|
|
7
|
+
// Avoid duplicate /json/version + /json/list reads when several agent calls
|
|
8
|
+
// request CDP readiness at once. Never coalesce different probe transports.
|
|
9
|
+
const pendingProbes = new Map();
|
|
7
10
|
const CACHE_MS = 3000;
|
|
8
11
|
const MAX_ENTRIES = 8;
|
|
9
12
|
const transportError = message =>
|
|
@@ -39,30 +42,43 @@ export async function probeBrowserReadiness({
|
|
|
39
42
|
let key;
|
|
40
43
|
try { key = endpointUrl(endpoint).origin; }
|
|
41
44
|
catch { return { status:'blocked_endpoint', ready:false, candidate_pages:0, next_step:'check_local_cdp_config' }; }
|
|
45
|
+
const timeout = Math.max(100, Math.min(800, Number(timeoutMs) || 350));
|
|
42
46
|
if (cache) {
|
|
43
47
|
const previous = records.get(key);
|
|
44
|
-
if (previous && previous.expires > now
|
|
48
|
+
if (previous && previous.expires > now && previous.request === request &&
|
|
49
|
+
previous.timeout === timeout) return { ...previous.value, cache_hit:true };
|
|
45
50
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
const load = async () => {
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
const version = await request('/json/version', endpoint, { timeout_ms:timeout });
|
|
55
|
+
// CDP version alone is not evidence a useful debuggable tab exists.
|
|
56
|
+
if (!version || typeof version !== 'object' || !(version.Browser || version.webSocketDebuggerUrl)) {
|
|
57
|
+
result = { status:'invalid_response', ready:false, candidate_pages:0 };
|
|
58
|
+
} else {
|
|
59
|
+
const targets = await request('/json/list', endpoint, { timeout_ms:timeout });
|
|
60
|
+
result = classifyBrowserReadiness(version, targets, endpoint);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
result = { status:transportError(String(error?.message || '')) ? 'unavailable' : 'invalid_response',
|
|
64
|
+
ready:false, candidate_pages:0 };
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const output = { ...result, next_step:result.ready ? 'use_browser_tabs' : 'use_native_accessibility' };
|
|
67
|
+
if (cache) {
|
|
68
|
+
records.set(key, { expires:now + CACHE_MS, value:output, request, timeout });
|
|
69
|
+
while (records.size > MAX_ENTRIES) records.delete(records.keys().next().value);
|
|
70
|
+
}
|
|
71
|
+
return output;
|
|
72
|
+
};
|
|
73
|
+
if (!cache) return load();
|
|
74
|
+
const pending = pendingProbes.get(key);
|
|
75
|
+
if (pending && pending.request === request && pending.timeout === timeout) return pending.work;
|
|
76
|
+
// Bounded memory; mismatched transports must never share probe results.
|
|
77
|
+
if (pending || pendingProbes.size >= MAX_ENTRIES) return load();
|
|
78
|
+
const work = Promise.resolve().then(load);
|
|
79
|
+
pendingProbes.set(key, { request, timeout, work });
|
|
80
|
+
try { return await work; }
|
|
81
|
+
finally {
|
|
82
|
+
if (pendingProbes.get(key)?.work === work) pendingProbes.delete(key);
|
|
66
83
|
}
|
|
67
|
-
return output;
|
|
68
84
|
}
|
package/src/patch.mjs
CHANGED
|
@@ -87,7 +87,12 @@ function locate(lines, hunk) {
|
|
|
87
87
|
// A hunk with no context and no removals is a pure insertion: `@@ -N,0 +M,K @@` inserts after
|
|
88
88
|
// line N, so the zero-based insertion point is N (and the end of file when N is the last line).
|
|
89
89
|
// Using N-1 here inserted every such hunk one line too early while still reporting success.
|
|
90
|
-
if (!expected.length)
|
|
90
|
+
if (!expected.length) {
|
|
91
|
+
// A stale or malformed insertion offset must fail instead of silently
|
|
92
|
+
// appending to EOF (which can corrupt an unrelated section of a file).
|
|
93
|
+
if (!Number.isSafeInteger(hunk.oldStart) || hunk.oldStart < 0 || hunk.oldStart > lines.length) return null;
|
|
94
|
+
return { index: hunk.oldStart, fuzz: 0 };
|
|
95
|
+
}
|
|
91
96
|
const candidates = [];
|
|
92
97
|
const anchor = Math.max(0, hunk.oldStart - 1);
|
|
93
98
|
for (let offset = 0; offset <= 200; offset += 1) {
|
|
@@ -234,7 +234,12 @@ export async function applyPatchTool(args) {
|
|
|
234
234
|
else throw error;
|
|
235
235
|
}
|
|
236
236
|
const { updated, applied, failed } = applyHunks(original, file.hunks);
|
|
237
|
-
|
|
237
|
+
// Never persist a partially applied file, even if one hunk did match.
|
|
238
|
+
// An MCP success response here would let an agent continue with broken code.
|
|
239
|
+
if (failed.length) {
|
|
240
|
+
results.push(`failed ${displayPath(absolute)}: ${failed.length}/${file.hunks.length} hunk(s) did not match; no changes written`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
238
243
|
const stats = diffStats(original, updated);
|
|
239
244
|
if (!dryRun) {
|
|
240
245
|
assertWritableSize(updated);
|
package/src/tools/terminal.mjs
CHANGED
|
@@ -69,9 +69,12 @@ function terminalStructured(value, fallbackText) {
|
|
|
69
69
|
// useful output arrives, keeping a short grace window so quick commands can
|
|
70
70
|
// still report their real exit status without requiring a follow-up tool call.
|
|
71
71
|
// Explicit timeout_ms remains an upper bound, including for silent commands.
|
|
72
|
+
// Buffered output needs only a short exit-status grace: 175ms held long-running
|
|
73
|
+
// commands even after their first useful text was already ready. A 45ms window
|
|
74
|
+
// lets immediate exits report their final code without delaying live feedback.
|
|
72
75
|
async function waitForInitialOutput(session, timeoutMs) {
|
|
73
76
|
const deadline = Date.now() + timeoutMs;
|
|
74
|
-
const graceUntil = Date.now() + Math.min(
|
|
77
|
+
const graceUntil = Date.now() + Math.min(45, timeoutMs);
|
|
75
78
|
while (!session.exited) {
|
|
76
79
|
const now = Date.now();
|
|
77
80
|
if (now >= deadline || (now >= graceUntil && hasNewOutput(session))) break;
|