@remcp/runtime 0.2.175 → 0.2.176
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 +7 -0
- package/package.json +1 -1
- package/src/extended/browser-endpoint.mjs +13 -2
- package/src/extended/browser-read.mjs +39 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.176 — 2026-10-23
|
|
4
|
+
|
|
5
|
+
- Reduce screenshot CDP round-trips by batching viewport resolution with capture and reusing measured element dimensions.
|
|
6
|
+
- Cache Accessibility enablement per WebSocket connection with single-flight behavior.
|
|
7
|
+
- Filter malformed, mismatched or unsafe page WebSocket targets during discovery rather than exposing unusable tabs.
|
|
8
|
+
- Keep browser URL/DNS validation, permissions and the existing 83 operation definitions unchanged.
|
|
9
|
+
|
|
3
10
|
## 0.2.175 — 2026-10-22
|
|
4
11
|
|
|
5
12
|
- Carry forward the public 0.2.174 browser-policy and diagnostics-recording module extraction without weakening CDP or recording controls.
|
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 } = {}) {
|
|
176
|
+
export async function filterBrowserTargets(list, { validate = assertResolvedPageUrl, targetId = null, endpoint = null } = {}) {
|
|
177
177
|
if (!Array.isArray(list)) return [];
|
|
178
178
|
const candidates = [];
|
|
179
179
|
for (const item of list) {
|
|
180
180
|
if (!item || item.type !== 'page' || !item.webSocketDebuggerUrl) continue;
|
|
181
181
|
if (targetId && item.id !== targetId) continue;
|
|
182
|
+
// Reject fabricated/stale CDP socket metadata *before* DNS validation.
|
|
183
|
+
// browser_tabs must never advertise a target that cannot pass chooseTarget.
|
|
184
|
+
if (endpoint) {
|
|
185
|
+
try { validatedTargetWebSocketUrl(item, endpoint); }
|
|
186
|
+
catch { continue; }
|
|
187
|
+
}
|
|
182
188
|
candidates.push(item);
|
|
183
189
|
if (candidates.length >= 1000) break;
|
|
184
190
|
}
|
|
@@ -242,13 +248,18 @@ export async function listBrowserTargets(endpoint, { targetId = null, readOnlySi
|
|
|
242
248
|
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 })}`);
|
|
243
249
|
}
|
|
244
250
|
// Retain the current bounded concurrent DNS/security checks for up to 1000 tabs.
|
|
245
|
-
return filterBrowserTargets(list, { targetId, validate });
|
|
251
|
+
return filterBrowserTargets(list, { targetId, validate, endpoint:base });
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
export function validatedTargetWebSocketUrl(target, endpoint) {
|
|
249
255
|
let socketUrl;
|
|
250
256
|
try { socketUrl = new URL(String(target?.webSocketDebuggerUrl)); }
|
|
251
257
|
catch { throw new Error('Browser returned an invalid CDP WebSocket URL'); }
|
|
258
|
+
const targetId = String(target?.id ?? '');
|
|
259
|
+
if (!targetId || targetId.length > 256 || socketUrl.username || socketUrl.password ||
|
|
260
|
+
socketUrl.search || socketUrl.hash) {
|
|
261
|
+
throw new Error('Browser returned an invalid CDP WebSocket target');
|
|
262
|
+
}
|
|
252
263
|
if (!['ws:', 'wss:'].includes(socketUrl.protocol) || !isLoopbackHost(socketUrl.hostname)) {
|
|
253
264
|
throw new Error('Browser CDP WebSocket target must be loopback-only');
|
|
254
265
|
}
|
|
@@ -139,9 +139,42 @@ export function sensitiveAccessibilityDescendants(nodes) {
|
|
|
139
139
|
return secret;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// One viewport evaluation is enough for both dimensions, and may run while
|
|
143
|
+
// Chrome captures the screenshot. Selector capture already returned the viewport
|
|
144
|
+
// dimensions with the element bounds, so it needs no further CDP evaluation.
|
|
145
|
+
export async function captureViewportScreenshot(session, screenshotTarget = null) {
|
|
146
|
+
const dimensions = screenshotTarget
|
|
147
|
+
? Promise.resolve({width:screenshotTarget.viewport_width, height:screenshotTarget.viewport_height})
|
|
148
|
+
: evaluate(session, '({width:innerWidth,height:innerHeight})').catch(() => null);
|
|
149
|
+
const image = session.send('Page.captureScreenshot', {
|
|
150
|
+
format:'png',fromSurface:true,captureBeyondViewport:false,
|
|
151
|
+
});
|
|
152
|
+
const [size,captured] = await Promise.all([dimensions,image]);
|
|
153
|
+
return {
|
|
154
|
+
captured,
|
|
155
|
+
viewport:{scope:'viewport',width:size?.width ?? null,height:size?.height ?? null},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Enabling Accessibility is a per-CDP-socket state. Reuse the successful
|
|
160
|
+
// handshake for retained guarded sockets; a new socket always enables afresh.
|
|
161
|
+
const accessibilityReady = new WeakMap();
|
|
162
|
+
export async function ensureAccessibilityReady(session) {
|
|
163
|
+
let pending = accessibilityReady.get(session);
|
|
164
|
+
if (!pending) {
|
|
165
|
+
pending = Promise.resolve().then(() => session.send('Accessibility.enable'));
|
|
166
|
+
accessibilityReady.set(session,pending);
|
|
167
|
+
try { await pending; }
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (accessibilityReady.get(session)===pending) accessibilityReady.delete(session);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
} else await pending;
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
export async function browserSnapshot(args) {
|
|
143
176
|
return withTarget(args, async (session, target) => {
|
|
144
|
-
await session
|
|
177
|
+
await ensureAccessibilityReady(session);
|
|
145
178
|
const { nodes = [] } = await session.send('Accessibility.getFullAXTree');
|
|
146
179
|
const limit = clamp(args.max_nodes, 1500, 1, 10_000);
|
|
147
180
|
const redacted = sensitiveAccessibilityDescendants(nodes);
|
|
@@ -206,27 +239,19 @@ export async function browserSnapshot(args) {
|
|
|
206
239
|
const { original_scroll_x: _originalX, original_scroll_y: _originalY, ...reportedBounds } = bounds;
|
|
207
240
|
payload.screenshot_target = { selector, ...reportedBounds };
|
|
208
241
|
}
|
|
209
|
-
|
|
210
|
-
scope: 'viewport',
|
|
211
|
-
width: await evaluate(session, 'innerWidth').catch(() => null),
|
|
212
|
-
height: await evaluate(session, 'innerHeight').catch(() => null),
|
|
213
|
-
};
|
|
214
|
-
const result = jsonResult(payload);
|
|
215
|
-
let captured;
|
|
242
|
+
let screenshot;
|
|
216
243
|
try {
|
|
217
|
-
|
|
218
|
-
format: 'png',
|
|
219
|
-
fromSurface: true,
|
|
220
|
-
captureBeyondViewport: false,
|
|
221
|
-
});
|
|
244
|
+
screenshot = await captureViewportScreenshot(session, payload.screenshot_target);
|
|
222
245
|
} finally {
|
|
223
246
|
if (restoreScroll) {
|
|
224
247
|
await evaluate(session, `window.scrollTo({left:${restoreScroll.x},top:${restoreScroll.y},behavior:'instant'})`).catch(() => {});
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
|
-
const data = String(captured?.data || '');
|
|
250
|
+
const data = String(screenshot.captured?.data || '');
|
|
228
251
|
if (!data) throw new Error('CDP returned no screenshot data');
|
|
229
252
|
if (Buffer.byteLength(data, 'base64') > 8 * 1024 * 1024) throw new Error('Browser screenshot exceeds the 8 MiB inline image limit');
|
|
253
|
+
payload.screenshot = screenshot.viewport;
|
|
254
|
+
const result = jsonResult(payload);
|
|
230
255
|
result.content.push({ type:'image', data, mimeType:'image/png' });
|
|
231
256
|
return result;
|
|
232
257
|
});
|