neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) 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 +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +12 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -10,6 +10,11 @@ const {
10
10
  normalizeAccessMode,
11
11
  withConnectionAccessMode,
12
12
  } = require('./access');
13
+ const {
14
+ abortError,
15
+ waitForAbortableResult,
16
+ } = require('./http');
17
+ const { throwIfAborted } = require('../../utils/abort');
13
18
 
14
19
  const OAUTH_STATE_PATTERN = /^[a-f0-9]{32,128}$/i;
15
20
 
@@ -54,8 +59,10 @@ class IntegrationManager {
54
59
  constructor(options = {}) {
55
60
  this.app = options.app || null;
56
61
  this.registry = createIntegrationRegistry({
62
+ app: this.app,
57
63
  io: this.app?.locals?.io || null,
58
64
  });
65
+ this.connectionExecutionQueues = new Map();
59
66
  }
60
67
 
61
68
  parseCredentials(credentialsJson) {
@@ -152,6 +159,23 @@ class IntegrationManager {
152
159
  ).run();
153
160
  }
154
161
 
162
+ claimOAuthState(state) {
163
+ return db.transaction(() => {
164
+ const stateRow = db
165
+ .prepare(
166
+ `SELECT * FROM integration_oauth_states
167
+ WHERE state = ? AND datetime(expires_at) > datetime('now')`,
168
+ )
169
+ .get(state);
170
+ if (!stateRow) return null;
171
+ const deleted = db.prepare(
172
+ `DELETE FROM integration_oauth_states
173
+ WHERE state = ? AND datetime(expires_at) > datetime('now')`,
174
+ ).run(state);
175
+ return deleted.changes === 1 ? stateRow : null;
176
+ })();
177
+ }
178
+
155
179
  listConnections(userId, providerKey = null, agentId = null) {
156
180
  const scopedAgentId = resolveAgentId(userId, agentId);
157
181
  const query = providerKey
@@ -225,6 +249,7 @@ class IntegrationManager {
225
249
  userId,
226
250
  agentId,
227
251
  appKey,
252
+ signal: options.signal || null,
228
253
  });
229
254
  const url = String(result?.url || '').trim();
230
255
  const absoluteUrl = url.startsWith('http')
@@ -247,6 +272,7 @@ class IntegrationManager {
247
272
  userId,
248
273
  agentId,
249
274
  appKey,
275
+ signal: options.signal || null,
250
276
  });
251
277
 
252
278
  db.prepare(
@@ -290,7 +316,8 @@ class IntegrationManager {
290
316
  );
291
317
  }
292
318
 
