neoagent 3.2.1-beta.2 → 3.2.1-beta.4

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 (145) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/messaging/whatsapp.js +22 -14
  116. package/server/services/network/http.js +210 -0
  117. package/server/services/network/safe_request.js +307 -0
  118. package/server/services/runtime/backends/local-vm.js +214 -66
  119. package/server/services/runtime/manager.js +17 -12
  120. package/server/services/social_reach/channels/github.js +10 -4
  121. package/server/services/social_reach/channels/reddit.js +4 -4
  122. package/server/services/social_reach/channels/rss.js +2 -2
  123. package/server/services/social_reach/channels/social_video.js +12 -7
  124. package/server/services/social_reach/channels/v2ex.js +21 -8
  125. package/server/services/social_reach/channels/x.js +2 -2
  126. package/server/services/social_reach/channels/xueqiu.js +5 -5
  127. package/server/services/social_reach/service.js +9 -6
  128. package/server/services/social_reach/utils.js +65 -14
  129. package/server/services/social_video/service.js +160 -50
  130. package/server/services/tasks/integration_runtime.js +18 -8
  131. package/server/services/tasks/runtime.js +39 -4
  132. package/server/services/voice/agentBridge.js +17 -4
  133. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  134. package/server/services/voice/liveSession.js +31 -0
  135. package/server/services/voice/openaiSpeech.js +33 -8
  136. package/server/services/voice/providers.js +233 -151
  137. package/server/services/voice/runtimeManager.js +118 -20
  138. package/server/services/voice/turnRunner.js +6 -0
  139. package/server/services/wearable/firmware_manifest.js +51 -13
  140. package/server/services/wearable/service.js +1 -0
  141. package/server/utils/abort.js +96 -0
  142. package/server/utils/cloud-security.js +110 -3
  143. package/server/utils/files.js +31 -0
  144. package/server/utils/image_payload.js +95 -0
  145. package/server/utils/retry.js +107 -0
@@ -6,6 +6,8 @@ const { decryptValue } = require('../secrets');
6
6
  const { getConnectionAccessMode } = require('../access');
7
7
  const { githubToolDefinitions, executeGithubTool } = require('./repos');
8
8
  const { base64UrlSha256 } = require('./common');
9
+ const { fetchJson } = require('../oauth_provider');
10
+ const { fetchResponseText } = require('../http');
9
11
 
10
12
  const GITHUB_ACCOUNT_IDENTITY_SCOPES = [
11
13
  'read:user',
@@ -159,8 +161,9 @@ function summarizeAppConnection(app, connectionRows, envStatus) {
159
161
  };
160
162
  }
161
163
 
