neoagent 3.2.1-beta.0 → 3.2.1-beta.10

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.
Files changed (175) hide show
  1. package/docs/agent-run-lifecycle.md +10 -0
  2. package/extensions/chrome-browser/background.mjs +318 -88
  3. package/extensions/chrome-browser/http.mjs +136 -0
  4. package/extensions/chrome-browser/protocol.mjs +654 -90
  5. package/flutter_app/lib/main_chat.dart +118 -739
  6. package/flutter_app/lib/main_controller.dart +111 -20
  7. package/flutter_app/lib/main_integrations.dart +607 -8
  8. package/flutter_app/lib/main_models.dart +3 -0
  9. package/flutter_app/lib/main_operations.dart +334 -321
  10. package/flutter_app/lib/main_security.dart +266 -112
  11. package/flutter_app/lib/main_settings.dart +4 -3
  12. package/flutter_app/lib/main_shared.dart +14 -11
  13. package/flutter_app/lib/src/backend_client.dart +78 -0
  14. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  15. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  16. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  17. package/landing/index.html +3 -1
  18. package/lib/manager.js +106 -89
  19. package/lib/schema_migrations.js +145 -13
  20. package/package.json +30 -15
  21. package/runtime/paths.js +49 -5
  22. package/server/db/database.js +4 -4
  23. package/server/guest-agent.cli.package.json +13 -0
  24. package/server/guest_agent.js +85 -40
  25. package/server/http/middleware.js +24 -0
  26. package/server/http/routes.js +11 -6
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  29. package/server/public/flutter_bootstrap.js +2 -2
  30. package/server/public/main.dart.js +73083 -72209
  31. package/server/routes/admin.js +1 -1
  32. package/server/routes/agents.js +35 -2
  33. package/server/routes/android.js +30 -34
  34. package/server/routes/browser.js +23 -15
  35. package/server/routes/desktop.js +18 -1
  36. package/server/routes/integrations.js +107 -1
  37. package/server/routes/memory.js +1 -0
  38. package/server/routes/settings.js +16 -5
  39. package/server/routes/social_reach.js +12 -3
  40. package/server/routes/social_video.js +4 -0
  41. package/server/services/agents/manager.js +1 -1
  42. package/server/services/ai/capabilityHealth.js +62 -96
  43. package/server/services/ai/compaction.js +7 -2
  44. package/server/services/ai/history.js +45 -6
  45. package/server/services/ai/integrated_tools/http_request.js +8 -0
  46. package/server/services/ai/loop/agent_engine_core.js +496 -166
  47. package/server/services/ai/loop/blank_recovery.js +5 -4
  48. package/server/services/ai/loop/callbacks.js +1 -0
  49. package/server/services/ai/loop/completion_judge.js +121 -5
  50. package/server/services/ai/loop/conversation_loop.js +620 -340
  51. package/server/services/ai/loop/lifecycle.js +108 -0
  52. package/server/services/ai/loop/messaging_delivery.js +129 -57
  53. package/server/services/ai/loop/model_call_guard.js +91 -0
  54. package/server/services/ai/loop/model_io.js +48 -56
  55. package/server/services/ai/loop/progress_classification.js +2 -0
  56. package/server/services/ai/loop/tool_dispatch.js +28 -8
  57. package/server/services/ai/loopPolicy.js +48 -21
  58. package/server/services/ai/messagingFallback.js +17 -17
  59. package/server/services/ai/model_discovery.js +227 -0
  60. package/server/services/ai/model_failure_cache.js +108 -0
  61. package/server/services/ai/model_identity.js +71 -0
  62. package/server/services/ai/models.js +68 -163
  63. package/server/services/ai/providerRetry.js +17 -59
  64. package/server/services/ai/provider_selector.js +166 -0
  65. package/server/services/ai/providers/anthropic.js +4 -4
  66. package/server/services/ai/providers/claudeCode.js +21 -33
  67. package/server/services/ai/providers/githubCopilot.js +41 -20
  68. package/server/services/ai/providers/google.js +135 -97
  69. package/server/services/ai/providers/grok.js +6 -5
  70. package/server/services/ai/providers/grokOauth.js +19 -27
  71. package/server/services/ai/providers/nvidia.js +12 -7
  72. package/server/services/ai/providers/ollama.js +114 -86
  73. package/server/services/ai/providers/ollama_stream.js +142 -0
  74. package/server/services/ai/providers/openai.js +41 -7
  75. package/server/services/ai/providers/openaiCodex.js +13 -4
  76. package/server/services/ai/providers/openrouter.js +31 -9
  77. package/server/services/ai/providers/provider_error.js +36 -0
  78. package/server/services/ai/settings.js +26 -2
  79. package/server/services/ai/systemPrompt.js +19 -12
  80. package/server/services/ai/taskAnalysis.js +58 -10
  81. package/server/services/ai/terminal_reply.js +18 -0
  82. package/server/services/ai/toolEvidence.js +350 -29
  83. package/server/services/ai/tools.js +190 -111
  84. package/server/services/android/controller.js +770 -237
  85. package/server/services/android/process.js +140 -0
  86. package/server/services/android/sdk_download.js +143 -0
  87. package/server/services/android/uia.js +6 -5
  88. package/server/services/artifacts/store.js +24 -0
  89. package/server/services/browser/controller.js +843 -385
  90. package/server/services/browser/extension/gateway.js +40 -16
  91. package/server/services/browser/extension/protocol.js +15 -1
  92. package/server/services/browser/extension/provider.js +71 -47
  93. package/server/services/browser/extension/registry.js +155 -34
  94. package/server/services/cli/executor.js +62 -9
  95. package/server/services/credentials/bitwarden_cli.js +322 -0
  96. package/server/services/credentials/broker.js +594 -0
  97. package/server/services/desktop/gateway.js +41 -4
  98. package/server/services/desktop/protocol.js +3 -0
  99. package/server/services/desktop/provider.js +39 -42
  100. package/server/services/desktop/registry.js +137 -52
  101. package/server/services/integrations/bitwarden/constants.js +14 -0
  102. package/server/services/integrations/bitwarden/provider.js +197 -0
  103. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  104. package/server/services/integrations/figma/provider.js +78 -12
  105. package/server/services/integrations/github/common.js +11 -6
  106. package/server/services/integrations/github/provider.js +52 -53
  107. package/server/services/integrations/google/provider.js +55 -19
  108. package/server/services/integrations/home_assistant/network.js +17 -20
  109. package/server/services/integrations/home_assistant/provider.js +7 -5
  110. package/server/services/integrations/home_assistant/tools.js +17 -5
  111. package/server/services/integrations/http.js +51 -0
  112. package/server/services/integrations/manager.js +159 -53
  113. package/server/services/integrations/microsoft/provider.js +80 -13
  114. package/server/services/integrations/neoarchive/provider.js +55 -29
  115. package/server/services/integrations/neorecall/client.js +17 -10
  116. package/server/services/integrations/neorecall/provider.js +20 -11
  117. package/server/services/integrations/notion/provider.js +16 -13
  118. package/server/services/integrations/oauth_provider.js +115 -51
  119. package/server/services/integrations/registry.js +2 -0
  120. package/server/services/integrations/slack/provider.js +98 -9
  121. package/server/services/integrations/spotify/provider.js +67 -71
  122. package/server/services/integrations/trello/provider.js +21 -7
  123. package/server/services/integrations/weather/provider.js +18 -12
  124. package/server/services/integrations/whatsapp/provider.js +76 -16
  125. package/server/services/manager.js +110 -1
  126. package/server/services/memory/embedding_index.js +20 -8
  127. package/server/services/memory/embeddings.js +151 -90
  128. package/server/services/memory/ingestion.js +50 -9
  129. package/server/services/memory/ingestion_documents.js +13 -3
  130. package/server/services/memory/manager.js +52 -19
  131. package/server/services/messaging/automation.js +85 -10
  132. package/server/services/messaging/formatting_guides.js +7 -4
  133. package/server/services/messaging/http_platforms.js +33 -13
  134. package/server/services/messaging/inbound_queue.js +78 -24
  135. package/server/services/messaging/inbound_store.js +224 -0
  136. package/server/services/messaging/manager.js +326 -51
  137. package/server/services/messaging/typing_keepalive.js +5 -2
  138. package/server/services/messaging/whatsapp.js +22 -14
  139. package/server/services/network/http.js +210 -0
  140. package/server/services/network/safe_request.js +307 -0
  141. package/server/services/runtime/backends/local-vm.js +227 -67
  142. package/server/services/runtime/docker-vm-manager.js +9 -0
  143. package/server/services/runtime/guest_bootstrap.js +30 -4
  144. package/server/services/runtime/guest_image.js +43 -12
  145. package/server/services/runtime/manager.js +77 -23
  146. package/server/services/runtime/validation.js +7 -6
  147. package/server/services/security/tool_categories.js +6 -0
  148. package/server/services/social_reach/channels/github.js +10 -4
  149. package/server/services/social_reach/channels/reddit.js +4 -4
  150. package/server/services/social_reach/channels/rss.js +2 -2
  151. package/server/services/social_reach/channels/social_video.js +12 -7
  152. package/server/services/social_reach/channels/v2ex.js +21 -8
  153. package/server/services/social_reach/channels/x.js +2 -2
  154. package/server/services/social_reach/channels/xueqiu.js +5 -5
  155. package/server/services/social_reach/service.js +9 -6
  156. package/server/services/social_reach/utils.js +65 -14
  157. package/server/services/social_video/service.js +160 -50
  158. package/server/services/tasks/integration_runtime.js +18 -8
  159. package/server/services/tasks/runtime.js +39 -4
  160. package/server/services/voice/agentBridge.js +17 -4
  161. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  162. package/server/services/voice/liveSession.js +31 -0
  163. package/server/services/voice/message.js +1 -1
  164. package/server/services/voice/openaiSpeech.js +33 -8
  165. package/server/services/voice/providers.js +233 -151
  166. package/server/services/voice/runtime.js +2 -2
  167. package/server/services/voice/runtimeManager.js +118 -20
  168. package/server/services/voice/turnRunner.js +6 -0
  169. package/server/services/wearable/firmware_manifest.js +51 -13
  170. package/server/services/wearable/service.js +1 -0
  171. package/server/utils/abort.js +96 -0
  172. package/server/utils/cloud-security.js +110 -3
  173. package/server/utils/files.js +31 -0
  174. package/server/utils/image_payload.js +95 -0
  175. package/server/utils/retry.js +107 -0
