neoagent 2.3.1-beta.4 → 2.3.1-beta.41

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 (290) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +12 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +99 -0
  66. package/flutter_app/lib/main_account_settings.dart +1250 -0
  67. package/flutter_app/lib/main_admin.dart +886 -0
  68. package/flutter_app/lib/main_app_shell.dart +1682 -0
  69. package/flutter_app/lib/main_chat.dart +3352 -0
  70. package/flutter_app/lib/main_controller.dart +6781 -0
  71. package/flutter_app/lib/main_devices.dart +2301 -0
  72. package/flutter_app/lib/main_integrations.dart +1129 -0
  73. package/flutter_app/lib/main_launcher.dart +959 -0
  74. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  75. package/flutter_app/lib/main_models.dart +3546 -0
  76. package/flutter_app/lib/main_navigation.dart +193 -0
  77. package/flutter_app/lib/main_operations.dart +4851 -0
  78. package/flutter_app/lib/main_recordings.dart +870 -0
  79. package/flutter_app/lib/main_runtime.dart +806 -0
  80. package/flutter_app/lib/main_settings.dart +2024 -0
  81. package/flutter_app/lib/main_shared.dart +2861 -0
  82. package/flutter_app/lib/main_theme.dart +204 -0
  83. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  84. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  85. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  86. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  87. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  88. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  89. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  90. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  91. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  92. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  93. package/flutter_app/lib/src/backend_client.dart +1833 -0
  94. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  95. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  96. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  97. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  98. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  99. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  100. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  101. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  102. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  103. package/flutter_app/lib/src/health_bridge.dart +136 -0
  104. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  105. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  106. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  107. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  108. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  109. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  110. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  111. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  112. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  113. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  114. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  115. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  116. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  117. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  118. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  119. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  120. package/flutter_app/lib/src/theme/palette.dart +81 -0
  121. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  122. package/flutter_app/linux/CMakeLists.txt +128 -0
  123. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  124. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  125. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  126. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  127. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  128. package/flutter_app/linux/runner/main.cc +6 -0
  129. package/flutter_app/linux/runner/my_application.cc +144 -0
  130. package/flutter_app/linux/runner/my_application.h +18 -0
  131. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  132. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  133. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  134. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  135. package/flutter_app/macos/Podfile +42 -0
  136. package/flutter_app/macos/Podfile.lock +87 -0
  137. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  138. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  139. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  140. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  141. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  142. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  143. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  144. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  145. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  146. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  147. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  148. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  150. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  151. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  152. package/flutter_app/macos/Runner/Info.plist +36 -0
  153. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  154. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  155. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  156. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  157. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  158. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  159. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  160. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  161. package/flutter_app/patch_strings.py +12 -0
  162. package/flutter_app/pubspec.lock +1088 -0
  163. package/flutter_app/pubspec.yaml +53 -0
  164. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  165. package/flutter_app/test/recording_payloads_test.dart +53 -0
  166. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  167. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  168. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  169. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  170. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  171. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  172. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  173. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  174. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  175. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  176. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  177. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  178. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  179. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  180. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  181. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  182. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  183. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  184. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  185. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  186. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  187. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  188. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  189. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  190. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  191. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  192. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  193. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  194. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  195. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  196. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  197. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  198. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  199. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  200. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  201. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  202. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  203. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  204. package/flutter_app/web/favicon.png +0 -0
  205. package/flutter_app/web/favicon.svg +12 -0
  206. package/flutter_app/web/icons/Icon-192.png +0 -0
  207. package/flutter_app/web/icons/Icon-512.png +0 -0
  208. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  209. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  210. package/flutter_app/web/index.html +39 -0
  211. package/flutter_app/web/manifest.json +35 -0
  212. package/flutter_app/windows/CMakeLists.txt +108 -0
  213. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  214. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  215. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  216. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  217. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  218. package/flutter_app/windows/runner/Runner.rc +121 -0
  219. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  220. package/flutter_app/windows/runner/flutter_window.h +37 -0
  221. package/flutter_app/windows/runner/main.cpp +53 -0
  222. package/flutter_app/windows/runner/resource.h +16 -0
  223. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  224. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  225. package/flutter_app/windows/runner/utils.cpp +65 -0
  226. package/flutter_app/windows/runner/utils.h +19 -0
  227. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  228. package/flutter_app/windows/runner/win32_window.h +102 -0
  229. package/lib/install_helpers.js +31 -0
  230. package/lib/manager.js +227 -6
  231. package/package.json +3 -1
  232. package/server/db/database.js +110 -0
  233. package/server/http/middleware.js +55 -2
  234. package/server/http/routes.js +1 -0
  235. package/server/index.js +3 -0
  236. package/server/public/.last_build_id +1 -1
  237. package/server/public/assets/NOTICES +1 -1
  238. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  239. package/server/public/canvaskit/wimp.wasm +0 -0
  240. package/server/public/flutter_bootstrap.js +2 -2
  241. package/server/public/main.dart.js +74324 -73132
  242. package/server/routes/integrations.js +108 -1
  243. package/server/routes/memory.js +11 -2
  244. package/server/{http/routes → routes}/screenHistory.js +2 -2
  245. package/server/routes/settings.js +75 -2
  246. package/server/{http/routes → routes}/triggers.js +2 -2
  247. package/server/routes/wearable.js +67 -0
  248. package/server/services/ai/models.js +30 -0
  249. package/server/services/ai/providers/githubCopilot.js +97 -0
  250. package/server/services/ai/providers/openai.js +2 -1
  251. package/server/services/ai/providers/openaiCodex.js +31 -0
  252. package/server/services/ai/settings.js +20 -0
  253. package/server/services/ai/toolSelector.js +14 -1
  254. package/server/services/ai/tools.js +77 -4
  255. package/server/services/desktop/screenRecorder.js +65 -9
  256. package/server/services/integrations/env.js +5 -0
  257. package/server/services/integrations/figma/provider.js +1 -0
  258. package/server/services/integrations/github/common.js +106 -0
  259. package/server/services/integrations/github/provider.js +499 -0
  260. package/server/services/integrations/github/repos.js +1124 -0
  261. package/server/services/integrations/google/provider.js +1 -0
  262. package/server/services/integrations/home_assistant/provider.js +325 -26
  263. package/server/services/integrations/manager.js +88 -12
  264. package/server/services/integrations/microsoft/provider.js +1 -0
  265. package/server/services/integrations/oauth_provider.js +25 -8
  266. package/server/services/integrations/provider_config_store.js +85 -0
  267. package/server/services/integrations/registry.js +4 -0
  268. package/server/services/integrations/spotify/provider.js +1 -0
  269. package/server/services/integrations/trello/provider.js +842 -0
  270. package/server/services/manager.js +46 -1
  271. package/server/services/mcp/client.js +120 -23
  272. package/server/services/memory/manager.js +39 -2
  273. package/server/services/messaging/access_policy.js +10 -0
  274. package/server/services/messaging/manager.js +49 -0
  275. package/server/services/messaging/meshtastic.js +260 -0
  276. package/server/services/messaging/meshtastic_env.js +100 -0
  277. package/server/services/messaging/meshtastic_protocol.js +476 -0
  278. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  279. package/server/services/tasks/runtime.js +1 -1
  280. package/server/services/voice/openaiClient.js +4 -1
  281. package/server/services/voice/openaiSpeech.js +6 -1
  282. package/server/services/voice/providers.js +52 -12
  283. package/server/services/voice/runtimeManager.js +136 -19
  284. package/server/services/voice/turnRunner.js +29 -9
  285. package/server/services/wearable/firmware_manifest.js +370 -0
  286. package/server/services/wearable/gateway.js +350 -0
  287. package/server/services/wearable/protocol.js +45 -0
  288. package/server/services/wearable/service.js +244 -0
  289. package/server/utils/local_secrets.js +56 -0
  290. package/server/utils/logger.js +37 -9