162
- async function executeGithubRepoTool(toolName, args, connection) {
164
+ async function executeGithubRepoTool(toolName, args, connection, executionOptions = {}) {
163
165
  const auth = await buildAuthorizedClient(connection);
166
+ auth.signal = executionOptions.signal || null;
164
167
  if (!auth.token) {
165
168
  throw new Error('GitHub access token is missing or expired. Please reconnect your GitHub account.');
166
169
  }
@@ -326,40 +329,32 @@ function createGithubProvider() {
326
329
  const url = `https://github.com/login/oauth/authorize?${params.toString()}`;
327
330
  return { url, appId: app.id };
328
331
  },
329
- async finishOAuth({ code, codeVerifier, appKey }) {
332
+ async finishOAuth({ code, codeVerifier, appKey, signal }) {
330
333
  const app = getApp(appKey);
331
334
  if (!app) {
332
335
  throw new Error(`Unknown GitHub app: ${appKey}`);
333
336
  }
334
337
  const { config } = createOAuthClient();
335
338
 
336
- const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
337
- method: 'POST',
338
- headers: {
339
- 'Accept': 'application/json',
340
- 'Content-Type': 'application/x-www-form-urlencoded',
339
+ const tokenData = await fetchJson(
340
+ 'https://github.com/login/oauth/access_token',
341
+ {
342
+ method: 'POST',
343
+ form: {
344
+ client_id: config.clientId,
345
+ client_secret: config.clientSecret,
346
+ code,
347
+ code_verifier: codeVerifier,
348
+ redirect_uri: config.redirectUri,
349
+ },
350
+ signal,
341
351
  },
342
- body: new URLSearchParams({
343
- client_id: config.clientId,
344
- client_secret: config.clientSecret,
345
- code,
346
- code_verifier: codeVerifier,
347
- redirect_uri: config.redirectUri,
348
- }).toString(),
349
- });
350
-
351
- const tokenBody = await tokenResponse.text();
352
- let tokenData = {};
353
- try {
354
- tokenData = tokenBody ? JSON.parse(tokenBody) : {};
355
- } catch {
356
- tokenData = {};
357
- }
358
- if (!tokenResponse.ok) {
359
- throw new Error(`GitHub OAuth token exchange failed (${tokenResponse.status}): ${tokenBody || 'No response body'}`);
360
- }
361
- if (tokenData.error) {
362
- throw new Error(`GitHub OAuth error: ${tokenData.error_description || tokenData.error}`);
352
+ { serviceName: 'GitHub OAuth token exchange' },
353
+ );
354
+ if (tokenData?.error) {
355
+ throw new Error(
356
+ `GitHub OAuth error: ${tokenData.error_description || tokenData.error}`,
357
+ );
363
358
  }
364
359
 
365
360
  const accessToken = tokenData.access_token;
@@ -367,17 +362,17 @@ function createGithubProvider() {
367
362
  throw new Error('GitHub OAuth did not return an access token.');
368
363
  }
369
364
 
370
- const userResponse = await fetch('https://api.github.com/user', {
371
- headers: {
372
- 'Authorization': `Bearer ${accessToken}`,
373
- 'Accept': 'application/vnd.github.v3+json',
365
+ const userData = await fetchJson(
366
+ 'https://api.github.com/user',
367
+ {
368
+ headers: {
369
+ 'Authorization': `Bearer ${accessToken}`,
370
+ 'Accept': 'application/vnd.github.v3+json',
371
+ },
372
+ signal,
374
373
  },
375
- });
376
- if (!userResponse.ok) {
377
- const errorBody = await userResponse.text().catch(() => 'Unknown error');
378
- throw new Error(`GitHub user profile request failed (${userResponse.status}): ${errorBody}`);
379
- }
380
- const userData = await userResponse.json();
374
+ { serviceName: 'GitHub user profile' },
375
+ );
381
376
  const accountEmail = String(userData.login || '').trim();
382
377
  if (!accountEmail) {
383
378
  throw new Error('GitHub API did not return a user login.');
@@ -398,7 +393,7 @@ function createGithubProvider() {
398
393
  },
399
394
  };
400
395
  },
401
- async disconnect(connectionRow) {
396
+ async disconnect(connectionRow, executionOptions = {}) {
402
397
  if (!connectionRow?.credentials_json) return;
403
398
  try {
404
399
  const credentials = JSON.parse(decryptValue(connectionRow.credentials_json));
@@ -415,20 +410,24 @@ function createGithubProvider() {
415
410
 
416
411
  const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
417
412
  const revokeUrl = `https://api.github.com/applications/${encodeURIComponent(clientId)}/token`;
418
- const revokeResponse = await fetch(revokeUrl, {
419
- method: 'DELETE',
420
- headers: {
421
- 'Authorization': `Basic ${basic}`,
422
- 'Accept': 'application/json',
423
- 'Content-Type': 'application/json',
413
+ const { response: revokeResponse, text: revokeBody } = await fetchResponseText(
414
+ revokeUrl,
415
+ {
416
+ method: 'DELETE',
417
+ headers: {
418
+ 'Authorization': `Basic ${basic}`,
419
+ 'Accept': 'application/json',
420
+ 'Content-Type': 'application/json',
421
+ },
422
+ body: JSON.stringify({
423
+ access_token: accessToken,
424
+ }),
425
+ signal: executionOptions.signal || null,
424
426
  },
425
- body: JSON.stringify({
426
- access_token: accessToken,
427
- }),
428
- });
427
+ { serviceName: 'GitHub token revocation' },
428
+ );
429
429
 
430
430
  if (!revokeResponse.ok) {
431
- const revokeBody = await revokeResponse.text().catch(() => 'Unknown error');
432
431
  console.warn(
433
432
  `[GitHub] Failed to revoke token for disconnect (connection ${connectionRow?.id || 'unknown'}): ${revokeResponse.status} ${revokeBody}`,
434
433
  );
@@ -439,8 +438,8 @@ function createGithubProvider() {
439
438
  );
440
439
  }
441
440
  },
442
- async executeTool(toolName, args, connectionRow) {
443
- return executeGithubRepoTool(toolName, args, connectionRow);
441
+ async executeTool(toolName, args, connectionRow, executionOptions = {}) {
442
+ return executeGithubRepoTool(toolName, args, connectionRow, executionOptions);
444
443
  },
445
444
  summarizeConnection(connectionRows) {
446
445
  const snapshot = this.buildSnapshot(connectionRows);
@@ -496,4 +495,4 @@ module.exports = {
496
495
  scopes: scopes.slice(),
497
496
  }),
498
497
  ),
499
- };
498
+ };
@@ -10,6 +10,7 @@ const { calendarToolDefinitions, executeCalendarTool } = require('./calendar');
10
10
  const { driveToolDefinitions, executeDriveTool } = require('./drive');
11
11
  const { docsToolDefinitions, executeDocsTool } = require('./docs');
12
12
  const { sheetsToolDefinitions, executeSheetsTool } = require('./sheets');
13
+ const { waitForAbortableResult } = require('../http');
13
14
 
14
15
  const GOOGLE_ACCOUNT_IDENTITY_SCOPES = [
15
16
  'openid',
@@ -118,7 +119,17 @@ function createOAuthClient() {
118
119
  };
119
120
  }
120
121
 
121
- async function buildAuthorizedClient(connection) {
122
+ function attachAbortSignal(client, signal) {
123
+ if (!signal) return client;
124
+ const request = client.transporter.request.bind(client.transporter);
125
+ client.transporter.request = (requestOptions = {}) => request({
126
+ ...requestOptions,
127
+ signal: requestOptions.signal || signal,
128
+ });
129
+ return client;
130
+ }
131
+
132
+ async function buildAuthorizedClient(connection, options = {}) {
122
133
  const { client } = createOAuthClient();
123
134
  let credentials = {};
124
135
  try {
@@ -142,7 +153,7 @@ async function buildAuthorizedClient(connection) {
142
153
  });
143
154
  }
144
155
  });
145
- return client;
156
+ return attachAbortSignal(client, options.signal);
146
157
  }
147
158
 
148
159
  function getApp(appId) {
@@ -247,21 +258,29 @@ function summarizeAppConnection(app, connectionRows, envStatus) {
247
258
  };
248
259
  }
249
260
 
250
- async function executeGoogleWorkspaceTool(toolName, args, connection) {
251
- const auth = await buildAuthorizedClient(connection);
261
+ async function executeGoogleWorkspaceTool(
262
+ toolName,
263
+ args,
264
+ connection,
265
+ executionOptions = {},
266
+ ) {
267
+ const auth = await buildAuthorizedClient(connection, executionOptions);
252
268
  const appId = toolAppMap.get(toolName);
253
269
  const app = getApp(appId);
254
270
  if (!app) {
255
271
  throw new Error(`Unknown tool: ${toolName}`);
256
272
  }
257
- const result = await app.executor(toolName, args, auth);
273
+ const result = await waitForAbortableResult(
274
+ app.executor(toolName, args, auth),
275
+ executionOptions.signal,
276
+ );
258
277
  if (result === null) {
259
278
  throw new Error(`Unknown tool: ${toolName}`);
260
279
  }
261
280
  return { result, credentials: auth.credentials };
262
281
  }
263
282
 
264
- async function collectGoogleMemoryDocuments({ connection, sourceTypes = [] }) {
283
+ async function collectGoogleMemoryDocuments({ connection, sourceTypes = [], signal = null }) {
265
284
  const appKey = String(connection?.app_key || '').trim();
266
285
  const documents = [];
267
286
  const collectedAt = new Date().toISOString();
@@ -271,6 +290,7 @@ async function collectGoogleMemoryDocuments({ connection, sourceTypes = [] }) {
271
290
  'google_workspace_gmail_search_threads',
272
291
  { query: 'newer_than:7d', max_results: 8 },
273
292
  connection,
293
+ { signal },
274
294
  );
275
295
  for (const thread of Array.isArray(result?.threads) ? result.threads : []) {
276
296
  const subject = String(thread.subject || 'Gmail thread').trim();
@@ -300,6 +320,7 @@ async function collectGoogleMemoryDocuments({ connection, sourceTypes = [] }) {
300
320
  'google_workspace_calendar_list_events',
301
321
  { time_min: timeMin, time_max: timeMax, max_results: 12 },
302
322
  connection,
323
+ { signal },
303
324
  );
304
325
  for (const event of Array.isArray(result?.events) ? result.events : []) {
305
326
  const title = String(event.summary || 'Calendar event').trim();
@@ -489,19 +510,23 @@ function createGoogleWorkspaceProvider() {
489
510
  });
490
511
  return { url, appId: app.id };
491
512
  },
492
- async finishOAuth({ code, codeVerifier, appKey }) {
513
+ async finishOAuth({ code, codeVerifier, appKey, signal }) {
493
514
  const app = getApp(appKey);
494
515
  if (!app) {
495
516
  throw new Error(`Unknown Google Workspace app: ${appKey}`);
496
517
  }
497
- const { client } = createOAuthClient();
498
- const { tokens } = await client.getToken({
499
- code,
500
- codeVerifier,
501
- });
518
+ const oauthClient = createOAuthClient();
519
+ const client = attachAbortSignal(oauthClient.client, signal);
520
+ const { tokens } = await waitForAbortableResult(
521
+ client.getToken({
522
+ code,
523
+ codeVerifier,
524
+ }),
525
+ signal,
526
+ );
502
527
  client.setCredentials(tokens);
503
528
  const oauth2 = google.oauth2({ version: 'v2', auth: client });
504
- const profile = await oauth2.userinfo.get();
529
+ const profile = await waitForAbortableResult(oauth2.userinfo.get(), signal);
505
530
  const accountEmail = String(profile.data.email || '').trim();
506
531
  if (!accountEmail) {
507
532
  throw new Error('Google OAuth did not return an account email address.');
@@ -523,26 +548,37 @@ function createGoogleWorkspaceProvider() {
523
548
  },
524
549
  };
525
550
  },
526
- async disconnect(connectionRow) {
527
- const auth = await buildAuthorizedClient(connectionRow);
551
+ async disconnect(connectionRow, executionOptions = {}) {
552
+ const auth = await buildAuthorizedClient(connectionRow, executionOptions);
528
553
  const refreshToken = auth.credentials.refresh_token;
529
554
  const accessToken = auth.credentials.access_token;
530
555
  if (refreshToken) {
531
- await auth.revokeToken(refreshToken).catch((error) => {
556
+ await waitForAbortableResult(
557
+ auth.revokeToken(refreshToken),
558
+ executionOptions.signal,
559
+ ).catch((error) => {
532
560
  console.warn(
533
561
  `[Google Workspace] Failed to revoke refresh token for disconnect (connection ${connectionRow?.id || 'unknown'}): ${error?.message || error}`,
534
562
  );
535
563
  });
536
564
  } else if (accessToken) {
537
- await auth.revokeToken(accessToken).catch((error) => {
565
+ await waitForAbortableResult(
566
+ auth.revokeToken(accessToken),
567
+ executionOptions.signal,
568
+ ).catch((error) => {
538
569
  console.warn(
539
570
  `[Google Workspace] Failed to revoke access token for disconnect (connection ${connectionRow?.id || 'unknown'}): ${error?.message || error}`,
540
571
  );
541
572
  });
542
573
  }
543
574
  },
544
- async executeTool(toolName, args, connectionRow) {
545
- return executeGoogleWorkspaceTool(toolName, args, connectionRow);
575
+ async executeTool(toolName, args, connectionRow, executionOptions = {}) {
576
+ return executeGoogleWorkspaceTool(
577
+ toolName,
578
+ args,
579
+ connectionRow,
580
+ executionOptions,
581
+ );
546
582
  },
547
583
  async collectMemoryDocuments(options) {
548
584
  return collectGoogleMemoryDocuments(options);
@@ -2,8 +2,11 @@
2
2
 
3
3
  const dns = require('dns').promises;
4
4
  const net = require('net');
5
+ const {
6
+ fetchResponseText,
7
+ waitForAbortableResult,
8
+ } = require('../http');
5
9
 
6
- const HTTP_TIMEOUT_MS = 15000;
7
10
  const ALLOWED_PORTS = new Set(['', '443', '8123']);
8
11
 
9
12
  function trimText(value) {
@@ -100,7 +103,7 @@ function isBlockedIpAddress(address) {
100
103
  return true;
101
104
  }
102
105
 
103
- async function assertPublicHomeAssistantEndpoint(baseUrl) {
106
+ async function assertPublicHomeAssistantEndpoint(baseUrl, options = {}) {
104
107
  const url = new URL(normalizeHomeAssistantBaseUrl(baseUrl));
105
108
  const hostname = String(url.hostname || '').replace(/^\[|\]$/g, '');
106
109
  if (net.isIP(hostname) && isBlockedIpAddress(hostname)) {
@@ -109,7 +112,10 @@ async function assertPublicHomeAssistantEndpoint(baseUrl) {
109
112
 
110
113
  let addresses;
111
114
  try {
112
- addresses = await dns.lookup(hostname, { all: true, verbatim: true });
115
+ addresses = await waitForAbortableResult(
116
+ dns.lookup(hostname, { all: true, verbatim: true }),
117
+ options.signal,
118
+ );
113
119
  } catch (error) {
114
120
  throw new Error(`Could not resolve Home Assistant host: ${error?.message || 'DNS lookup failed'}`);
115
121
  }
@@ -140,7 +146,7 @@ function buildHomeAssistantUrl(baseUrl, path, query = {}) {
140
146
  async function homeAssistantRequest(credentials, options = {}) {
141
147
  const baseUrl = normalizeHomeAssistantBaseUrl(credentials.baseUrl);
142
148
  const token = requireText(credentials.token, 'Home Assistant token');
143
- await assertPublicHomeAssistantEndpoint(baseUrl);
149
+ await assertPublicHomeAssistantEndpoint(baseUrl, { signal: options.signal });
144
150
 
145
151
  const method = String(options.method || 'GET').trim().toUpperCase();
146
152
  if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
@@ -154,31 +160,22 @@ async function homeAssistantRequest(credentials, options = {}) {
154
160
  body = JSON.stringify(options.body);
155
161
  }
156
162
 
157
- const controller = new AbortController();
158
- const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
159
- let response;
160
- try {
161
- response = await fetch(buildHomeAssistantUrl(baseUrl, options.path, options.query), {
163
+ const { response, text } = await fetchResponseText(
164
+ buildHomeAssistantUrl(baseUrl, options.path, options.query),
165
+ {
162
166
  method,
163
167
  headers,
164
168
  body,
165
169
  redirect: 'manual',
166
- signal: controller.signal,
167
- });
168
- } catch (error) {
169
- if (error?.name === 'AbortError') {
170
- throw new Error(`Home Assistant request timed out after ${HTTP_TIMEOUT_MS}ms.`);
171
- }
172
- throw error;
173
- } finally {
174
- clearTimeout(timer);
175
- }
170
+ signal: options.signal,
171
+ },
172
+ { serviceName: 'Home Assistant' },
173
+ );
176
174
 
177
175
  if (response.status >= 300 && response.status < 400) {
178
176
  throw new Error('Home Assistant redirected the API request; redirects are not followed.');
179
177
  }
180
178
 
181
- const text = await response.text();
182
179
  let data = null;
183
180
  try {
184
181
  data = text ? JSON.parse(text) : null;
@@ -172,8 +172,10 @@ function createHomeAssistantProvider() {
172
172
  }
173
173
  return 'Home Assistant: native Home Assistant access is connected in this run with tools for config, states, entity state, service calls, and /api requests.';
174
174
  },
175
- async executeTool(toolName, args, connection) {
176
- return executeHomeAssistantTool(toolName, args, connection);
175
+ async executeTool(toolName, args, connection, executionOptions = {}) {
176
+ return executeHomeAssistantTool(toolName, args, connection, {
177
+ signal: executionOptions.signal || null,
178
+ });
177
179
  },
178
180
  getUserConfig({ userId, agentId }) {
179
181
  const normalizedUserId = Number(userId);
@@ -186,7 +188,7 @@ function createHomeAssistantProvider() {
186
188
  hasConnectedAccount: connection?.status === 'connected',
187
189
  };
188
190
  },
189
- async saveUserConfig({ userId, agentId, config }) {
191
+ async saveUserConfig({ userId, agentId, config, signal }) {
190
192
  const normalizedUserId = Number(userId);
191
193
  if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
192
194
  throw new Error('A valid user is required to save Home Assistant configuration.');
@@ -203,10 +205,10 @@ function createHomeAssistantProvider() {
203
205
  if (!token) throw new Error('Home Assistant Long-Lived Access Token is required.');
204
206
 
205
207
  const credentials = { baseUrl, token };
206
- await assertPublicHomeAssistantEndpoint(baseUrl);
208
+ await assertPublicHomeAssistantEndpoint(baseUrl, { signal });
207
209
  let haConfig;
208
210
  try {
209
- haConfig = await fetchHomeAssistantConfig(credentials);
211
+ haConfig = await fetchHomeAssistantConfig(credentials, { signal });
210
212
  } catch (error) {
211
213
  const message = String(error?.message || '').toLowerCase();
212
214
  if (message.includes('401') || message.includes('unauthorized')) {
@@ -3,8 +3,12 @@
3
3
  const { decryptValue } = require('../secrets');
4
4
  const { homeAssistantRequest, normalizeHomeAssistantBaseUrl, requireText, trimText } = require('./network');
5
5
 
6
- async function fetchHomeAssistantConfig(credentials) {
7
- return homeAssistantRequest(credentials, { method: 'GET', path: '/api/config' });
6
+ async function fetchHomeAssistantConfig(credentials, options = {}) {
7
+ return homeAssistantRequest(credentials, {
8
+ method: 'GET',
9
+ path: '/api/config',
10
+ signal: options.signal,
11
+ });
8
12
  }
9
13
 
10
14
  function parseCredentials(connection) {
@@ -46,16 +50,21 @@ function validateServiceSegment(value, label) {
46
50
  return text;
47
51
  }
48
52
 
49
- async function executeHomeAssistantTool(toolName, args, connection) {
53
+ async function executeHomeAssistantTool(toolName, args, connection, options = {}) {
50
54
  const credentials = connectionCredentials(connection);
55
+ const signal = options.signal || null;
51
56
 
52
57
  switch (toolName) {
53
58
  case 'home_assistant_get_config':
54
- return { result: await fetchHomeAssistantConfig(credentials) };
59
+ return { result: await fetchHomeAssistantConfig(credentials, { signal }) };
55
60
  case 'home_assistant_list_states':
56
61
  return {
57
62
  result: filterStates(
58
- await homeAssistantRequest(credentials, { method: 'GET', path: '/api/states' }),
63
+ await homeAssistantRequest(credentials, {
64
+ method: 'GET',
65
+ path: '/api/states',
66
+ signal,
67
+ }),
59
68
  args,
60
69
  ),
61
70
  };
@@ -64,6 +73,7 @@ async function executeHomeAssistantTool(toolName, args, connection) {
64
73
  result: await homeAssistantRequest(credentials, {
65
74
  method: 'GET',
66
75
  path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
76
+ signal,
67
77
  }),
68
78
  };
69
79
  case 'home_assistant_call_service': {
@@ -77,6 +87,7 @@ async function executeHomeAssistantTool(toolName, args, connection) {
77
87
  method: 'POST',
78
88
  path: `/api/services/${domain}/${service}`,
79
89
  body: serviceData,
90
+ signal,
80
91
  }),
81
92
  };
82
93
  }
@@ -87,6 +98,7 @@ async function executeHomeAssistantTool(toolName, args, connection) {
87
98
  path: requireText(args.path, 'path'),
88
99
  query: args.query && typeof args.query === 'object' ? args.query : {},
89
100
  body: args.body && typeof args.body === 'object' ? args.body : undefined,
101
+ signal,
90
102
  }),
91
103
  };
92
104
  default:
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { createAbortError } = require('../../utils/abort');
4
+ const {
5
+ DEFAULT_MAX_RESPONSE_BYTES,
6
+ fetchResponseText: fetchBoundedResponseText,
7
+ readResponseText: readBoundedResponseText,
8
+ waitForAbortableResult,
9
+ waitForBoundedResult: waitForBoundedIo,
10
+ } = require('../network/http');
11
+
12
+ const DEFAULT_INTEGRATION_HTTP_TIMEOUT_MS = 15000;
13
+
14
+ function abortError(signal, fallback = 'Integration request aborted.') {
15
+ return createAbortError(signal, fallback);
16
+ }
17
+
18
+ async function readResponseText(response, options = {}) {
19
+ return readBoundedResponseText(response, {
20
+ ...options,
21
+ serviceName: String(options.serviceName || 'Integration').trim() || 'Integration',
22
+ tooLargeCode: 'INTEGRATION_RESPONSE_TOO_LARGE',
23
+ });
24
+ }
25
+
26
+ async function fetchResponseText(url, options = {}, context = {}) {
27
+ return fetchBoundedResponseText(url, {
28
+ ...options,
29
+ serviceName: String(context.serviceName || 'Integration').trim() || 'Integration',
30
+ timeoutCode: 'INTEGRATION_HTTP_TIMEOUT',
31
+ tooLargeCode: 'INTEGRATION_RESPONSE_TOO_LARGE',
32
+ });
33
+ }
34
+
35
+ async function waitForBoundedResult(promise, options = {}) {
36
+ return waitForBoundedIo(promise, {
37
+ ...options,
38
+ serviceName: String(options.serviceName || 'Integration').trim() || 'Integration',
39
+ timeoutCode: 'INTEGRATION_HTTP_TIMEOUT',
40
+ });
41
+ }
42
+
43
+ module.exports = {
44
+ DEFAULT_INTEGRATION_HTTP_TIMEOUT_MS,
45
+ DEFAULT_MAX_RESPONSE_BYTES,
46
+ abortError,
47
+ fetchResponseText,
48
+ readResponseText,
49
+ waitForAbortableResult,
50
+ waitForBoundedResult,
51
+ };