@@ -1,3 +1,12 @@
1
+ export const EXTENSION_PROTOCOL_VERSION = 1;
2
+
3
+ export const MESSAGE_TYPES = Object.freeze({
4
+ COMMAND: 'command',
5
+ RESULT: 'result',
6
+ URL_VALIDATION_REQUEST: 'urlValidationRequest',
7
+ URL_VALIDATION_RESULT: 'urlValidationResult',
8
+ });
9
+
1
10
  export const COMMANDS = Object.freeze({
2
11
  LAUNCH: 'launch',
3
12
  NAVIGATE: 'navigate',
@@ -13,8 +22,24 @@ export const COMMANDS = Object.freeze({
13
22
  CLOSE: 'close',
14
23
  GET_PAGE_INFO: 'getPageInfo',
15
24
  GET_COOKIES: 'getCookies',
25
+ FILL_CREDENTIAL: 'fillCredential',
26
+ SUBMIT_CREDENTIAL: 'submitCredential',
27
+ CANCEL_CREDENTIAL: 'cancelCredential',
28
+ CANCEL_COMMAND: 'cancelCommand',
16
29
  });
17
30
 
31
+ function abortError(signal) {
32
+ if (signal?.reason instanceof Error) return signal.reason;
33
+ const error = new Error(String(signal?.reason || 'Browser extension command aborted.'));
34
+ error.name = 'AbortError';
35
+ error.code = 'ABORT_ERR';
36
+ return error;
37
+ }
38
+
39
+ function throwIfAborted(signal) {
40
+ if (signal?.aborted) throw abortError(signal);
41
+ }
42
+
18
43
  function chromeCall(chromeApi, namespace, method, ...args) {
19
44
  return new Promise((resolve, reject) => {
20
45
  chromeApi[namespace][method](...args, (result) => {
@@ -28,8 +53,50 @@ function chromeCall(chromeApi, namespace, method, ...args) {
28
53
  });
29
54
  }
30
55
 
31
- function delay(ms) {
32
- return new Promise((resolve) => setTimeout(resolve, ms));
56
+ function waitForAbortable(promise, signal = null) {
57
+ throwIfAborted(signal);
58
+ if (!signal) return promise;
59
+ return new Promise((resolve, reject) => {
60
+ let settled = false;
61
+ const finish = (callback, value) => {
62
+ if (settled) return;
63
+ settled = true;
64
+ signal.removeEventListener('abort', onAbort);
65
+ callback(value);
66
+ };
67
+ const onAbort = () => finish(reject, abortError(signal));
68
+ signal.addEventListener('abort', onAbort, { once: true });
69
+ if (signal.aborted) onAbort();
70
+ Promise.resolve(promise).then(
71
+ (value) => finish(resolve, value),
72
+ (error) => finish(reject, error),
73
+ );
74
+ });
75
+ }
76
+
77
+ async function optionalResult(promise, fallback, signal = null) {
78
+ try {
79
+ return await promise;
80
+ } catch {
81
+ throwIfAborted(signal);
82
+ return fallback;
83
+ }
84
+ }
85
+
86
+ function delay(ms, signal = null) {
87
+ throwIfAborted(signal);
88
+ return new Promise((resolve, reject) => {
89
+ const timer = setTimeout(() => {
90
+ signal?.removeEventListener('abort', onAbort);
91
+ resolve();
92
+ }, ms);
93
+ const onAbort = () => {
94
+ clearTimeout(timer);
95
+ reject(abortError(signal));
96
+ };
97
+ signal?.addEventListener('abort', onAbort, { once: true });
98
+ if (signal?.aborted) onAbort();
99
+ });
33
100
  }
34
101
 
35
102
  function jsString(value) {
@@ -58,113 +125,414 @@ function keyCodeFor(key) {
58
125
  return map[normalized] || (normalized.length === 1 ? normalized.toUpperCase().charCodeAt(0) : 0);
59
126
  }
60
127
 
61
- export function createBrowserProtocol(chromeApi) {
128
+ function normalizePointCoordinate(value, label) {
129
+ if (value == null || value === '') {
130
+ throw new Error(`${label} coordinate is required.`);
131
+ }
132
+ const normalized = Number(value);
133
+ if (!Number.isFinite(normalized)) {
134
+ throw new Error(`${label} coordinate must be a finite number.`);
135
+ }
136
+ return Math.round(normalized);
137
+ }
138
+
139
+ function ipv4Octets(hostname) {
140
+ if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname)) return null;
141
+ const parts = hostname.split('.').map(Number);
142
+ return parts.every((part) => part >= 0 && part <= 255) ? parts : null;
143
+ }
144
+
145
+ export function isPrivateNetworkHostname(hostname) {
146
+ const normalized = String(hostname || '')
147
+ .toLowerCase()
148
+ .replace(/^\[|\]$/g, '')
149
+ .replace(/\.$/, '');
150
+ if (!normalized) return true;
151
+ if (
152
+ normalized === 'localhost'
153
+ || normalized === 'localhost.localdomain'
154
+ || normalized.endsWith('.localhost')
155
+ || normalized.endsWith('.local')
156
+ || normalized.endsWith('.internal')
157
+ ) {
158
+ return true;
159
+ }
160
+
161
+ const octets = ipv4Octets(normalized);
162
+ if (octets) {
163
+ const [a, b, c] = octets;
164
+ return a === 0
165
+ || a === 10
166
+ || a === 127
167
+ || a >= 224
168
+ || (a === 100 && b >= 64 && b <= 127)
169
+ || (a === 169 && b === 254)
170
+ || (a === 172 && b >= 16 && b <= 31)
171
+ || (a === 192 && b === 168)
172
+ || (a === 192 && b === 0 && c === 0)
173
+ || (a === 192 && b === 0 && c === 2)
174
+ || (a === 198 && (b === 18 || b === 19))
175
+ || (a === 198 && b === 51 && c === 100)
176
+ || (a === 203 && b === 0 && c === 113);
177
+ }
178
+
179
+ if (normalized.includes(':')) {
180
+ if (normalized === '::' || normalized === '::1') return true;
181
+ if (normalized.startsWith('::ffff:')) return true;
182
+ const first = Number.parseInt(normalized.split(':').find(Boolean) || '0', 16);
183
+ if (!Number.isFinite(first)) return true;
184
+ if ((first & 0xfe00) === 0xfc00) return true;
185
+ if ((first & 0xffc0) === 0xfe80) return true;
186
+ if ((first & 0xffc0) === 0xfec0) return true;
187
+ if ((first & 0xff00) === 0xff00) return true;
188
+ if (normalized.startsWith('2001:db8:')) return true;
189
+ }
190
+
191
+ return false;
192
+ }
193
+
194
+ export function normalizeNetworkValidationUrl(value) {
195
+ const raw = String(value || '');
196
+ if (!raw || raw.length > 8192) return null;
197
+ let parsed;
198
+ try {
199
+ parsed = new URL(raw);
200
+ } catch {
201
+ return null;
202
+ }
203
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol)) return null;
204
+ if (parsed.username || parsed.password || isPrivateNetworkHostname(parsed.hostname)) return null;
205
+ const protocol = parsed.protocol === 'ws:'
206
+ ? 'http:'
207
+ : (parsed.protocol === 'wss:' ? 'https:' : parsed.protocol);
208
+ return `${protocol}//${parsed.host}/`;
209
+ }
210
+
211
+ const PAGE_ACCESS_COMMANDS = new Set([
212
+ COMMANDS.LAUNCH,
213
+ COMMANDS.CLICK,
214
+ COMMANDS.CLICK_POINT,
215
+ COMMANDS.TYPE,
216
+ COMMANDS.TYPE_TEXT,
217
+ COMMANDS.PRESS_KEY,
218
+ COMMANDS.SCROLL,
219
+ COMMANDS.EXTRACT,
220
+ COMMANDS.EVALUATE,
221
+ COMMANDS.SCREENSHOT,
222
+ COMMANDS.GET_PAGE_INFO,
223
+ COMMANDS.FILL_CREDENTIAL,
224
+ COMMANDS.SUBMIT_CREDENTIAL,
225
+ COMMANDS.CANCEL_CREDENTIAL,
226
+ ]);
227
+
228
+ export function createBrowserProtocol(chromeApi, options = {}) {
62
229
  let attachedTabId = null;
63
230
  let activeTabId = null;
231
+ let protectedCredentialFill = null;
232
+ const pausedRequests = new Map();
233
+ const maxPausedRequests = Math.max(1, Math.min(Number(options.maxPausedRequests) || 256, 1024));
234
+ const validateUrl = typeof options.validateUrl === 'function'
235
+ ? options.validateUrl
236
+ : async () => false;
64
237
 
65
238
  const debuggee = () => ({ tabId: activeTabId });
66
239
 
67
- async function ensureTab() {
240
+ const call = (signal, namespace, method, ...args) => waitForAbortable(
241
+ chromeCall(chromeApi, namespace, method, ...args),
242
+ signal,
243
+ );
244
+
245
+ function abortPausedRequests(tabId, reason) {
246
+ for (const entry of pausedRequests.values()) {
247
+ if (entry.tabId === tabId && !entry.controller.signal.aborted) {
248
+ entry.controller.abort(reason);
249
+ }
250
+ }
251
+ }
252
+
253
+ async function sendToTab(tabId, method, params = {}, signal = null) {
254
+ try {
255
+ return await call(signal, 'debugger', 'sendCommand', { tabId }, method, params);
256
+ } catch (error) {
257
+ if (
258
+ tabId === attachedTabId
259
+ && /not attached|no tab with given id|target closed/i.test(String(error?.message || ''))
260
+ ) {
261
+ attachedTabId = null;
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ async function isNetworkUrlAllowed(url, signal = null) {
268
+ throwIfAborted(signal);
269
+ const normalized = normalizeNetworkValidationUrl(url);
270
+ if (!normalized) return false;
271
+ const result = await validateUrl(normalized, { signal });
272
+ throwIfAborted(signal);
273
+ return result === true || result?.allowed === true;
274
+ }
275
+
276
+ async function handlePausedRequest(source, params) {
277
+ const tabId = source?.tabId;
278
+ const requestId = String(params?.requestId || '');
279
+ if (tabId == null || tabId !== attachedTabId || !requestId) return;
280
+ if (pausedRequests.size >= maxPausedRequests) {
281
+ await sendToTab(tabId, 'Fetch.failRequest', {
282
+ requestId,
283
+ errorReason: 'BlockedByClient',
284
+ }).catch(() => {});
285
+ return;
286
+ }
287
+
288
+ const key = `${tabId}:${requestId}`;
289
+ const previous = pausedRequests.get(key);
290
+ previous?.controller.abort(new Error('Browser request interception was superseded.'));
291
+ const controller = new AbortController();
292
+ pausedRequests.set(key, { tabId, controller });
293
+ let allowed = false;
294
+ try {
295
+ allowed = await isNetworkUrlAllowed(params?.request?.url, controller.signal);
296
+ } catch {
297
+ allowed = false;
298
+ }
299
+ try {
300
+ await sendToTab(
301
+ tabId,
302
+ allowed ? 'Fetch.continueRequest' : 'Fetch.failRequest',
303
+ allowed
304
+ ? { requestId }
305
+ : { requestId, errorReason: 'BlockedByClient' },
306
+ );
307
+ } catch {
308
+ // Detach and tab-close races make the paused request disappear on their own.
309
+ } finally {
310
+ if (pausedRequests.get(key)?.controller === controller) pausedRequests.delete(key);
311
+ }
312
+ }
313
+
314
+ chromeApi.debugger?.onEvent?.addListener?.((source, method, params) => {
315
+ if (method !== 'Fetch.requestPaused') return;
316
+ handlePausedRequest(source, params).catch(() => {});
317
+ });
318
+ chromeApi.debugger?.onDetach?.addListener?.((source) => {
319
+ if (source?.tabId === attachedTabId) {
320
+ abortPausedRequests(source.tabId, new Error('Browser debugger detached.'));
321
+ attachedTabId = null;
322
+ }
323
+ });
324
+ chromeApi.tabs?.onRemoved?.addListener?.((tabId) => {
325
+ if (tabId === activeTabId) activeTabId = null;
326
+ if (tabId === attachedTabId) {
327
+ abortPausedRequests(tabId, new Error('Browser tab closed.'));
328
+ attachedTabId = null;
329
+ }
330
+ });
331
+
332
+ async function ensureTab(signal = null) {
333
+ throwIfAborted(signal);
68
334
  if (activeTabId != null) {
69
335
  try {
70
- await chromeCall(chromeApi, 'tabs', 'get', activeTabId);
336
+ await call(signal, 'tabs', 'get', activeTabId);
71
337
  return activeTabId;
72
338
  } catch {
339
+ throwIfAborted(signal);
73
340
  activeTabId = null;
74
341
  }
75
342
  }
76
343
 
77
- const tabs = await chromeCall(chromeApi, 'tabs', 'query', { active: true, currentWindow: true });
344
+ const tabs = await call(signal, 'tabs', 'query', { active: true, currentWindow: true });
78
345
  if (tabs && tabs[0]?.id != null) {
79
346
  activeTabId = tabs[0].id;
80
347
  return activeTabId;
81
348
  }
82
- const tab = await chromeCall(chromeApi, 'tabs', 'create', { url: 'about:blank', active: true });
349
+ const tab = await call(signal, 'tabs', 'create', { url: 'about:blank', active: true });
83
350
  activeTabId = tab.id;
84
351
  return activeTabId;
85
352
  }
86
353
 
87
- async function attach() {
88
- await ensureTab();
354
+ async function attach(signal = null) {
355
+ await ensureTab(signal);
89
356
  if (attachedTabId === activeTabId) return;
90
357
  if (attachedTabId != null) {
91
- await chromeCall(chromeApi, 'debugger', 'detach', { tabId: attachedTabId }).catch(() => {});
358
+ abortPausedRequests(attachedTabId, new Error('Browser control moved to another tab.'));
359
+ await optionalResult(
360
+ call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
361
+ undefined,
362
+ signal,
363
+ );
92
364
  }
93
365
  try {
94
- await chromeCall(chromeApi, 'debugger', 'attach', debuggee(), '1.3');
366
+ await call(signal, 'debugger', 'attach', debuggee(), '1.3');
95
367
  } catch (error) {
368
+ throwIfAborted(signal);
96
369
  if (!/another debugger|already attached|debugger is already attached/i.test(error.message)) {
97
370
  throw error;
98
371
  }
99
372
  }
100
373
  attachedTabId = activeTabId;
101
- await send('Page.enable').catch(() => {});
102
- await send('Runtime.enable').catch(() => {});
103
- await send('DOM.enable').catch(() => {});
374
+ try {
375
+ await sendToTab(attachedTabId, 'Fetch.enable', {
376
+ patterns: [
377
+ { urlPattern: 'http://*/*', requestStage: 'Request' },
378
+ { urlPattern: 'https://*/*', requestStage: 'Request' },
379
+ { urlPattern: 'ws://*/*', requestStage: 'Request' },
380
+ { urlPattern: 'wss://*/*', requestStage: 'Request' },
381
+ ],
382
+ }, signal);
383
+ await optionalResult(send('Page.enable', {}, signal), undefined, signal);
384
+ await optionalResult(send('Runtime.enable', {}, signal), undefined, signal);
385
+ await optionalResult(send('DOM.enable', {}, signal), undefined, signal);
386
+ } catch (error) {
387
+ const failedTabId = attachedTabId;
388
+ abortPausedRequests(failedTabId, error);
389
+ attachedTabId = null;
390
+ if (failedTabId != null) {
391
+ await chromeCall(chromeApi, 'debugger', 'detach', { tabId: failedTabId }).catch(() => {});
392
+ }
393
+ throw error;
394
+ }
104
395
  }
105
396
 
106
- async function send(method, params = {}) {
107
- return chromeCall(chromeApi, 'debugger', 'sendCommand', debuggee(), method, params);
397
+ async function send(method, params = {}, signal = null) {
398
+ const tabId = attachedTabId ?? activeTabId;
399
+ if (tabId == null) throw new Error('No browser tab is available.');
400
+ return sendToTab(tabId, method, params, signal);
108
401
  }
109
402
 
110
- async function evalJs(expression, options = {}) {
111
- await attach();
403
+ async function evalJs(expression, options = {}, signal = null) {
404
+ await attach(signal);
112
405
  const response = await send('Runtime.evaluate', {
113
406
  expression,
114
407
  awaitPromise: options.awaitPromise !== false,
115
408
  returnByValue: true,
116
- });
409
+ }, signal);
117
410
  if (response?.exceptionDetails) {
118
411
  throw new Error(response.exceptionDetails.text || 'JavaScript evaluation failed.');
119
412
  }
120
413
  return response?.result?.value;
121
414
  }
122
415
 
123
- async function waitForLoad(timeoutMs = 30000) {
416
+ async function waitForLoad(timeoutMs = 30000, signal = null) {
124
417
  const started = Date.now();
125
418
  while (Date.now() - started < timeoutMs) {
126
- const ready = await evalJs('document.readyState === "complete" || document.readyState === "interactive"', { awaitPromise: false })
127
- .catch(() => false);
419
+ throwIfAborted(signal);
420
+ const ready = await optionalResult(
421
+ evalJs(
422
+ 'document.readyState === "complete" || document.readyState === "interactive"',
423
+ { awaitPromise: false },
424
+ signal,
425
+ ),
426
+ false,
427
+ signal,
428
+ );
128
429
  if (ready) return;
129
- await delay(250);
430
+ await delay(250, signal);
130
431
  }
432
+ throw new Error(`Page did not finish loading within ${timeoutMs}ms.`);
131
433
  }
132
434
 
133
- async function waitForSelector(selector, timeoutMs = 10000) {
435
+ async function markCurrentDocument(signal = null) {
436
+ const key = `__neoagent_document_${crypto.randomUUID().replace(/-/g, '')}`;
437
+ const token = crypto.randomUUID();
438
+ const marked = await optionalResult(
439
+ evalJs(
440
+ `globalThis[${jsString(key)}] = ${jsString(token)}; true`,
441
+ { awaitPromise: false },
442
+ signal,
443
+ ),
444
+ false,
445
+ signal,
446
+ );
447
+ return marked ? { key, token } : null;
448
+ }
449
+
450
+ async function waitForDocumentReplacement(marker, timeoutMs = 30000, signal = null) {
451
+ if (!marker) {
452
+ await delay(250, signal);
453
+ return;
454
+ }
455
+ const started = Date.now();
456
+ const expression = `globalThis[${jsString(marker.key)}] !== ${jsString(marker.token)}`;
457
+ while (Date.now() - started < timeoutMs) {
458
+ throwIfAborted(signal);
459
+ const replaced = await optionalResult(
460
+ evalJs(expression, { awaitPromise: false }, signal),
461
+ false,
462
+ signal,
463
+ );
464
+ if (replaced) return;
465
+ await delay(100, signal);
466
+ }
467
+ throw new Error(`Page navigation did not commit within ${timeoutMs}ms.`);
468
+ }
469
+
470
+ async function assertNavigationSucceeded(signal = null) {
471
+ const frameTree = await send('Page.getFrameTree', {}, signal);
472
+ const unreachableUrl = String(frameTree?.frameTree?.frame?.unreachableUrl || '');
473
+ if (unreachableUrl) {
474
+ throw new Error('Browser navigation failed before the destination loaded.');
475
+ }
476
+ await assertCurrentPageAllowed(signal);
477
+ }
478
+
479
+ async function waitForSelector(selector, timeoutMs = 10000, signal = null) {
134
480
  if (!selector) return;
135
481
  const expression = `Boolean(document.querySelector(${jsString(selector)}))`;
136
482
  const started = Date.now();
137
483
  while (Date.now() - started < timeoutMs) {
138
- if (await evalJs(expression, { awaitPromise: false }).catch(() => false)) return;
139
- await delay(200);
484
+ throwIfAborted(signal);
485
+ if (await optionalResult(evalJs(expression, { awaitPromise: false }, signal), false, signal)) return;
486
+ await delay(200, signal);
140
487
  }
488
+ throw new Error(`Element not found within ${timeoutMs}ms: ${selector}`);
141
489
  }
142
490
 
143
- async function currentTab() {
144
- await ensureTab();
145
- return chromeCall(chromeApi, 'tabs', 'get', activeTabId);
491
+ async function currentTab(signal = null) {
492
+ await ensureTab(signal);
493
+ return call(signal, 'tabs', 'get', activeTabId);
146
494
  }
147
495
 
148
- async function screenshotDataUrl(options = {}) {
149
- await attach();
496
+ async function assertCurrentPageAllowed(signal = null) {
497
+ const tab = await currentTab(signal);
498
+ const url = String(tab?.url || '');
499
+ if (url === 'about:blank') return tab;
500
+ if (!await isNetworkUrlAllowed(url, signal)) {
501
+ throw new Error('The current browser page is not permitted.');
502
+ }
503
+ return tab;
504
+ }
505
+
506
+ async function screenshotDataUrl(options = {}, signal = null) {
507
+ throwIfAborted(signal);
508
+ await attach(signal);
150
509
  const capture = await send('Page.captureScreenshot', {
151
510
  format: 'png',
152
511
  captureBeyondViewport: options.fullPage === true,
153
512
  fromSurface: true,
154
- });
155
- return `data:image/png;base64,${capture?.data || ''}`;
513
+ }, signal);
514
+ const encoded = String(capture?.data || '');
515
+ if (encoded.length > 24 * 1024 * 1024) {
516
+ throw new Error('Browser screenshot is too large to transfer. Try a viewport screenshot instead.');
517
+ }
518
+ return `data:image/png;base64,${encoded}`;
156
519
  }
157
520
 
158
- async function pageSnapshot(options = {}) {
159
- const tab = await currentTab();
160
- const title = await evalJs('document.title || ""', { awaitPromise: false }).catch(() => tab.title || '');
161
- const bodyText = await evalJs(`(() => {
521
+ async function pageSnapshot(options = {}, signal = null) {
522
+ throwIfAborted(signal);
523
+ const tab = await currentTab(signal);
524
+ const title = await optionalResult(
525
+ evalJs('document.title || ""', { awaitPromise: false }, signal),
526
+ tab.title || '',
527
+ signal,
528
+ );
529
+ const bodyText = await optionalResult(evalJs(`(() => {
162
530
  const body = document.body;
163
531
  if (!body) return '';
164
532
  const clone = body.cloneNode(true);
165
533
  clone.querySelectorAll('script, style, noscript').forEach((node) => node.remove());
166
534
  return String(clone.innerText || '').slice(0, 10000);
167
- })()`).catch(() => '');
535
+ })()`, {}, signal), '', signal);
168
536
  const result = {
169
537
  title: title || tab.title || '',
170
538
  url: tab.url || '',
@@ -172,12 +540,13 @@ export function createBrowserProtocol(chromeApi) {
172
540
  bodyText,
173
541
  };
174
542
  if (options.screenshot !== false) {
175
- result.screenshotDataUrl = await screenshotDataUrl(options);
543
+ result.screenshotDataUrl = await screenshotDataUrl(options, signal);
176
544
  }
177
545
  return result;
178
546
  }
179
547
 
180
- async function locateTarget(payload = {}) {
548
+ async function locateTarget(payload = {}, signal = null) {
549
+ throwIfAborted(signal);
181
550
  const selector = String(payload.selector || '').trim();
182
551
  const text = String(payload.text || '').trim().toLowerCase();
183
552
  const expression = selector
@@ -194,42 +563,71 @@ export function createBrowserProtocol(chromeApi) {
194
563
  const rect = target.getBoundingClientRect();
195
564
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
196
565
  })()`;
197
- const point = await evalJs(expression);
566
+ const point = await evalJs(expression, {}, signal);
198
567
  if (!point || !Number.isFinite(Number(point.x)) || !Number.isFinite(Number(point.y))) {
199
568
  throw new Error(selector ? `Element not found: ${selector}` : `No clickable element found with text: ${payload.text}`);
200
569
  }
201
570
  return { x: Math.round(point.x), y: Math.round(point.y) };
202
571
  }
203
572
 
204
- async function clickPoint(x, y) {
205
- await attach();
206
- const px = Math.max(0, Math.round(Number(x) || 0));
207
- const py = Math.max(0, Math.round(Number(y) || 0));
208
- await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: px, y: py });
209
- await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: px, y: py, button: 'left', clickCount: 1 });
210
- await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: px, y: py, button: 'left', clickCount: 1 });
211
- await delay(500);
573
+ async function clickPoint(x, y, signal = null) {
574
+ throwIfAborted(signal);
575
+ await attach(signal);
576
+ const px = Math.max(0, normalizePointCoordinate(x, 'x'));
577
+ const py = Math.max(0, normalizePointCoordinate(y, 'y'));
578
+ await send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: px, y: py }, signal);
579
+ await send('Input.dispatchMouseEvent', { type: 'mousePressed', x: px, y: py, button: 'left', clickCount: 1 }, signal);
580
+ await send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: px, y: py, button: 'left', clickCount: 1 }, signal);
581
+ await delay(500, signal);
212
582
  return { x: px, y: py };
213
583
  }
214
584
 
215
- async function typeKey(key) {
216
- await attach();
585
+ async function typeKey(key, signal = null) {
586
+ await attach(signal);
217
587
  const normalized = String(key || '').trim();
218
588
  const code = keyCodeFor(normalized);
219
- await send('Input.dispatchKeyEvent', { type: 'keyDown', key: normalized, windowsVirtualKeyCode: code });
220
- await send('Input.dispatchKeyEvent', { type: 'keyUp', key: normalized, windowsVirtualKeyCode: code });
589
+ await send('Input.dispatchKeyEvent', { type: 'keyDown', key: normalized, windowsVirtualKeyCode: code }, signal);
590
+ await send('Input.dispatchKeyEvent', { type: 'keyUp', key: normalized, windowsVirtualKeyCode: code }, signal);
221
591
  }
222
592
 
223
- async function run(command, payload = {}) {
593
+ async function run(command, payload = {}, options = {}) {
594
+ const signal = options.signal || null;
595
+ throwIfAborted(signal);
596
+ if (protectedCredentialFill?.expiresAt <= Date.now()) {
597
+ const expired = protectedCredentialFill;
598
+ await evalJs(`(() => {
599
+ for (const selector of [${jsString(expired.usernameSelector)}, ${jsString(expired.passwordSelector)}].filter(Boolean)) {
600
+ const el = document.querySelector(selector);
601
+ if (el && 'value' in el) el.value = '';
602
+ }
603
+ })()`, {}, signal).catch(() => {});
604
+ protectedCredentialFill = null;
605
+ }
606
+ if (
607
+ protectedCredentialFill
608
+ && ![
609
+ COMMANDS.GET_PAGE_INFO,
610
+ COMMANDS.SUBMIT_CREDENTIAL,
611
+ COMMANDS.CANCEL_CREDENTIAL,
612
+ COMMANDS.CLOSE,
613
+ ].includes(command)
614
+ ) {
615
+ throw new Error('Browser control is paused while a protected credential fill is active. Submit or cancel it first.');
616
+ }
617
+ if (PAGE_ACCESS_COMMANDS.has(command)) {
618
+ await assertCurrentPageAllowed(signal);
619
+ }
224
620
  switch (command) {
225
621
  case COMMANDS.GET_COOKIES: {
226
622
  const domains = Array.isArray(payload.domains)
227
623
  ? payload.domains.map((item) => String(item || '').replace(/^\./, '').toLowerCase()).filter(Boolean)
228
624
  : [];
229
625
  if (domains.length === 0) throw new Error('At least one cookie domain is required.');
626
+ if (domains.length > 50) throw new Error('At most 50 cookie domains may be requested.');
230
627
  const all = [];
231
628
  for (const domain of domains) {
232
- const cookies = await chromeCall(chromeApi, 'cookies', 'getAll', { domain });
629
+ throwIfAborted(signal);
630
+ const cookies = await call(signal, 'cookies', 'getAll', { domain });
233
631
  all.push(...(Array.isArray(cookies) ? cookies : []));
234
632
  }
235
633
  const seen = new Set();
@@ -258,50 +656,185 @@ export function createBrowserProtocol(chromeApi) {
258
656
  };
259
657
  }
260
658
  case COMMANDS.LAUNCH:
261
- await attach();
262
- return pageSnapshot({ screenshot: false });
659
+ await attach(signal);
660
+ return pageSnapshot({ screenshot: false }, signal);
263
661
  case COMMANDS.NAVIGATE:
264
- await attach();
265
662
  if (!payload.url) throw new Error('url required');
266
- await send('Page.navigate', { url: String(payload.url) });
267
- await waitForLoad();
268
- await waitForSelector(payload.waitFor);
269
- return pageSnapshot(payload);
663
+ if (!await isNetworkUrlAllowed(payload.url, signal)) {
664
+ throw new Error('This browser URL is not permitted.');
665
+ }
666
+ await attach(signal);
667
+ {
668
+ const marker = await markCurrentDocument(signal);
669
+ let completed = false;
670
+ try {
671
+ const navigation = await send('Page.navigate', { url: String(payload.url) }, signal);
672
+ if (navigation?.errorText) {
673
+ throw new Error(`Browser navigation failed: ${navigation.errorText}`);
674
+ }
675
+ if (navigation?.isDownload === true) {
676
+ throw new Error('Browser navigation started a download instead of loading a page.');
677
+ }
678
+ if (navigation?.loaderId) {
679
+ await waitForDocumentReplacement(marker, 30000, signal);
680
+ } else {
681
+ await delay(100, signal);
682
+ }
683
+ await waitForLoad(30000, signal);
684
+ await assertNavigationSucceeded(signal);
685
+ await waitForSelector(payload.waitFor, 10000, signal);
686
+ throwIfAborted(signal);
687
+ completed = true;
688
+ return pageSnapshot(payload, signal);
689
+ } finally {
690
+ if (!completed) await send('Page.stopLoading').catch(() => {});
691
+ }
692
+ }
270
693
  case COMMANDS.CLICK: {
271
- const point = await locateTarget(payload);
272
- await clickPoint(point.x, point.y);
273
- return pageSnapshot({ screenshot: payload.screenshot !== false });
694
+ const point = await locateTarget(payload, signal);
695
+ await clickPoint(point.x, point.y, signal);
696
+ throwIfAborted(signal);
697
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
274
698
  }
275
699
  case COMMANDS.CLICK_POINT:
276
- await clickPoint(payload.x, payload.y);
277
- return pageSnapshot({ screenshot: payload.screenshot !== false });
700
+ await clickPoint(payload.x, payload.y, signal);
701
+ throwIfAborted(signal);
702
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
278
703
  case COMMANDS.TYPE:
279
704
  if (!payload.selector) throw new Error('selector required');
280
705
  if (payload.clear !== false) {
281
706
  await evalJs(`(() => {
282
707
  const el = document.querySelector(${jsString(payload.selector)});
283
- if (!el) throw new Error('Element not found: ${String(payload.selector).replace(/'/g, "\\'")}');
708
+ if (!el) throw new Error(${jsString(`Element not found: ${payload.selector}`)});
284
709
  el.focus();
285
710
  if ('value' in el) el.value = '';
286
- })()`);
711
+ })()`, {}, signal);
287
712
  } else {
288
- await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()`);
713
+ await evalJs(`document.querySelector(${jsString(payload.selector)})?.focus()`, {}, signal);
289
714
  }
290
- await send('Input.insertText', { text: String(payload.text || '') });
291
- if (payload.pressEnter) await typeKey('Enter');
292
- return pageSnapshot({ screenshot: payload.screenshot !== false });
715
+ await send('Input.insertText', { text: String(payload.text || '') }, signal);
716
+ throwIfAborted(signal);
717
+ if (payload.pressEnter) await typeKey('Enter', signal);
718
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
293
719
  case COMMANDS.TYPE_TEXT:
294
- await attach();
295
- await send('Input.insertText', { text: String(payload.text || '') });
296
- if (payload.pressEnter) await typeKey('Enter');
297
- return pageSnapshot({ screenshot: payload.screenshot !== false });
720
+ await attach(signal);
721
+ await send('Input.insertText', { text: String(payload.text || '') }, signal);
722
+ throwIfAborted(signal);
723
+ if (payload.pressEnter) await typeKey('Enter', signal);
724
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
725
+ case COMMANDS.FILL_CREDENTIAL: {
726
+ if (protectedCredentialFill) throw new Error('A protected credential fill is already active.');
727
+ const allowedOrigin = new URL(String(payload.allowedOrigin || '')).origin;
728
+ const tab = await currentTab(signal);
729
+ if (new URL(String(tab.url || '')).origin !== allowedOrigin) {
730
+ throw new Error('The browser origin changed before credential fill.');
731
+ }
732
+ const usernameSelector = String(payload.usernameSelector || '').trim();
733
+ const passwordSelector = String(payload.passwordSelector || '').trim();
734
+ if (!usernameSelector && !passwordSelector) throw new Error('At least one credential field selector is required.');
735
+ await attach(signal);
736
+ const marker = await markCurrentDocument(signal);
737
+ await send('Page.reload', {}, signal);
738
+ await waitForDocumentReplacement(marker, 30000, signal);
739
+ await waitForLoad(30000, signal);
740
+ const reloaded = await currentTab(signal);
741
+ if (new URL(String(reloaded.url || '')).origin !== allowedOrigin) {
742
+ throw new Error('The browser origin changed while preparing credential fill.');
743
+ }
744
+ if (usernameSelector) {
745
+ await waitForSelector(usernameSelector, 10000, signal);
746
+ await evalJs(`(() => {
747
+ const el = document.querySelector(${jsString(usernameSelector)});
748
+ if (!el) throw new Error('Username field not found.');
749
+ el.value = ${jsString(String(payload.username || ''))};
750
+ el.dispatchEvent(new Event('input', { bubbles: true }));
751
+ el.dispatchEvent(new Event('change', { bubbles: true }));
752
+ })()`, {}, signal);
753
+ }
754
+ if (passwordSelector) {
755
+ await waitForSelector(passwordSelector, 10000, signal);
756
+ await evalJs(`(() => {
757
+ const el = document.querySelector(${jsString(passwordSelector)});
758
+ if (!el) throw new Error('Password field not found.');
759
+ el.value = ${jsString(String(payload.password || ''))};
760
+ el.dispatchEvent(new Event('input', { bubbles: true }));
761
+ el.dispatchEvent(new Event('change', { bubbles: true }));
762
+ })()`, {}, signal);
763
+ }
764
+ const protectedFillId = globalThis.crypto.randomUUID();
765
+ protectedCredentialFill = {
766
+ id: protectedFillId,
767
+ allowedOrigin,
768
+ usernameSelector,
769
+ passwordSelector,
770
+ submitSelector: passwordSelector || usernameSelector,
771
+ expiresAt: Date.now() + 5 * 60 * 1000,
772
+ };
773
+ return { success: true, protectedFillId, origin: allowedOrigin };
774
+ }
775
+ case COMMANDS.SUBMIT_CREDENTIAL: {
776
+ const fill = protectedCredentialFill;
777
+ if (!fill || fill.id !== String(payload.protectedFillId || '')) {
778
+ throw new Error('Protected credential fill is missing or expired.');
779
+ }
780
+ const tab = await currentTab(signal);
781
+ if (new URL(String(tab.url || '')).origin !== fill.allowedOrigin) {
782
+ protectedCredentialFill = null;
783
+ throw new Error('The protected credential page changed before submission.');
784
+ }
785
+ try {
786
+ await evalJs(`(() => {
787
+ const el = document.querySelector(${jsString(fill.submitSelector)});
788
+ if (!el) throw new Error('Credential field not found.');
789
+ const form = el.form;
790
+ if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
791
+ else el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
792
+ })()`, {}, signal);
793
+ await delay(500, signal);
794
+ await waitForLoad(10000, signal).catch(() => {});
795
+ const resultTab = await currentTab(signal);
796
+ return {
797
+ success: true,
798
+ url: resultTab.url || null,
799
+ title: resultTab.title || null,
800
+ protected: false,
801
+ };
802
+ } finally {
803
+ await evalJs(`(() => {
804
+ for (const selector of [${jsString(fill.usernameSelector)}, ${jsString(fill.passwordSelector)}].filter(Boolean)) {
805
+ const el = document.querySelector(selector);
806
+ if (el && 'value' in el) el.value = '';
807
+ }
808
+ })()`, {}, signal).catch(() => {});
809
+ protectedCredentialFill = null;
810
+ }
811
+ }
812
+ case COMMANDS.CANCEL_CREDENTIAL: {
813
+ const fill = protectedCredentialFill;
814
+ if (!fill || fill.id !== String(payload.protectedFillId || '')) {
815
+ throw new Error('Protected credential fill is missing or expired.');
816
+ }
817
+ await evalJs(`(() => {
818
+ for (const selector of [${jsString(fill.usernameSelector)}, ${jsString(fill.passwordSelector)}].filter(Boolean)) {
819
+ const el = document.querySelector(selector);
820
+ if (el && 'value' in el) el.value = '';
821
+ }
822
+ })()`, {}, signal).catch(() => {});
823
+ protectedCredentialFill = null;
824
+ return { success: true, protected: false };
825
+ }
298
826
  case COMMANDS.PRESS_KEY:
299
- await typeKey(payload.key);
300
- return pageSnapshot({ screenshot: payload.screenshot !== false });
827
+ await typeKey(payload.key, signal);
828
+ throwIfAborted(signal);
829
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
301
830
  case COMMANDS.SCROLL:
302
- await evalJs(`window.scrollBy(${Math.round(Number(payload.deltaX) || 0)}, ${Math.round(Number(payload.deltaY) || 0)})`);
303
- await delay(250);
304
- return pageSnapshot({ screenshot: payload.screenshot !== false });
831
+ await evalJs(
832
+ `window.scrollBy(${Math.round(Number(payload.deltaX) || 0)}, ${Math.round(Number(payload.deltaY) || 0)})`,
833
+ {},
834
+ signal,
835
+ );
836
+ await delay(250, signal);
837
+ return pageSnapshot({ screenshot: payload.screenshot !== false }, signal);
305
838
  case COMMANDS.EXTRACT: {
306
839
  const selector = payload.selector || 'body';
307
840
  const attribute = payload.attribute || '';
@@ -314,26 +847,55 @@ export function createBrowserProtocol(chromeApi) {
314
847
  return el.innerText || '';
315
848
  };
316
849
  const els = Array.from(document.querySelectorAll(${jsString(selector)}));
317
- if (${payload.all === true}) return { results: els.slice(0, 100).map(read) };
850
+ if (${payload.all === true}) {
851
+ return { results: els.slice(0, 100).map((el) => String(read(el)).slice(0, 50000)) };
852
+ }
318
853
  return { result: els[0] ? String(read(els[0])).slice(0, 50000) : '' };
319
854
  })()`;
320
- return evalJs(expression);
855
+ return evalJs(expression, {}, signal);
321
856
  }
322
857
  case COMMANDS.EVALUATE: {
323
- const value = await evalJs(buildIsolatedEvaluationExpression(payload.script));
324
- return { result: typeof value === 'object' ? JSON.stringify(value) : String(value) };
858
+ if (String(payload.script || '').length > 10000) {
859
+ throw new Error('script exceeds maximum length (10000)');
860
+ }
861
+ const value = await evalJs(buildIsolatedEvaluationExpression(payload.script), {}, signal);
862
+ throwIfAborted(signal);
863
+ const serialized = typeof value === 'object' ? JSON.stringify(value) : String(value);
864
+ const maxChars = 1024 * 1024;
865
+ return {
866
+ result: String(serialized ?? '').slice(0, maxChars),
867
+ truncated: String(serialized ?? '').length > maxChars,
868
+ };
325
869
  }
326
870
  case COMMANDS.SCREENSHOT:
327
- return { screenshotDataUrl: await screenshotDataUrl(payload), fullPage: payload.fullPage === true };
871
+ return { screenshotDataUrl: await screenshotDataUrl(payload, signal), fullPage: payload.fullPage === true };
328
872
  case COMMANDS.GET_PAGE_INFO: {
329
- const tab = await currentTab();
330
- return { url: tab.url || null, title: tab.title || null };
873
+ const tab = await currentTab(signal);
874
+ return {
875
+ url: tab.url || null,
876
+ title: tab.title || null,
877
+ protectedCredentialFill: Boolean(protectedCredentialFill),
878
+ };
331
879
  }
332
880
  case COMMANDS.CLOSE:
881
+ if (protectedCredentialFill) {
882
+ await evalJs(`(() => {
883
+ for (const selector of [${jsString(protectedCredentialFill.usernameSelector)}, ${jsString(protectedCredentialFill.passwordSelector)}].filter(Boolean)) {
884
+ const el = document.querySelector(selector);
885
+ if (el && 'value' in el) el.value = '';
886
+ }
887
+ })()`, {}, signal).catch(() => {});
888
+ }
333
889
  if (attachedTabId != null) {
334
- await chromeCall(chromeApi, 'debugger', 'detach', { tabId: attachedTabId }).catch(() => {});
890
+ abortPausedRequests(attachedTabId, new Error('Browser control closed.'));
891
+ await optionalResult(
892
+ call(signal, 'debugger', 'detach', { tabId: attachedTabId }),
893
+ undefined,
894
+ signal,
895
+ );
335
896
  }
336
897
  attachedTabId = null;
898
+ protectedCredentialFill = null;
337
899
  return { success: true };
338
900
  default:
339
901
  throw new Error(`Unsupported command: ${command}`);
@@ -347,6 +909,8 @@ export function createBrowserProtocol(chromeApi) {
347
909
  attach,
348
910
  send,
349
911
  evalJs,
912
+ handlePausedRequest,
913
+ isNetworkUrlAllowed,
350
914
  },
351
915
  };
352
916
  }