@@ -300,6 +300,7 @@ function createGoogleWorkspaceProvider() {
300
300
  description:
301
301
  'Official Gmail, Calendar, Drive, Docs, and Sheets integrations with app-specific accounts.',
302
302
  icon: 'google',
303
+ requiresRefreshToken: true,
303
304
  apps: GOOGLE_WORKSPACE_APPS.map(({ id, label, description }) => ({
304
305
  id,
305
306
  label,
@@ -1,10 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const net = require('net');
5
+ const ipaddr = require('ipaddr.js');
6
+ const { resolveAgentId } = require('../../agents/manager');
4
7
  const {
5
8
  describeEnvStatus,
6
9
  resolveHomeAssistantOAuthConfig,
7
10
  } = require('../env');
11
+ const {
12
+ deleteProviderConfig,
13
+ getProviderConfig,
14
+ setProviderConfig,
15
+ } = require('../provider_config_store');
8
16
  const {
9
17
  appendQuery,
10
18
  createOAuthProvider,
@@ -93,6 +101,253 @@ const homeAssistantToolDefinitions = [
93
101
  },
94
102
  ];
95
103
 
104
+ function trimText(value) {
105
+ return String(value || '').trim();
106
+ }
107
+
108
+ function isTruthyEnv(name) {
109
+ const value = String(process.env[name] || '').trim().toLowerCase();
110
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
111
+ }
112
+
113
+ function isLikelyLocalHostname(hostname) {
114
+ const host = String(hostname || '').trim().toLowerCase();
115
+ if (!host) return false;
116
+ if (host === 'localhost' || host === 'host.docker.internal') return true;
117
+ if (host.endsWith('.localhost')) return true;
118
+ if (host.endsWith('.local') || host.endsWith('.lan') || host.endsWith('.internal')) {
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+
124
+ function isPrivateIpv4(hostname) {
125
+ const parts = String(hostname || '').split('.').map((part) => Number(part));
126
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
127
+ return false;
128
+ }
129
+ const [a, b] = parts;
130
+ if (a === 10) return true;
131
+ if (a === 127) return true;
132
+ if (a === 169 && b === 254) return true;
133
+ if (a === 172 && b >= 16 && b <= 31) return true;
134
+ if (a === 192 && b === 168) return true;
135
+ if (a === 100 && b >= 64 && b <= 127) return true;
136
+ if (a === 0) return true;
137
+ return false;
138
+ }
139
+
140
+ function isPrivateIpv6(hostname) {
141
+ const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
142
+ if (!host) return false;
143
+ try {
144
+ const parsed = ipaddr.parse(host);
145
+ if (parsed.kind() !== 'ipv6') return false;
146
+
147
+ // Normalize IPv4-mapped IPv6 literals and enforce the same local/private rules.
148
+ if (parsed.isIPv4MappedAddress()) {
149
+ return isPrivateIpv4(parsed.toIPv4Address().toString());
150
+ }
151
+
152
+ const range = parsed.range();
153
+ return range === 'loopback' || range === 'uniqueLocal' || range === 'linkLocal' || range === 'unspecified';
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function isPrivateOrLocalIp(hostname) {
160
+ const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
161
+ const kind = net.isIP(host);
162
+ if (kind === 4) return isPrivateIpv4(host);
163
+ if (kind === 6) return isPrivateIpv6(host);
164
+ return false;
165
+ }
166
+
167
+ function validateHomeAssistantBaseUrlSafety(parsedUrl) {
168
+ const allowPrivate = isTruthyEnv('HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL');
169
+ const host = String(parsedUrl.hostname || '').trim();
170
+ const localHostname = isLikelyLocalHostname(host);
171
+ const localIp = isPrivateOrLocalIp(host);
172
+ const isLocalTarget = localHostname || localIp;
173
+
174
+ if (isLocalTarget && !allowPrivate) {
175
+ throw new Error(
176
+ 'Home Assistant base URL cannot target localhost/private network addresses unless HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1 is set on the server.',
177
+ );
178
+ }
179
+
180
+ if (parsedUrl.protocol === 'http:' && !isLocalTarget) {
181
+ throw new Error('Home Assistant base URL must use HTTPS for non-local hosts.');
182
+ }
183
+ }
184
+
185
+ function normalizeBaseUrl(value) {
186
+ const text = trimText(value);
187
+ if (!text) return '';
188
+ return text.replace(/\/$/, '');
189
+ }
190
+
191
+ function normalizeOptionalAbsoluteUrl(value, label) {
192
+ const text = trimText(value);
193
+ if (!text) return '';
194
+ try {
195
+ const parsed = new URL(text);
196
+ if (!/^https?:$/.test(parsed.protocol)) {
197
+ throw new Error(`${label} must use http or https.`);
198
+ }
199
+ return parsed.toString().replace(/\/$/, '');
200
+ } catch {
201
+ throw new Error(`${label} must be a valid absolute URL.`);
202
+ }
203
+ }
204
+
205
+ function normalizeHomeAssistantBaseUrl(value) {
206
+ const text = trimText(value);
207
+ if (!text) return '';
208
+ let parsed;
209
+ try {
210
+ parsed = new URL(text);
211
+ } catch {
212
+ throw new Error('Home Assistant base URL must be a valid absolute URL.');
213
+ }
214
+ if (!/^https?:$/.test(parsed.protocol)) {
215
+ throw new Error('Home Assistant base URL must use http or https.');
216
+ }
217
+ validateHomeAssistantBaseUrlSafety(parsed);
218
+ return parsed.toString().replace(/\/$/, '');
219
+ }
220
+
221
+ function normalizeUserHomeAssistantConfig(rawConfig) {
222
+ const source = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
223
+ return {
224
+ baseUrl: normalizeBaseUrl(source.baseUrl),
225
+ clientId: trimText(source.clientId),
226
+ clientSecret: trimText(source.clientSecret),
227
+ redirectUri: trimText(source.redirectUri),
228
+ };
229
+ }
230
+
231
+ function resolveUserHomeAssistantConfig(userId, agentId = null) {
232
+ const userConfig = normalizeUserHomeAssistantConfig(
233
+ Number.isInteger(Number(userId)) && Number(userId) > 0
234
+ ? getProviderConfig(Number(userId), 'home_assistant', agentId)
235
+ : {},
236
+ );
237
+ const envConfig = resolveHomeAssistantOAuthConfig();
238
+ return {
239
+ baseUrl: userConfig.baseUrl || envConfig.baseUrl,
240
+ clientId: userConfig.clientId || envConfig.clientId,
241
+ clientSecret: userConfig.clientSecret || envConfig.clientSecret,
242
+ redirectUri: userConfig.redirectUri || envConfig.redirectUri,
243
+ };
244
+ }
245
+
246
+ function validateResolvedConfig(config) {
247
+ const missing = [];
248
+ if (!trimText(config.baseUrl)) missing.push('baseUrl');
249
+ if (!trimText(config.clientId)) missing.push('clientId');
250
+ if (!trimText(config.clientSecret)) missing.push('clientSecret');
251
+ return {
252
+ configured: missing.length === 0,
253
+ missing,
254
+ };
255
+ }
256
+
257
+ function resolveHomeAssistantConfigForUser(userId, agentId = null) {
258
+ const merged = resolveUserHomeAssistantConfig(userId, agentId);
259
+ const validatedBaseUrl = merged.baseUrl
260
+ ? normalizeHomeAssistantBaseUrl(merged.baseUrl)
261
+ : '';
262
+ const validatedRedirectUri = merged.redirectUri
263
+ ? normalizeOptionalAbsoluteUrl(
264
+ merged.redirectUri,
265
+ 'Home Assistant OAuth redirect URI',
266
+ )
267
+ : '';
268
+ const result = {
269
+ baseUrl: validatedBaseUrl,
270
+ clientId: trimText(merged.clientId),
271
+ clientSecret: trimText(merged.clientSecret),
272
+ redirectUri: validatedRedirectUri,
273
+ };
274
+ const status = validateResolvedConfig(result);
275
+ return {
276
+ ...result,
277
+ configured: status.configured,
278
+ missing: status.missing,
279
+ };
280
+ }
281
+
282
+ function sanitizeHomeAssistantUserConfigForClient(rawConfig) {
283
+ const config = normalizeUserHomeAssistantConfig(rawConfig);
284
+ return {
285
+ baseUrl: config.baseUrl,
286
+ clientId: config.clientId,
287
+ redirectUri: config.redirectUri,
288
+ hasClientSecret: Boolean(config.clientSecret),
289
+ };
290
+ }
291
+
292
+ function parseHomeAssistantConfigInput(input, existingConfig = {}) {
293
+ const source = input && typeof input === 'object' ? input : {};
294
+ const baseUrl = normalizeHomeAssistantBaseUrl(source.baseUrl);
295
+ const clientId = trimText(source.clientId);
296
+ const clientSecret =
297
+ trimText(source.clientSecret) || trimText(existingConfig.clientSecret);
298
+ const redirectUri = source.redirectUri
299
+ ? normalizeOptionalAbsoluteUrl(
300
+ source.redirectUri,
301
+ 'Home Assistant OAuth redirect URI',
302
+ )
303
+ : '';
304
+
305
+ if (!baseUrl) {
306
+ throw new Error('Home Assistant base URL is required.');
307
+ }
308
+ if (!clientId) {
309
+ throw new Error('Home Assistant OAuth client ID is required.');
310
+ }
311
+ if (!clientSecret) {
312
+ throw new Error('Home Assistant OAuth client secret is required.');
313
+ }
314
+
315
+ return {
316
+ baseUrl,
317
+ clientId,
318
+ clientSecret,
319
+ redirectUri,
320
+ };
321
+ }
322
+
323
+ function saveHomeAssistantUserConfig(userId, agentId = null, input) {
324
+ const normalizedUserId = Number(userId);
325
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
326
+ throw new Error('A valid user is required to save Home Assistant configuration.');
327
+ }
328
+ const scopedAgentId = resolveAgentId(normalizedUserId, agentId);
329
+ const existingConfig = normalizeUserHomeAssistantConfig(
330
+ getProviderConfig(normalizedUserId, 'home_assistant', scopedAgentId),
331
+ );
332
+ const config = parseHomeAssistantConfigInput(input, existingConfig);
333
+ setProviderConfig(normalizedUserId, 'home_assistant', config, scopedAgentId);
334
+ return sanitizeHomeAssistantUserConfigForClient(config);
335
+ }
336
+
337
+ function getHomeAssistantUserConfig(userId, agentId = null) {
338
+ const normalizedUserId = Number(userId);
339
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
340
+ return sanitizeHomeAssistantUserConfigForClient({});
341
+ }
342
+ return sanitizeHomeAssistantUserConfigForClient(
343
+ getProviderConfig(
344
+ normalizedUserId,
345
+ 'home_assistant',
346
+ resolveAgentId(normalizedUserId, agentId),
347
+ ),
348
+ );
349
+ }
350
+
96
351
  function requireText(value, label) {
97
352
  const text = String(value || '').trim();
98
353
  if (!text) throw new Error(`${label} is required.`);
@@ -102,7 +357,7 @@ function requireText(value, label) {
102
357
  function homeAssistantUrl(baseUrl, path, query) {
103
358
  const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
104
359
  if (!normalizedBase) {
105
- throw new Error('HOME_ASSISTANT_BASE_URL is required.');
360
+ throw new Error('Home Assistant base URL is required.');
106
361
  }
107
362
  const url = new URL(
108
363
  String(path || '').startsWith('http')
@@ -119,7 +374,10 @@ function homeAssistantUrl(baseUrl, path, query) {
119
374
  }
120
375
 
121
376
  async function homeAssistantRequest(credentials, options = {}) {
122
- const config = resolveHomeAssistantOAuthConfig();
377
+ const config = resolveHomeAssistantConfigForUser(
378
+ options.userId,
379
+ options.agentId,
380
+ );
123
381
  const accessToken = String(credentials?.access_token || '').trim();
124
382
  if (!accessToken) {
125
383
  throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
@@ -138,14 +396,22 @@ async function homeAssistantRequest(credentials, options = {}) {
138
396
  );
139
397
  }
140
398
 
141
- async function executeHomeAssistantTool(toolName, args, { credentials }) {
399
+ async function executeHomeAssistantTool(toolName, args, { connection, credentials }) {
142
400
  switch (toolName) {
143
401
  case 'home_assistant_get_config':
144
402
  return {
145
- result: await homeAssistantRequest(credentials, { path: '/api/config' }),
403
+ result: await homeAssistantRequest(credentials, {
404
+ path: '/api/config',
405
+ userId: connection?.user_id,
406
+ agentId: connection?.agent_id,
407
+ }),
146
408
  };
147
409
  case 'home_assistant_list_states': {
148
- const states = await homeAssistantRequest(credentials, { path: '/api/states' });
410
+ const states = await homeAssistantRequest(credentials, {
411
+ path: '/api/states',
412
+ userId: connection?.user_id,
413
+ agentId: connection?.agent_id,
414
+ });
149
415
  const domain = String(args.domain || '').trim().toLowerCase();
150
416
  const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
151
417
  const filtered = Array.isArray(states)
@@ -163,6 +429,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
163
429
  return {
164
430
  result: await homeAssistantRequest(credentials, {
165
431
  path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
432
+ userId: connection?.user_id,
433
+ agentId: connection?.agent_id,
166
434
  }),
167
435
  };
168
436
  case 'home_assistant_call_service':
@@ -171,6 +439,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
171
439
  method: 'POST',
172
440
  path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
173
441
  body: args.service_data || {},
442
+ userId: connection?.user_id,
443
+ agentId: connection?.agent_id,
174
444
  }),
175
445
  };
176
446
  case 'home_assistant_api_request':
@@ -180,6 +450,8 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
180
450
  path: requireText(args.path, 'path'),
181
451
  query: args.query,
182
452
  body: args.body,
453
+ userId: connection?.user_id,
454
+ agentId: connection?.agent_id,
183
455
  }),
184
456
  };
185
457
  default:
@@ -187,19 +459,25 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
187
459
  }
188
460
  }
189
461
 
190
- function resolveHomeAssistantEnvStatus() {
191
- const config = resolveHomeAssistantOAuthConfig();
192
- const missing = config.missing.slice();
193
- if (!config.baseUrl) {
194
- missing.push('HOME_ASSISTANT_BASE_URL');
462
+ function resolveHomeAssistantEnvStatus(userId, agentId = null) {
463
+ try {
464
+ const config = resolveHomeAssistantConfigForUser(userId, agentId);
465
+ return {
466
+ configured: config.configured,
467
+ missing: config.missing,
468
+ summary: config.configured
469
+ ? 'Home Assistant is ready for account connections.'
470
+ : 'Complete your personal Home Assistant setup to connect an account.',
471
+ setupMode: 'user',
472
+ };
473
+ } catch (error) {
474
+ return {
475
+ configured: false,
476
+ missing: ['baseUrl'],
477
+ summary: `Home Assistant setup is invalid: ${error?.message || 'unknown error'}`,
478
+ setupMode: 'user',
479
+ };
195
480
  }
196
- return describeEnvStatus(
197
- {
198
- configured: missing.length === 0,
199
- missing,
200
- },
201
- { label: 'Home Assistant' },
202
- );
203
481
  }
204
482
 
205
483
  function normalizeCurrentUser(currentUser) {
@@ -225,8 +503,8 @@ function stableAccountEmailLikeIdentifier(user, config) {
225
503
  return `homeassistant@${host}`;
226
504
  }
227
505
 
228
- async function fetchCurrentUser(token) {
229
- const config = resolveHomeAssistantOAuthConfig();
506
+ async function fetchCurrentUser(token, userId, agentId = null) {
507
+ const config = resolveHomeAssistantConfigForUser(userId, agentId);
230
508
  return fetchJson(
231
509
  homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
232
510
  {
@@ -246,15 +524,16 @@ function createHomeAssistantProvider() {
246
524
  description:
247
525
  'Official Home Assistant account connections for entity state reads, service control, and automation support.',
248
526
  icon: 'home_assistant',
527
+ requiresRefreshToken: true,
249
528
  apps: HOME_ASSISTANT_APPS,
250
529
  toolDefinitions: homeAssistantToolDefinitions,
251
530
  connectPrompt:
252
531
  'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
253
- getEnvStatus() {
254
- return resolveHomeAssistantEnvStatus();
532
+ getEnvStatus(context = {}) {
533
+ return resolveHomeAssistantEnvStatus(context.userId, context.agentId);
255
534
  },
256
- async beginOAuth({ state, codeVerifier, app }) {
257
- const config = resolveHomeAssistantOAuthConfig();
535
+ async beginOAuth({ state, codeVerifier, app, userId, agentId }) {
536
+ const config = resolveHomeAssistantConfigForUser(userId, agentId);
258
537
  const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
259
538
  return {
260
539
  url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
@@ -269,8 +548,8 @@ function createHomeAssistantProvider() {
269
548
  appId: app.id,
270
549
  };
271
550
  },
272
- async finishOAuth({ code, codeVerifier, app }) {
273
- const config = resolveHomeAssistantOAuthConfig();
551
+ async finishOAuth({ code, codeVerifier, app, userId, agentId }) {
552
+ const config = resolveHomeAssistantConfigForUser(userId, agentId);
274
553
  const token = await fetchJson(
275
554
  homeAssistantUrl(config.baseUrl, '/auth/token'),
276
555
  {
@@ -297,7 +576,7 @@ function createHomeAssistantProvider() {
297
576
  throw new Error('Home Assistant OAuth did not return a refresh token.');
298
577
  }
299
578
 
300
- const currentUser = await fetchCurrentUser(accessToken);
579
+ const currentUser = await fetchCurrentUser(accessToken, userId, agentId);
301
580
  const normalizedUser = normalizeCurrentUser(currentUser);
302
581
  const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
303
582
 
@@ -328,6 +607,24 @@ function createHomeAssistantProvider() {
328
607
  };
329
608
  },
330
609
  executeTool: executeHomeAssistantTool,
610
+ getUserConfig({ userId, agentId }) {
611
+ return getHomeAssistantUserConfig(userId, agentId);
612
+ },
613
+ saveUserConfig({ userId, agentId, config }) {
614
+ return saveHomeAssistantUserConfig(userId, agentId, config);
615
+ },
616
+ clearUserConfig({ userId, agentId }) {
617
+ const normalizedUserId = Number(userId);
618
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
619
+ throw new Error('A valid user is required to clear Home Assistant configuration.');
620
+ }
621
+ deleteProviderConfig(
622
+ normalizedUserId,
623
+ 'home_assistant',
624
+ resolveAgentId(normalizedUserId, agentId),
625
+ );
626
+ return { cleared: true };
627
+ },
331
628
  });
332
629
  }
333
630
 
@@ -346,5 +643,7 @@ function codeChallengeForVerifier(codeVerifier) {
346
643
  }
347
644
 
348
645
  module.exports = {
646
+ getHomeAssistantUserConfig,
647
+ saveHomeAssistantUserConfig,
349
648
  createHomeAssistantProvider,
350
649
  };
@@ -13,6 +13,43 @@ const {
13
13
 
14
14
  const OAUTH_STATE_PATTERN = /^[a-f0-9]{32,128}$/i;
15
15
 
16
+ function isLikelyExpiredConnectionError(error) {
17
+ const message = String(error?.message || error || '').toLowerCase();
18
+ if (!message) return false;
19
+ return [
20
+ 'invalid_grant',
21
+ 'token refresh failed',
22
+ 'token expired',
23
+ 'access token is missing',
24
+ 'refresh token is missing',
25
+ 'reconnect this integration account',
26
+ 'account is no longer authorized',
27
+ 'reauthorize',
28
+ 're-authorize',
29
+ ].some((hint) => message.includes(hint));
30
+ }
31
+
32
+ function assertDurableOAuthCredentials(provider, credentials) {
33
+ const label = String(provider?.label || 'This integration').trim() || 'This integration';
34
+ const normalizedCredentials =
35
+ credentials && typeof credentials === 'object' ? credentials : {};
36
+
37
+ if (!String(normalizedCredentials.access_token || '').trim()) {
38
+ throw new Error(
39
+ `${label} did not return an access token, so the connection could not be completed.`,
40
+ );
41
+ }
42
+
43
+ if (
44
+ provider?.requiresRefreshToken === true &&
45
+ !String(normalizedCredentials.refresh_token || '').trim()
46
+ ) {
47
+ throw new Error(
48
+ `${label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
49
+ );
50
+ }
51
+ }
52
+
16
53
  class IntegrationManager {
17
54
  constructor(options = {}) {
18
55
  this.app = options.app || null;
@@ -147,7 +184,12 @@ class IntegrationManager {
147
184
 
148
185
  return this.registry
149
186
  .list()
150
- .map((provider) => provider.buildSnapshot(rowsByProvider.get(provider.key) || []));
187
+ .map((provider) =>
188
+ provider.buildSnapshot(rowsByProvider.get(provider.key) || [], {
189
+ userId,
190
+ agentId: scopedAgentId,
191
+ }),
192
+ );
151
193
  }
152
194
 
153
195
  async beginOAuth(userId, providerKey, options = {}) {
@@ -163,7 +205,10 @@ class IntegrationManager {
163
205
  throw new Error(`Unknown ${provider.label} app: ${appKey || 'missing app key'}`);
164
206
  }
165
207
 
166
- const env = provider.getEnvStatus();
208
+ const env = provider.getEnvStatus({
209
+ userId,
210
+ agentId,
211
+ });
167
212
  if (!env.configured) {
168
213
  throw new Error(env.summary);
169
214
  }
@@ -193,6 +238,7 @@ class IntegrationManager {
193
238
  state,
194
239
  codeVerifier,
195
240
  userId,
241
+ agentId,
196
242
  appKey,
197
243
  });
198
244
 
@@ -264,6 +310,7 @@ class IntegrationManager {
264
310
 
265
311
  const result = await provider.finishOAuth({
266
312
  userId: stateRow.user_id,
313
+ agentId: stateRow.agent_id || resolveAgentId(stateRow.user_id, null),
267
314
  state: stateRow.state,
268
315
  code: normalizedCode,
269
316
  codeVerifier: decryptValue(stateRow.code_verifier),
@@ -277,11 +324,7 @@ class IntegrationManager {
277
324
  result.accountEmail,
278
325
  result.credentials,
279
326
  );
280
- if (!mergedCredentials.refresh_token) {
281
- throw new Error(
282
- `${provider.label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
283
- );
284
- }
327
+ assertDurableOAuthCredentials(provider, mergedCredentials);
285
328
 
286
329
  db.prepare(
287
330
  `INSERT INTO integration_connections (
@@ -389,7 +432,10 @@ class IntegrationManager {
389
432
  getToolDefinitions(userId, agentId = null) {
390
433
  const definitions = [];
391
434
  for (const provider of this.registry.list()) {
392
- const env = provider.getEnvStatus();
435
+ const env = provider.getEnvStatus({
436
+ userId,
437
+ agentId,
438
+ });
393
439
  if (!env.configured) continue;
394
440
  const connections = this.listConnections(userId, provider.key, agentId);
395
441
  const connectedAppIds = Array.from(
@@ -411,9 +457,15 @@ class IntegrationManager {
411
457
  if (!provider) {
412
458
  throw new Error(`Unknown integration provider: ${providerKey}`);
413
459
  }
414
- const env = provider.getEnvStatus();
460
+ const env = provider.getEnvStatus({
461
+ userId,
462
+ agentId,
463
+ });
415
464
  const connections = this.listConnections(userId, provider.key, agentId);
416
- const snapshot = provider.buildSnapshot(connections);
465
+ const snapshot = provider.buildSnapshot(connections, {
466
+ userId,
467
+ agentId,
468
+ });
417
469
  const connectedAppIds = snapshot.apps
418
470
  .filter((app) => app.connection.connected)
419
471
  .map((app) => app.id);
@@ -582,7 +634,10 @@ class IntegrationManager {
582
634
  for (const provider of this.registry.list()) {
583
635
  if (!provider.supportsTool(toolName)) continue;
584
636
  foundSupportingProvider = true;
585
- const env = provider.getEnvStatus();
637
+ const env = provider.getEnvStatus({
638
+ userId,
639
+ agentId,
640
+ });
586
641
  if (!env.configured) {
587
642
  return { error: env.summary };
588
643
  }
@@ -607,6 +662,17 @@ class IntegrationManager {
607
662
  selection.connection,
608
663
  );
609
664
  } catch (err) {
665
+ if (isLikelyExpiredConnectionError(err)) {
666
+ db.prepare(
667
+ `UPDATE integration_connections
668
+ SET status = 'expired', updated_at = datetime('now')
669
+ WHERE id = ? AND user_id = ? AND agent_id = ?`,
670
+ ).run(
671
+ selection.connection.id,
672
+ userId,
673
+ resolveAgentId(userId, agentId),
674
+ );
675
+ }
610
676
  return { error: err?.message || 'execution_error' };
611
677
  }
612
678
  if (!execution) {
@@ -641,7 +707,13 @@ class IntegrationManager {
641
707
  summarizeConnectedProviders(userId, agentId = null) {
642
708
  const providers = this.registry.list().map((provider) => ({
643
709
  provider,
644
- snapshot: provider.buildSnapshot(this.listConnections(userId, provider.key, agentId)),
710
+ snapshot: provider.buildSnapshot(
711
+ this.listConnections(userId, provider.key, agentId),
712
+ {
713
+ userId,
714
+ agentId,
715
+ },
716
+ ),
645
717
  }));
646
718
 
647
719
  if (providers.length === 0) {
@@ -655,6 +727,9 @@ class IntegrationManager {
655
727
  }
656
728
 
657
729
  if (!snapshot?.env?.configured) {
730
+ if (snapshot?.env?.setupMode === 'user') {
731
+ return `${provider.label}: setup is not complete for this user yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
732
+ }
658
733
  return `${provider.label}: available but not configured on the server yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
659
734
  }
660
735
 
@@ -669,5 +744,6 @@ class IntegrationManager {
669
744
  }
670
745
 
671
746
  module.exports = {
747
+ assertDurableOAuthCredentials,
672
748
  IntegrationManager,
673
749
  };
@@ -346,6 +346,7 @@ function createMicrosoftProvider() {
346
346
  description:
347
347
  'Official Microsoft 365 OAuth account connections for Outlook, Calendar, OneDrive, and Teams.',
348
348
  icon: 'microsoft',
349
+ requiresRefreshToken: true,
349
350
  apps: MICROSOFT_APPS,
350
351
  toolDefinitions: microsoftToolDefinitions,
351
352
  connectPrompt: