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