neoagent 2.3.1-beta.2 → 2.3.1-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +39 -0
- package/README.md +2 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +13 -5
- package/docs/integrations.md +4 -1
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +23057 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_integrations.dart +931 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3473 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +831 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/manager.js +231 -7
- package/package.json +3 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65262 -64422
- package/server/routes/integrations.js +86 -0
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +172 -0
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/home_assistant/provider.js +306 -26
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/trello/provider.js +744 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const ipaddr = require('ipaddr.js');
|
|
4
6
|
const {
|
|
5
7
|
describeEnvStatus,
|
|
6
8
|
resolveHomeAssistantOAuthConfig,
|
|
7
9
|
} = require('../env');
|
|
10
|
+
const {
|
|
11
|
+
deleteProviderConfig,
|
|
12
|
+
getProviderConfig,
|
|
13
|
+
setProviderConfig,
|
|
14
|
+
} = require('../provider_config_store');
|
|
8
15
|
const {
|
|
9
16
|
appendQuery,
|
|
10
17
|
createOAuthProvider,
|
|
@@ -93,6 +100,248 @@ const homeAssistantToolDefinitions = [
|
|
|
93
100
|
},
|
|
94
101
|
];
|
|
95
102
|
|
|
103
|
+
function trimText(value) {
|
|
104
|
+
return String(value || '').trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isTruthyEnv(name) {
|
|
108
|
+
const value = String(process.env[name] || '').trim().toLowerCase();
|
|
109
|
+
return value === '1' || value === 'true' || value === 'yes' || value === 'on';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isLikelyLocalHostname(hostname) {
|
|
113
|
+
const host = String(hostname || '').trim().toLowerCase();
|
|
114
|
+
if (!host) return false;
|
|
115
|
+
if (host === 'localhost' || host === 'host.docker.internal') return true;
|
|
116
|
+
if (host.endsWith('.localhost')) return true;
|
|
117
|
+
if (host.endsWith('.local') || host.endsWith('.lan') || host.endsWith('.internal')) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isPrivateIpv4(hostname) {
|
|
124
|
+
const parts = String(hostname || '').split('.').map((part) => Number(part));
|
|
125
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const [a, b] = parts;
|
|
129
|
+
if (a === 10) return true;
|
|
130
|
+
if (a === 127) return true;
|
|
131
|
+
if (a === 169 && b === 254) return true;
|
|
132
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
133
|
+
if (a === 192 && b === 168) return true;
|
|
134
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
135
|
+
if (a === 0) return true;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isPrivateIpv6(hostname) {
|
|
140
|
+
const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
|
|
141
|
+
if (!host) return false;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = ipaddr.parse(host);
|
|
144
|
+
if (parsed.kind() !== 'ipv6') return false;
|
|
145
|
+
|
|
146
|
+
// Normalize IPv4-mapped IPv6 literals and enforce the same local/private rules.
|
|
147
|
+
if (parsed.isIPv4MappedAddress()) {
|
|
148
|
+
return isPrivateIpv4(parsed.toIPv4Address().toString());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const range = parsed.range();
|
|
152
|
+
return range === 'loopback' || range === 'uniqueLocal' || range === 'linkLocal' || range === 'unspecified';
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isPrivateOrLocalIp(hostname) {
|
|
159
|
+
const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
|
|
160
|
+
const kind = net.isIP(host);
|
|
161
|
+
if (kind === 4) return isPrivateIpv4(host);
|
|
162
|
+
if (kind === 6) return isPrivateIpv6(host);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateHomeAssistantBaseUrlSafety(parsedUrl) {
|
|
167
|
+
const allowPrivate = isTruthyEnv('HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL');
|
|
168
|
+
const host = String(parsedUrl.hostname || '').trim();
|
|
169
|
+
const localHostname = isLikelyLocalHostname(host);
|
|
170
|
+
const localIp = isPrivateOrLocalIp(host);
|
|
171
|
+
const isLocalTarget = localHostname || localIp;
|
|
172
|
+
|
|
173
|
+
if (isLocalTarget && !allowPrivate) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
'Home Assistant base URL cannot target localhost/private network addresses unless HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1 is set on the server.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (parsedUrl.protocol === 'http:' && !isLocalTarget) {
|
|
180
|
+
throw new Error('Home Assistant base URL must use HTTPS for non-local hosts.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeBaseUrl(value) {
|
|
185
|
+
const text = trimText(value);
|
|
186
|
+
if (!text) return '';
|
|
187
|
+
return text.replace(/\/$/, '');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeOptionalAbsoluteUrl(value, label) {
|
|
191
|
+
const text = trimText(value);
|
|
192
|
+
if (!text) return '';
|
|
193
|
+
try {
|
|
194
|
+
const parsed = new URL(text);
|
|
195
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
196
|
+
throw new Error(`${label} must use http or https.`);
|
|
197
|
+
}
|
|
198
|
+
return parsed.toString().replace(/\/$/, '');
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error(`${label} must be a valid absolute URL.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeHomeAssistantBaseUrl(value) {
|
|
205
|
+
const text = trimText(value);
|
|
206
|
+
if (!text) return '';
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(text);
|
|
210
|
+
} catch {
|
|
211
|
+
throw new Error('Home Assistant base URL must be a valid absolute URL.');
|
|
212
|
+
}
|
|
213
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
214
|
+
throw new Error('Home Assistant base URL must use http or https.');
|
|
215
|
+
}
|
|
216
|
+
validateHomeAssistantBaseUrlSafety(parsed);
|
|
217
|
+
return parsed.toString().replace(/\/$/, '');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeUserHomeAssistantConfig(rawConfig) {
|
|
221
|
+
const source = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
222
|
+
return {
|
|
223
|
+
baseUrl: normalizeBaseUrl(source.baseUrl),
|
|
224
|
+
clientId: trimText(source.clientId),
|
|
225
|
+
clientSecret: trimText(source.clientSecret),
|
|
226
|
+
redirectUri: trimText(source.redirectUri),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveUserHomeAssistantConfig(userId) {
|
|
231
|
+
const userConfig = normalizeUserHomeAssistantConfig(
|
|
232
|
+
Number.isInteger(Number(userId)) && Number(userId) > 0
|
|
233
|
+
? getProviderConfig(Number(userId), 'home_assistant')
|
|
234
|
+
: {},
|
|
235
|
+
);
|
|
236
|
+
const envConfig = resolveHomeAssistantOAuthConfig();
|
|
237
|
+
return {
|
|
238
|
+
baseUrl: userConfig.baseUrl || envConfig.baseUrl,
|
|
239
|
+
clientId: userConfig.clientId || envConfig.clientId,
|
|
240
|
+
clientSecret: userConfig.clientSecret || envConfig.clientSecret,
|
|
241
|
+
redirectUri: userConfig.redirectUri || envConfig.redirectUri,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function validateResolvedConfig(config) {
|
|
246
|
+
const missing = [];
|
|
247
|
+
if (!trimText(config.baseUrl)) missing.push('baseUrl');
|
|
248
|
+
if (!trimText(config.clientId)) missing.push('clientId');
|
|
249
|
+
if (!trimText(config.clientSecret)) missing.push('clientSecret');
|
|
250
|
+
return {
|
|
251
|
+
configured: missing.length === 0,
|
|
252
|
+
missing,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resolveHomeAssistantConfigForUser(userId) {
|
|
257
|
+
const merged = resolveUserHomeAssistantConfig(userId);
|
|
258
|
+
const validatedBaseUrl = merged.baseUrl
|
|
259
|
+
? normalizeHomeAssistantBaseUrl(merged.baseUrl)
|
|
260
|
+
: '';
|
|
261
|
+
const validatedRedirectUri = merged.redirectUri
|
|
262
|
+
? normalizeOptionalAbsoluteUrl(
|
|
263
|
+
merged.redirectUri,
|
|
264
|
+
'Home Assistant OAuth redirect URI',
|
|
265
|
+
)
|
|
266
|
+
: '';
|
|
267
|
+
const result = {
|
|
268
|
+
baseUrl: validatedBaseUrl,
|
|
269
|
+
clientId: trimText(merged.clientId),
|
|
270
|
+
clientSecret: trimText(merged.clientSecret),
|
|
271
|
+
redirectUri: validatedRedirectUri,
|
|
272
|
+
};
|
|
273
|
+
const status = validateResolvedConfig(result);
|
|
274
|
+
return {
|
|
275
|
+
...result,
|
|
276
|
+
configured: status.configured,
|
|
277
|
+
missing: status.missing,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sanitizeHomeAssistantUserConfigForClient(rawConfig) {
|
|
282
|
+
const config = normalizeUserHomeAssistantConfig(rawConfig);
|
|
283
|
+
return {
|
|
284
|
+
baseUrl: config.baseUrl,
|
|
285
|
+
clientId: config.clientId,
|
|
286
|
+
redirectUri: config.redirectUri,
|
|
287
|
+
hasClientSecret: Boolean(config.clientSecret),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseHomeAssistantConfigInput(input, existingConfig = {}) {
|
|
292
|
+
const source = input && typeof input === 'object' ? input : {};
|
|
293
|
+
const baseUrl = normalizeHomeAssistantBaseUrl(source.baseUrl);
|
|
294
|
+
const clientId = trimText(source.clientId);
|
|
295
|
+
const clientSecret =
|
|
296
|
+
trimText(source.clientSecret) || trimText(existingConfig.clientSecret);
|
|
297
|
+
const redirectUri = source.redirectUri
|
|
298
|
+
? normalizeOptionalAbsoluteUrl(
|
|
299
|
+
source.redirectUri,
|
|
300
|
+
'Home Assistant OAuth redirect URI',
|
|
301
|
+
)
|
|
302
|
+
: '';
|
|
303
|
+
|
|
304
|
+
if (!baseUrl) {
|
|
305
|
+
throw new Error('Home Assistant base URL is required.');
|
|
306
|
+
}
|
|
307
|
+
if (!clientId) {
|
|
308
|
+
throw new Error('Home Assistant OAuth client ID is required.');
|
|
309
|
+
}
|
|
310
|
+
if (!clientSecret) {
|
|
311
|
+
throw new Error('Home Assistant OAuth client secret is required.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
baseUrl,
|
|
316
|
+
clientId,
|
|
317
|
+
clientSecret,
|
|
318
|
+
redirectUri,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function saveHomeAssistantUserConfig(userId, input) {
|
|
323
|
+
const normalizedUserId = Number(userId);
|
|
324
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
325
|
+
throw new Error('A valid user is required to save Home Assistant configuration.');
|
|
326
|
+
}
|
|
327
|
+
const existingConfig = normalizeUserHomeAssistantConfig(
|
|
328
|
+
getProviderConfig(normalizedUserId, 'home_assistant'),
|
|
329
|
+
);
|
|
330
|
+
const config = parseHomeAssistantConfigInput(input, existingConfig);
|
|
331
|
+
setProviderConfig(normalizedUserId, 'home_assistant', config);
|
|
332
|
+
return sanitizeHomeAssistantUserConfigForClient(config);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getHomeAssistantUserConfig(userId) {
|
|
336
|
+
const normalizedUserId = Number(userId);
|
|
337
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
338
|
+
return sanitizeHomeAssistantUserConfigForClient({});
|
|
339
|
+
}
|
|
340
|
+
return sanitizeHomeAssistantUserConfigForClient(
|
|
341
|
+
getProviderConfig(normalizedUserId, 'home_assistant'),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
96
345
|
function requireText(value, label) {
|
|
97
346
|
const text = String(value || '').trim();
|
|
98
347
|
if (!text) throw new Error(`${label} is required.`);
|
|
@@ -102,7 +351,7 @@ function requireText(value, label) {
|
|
|
102
351
|
function homeAssistantUrl(baseUrl, path, query) {
|
|
103
352
|
const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
|
|
104
353
|
if (!normalizedBase) {
|
|
105
|
-
throw new Error('
|
|
354
|
+
throw new Error('Home Assistant base URL is required.');
|
|
106
355
|
}
|
|
107
356
|
const url = new URL(
|
|
108
357
|
String(path || '').startsWith('http')
|
|
@@ -119,7 +368,7 @@ function homeAssistantUrl(baseUrl, path, query) {
|
|
|
119
368
|
}
|
|
120
369
|
|
|
121
370
|
async function homeAssistantRequest(credentials, options = {}) {
|
|
122
|
-
const config =
|
|
371
|
+
const config = resolveHomeAssistantConfigForUser(options.userId);
|
|
123
372
|
const accessToken = String(credentials?.access_token || '').trim();
|
|
124
373
|
if (!accessToken) {
|
|
125
374
|
throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
|
|
@@ -138,14 +387,20 @@ async function homeAssistantRequest(credentials, options = {}) {
|
|
|
138
387
|
);
|
|
139
388
|
}
|
|
140
389
|
|
|
141
|
-
async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
390
|
+
async function executeHomeAssistantTool(toolName, args, { connection, credentials }) {
|
|
142
391
|
switch (toolName) {
|
|
143
392
|
case 'home_assistant_get_config':
|
|
144
393
|
return {
|
|
145
|
-
result: await homeAssistantRequest(credentials, {
|
|
394
|
+
result: await homeAssistantRequest(credentials, {
|
|
395
|
+
path: '/api/config',
|
|
396
|
+
userId: connection?.user_id,
|
|
397
|
+
}),
|
|
146
398
|
};
|
|
147
399
|
case 'home_assistant_list_states': {
|
|
148
|
-
const states = await homeAssistantRequest(credentials, {
|
|
400
|
+
const states = await homeAssistantRequest(credentials, {
|
|
401
|
+
path: '/api/states',
|
|
402
|
+
userId: connection?.user_id,
|
|
403
|
+
});
|
|
149
404
|
const domain = String(args.domain || '').trim().toLowerCase();
|
|
150
405
|
const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
|
|
151
406
|
const filtered = Array.isArray(states)
|
|
@@ -163,6 +418,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
163
418
|
return {
|
|
164
419
|
result: await homeAssistantRequest(credentials, {
|
|
165
420
|
path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
|
|
421
|
+
userId: connection?.user_id,
|
|
166
422
|
}),
|
|
167
423
|
};
|
|
168
424
|
case 'home_assistant_call_service':
|
|
@@ -171,6 +427,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
171
427
|
method: 'POST',
|
|
172
428
|
path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
|
|
173
429
|
body: args.service_data || {},
|
|
430
|
+
userId: connection?.user_id,
|
|
174
431
|
}),
|
|
175
432
|
};
|
|
176
433
|
case 'home_assistant_api_request':
|
|
@@ -180,6 +437,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
180
437
|
path: requireText(args.path, 'path'),
|
|
181
438
|
query: args.query,
|
|
182
439
|
body: args.body,
|
|
440
|
+
userId: connection?.user_id,
|
|
183
441
|
}),
|
|
184
442
|
};
|
|
185
443
|
default:
|
|
@@ -187,19 +445,25 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
|
|
|
187
445
|
}
|
|
188
446
|
}
|
|
189
447
|
|
|
190
|
-
function resolveHomeAssistantEnvStatus() {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
448
|
+
function resolveHomeAssistantEnvStatus(userId) {
|
|
449
|
+
try {
|
|
450
|
+
const config = resolveHomeAssistantConfigForUser(userId);
|
|
451
|
+
return {
|
|
452
|
+
configured: config.configured,
|
|
453
|
+
missing: config.missing,
|
|
454
|
+
summary: config.configured
|
|
455
|
+
? 'Home Assistant is ready for account connections.'
|
|
456
|
+
: 'Complete your personal Home Assistant setup to connect an account.',
|
|
457
|
+
setupMode: 'user',
|
|
458
|
+
};
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
configured: false,
|
|
462
|
+
missing: ['baseUrl'],
|
|
463
|
+
summary: `Home Assistant setup is invalid: ${error?.message || 'unknown error'}`,
|
|
464
|
+
setupMode: 'user',
|
|
465
|
+
};
|
|
195
466
|
}
|
|
196
|
-
return describeEnvStatus(
|
|
197
|
-
{
|
|
198
|
-
configured: missing.length === 0,
|
|
199
|
-
missing,
|
|
200
|
-
},
|
|
201
|
-
{ label: 'Home Assistant' },
|
|
202
|
-
);
|
|
203
467
|
}
|
|
204
468
|
|
|
205
469
|
function normalizeCurrentUser(currentUser) {
|
|
@@ -225,8 +489,8 @@ function stableAccountEmailLikeIdentifier(user, config) {
|
|
|
225
489
|
return `homeassistant@${host}`;
|
|
226
490
|
}
|
|
227
491
|
|
|
228
|
-
async function fetchCurrentUser(token) {
|
|
229
|
-
const config =
|
|
492
|
+
async function fetchCurrentUser(token, userId) {
|
|
493
|
+
const config = resolveHomeAssistantConfigForUser(userId);
|
|
230
494
|
return fetchJson(
|
|
231
495
|
homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
|
|
232
496
|
{
|
|
@@ -250,11 +514,11 @@ function createHomeAssistantProvider() {
|
|
|
250
514
|
toolDefinitions: homeAssistantToolDefinitions,
|
|
251
515
|
connectPrompt:
|
|
252
516
|
'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
|
|
253
|
-
getEnvStatus() {
|
|
254
|
-
return resolveHomeAssistantEnvStatus();
|
|
517
|
+
getEnvStatus(context = {}) {
|
|
518
|
+
return resolveHomeAssistantEnvStatus(context.userId);
|
|
255
519
|
},
|
|
256
|
-
async beginOAuth({ state, codeVerifier, app }) {
|
|
257
|
-
const config =
|
|
520
|
+
async beginOAuth({ state, codeVerifier, app, userId }) {
|
|
521
|
+
const config = resolveHomeAssistantConfigForUser(userId);
|
|
258
522
|
const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
|
|
259
523
|
return {
|
|
260
524
|
url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
|
|
@@ -269,8 +533,8 @@ function createHomeAssistantProvider() {
|
|
|
269
533
|
appId: app.id,
|
|
270
534
|
};
|
|
271
535
|
},
|
|
272
|
-
async finishOAuth({ code, codeVerifier, app }) {
|
|
273
|
-
const config =
|
|
536
|
+
async finishOAuth({ code, codeVerifier, app, userId }) {
|
|
537
|
+
const config = resolveHomeAssistantConfigForUser(userId);
|
|
274
538
|
const token = await fetchJson(
|
|
275
539
|
homeAssistantUrl(config.baseUrl, '/auth/token'),
|
|
276
540
|
{
|
|
@@ -297,7 +561,7 @@ function createHomeAssistantProvider() {
|
|
|
297
561
|
throw new Error('Home Assistant OAuth did not return a refresh token.');
|
|
298
562
|
}
|
|
299
563
|
|
|
300
|
-
const currentUser = await fetchCurrentUser(accessToken);
|
|
564
|
+
const currentUser = await fetchCurrentUser(accessToken, userId);
|
|
301
565
|
const normalizedUser = normalizeCurrentUser(currentUser);
|
|
302
566
|
const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
|
|
303
567
|
|
|
@@ -328,6 +592,20 @@ function createHomeAssistantProvider() {
|
|
|
328
592
|
};
|
|
329
593
|
},
|
|
330
594
|
executeTool: executeHomeAssistantTool,
|
|
595
|
+
getUserConfig({ userId }) {
|
|
596
|
+
return getHomeAssistantUserConfig(userId);
|
|
597
|
+
},
|
|
598
|
+
saveUserConfig({ userId, config }) {
|
|
599
|
+
return saveHomeAssistantUserConfig(userId, config);
|
|
600
|
+
},
|
|
601
|
+
clearUserConfig({ userId }) {
|
|
602
|
+
const normalizedUserId = Number(userId);
|
|
603
|
+
if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
|
|
604
|
+
throw new Error('A valid user is required to clear Home Assistant configuration.');
|
|
605
|
+
}
|
|
606
|
+
deleteProviderConfig(normalizedUserId, 'home_assistant');
|
|
607
|
+
return { cleared: true };
|
|
608
|
+
},
|
|
331
609
|
});
|
|
332
610
|
}
|
|
333
611
|
|
|
@@ -346,5 +624,7 @@ function codeChallengeForVerifier(codeVerifier) {
|
|
|
346
624
|
}
|
|
347
625
|
|
|
348
626
|
module.exports = {
|
|
627
|
+
getHomeAssistantUserConfig,
|
|
628
|
+
saveHomeAssistantUserConfig,
|
|
349
629
|
createHomeAssistantProvider,
|
|
350
630
|
};
|
|
@@ -13,6 +13,22 @@ 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
|
+
|
|
16
32
|
class IntegrationManager {
|
|
17
33
|
constructor(options = {}) {
|
|
18
34
|
this.app = options.app || null;
|
|
@@ -147,7 +163,12 @@ class IntegrationManager {
|
|
|
147
163
|
|
|
148
164
|
return this.registry
|
|
149
165
|
.list()
|
|
150
|
-
.map((provider) =>
|
|
166
|
+
.map((provider) =>
|
|
167
|
+
provider.buildSnapshot(rowsByProvider.get(provider.key) || [], {
|
|
168
|
+
userId,
|
|
169
|
+
agentId: scopedAgentId,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
151
172
|
}
|
|
152
173
|
|
|
153
174
|
async beginOAuth(userId, providerKey, options = {}) {
|
|
@@ -163,7 +184,10 @@ class IntegrationManager {
|
|
|
163
184
|
throw new Error(`Unknown ${provider.label} app: ${appKey || 'missing app key'}`);
|
|
164
185
|
}
|
|
165
186
|
|
|
166
|
-
const env = provider.getEnvStatus(
|
|
187
|
+
const env = provider.getEnvStatus({
|
|
188
|
+
userId,
|
|
189
|
+
agentId,
|
|
190
|
+
});
|
|
167
191
|
if (!env.configured) {
|
|
168
192
|
throw new Error(env.summary);
|
|
169
193
|
}
|
|
@@ -389,7 +413,10 @@ class IntegrationManager {
|
|
|
389
413
|
getToolDefinitions(userId, agentId = null) {
|
|
390
414
|
const definitions = [];
|
|
391
415
|
for (const provider of this.registry.list()) {
|
|
392
|
-
const env = provider.getEnvStatus(
|
|
416
|
+
const env = provider.getEnvStatus({
|
|
417
|
+
userId,
|
|
418
|
+
agentId,
|
|
419
|
+
});
|
|
393
420
|
if (!env.configured) continue;
|
|
394
421
|
const connections = this.listConnections(userId, provider.key, agentId);
|
|
395
422
|
const connectedAppIds = Array.from(
|
|
@@ -411,9 +438,15 @@ class IntegrationManager {
|
|
|
411
438
|
if (!provider) {
|
|
412
439
|
throw new Error(`Unknown integration provider: ${providerKey}`);
|
|
413
440
|
}
|
|
414
|
-
const env = provider.getEnvStatus(
|
|
441
|
+
const env = provider.getEnvStatus({
|
|
442
|
+
userId,
|
|
443
|
+
agentId,
|
|
444
|
+
});
|
|
415
445
|
const connections = this.listConnections(userId, provider.key, agentId);
|
|
416
|
-
const snapshot = provider.buildSnapshot(connections
|
|
446
|
+
const snapshot = provider.buildSnapshot(connections, {
|
|
447
|
+
userId,
|
|
448
|
+
agentId,
|
|
449
|
+
});
|
|
417
450
|
const connectedAppIds = snapshot.apps
|
|
418
451
|
.filter((app) => app.connection.connected)
|
|
419
452
|
.map((app) => app.id);
|
|
@@ -582,7 +615,10 @@ class IntegrationManager {
|
|
|
582
615
|
for (const provider of this.registry.list()) {
|
|
583
616
|
if (!provider.supportsTool(toolName)) continue;
|
|
584
617
|
foundSupportingProvider = true;
|
|
585
|
-
const env = provider.getEnvStatus(
|
|
618
|
+
const env = provider.getEnvStatus({
|
|
619
|
+
userId,
|
|
620
|
+
agentId,
|
|
621
|
+
});
|
|
586
622
|
if (!env.configured) {
|
|
587
623
|
return { error: env.summary };
|
|
588
624
|
}
|
|
@@ -607,6 +643,17 @@ class IntegrationManager {
|
|
|
607
643
|
selection.connection,
|
|
608
644
|
);
|
|
609
645
|
} catch (err) {
|
|
646
|
+
if (isLikelyExpiredConnectionError(err)) {
|
|
647
|
+
db.prepare(
|
|
648
|
+
`UPDATE integration_connections
|
|
649
|
+
SET status = 'expired', updated_at = datetime('now')
|
|
650
|
+
WHERE id = ? AND user_id = ? AND agent_id = ?`,
|
|
651
|
+
).run(
|
|
652
|
+
selection.connection.id,
|
|
653
|
+
userId,
|
|
654
|
+
resolveAgentId(userId, agentId),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
610
657
|
return { error: err?.message || 'execution_error' };
|
|
611
658
|
}
|
|
612
659
|
if (!execution) {
|
|
@@ -641,7 +688,13 @@ class IntegrationManager {
|
|
|
641
688
|
summarizeConnectedProviders(userId, agentId = null) {
|
|
642
689
|
const providers = this.registry.list().map((provider) => ({
|
|
643
690
|
provider,
|
|
644
|
-
snapshot: provider.buildSnapshot(
|
|
691
|
+
snapshot: provider.buildSnapshot(
|
|
692
|
+
this.listConnections(userId, provider.key, agentId),
|
|
693
|
+
{
|
|
694
|
+
userId,
|
|
695
|
+
agentId,
|
|
696
|
+
},
|
|
697
|
+
),
|
|
645
698
|
}));
|
|
646
699
|
|
|
647
700
|
if (providers.length === 0) {
|
|
@@ -655,6 +708,9 @@ class IntegrationManager {
|
|
|
655
708
|
}
|
|
656
709
|
|
|
657
710
|
if (!snapshot?.env?.configured) {
|
|
711
|
+
if (snapshot?.env?.setupMode === 'user') {
|
|
712
|
+
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.`;
|
|
713
|
+
}
|
|
658
714
|
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
715
|
}
|
|
660
716
|
|
|
@@ -247,8 +247,8 @@ function createOAuthProvider(options = {}) {
|
|
|
247
247
|
getToolAppId(toolName) {
|
|
248
248
|
return toolAppMap.get(String(toolName || '').trim()) || null;
|
|
249
249
|
},
|
|
250
|
-
getEnvStatus() {
|
|
251
|
-
return options.getEnvStatus();
|
|
250
|
+
getEnvStatus(context = {}) {
|
|
251
|
+
return options.getEnvStatus(context);
|
|
252
252
|
},
|
|
253
253
|
getToolDefinitions(toolOptions = {}) {
|
|
254
254
|
const connectedAppIds = new Set(toolOptions.connectedAppIds || []);
|
|
@@ -257,8 +257,8 @@ function createOAuthProvider(options = {}) {
|
|
|
257
257
|
supportsTool(toolName) {
|
|
258
258
|
return toolAppMap.has(String(toolName || '').trim());
|
|
259
259
|
},
|
|
260
|
-
buildSnapshot(connectionRows) {
|
|
261
|
-
const env = this.getEnvStatus();
|
|
260
|
+
buildSnapshot(connectionRows, context = {}) {
|
|
261
|
+
const env = this.getEnvStatus(context);
|
|
262
262
|
const byApp = new Map();
|
|
263
263
|
for (const row of Array.isArray(connectionRows) ? connectionRows : []) {
|
|
264
264
|
const appId = String(row.app_key || '').trim();
|
|
@@ -319,7 +319,7 @@ function createOAuthProvider(options = {}) {
|
|
|
319
319
|
throw new Error(`Unknown ${this.label} app: ${appKey}`);
|
|
320
320
|
}
|
|
321
321
|
const normalizedState = assertValidOAuthState(state);
|
|
322
|
-
const env = this.getEnvStatus();
|
|
322
|
+
const env = this.getEnvStatus({ userId });
|
|
323
323
|
if (!env.configured) {
|
|
324
324
|
throw new Error(env.summary);
|
|
325
325
|
}
|
|
@@ -343,7 +343,7 @@ function createOAuthProvider(options = {}) {
|
|
|
343
343
|
userId,
|
|
344
344
|
state: normalizedState,
|
|
345
345
|
app,
|
|
346
|
-
env: this.getEnvStatus(),
|
|
346
|
+
env: this.getEnvStatus({ userId }),
|
|
347
347
|
});
|
|
348
348
|
},
|
|
349
349
|
async disconnect(connectionRow) {
|
|
@@ -368,12 +368,16 @@ function createOAuthProvider(options = {}) {
|
|
|
368
368
|
appId: toolAppMap.get(String(toolName || '').trim()) || connectionRow.app_key,
|
|
369
369
|
connection: connectionRow,
|
|
370
370
|
credentials,
|
|
371
|
+
env: this.getEnvStatus({ userId: connectionRow?.user_id }),
|
|
371
372
|
});
|
|
372
373
|
},
|
|
373
374
|
summarizeConnection(connectionRows) {
|
|
374
375
|
const snapshot = this.buildSnapshot(connectionRows);
|
|
375
376
|
if (!snapshot.connection.connected) {
|
|
376
377
|
if (snapshot.connection.status === 'env_not_configured') {
|
|
378
|
+
if (snapshot?.env?.setupMode === 'user') {
|
|
379
|
+
return `${this.label} still needs per-user setup before accounts can connect.`;
|
|
380
|
+
}
|
|
377
381
|
return `${this.label} still needs administrator setup before accounts can connect.`;
|
|
378
382
|
}
|
|
379
383
|
return `${this.label} is not connected.`;
|
|
@@ -392,6 +396,9 @@ function createOAuthProvider(options = {}) {
|
|
|
392
396
|
},
|
|
393
397
|
summarizeForModel(snapshot) {
|
|
394
398
|
if (!snapshot?.env?.configured) {
|
|
399
|
+
if (snapshot?.env?.setupMode === 'user') {
|
|
400
|
+
return `${this.label}: setup is user-managed and is not complete yet. If the user wants to use it, tell them to open Official Integrations and finish setup in their account first.`;
|
|
401
|
+
}
|
|
395
402
|
return `${this.label}: workspace setup is not complete yet. If the user wants to use it, tell them to open Official Integrations and ask an administrator to finish setup first.`;
|
|
396
403
|
}
|
|
397
404
|
|