293
- async finishOAuth(state, code) {
319
+ async finishOAuth(state, code, options = {}) {
320
+ throwIfAborted(options.signal, 'OAuth callback request aborted.');
294
321
  this.cleanupExpiredOauthStates();
295
322
  const normalizedState = String(state || '').trim();
296
323
  if (!OAUTH_STATE_PATTERN.test(normalizedState)) {
@@ -300,12 +327,7 @@ class IntegrationManager {
300
327
  if (!normalizedCode) {
301
328
  throw new Error('OAuth authorization code is required.');
302
329
  }
303
- const stateRow = db
304
- .prepare(
305
- `SELECT * FROM integration_oauth_states
306
- WHERE state = ? AND datetime(expires_at) > datetime('now')`,
307
- )
308
- .get(normalizedState);
330
+ const stateRow = this.claimOAuthState(normalizedState);
309
331
  if (!stateRow) {
310
332
  throw new Error('OAuth state is missing or expired.');
311
333
  }
@@ -322,6 +344,7 @@ class IntegrationManager {
322
344
  code: normalizedCode,
323
345
  codeVerifier: decryptValue(stateRow.code_verifier),
324
346
  appKey: stateRow.app_key,
347
+ signal: options.signal || null,
325
348
  });
326
349
 
327
350
  const mergedCredentials = this.mergeWithReusableCredentials(
@@ -386,10 +409,6 @@ class IntegrationManager {
386
409
  result.accountEmail,
387
410
  );
388
411
 
389
- db.prepare('DELETE FROM integration_oauth_states WHERE state = ?').run(
390
- stateRow.state,
391
- );
392
-
393
412
  return {
394
413
  provider: provider.key,
395
414
  appId: stateRow.app_key,
@@ -420,7 +439,11 @@ class IntegrationManager {
420
439
  };
421
440
  }
422
441
 
423
- await provider.disconnect(connection).catch(() => {});
442
+ if (typeof provider.disconnect === 'function') {
443
+ await provider.disconnect(connection, {
444
+ signal: options.signal || null,
445
+ }).catch(() => {});
446
+ }
424
447
  db.prepare('DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND id = ?').run(
425
448
  userId,
426
449
  agentId,
@@ -636,7 +659,46 @@ class IntegrationManager {
636
659
  };
637
660
  }
638
661
 
639
- async executeTool(userId, toolName, args, agentId = null) {
662
+ connectionExecutionKey(connection) {
663
+ return [
664
+ connection.user_id,
665
+ connection.agent_id,
666
+ connection.provider_key,
667
+ String(connection.account_email || '').trim().toLowerCase(),
668
+ ].join(':');
669
+ }
670
+
671
+ async acquireConnectionExecution(connection, signal) {
672
+ const key = this.connectionExecutionKey(connection);
673
+ const previous = this.connectionExecutionQueues.get(key) || Promise.resolve();
674
+ let releaseGate;
675
+ const gate = new Promise((resolve) => {
676
+ releaseGate = resolve;
677
+ });
678
+ const tail = previous.catch(() => {}).then(() => gate);
679
+ this.connectionExecutionQueues.set(key, tail);
680
+ void tail.finally(() => {
681
+ if (this.connectionExecutionQueues.get(key) === tail) {
682
+ this.connectionExecutionQueues.delete(key);
683
+ }
684
+ });
685
+
686
+ try {
687
+ await waitForAbortableResult(previous.catch(() => {}), signal);
688
+ } catch (error) {
689
+ releaseGate();
690
+ throw error;
691
+ }
692
+
693
+ let released = false;
694
+ return () => {
695
+ if (released) return;
696
+ released = true;
697
+ releaseGate();
698
+ };
699
+ }
700
+
701
+ async executeTool(userId, toolName, args, agentId = null, options = {}) {
640
702
  let foundSupportingProvider = false;
641
703
  for (const provider of this.registry.list()) {
642
704
  if (!provider.supportsTool(toolName)) continue;
@@ -661,54 +723,86 @@ class IntegrationManager {
661
723
  };
662
724
  }
663
725
 
664
- let execution;
726
+ const storedCredentials = this.parseCredentials(
727
+ selection.connection.credentials_json,
728
+ );
729
+ const needsCredentialLock =
730
+ provider.requiresRefreshToken === true ||
731
+ Boolean(storedCredentials.refresh_token);
732
+ const releaseExecution = needsCredentialLock
733
+ ? await this.acquireConnectionExecution(selection.connection, options.signal)
734
+ : () => {};
665
735
  try {
666
- execution = await provider.executeTool(
667
- toolName,
668
- args,
669
- selection.connection,
736
+ const connection = this.getConnectionById(
737
+ userId,
738
+ selection.connection.id,
739
+ agentId,
670
740
  );
671
- } catch (err) {
672
- if (isLikelyExpiredConnectionError(err)) {
673
- db.prepare(
674
- `UPDATE integration_connections
675
- SET status = 'expired', updated_at = datetime('now')
676
- WHERE id = ? AND user_id = ? AND agent_id = ?`,
677
- ).run(
678
- selection.connection.id,
741
+ if (!connection || connection.status !== 'connected') {
742
+ return {
743
+ error: `The selected ${provider.label} connection is no longer available.`,
744
+ };
745
+ }
746
+
747
+ let execution;
748
+ try {
749
+ execution = await provider.executeTool(
750
+ toolName,
751
+ args,
752
+ connection,
753
+ { signal: options.signal || null },
754
+ );
755
+ } catch (err) {
756
+ if (
757
+ options.signal?.aborted ||
758
+ err?.name === 'AbortError' ||
759
+ err?.code === 'ABORT_ERR'
760
+ ) {
761
+ throw options.signal?.aborted ? abortError(options.signal) : err;
762
+ }
763
+ if (isLikelyExpiredConnectionError(err)) {
764
+ db.prepare(
765
+ `UPDATE integration_connections
766
+ SET status = 'expired', updated_at = datetime('now')
767
+ WHERE id = ? AND user_id = ? AND agent_id = ?`,
768
+ ).run(
769
+ connection.id,
770
+ userId,
771
+ resolveAgentId(userId, agentId),
772
+ );
773
+ return {
774
+ error: `Your ${provider.label} account connection has expired and needs to be reconnected. Open Official Integrations to reconnect your account.`,
775
+ expired: true,
776
+ connectionId: connection.id,
777
+ };
778
+ }
779
+ return { error: err?.message || 'execution_error' };
780
+ }
781
+ if (!execution) {
782
+ return { error: 'execution_failed' };
783
+ }
784
+
785
+ if (execution.credentials) {
786
+ const existingCredentials = this.parseCredentials(
787
+ connection.credentials_json,
788
+ );
789
+ const mergedCredentials = this.mergeUpdatedCredentials(
790
+ existingCredentials,
791
+ execution.credentials,
792
+ );
793
+ this.persistSharedCredentials(
679
794
  userId,
680
795
  resolveAgentId(userId, agentId),
796
+ provider.key,
797
+ connection.account_email,
798
+ mergedCredentials,
681
799
  );
682
- return {
683
- error: `Your ${provider.label} account connection has expired and needs to be reconnected. Open Official Integrations to reconnect your account.`,
684
- expired: true,
685
- connectionId: selection.connection.id,
686
- };
687
800
  }
688
- return { error: err?.message || 'execution_error' };
689
- }
690
- if (!execution) {
691
- return { error: 'execution_failed' };
692
- }
693
801
 
694
- if (execution.credentials) {
695
- const existingCredentials = this.parseCredentials(
696
- selection.connection.credentials_json,
697
- );
698
- const mergedCredentials = this.mergeUpdatedCredentials(
699
- existingCredentials,
700
- execution.credentials,
701
- );
702
- this.persistSharedCredentials(
703
- userId,
704
- resolveAgentId(userId, agentId),
705
- provider.key,
706
- selection.connection.account_email,
707
- mergedCredentials,
708
- );
802
+ return execution.result;
803
+ } finally {
804
+ releaseExecution();
709
805
  }
710
-
711
- return execution.result;
712
806
  }
713
807
 
714
808
  return foundSupportingProvider
@@ -716,6 +810,18 @@ class IntegrationManager {
716
810
  : { error: 'no_provider_support' };
717
811
  }
718
812
 
813
+ async shutdown() {
814
+ const providers = this.registry.list();
815
+ await Promise.allSettled(
816
+ providers.map((provider) => {
817
+ if (typeof provider.shutdown !== 'function') return null;
818
+ return provider.shutdown();
819
+ }),
820
+ );
821
+ this.connectionExecutionQueues.clear();
822
+ return { state: 'stopped', providerCount: providers.length };
823
+ }
824
+
719
825
  summarizeConnectedProviders(userId, agentId = null) {
720
826
  const scopedAgentId = resolveAgentId(userId, agentId);
721
827
  const ingestionService = this.app?.locals?.memoryIngestionService || null;
@@ -202,19 +202,83 @@ function graphUrl(path, query) {
202
202
  return url.toString();
203
203
  }
204
204
 
205
- async function graphRequest(credentials, { method = 'GET', path, query, body }) {
206
- return fetchJson(
205
+ function expiresAtFromSeconds(expiresIn) {
206
+ return new Date(
207
+ Date.now() + Math.max(1, Number(expiresIn) || 3600) * 1000,
208
+ ).toISOString();
209
+ }
210
+
211
+ function tokenExpiresSoon(credentials) {
212
+ const expiresAt = Date.parse(String(credentials?.expires_at || ''));
213
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now() + 60 * 1000;
214
+ }
215
+
216
+ async function refreshMicrosoftCredentials(credentials, signal) {
217
+ const refreshToken = String(credentials?.refresh_token || '').trim();
218
+ if (!refreshToken) {
219
+ throw new Error(
220
+ 'Microsoft 365 refresh token is missing. Reconnect this integration account.',
221
+ );
222
+ }
223
+ const { config, tokenUrl } = getMicrosoftEndpoints();
224
+ const token = await fetchJson(
225
+ tokenUrl,
226
+ {
227
+ method: 'POST',
228
+ form: {
229
+ client_id: config.clientId,
230
+ client_secret: config.clientSecret,
231
+ grant_type: 'refresh_token',
232
+ refresh_token: refreshToken,
233
+ scope: credentials.scope || undefined,
234
+ },
235
+ signal,
236
+ },
237
+ { serviceName: 'Microsoft 365 token refresh' },
238
+ );
239
+ if (!String(token?.access_token || '').trim()) {
240
+ throw new Error('Microsoft 365 token refresh did not return an access token.');
241
+ }
242
+ return {
243
+ ...credentials,
244
+ access_token: token.access_token,
245
+ refresh_token: token.refresh_token || refreshToken,
246
+ expires_in: token.expires_in,
247
+ expires_at: expiresAtFromSeconds(token.expires_in),
248
+ scope: token.scope || credentials.scope,
249
+ token_type: token.token_type || credentials.token_type,
250
+ };
251
+ }
252
+
253
+ async function graphRequest(context, { method = 'GET', path, query, body }) {
254
+ const { signal } = context;
255
+ let credentials = context.credentials;
256
+ if (tokenExpiresSoon(credentials)) {
257
+ credentials = await refreshMicrosoftCredentials(credentials, signal);
258
+ context.updateCredentials(credentials);
259
+ }
260
+ const performRequest = (activeCredentials) => fetchJson(
207
261
  graphUrl(path, query),
208
262
  {
209
263
  method: String(method || 'GET').toUpperCase(),
210
- headers: { Authorization: `Bearer ${credentials.access_token}` },
264
+ headers: { Authorization: `Bearer ${activeCredentials.access_token}` },
211
265
  ...(body === undefined ? {} : { json: body }),
266
+ signal,
212
267
  },
213
268
  { serviceName: 'Microsoft Graph' },
214
269
  );
270
+
271
+ try {
272
+ return await performRequest(credentials);
273
+ } catch (error) {
274
+ if (error?.status !== 401 || !credentials.refresh_token) throw error;
275
+ credentials = await refreshMicrosoftCredentials(credentials, signal);
276
+ context.updateCredentials(credentials);
277
+ return performRequest(credentials);
278
+ }
215
279
  }
216
280
 
217
- async function executeMicrosoftTool(toolName, args, { credentials }) {
281
+ async function executeMicrosoftTool(toolName, args, context) {
218
282
  switch (toolName) {
219
283
  case 'microsoft_365_outlook_list_messages': {
220
284
  const folder = String(args.folder_id || 'inbox').trim();
@@ -222,7 +286,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
222
286
  ? '/v1.0/me/mailFolders/inbox/messages'
223
287
  : `/v1.0/me/mailFolders/${encodeURIComponent(folder)}/messages`;
224
288
  return {
225
- result: await graphRequest(credentials, {
289
+ result: await graphRequest(context, {
226
290
  path,
227
291
  query: {
228
292
  '$top': Math.max(1, Math.min(Number(args.top) || 10, 50)),
@@ -233,7 +297,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
233
297
  };
234
298
  }
235
299
  case 'microsoft_365_outlook_send_mail':
236
- await graphRequest(credentials, {
300
+ await graphRequest(context, {
237
301
  method: 'POST',
238
302
  path: '/v1.0/me/sendMail',
239
303
  body: {
@@ -255,7 +319,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
255
319
  case 'microsoft_365_calendar_list_events': {
256
320
  const hasWindow = args.start && args.end;
257
321
  return {
258
- result: await graphRequest(credentials, {
322
+ result: await graphRequest(context, {
259
323
  path: hasWindow ? '/v1.0/me/calendarView' : '/v1.0/me/events',
260
324
  query: {
261
325
  '$top': Math.max(1, Math.min(Number(args.top) || 10, 50)),
@@ -268,7 +332,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
268
332
  }
269
333
  case 'microsoft_365_calendar_create_event':
270
334
  return {
271
- result: await graphRequest(credentials, {
335
+ result: await graphRequest(context, {
272
336
  method: 'POST',
273
337
  path: '/v1.0/me/events',
274
338
  body: {
@@ -294,7 +358,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
294
358
  };
295
359
  case 'microsoft_365_onedrive_list_children':
296
360
  return {
297
- result: await graphRequest(credentials, {
361
+ result: await graphRequest(context, {
298
362
  path: args.item_id
299
363
  ? `/v1.0/me/drive/items/${encodeURIComponent(String(args.item_id))}/children`
300
364
  : '/v1.0/me/drive/root/children',
@@ -303,14 +367,14 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
303
367
  };
304
368
  case 'microsoft_365_teams_list_chats':
305
369
  return {
306
- result: await graphRequest(credentials, {
370
+ result: await graphRequest(context, {
307
371
  path: '/v1.0/me/chats',
308
372
  query: { '$top': Math.max(1, Math.min(Number(args.top) || 20, 50)) },
309
373
  }),
310
374
  };
311
375
  case 'microsoft_365_teams_send_chat_message':
312
376
  return {
313
- result: await graphRequest(credentials, {
377
+ result: await graphRequest(context, {
314
378
  method: 'POST',
315
379
  path: `/v1.0/chats/${encodeURIComponent(requireText(args.chat_id, 'chat_id'))}/messages`,
316
380
  body: {
@@ -327,7 +391,7 @@ async function executeMicrosoftTool(toolName, args, { credentials }) {
327
391
  throw new Error('microsoft_365_*_graph_request tools are disabled by default. Set NEOAGENT_ENABLE_MICROSOFT_DYNAMIC_GRAPH_REQUEST=true to enable them.');
328
392
  }
329
393
  return {
330
- result: await graphRequest(credentials, {
394
+ result: await graphRequest(context, {
331
395
  method: args.method,
332
396
  path: requireText(args.path, 'path'),
333
397
  query: args.query,
@@ -371,7 +435,7 @@ function createMicrosoftProvider() {
371
435
  appId: app.id,
372
436
  };
373
437
  },
374
- async finishOAuth({ code, app }) {
438
+ async finishOAuth({ code, app, signal }) {
375
439
  const { config, tokenUrl } = getMicrosoftEndpoints();
376
440
  const token = await fetchJson(
377
441
  tokenUrl,
@@ -385,6 +449,7 @@ function createMicrosoftProvider() {
385
449
  redirect_uri: config.redirectUri,
386
450
  scope: escapeScope(app.scopes),
387
451
  },
452
+ signal,
388
453
  },
389
454
  { serviceName: 'Microsoft 365' },
390
455
  );
@@ -395,6 +460,7 @@ function createMicrosoftProvider() {
395
460
  headers: {
396
461
  Authorization: `Bearer ${token.access_token}`,
397
462
  },
463
+ signal,
398
464
  },
399
465
  { serviceName: 'Microsoft 365' },
400
466
  );
@@ -413,6 +479,7 @@ function createMicrosoftProvider() {
413
479
  access_token: token.access_token,
414
480
  refresh_token: token.refresh_token,
415
481
  expires_in: token.expires_in,
482
+ expires_at: expiresAtFromSeconds(token.expires_in),
416
483
  scope: token.scope,
417
484
  token_type: token.token_type,
418
485
  },