neoagent 3.2.1-beta.1 → 3.2.1-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- 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/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -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 +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- 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/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- 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/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- 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/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- 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 +159 -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/registry.js +2 -0
- 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 +110 -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 +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -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 +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- 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 +13 -8
- 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/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- 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/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- 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/logger.js +19 -0
- package/server/utils/retry.js +107 -0
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
EXTENSION_PROTOCOL_VERSION,
|
|
3
|
+
MESSAGE_TYPES,
|
|
4
|
+
createBrowserProtocol,
|
|
5
|
+
} from './protocol.mjs';
|
|
2
6
|
import { DEFAULT_SERVER_URL } from './config.mjs';
|
|
7
|
+
import { fetchJsonWithTimeout } from './http.mjs';
|
|
3
8
|
|
|
4
9
|
const STORAGE_KEYS = ['serverUrl', 'configuredServerUrl', 'token', 'pairingId', 'pairingSecret', 'approvalUrl', 'status', 'extensionName'];
|
|
5
|
-
const protocol = createBrowserProtocol(chrome);
|
|
6
10
|
let socket = null;
|
|
7
11
|
let reconnectTimer = null;
|
|
8
|
-
let
|
|
9
|
-
|
|
12
|
+
let connectPromise = null;
|
|
13
|
+
let connectionEpoch = 0;
|
|
14
|
+
let reconnectAttempt = 0;
|
|
15
|
+
let statusUpdateQueue = Promise.resolve();
|
|
16
|
+
let commandQueue = Promise.resolve();
|
|
17
|
+
const activeCommandControllers = new Map();
|
|
18
|
+
const pendingUrlValidations = new Map();
|
|
10
19
|
const DEFAULT_WS_CONNECT_TIMEOUT_MS = 10000;
|
|
20
|
+
const DEFAULT_URL_VALIDATION_TIMEOUT_MS = 6500;
|
|
21
|
+
const MAX_PENDING_URL_VALIDATIONS = 256;
|
|
22
|
+
const RECONNECT_BASE_DELAY_MS = 1000;
|
|
23
|
+
const RECONNECT_MAX_DELAY_MS = 60 * 1000;
|
|
11
24
|
const KEEPALIVE_ALARM_NAME = 'neoagent-extension-keepalive';
|
|
12
25
|
const KEEPALIVE_ALARM_MINUTES = 1;
|
|
13
|
-
const
|
|
26
|
+
const protocol = createBrowserProtocol(chrome, {
|
|
27
|
+
validateUrl: requestUrlValidation,
|
|
28
|
+
});
|
|
14
29
|
|
|
15
30
|
function getStorage(keys = STORAGE_KEYS) {
|
|
16
31
|
return chrome.storage.local.get(keys);
|
|
@@ -25,7 +40,24 @@ function removeStorage(keys) {
|
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
function normalizeServerUrl(value) {
|
|
28
|
-
|
|
43
|
+
const raw = String(value || '').trim();
|
|
44
|
+
if (!raw) return '';
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error('NeoAgent server URL is invalid.');
|
|
50
|
+
}
|
|
51
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
52
|
+
throw new Error('NeoAgent server URL must use http or https.');
|
|
53
|
+
}
|
|
54
|
+
if (parsed.username || parsed.password) {
|
|
55
|
+
throw new Error('NeoAgent server URL must not contain credentials.');
|
|
56
|
+
}
|
|
57
|
+
parsed.search = '';
|
|
58
|
+
parsed.hash = '';
|
|
59
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, '');
|
|
60
|
+
return parsed.toString().replace(/\/+$/, '');
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
function configuredServerUrl() {
|
|
@@ -40,7 +72,7 @@ async function resolveServerUrl(preferred) {
|
|
|
40
72
|
}
|
|
41
73
|
|
|
42
74
|
function websocketUrl(serverUrl, token) {
|
|
43
|
-
const url = new URL('
|
|
75
|
+
const url = new URL('api/browser-extension/ws', `${serverUrl}/`);
|
|
44
76
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
45
77
|
// Token in the URL is required for the HTTP upgrade handshake; the browser
|
|
46
78
|
// WebSocket API does not support custom headers. Ensure the server's access
|
|
@@ -49,19 +81,6 @@ function websocketUrl(serverUrl, token) {
|
|
|
49
81
|
return url.toString();
|
|
50
82
|
}
|
|
51
83
|
|
|
52
|
-
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_FETCH_TIMEOUT_MS) {
|
|
53
|
-
const controller = new AbortController();
|
|
54
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
55
|
-
try {
|
|
56
|
-
return await fetch(url, {
|
|
57
|
-
...options,
|
|
58
|
-
signal: controller.signal,
|
|
59
|
-
});
|
|
60
|
-
} finally {
|
|
61
|
-
clearTimeout(timer);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
84
|
function compareVersions(a, b) {
|
|
66
85
|
const left = String(a || '0').split('.').map((part) => Number(part) || 0);
|
|
67
86
|
const right = String(b || '0').split('.').map((part) => Number(part) || 0);
|
|
@@ -73,9 +92,14 @@ function compareVersions(a, b) {
|
|
|
73
92
|
return 0;
|
|
74
93
|
}
|
|
75
94
|
|
|
76
|
-
async function updateStatus(status) {
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
async function updateStatus(status, expectedEpoch = null) {
|
|
96
|
+
const task = statusUpdateQueue.catch(() => {}).then(async () => {
|
|
97
|
+
if (expectedEpoch != null && expectedEpoch !== connectionEpoch) return;
|
|
98
|
+
await setStorage({ status });
|
|
99
|
+
await Promise.resolve(chrome.runtime.sendMessage({ type: 'status', status })).catch(() => {});
|
|
100
|
+
});
|
|
101
|
+
statusUpdateQueue = task.catch(() => {});
|
|
102
|
+
return task;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
function clearReconnectTimer() {
|
|
@@ -83,113 +107,298 @@ function clearReconnectTimer() {
|
|
|
83
107
|
reconnectTimer = null;
|
|
84
108
|
}
|
|
85
109
|
|
|
110
|
+
function scheduleReconnect() {
|
|
111
|
+
clearReconnectTimer();
|
|
112
|
+
const exponentialDelay = Math.min(
|
|
113
|
+
RECONNECT_MAX_DELAY_MS,
|
|
114
|
+
RECONNECT_BASE_DELAY_MS * (2 ** Math.min(reconnectAttempt, 6)),
|
|
115
|
+
);
|
|
116
|
+
reconnectAttempt += 1;
|
|
117
|
+
const jitterMs = Math.floor(exponentialDelay * Math.random() * 0.25);
|
|
118
|
+
reconnectTimer = setTimeout(() => connect().catch(() => {}), exponentialDelay + jitterMs);
|
|
119
|
+
}
|
|
120
|
+
|
|
86
121
|
function ensureKeepaliveAlarm() {
|
|
87
122
|
chrome.alarms?.create?.(KEEPALIVE_ALARM_NAME, {
|
|
88
123
|
periodInMinutes: KEEPALIVE_ALARM_MINUTES,
|
|
89
124
|
});
|
|
90
125
|
}
|
|
91
126
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
function rejectPendingUrlValidations(error, expectedSocket = null) {
|
|
128
|
+
for (const entry of Array.from(pendingUrlValidations.values())) {
|
|
129
|
+
if (!expectedSocket || entry.socket === expectedSocket) entry.reject(error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function requestUrlValidation(url, options = {}) {
|
|
134
|
+
const ws = socket;
|
|
135
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
136
|
+
return Promise.reject(new Error('NeoAgent browser security validation is unavailable.'));
|
|
137
|
+
}
|
|
138
|
+
if (pendingUrlValidations.size >= MAX_PENDING_URL_VALIDATIONS) {
|
|
139
|
+
return Promise.reject(new Error('Too many browser URL validations are pending.'));
|
|
95
140
|
}
|
|
141
|
+
|
|
142
|
+
const id = crypto.randomUUID();
|
|
143
|
+
const signal = options.signal || null;
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
let settled = false;
|
|
146
|
+
let timer = null;
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
signal?.removeEventListener('abort', onAbort);
|
|
150
|
+
pendingUrlValidations.delete(id);
|
|
151
|
+
};
|
|
152
|
+
const finish = (callback, value) => {
|
|
153
|
+
if (settled) return;
|
|
154
|
+
settled = true;
|
|
155
|
+
cleanup();
|
|
156
|
+
callback(value);
|
|
157
|
+
};
|
|
158
|
+
const onAbort = () => finish(
|
|
159
|
+
reject,
|
|
160
|
+
signal.reason instanceof Error
|
|
161
|
+
? signal.reason
|
|
162
|
+
: new Error('Browser URL validation was aborted.'),
|
|
163
|
+
);
|
|
164
|
+
const entry = {
|
|
165
|
+
socket: ws,
|
|
166
|
+
resolve: (allowed) => finish(resolve, allowed === true),
|
|
167
|
+
reject: (error) => finish(reject, error),
|
|
168
|
+
};
|
|
169
|
+
timer = setTimeout(() => {
|
|
170
|
+
entry.reject(new Error('Browser URL validation timed out.'));
|
|
171
|
+
}, DEFAULT_URL_VALIDATION_TIMEOUT_MS);
|
|
172
|
+
if (signal?.aborted) {
|
|
173
|
+
onAbort();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
177
|
+
pendingUrlValidations.set(id, entry);
|
|
178
|
+
if (!sendSocketMessage(ws, {
|
|
179
|
+
type: MESSAGE_TYPES.URL_VALIDATION_REQUEST,
|
|
180
|
+
version: EXTENSION_PROTOCOL_VERSION,
|
|
181
|
+
id,
|
|
182
|
+
url: String(url || ''),
|
|
183
|
+
})) {
|
|
184
|
+
entry.reject(new Error('Browser URL validation could not be sent.'));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleSocketDisconnected(ws, socketEpoch) {
|
|
190
|
+
if (socket !== ws || socketEpoch !== connectionEpoch) return;
|
|
96
191
|
socket = null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
const disconnectedEpoch = ++connectionEpoch;
|
|
193
|
+
try {
|
|
194
|
+
if (ws.readyState !== WebSocket.CLOSED) ws.close();
|
|
195
|
+
} catch {}
|
|
196
|
+
for (const controller of activeCommandControllers.values()) {
|
|
197
|
+
controller.abort(new Error('NeoAgent browser connection closed.'));
|
|
100
198
|
}
|
|
199
|
+
activeCommandControllers.clear();
|
|
200
|
+
rejectPendingUrlValidations(new Error('NeoAgent browser connection closed.'), ws);
|
|
101
201
|
const { token } = await getStorage(['token']);
|
|
202
|
+
if (connectionEpoch !== disconnectedEpoch || socket) return;
|
|
102
203
|
if (!token) {
|
|
103
|
-
await updateStatus('not_paired');
|
|
204
|
+
await updateStatus('not_paired', disconnectedEpoch);
|
|
104
205
|
return;
|
|
105
206
|
}
|
|
106
|
-
await updateStatus('disconnected');
|
|
107
|
-
|
|
108
|
-
reconnectTimer = setTimeout(() => connect().catch(() => {}), 5000);
|
|
207
|
+
await updateStatus('disconnected', disconnectedEpoch);
|
|
208
|
+
if (connectionEpoch === disconnectedEpoch && !socket) scheduleReconnect();
|
|
109
209
|
}
|
|
110
210
|
|
|
111
|
-
async function
|
|
112
|
-
const
|
|
113
|
-
const serverUrl = await
|
|
211
|
+
async function connectOnce() {
|
|
212
|
+
const startingEpoch = connectionEpoch;
|
|
213
|
+
const { token, serverUrl: storedServerUrl } = await getStorage(['token', 'serverUrl']);
|
|
214
|
+
if (startingEpoch !== connectionEpoch) return { connected: false, cancelled: true };
|
|
215
|
+
const serverUrl = normalizeServerUrl(storedServerUrl) || configuredServerUrl();
|
|
114
216
|
if (!serverUrl || !token) {
|
|
115
|
-
await updateStatus('not_paired');
|
|
217
|
+
await updateStatus('not_paired', startingEpoch);
|
|
116
218
|
return { connected: false };
|
|
117
219
|
}
|
|
118
220
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
119
221
|
return { connected: true };
|
|
120
222
|
}
|
|
121
|
-
if (socket) {
|
|
122
|
-
|
|
223
|
+
if (socket && socket.readyState === WebSocket.CONNECTING) {
|
|
224
|
+
return { connected: false, connecting: true };
|
|
225
|
+
}
|
|
226
|
+
const staleSocket = socket;
|
|
227
|
+
socket = null;
|
|
228
|
+
if (staleSocket) {
|
|
229
|
+
rejectPendingUrlValidations(new Error('NeoAgent browser connection was replaced.'), staleSocket);
|
|
230
|
+
try { staleSocket.close(); } catch {}
|
|
123
231
|
}
|
|
124
232
|
clearReconnectTimer();
|
|
125
|
-
suppressSocketClose = false;
|
|
126
233
|
|
|
127
|
-
const
|
|
234
|
+
const socketEpoch = ++connectionEpoch;
|
|
235
|
+
let ws;
|
|
236
|
+
try {
|
|
237
|
+
ws = new WebSocket(websocketUrl(serverUrl, token));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
await updateStatus('disconnected', socketEpoch);
|
|
240
|
+
scheduleReconnect();
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
128
243
|
socket = ws;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
244
|
+
const ready = new Promise((resolve, reject) => {
|
|
245
|
+
let settled = false;
|
|
246
|
+
const finish = (callback, value) => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
settled = true;
|
|
249
|
+
clearTimeout(connectTimeout);
|
|
250
|
+
callback(value);
|
|
251
|
+
};
|
|
252
|
+
const disconnect = (error, logLabel) => {
|
|
253
|
+
finish(reject, error);
|
|
254
|
+
handleSocketDisconnected(ws, socketEpoch).catch((disconnectError) => {
|
|
255
|
+
console.error(logLabel, disconnectError);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
const connectTimeout = setTimeout(() => {
|
|
259
|
+
disconnect(
|
|
260
|
+
new Error(`NeoAgent browser connection timed out after ${DEFAULT_WS_CONNECT_TIMEOUT_MS}ms.`),
|
|
261
|
+
'NeoAgent connection timeout handling failed',
|
|
262
|
+
);
|
|
263
|
+
}, DEFAULT_WS_CONNECT_TIMEOUT_MS);
|
|
135
264
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
265
|
+
ws.addEventListener('open', () => {
|
|
266
|
+
if (socket !== ws || socketEpoch !== connectionEpoch) {
|
|
267
|
+
try { ws.close(); } catch {}
|
|
268
|
+
finish(reject, new Error('NeoAgent browser connection was superseded.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
reconnectAttempt = 0;
|
|
272
|
+
updateStatus('connected', socketEpoch).catch((error) => {
|
|
273
|
+
console.error('NeoAgent connected status update failed', error);
|
|
274
|
+
});
|
|
275
|
+
finish(resolve, { connected: true });
|
|
145
276
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
277
|
+
ws.addEventListener('close', () => {
|
|
278
|
+
disconnect(
|
|
279
|
+
new Error('NeoAgent browser connection closed before it was ready.'),
|
|
280
|
+
'NeoAgent disconnect handling failed',
|
|
281
|
+
);
|
|
151
282
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
283
|
+
ws.addEventListener('error', () => {
|
|
284
|
+
disconnect(
|
|
285
|
+
new Error('NeoAgent browser connection failed.'),
|
|
286
|
+
'NeoAgent socket error handling failed',
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
ws.addEventListener('message', (event) => {
|
|
290
|
+
handleSocketMessage(ws, event.data).catch((error) => {
|
|
291
|
+
console.error('NeoAgent command handling failed', error);
|
|
292
|
+
});
|
|
156
293
|
});
|
|
157
294
|
});
|
|
295
|
+
// The socket can fail while the storage status write is still pending. Attach
|
|
296
|
+
// a handler immediately; returning `ready` below still preserves rejection.
|
|
297
|
+
ready.catch(() => {});
|
|
158
298
|
|
|
159
|
-
|
|
299
|
+
await updateStatus('connecting', socketEpoch).catch((error) => {
|
|
300
|
+
console.error('NeoAgent connecting status update failed', error);
|
|
301
|
+
});
|
|
302
|
+
return ready;
|
|
160
303
|
}
|
|
161
304
|
|
|
162
|
-
|
|
305
|
+
function connect() {
|
|
306
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
307
|
+
return Promise.resolve({ connected: true });
|
|
308
|
+
}
|
|
309
|
+
if (connectPromise) return connectPromise;
|
|
310
|
+
const pending = connectOnce();
|
|
311
|
+
connectPromise = pending;
|
|
312
|
+
pending.then(
|
|
313
|
+
() => { if (connectPromise === pending) connectPromise = null; },
|
|
314
|
+
() => { if (connectPromise === pending) connectPromise = null; },
|
|
315
|
+
);
|
|
316
|
+
return pending;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sendSocketMessage(ws, message) {
|
|
320
|
+
if (socket !== ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
321
|
+
try {
|
|
322
|
+
ws.send(JSON.stringify(message));
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function handleSocketMessage(ws, raw) {
|
|
163
330
|
let message;
|
|
164
331
|
try {
|
|
165
332
|
message = JSON.parse(raw);
|
|
166
333
|
} catch {
|
|
167
334
|
return;
|
|
168
335
|
}
|
|
169
|
-
if (!message ||
|
|
336
|
+
if (!message || !message.id) {
|
|
170
337
|
return;
|
|
171
338
|
}
|
|
339
|
+
if (message.type === MESSAGE_TYPES.URL_VALIDATION_RESULT) {
|
|
340
|
+
const pending = pendingUrlValidations.get(String(message.id));
|
|
341
|
+
if (!pending || pending.socket !== ws) return;
|
|
342
|
+
pending.resolve(
|
|
343
|
+
Number(message.version) === EXTENSION_PROTOCOL_VERSION
|
|
344
|
+
&& message.allowed === true,
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (message.type !== MESSAGE_TYPES.COMMAND) return;
|
|
172
349
|
if (message.version != null && Number(message.version) !== EXTENSION_PROTOCOL_VERSION) {
|
|
173
|
-
|
|
174
|
-
type:
|
|
350
|
+
sendSocketMessage(ws, {
|
|
351
|
+
type: MESSAGE_TYPES.RESULT,
|
|
175
352
|
version: EXTENSION_PROTOCOL_VERSION,
|
|
176
353
|
id: message.id,
|
|
177
354
|
ok: false,
|
|
178
355
|
error: `Unsupported protocol version: ${message.version}`,
|
|
179
|
-
})
|
|
356
|
+
});
|
|
180
357
|
return;
|
|
181
358
|
}
|
|
359
|
+
|
|
360
|
+
if (message.command === 'cancelCommand') {
|
|
361
|
+
const commandId = String(message.payload?.commandId || '');
|
|
362
|
+
const controller = activeCommandControllers.get(commandId);
|
|
363
|
+
controller?.abort(new Error('Browser command cancelled by NeoAgent.'));
|
|
364
|
+
sendSocketMessage(ws, {
|
|
365
|
+
type: MESSAGE_TYPES.RESULT,
|
|
366
|
+
version: EXTENSION_PROTOCOL_VERSION,
|
|
367
|
+
id: message.id,
|
|
368
|
+
ok: true,
|
|
369
|
+
result: { success: Boolean(controller), commandId },
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
activeCommandControllers.set(message.id, controller);
|
|
376
|
+
const execute = () => protocol.run(message.command, message.payload || {}, {
|
|
377
|
+
signal: controller.signal,
|
|
378
|
+
});
|
|
379
|
+
const queued = commandQueue.then(execute, execute);
|
|
380
|
+
commandQueue = queued.catch(() => {});
|
|
182
381
|
try {
|
|
183
|
-
const result = await
|
|
184
|
-
|
|
382
|
+
const result = await queued;
|
|
383
|
+
sendSocketMessage(ws, {
|
|
384
|
+
type: MESSAGE_TYPES.RESULT,
|
|
385
|
+
version: EXTENSION_PROTOCOL_VERSION,
|
|
386
|
+
id: message.id,
|
|
387
|
+
ok: true,
|
|
388
|
+
result,
|
|
389
|
+
});
|
|
185
390
|
} catch (error) {
|
|
186
|
-
|
|
187
|
-
type:
|
|
391
|
+
sendSocketMessage(ws, {
|
|
392
|
+
type: MESSAGE_TYPES.RESULT,
|
|
188
393
|
version: EXTENSION_PROTOCOL_VERSION,
|
|
189
394
|
id: message.id,
|
|
190
395
|
ok: false,
|
|
191
396
|
error: error?.message || String(error),
|
|
192
|
-
})
|
|
397
|
+
});
|
|
398
|
+
} finally {
|
|
399
|
+
if (activeCommandControllers.get(message.id) === controller) {
|
|
400
|
+
activeCommandControllers.delete(message.id);
|
|
401
|
+
}
|
|
193
402
|
}
|
|
194
403
|
}
|
|
195
404
|
|
|
@@ -198,16 +407,25 @@ async function startPairing(serverUrl) {
|
|
|
198
407
|
if (!normalized) throw new Error('NeoAgent server URL required.');
|
|
199
408
|
const { extensionName } = await getStorage(['extensionName']);
|
|
200
409
|
const nameToUse = String(extensionName || 'Chrome Extension').trim() || 'Chrome Extension';
|
|
201
|
-
const response = await
|
|
410
|
+
const { response, payload } = await fetchJsonWithTimeout(`${normalized}/api/browser-extension/pairing/request`, {
|
|
202
411
|
method: 'POST',
|
|
203
412
|
headers: { 'content-type': 'application/json' },
|
|
204
413
|
body: JSON.stringify({ extensionName: nameToUse }),
|
|
205
414
|
});
|
|
206
|
-
const payload = await response.json().catch(() => ({}));
|
|
207
415
|
if (!response.ok) throw new Error(payload.error || `Pairing failed: ${response.status}`);
|
|
416
|
+
if (!payload.pairingId || !payload.pairingSecret) {
|
|
417
|
+
throw new Error('NeoAgent server returned an incomplete pairing response.');
|
|
418
|
+
}
|
|
208
419
|
const approvalUrl = String(payload.approvalUrl || '');
|
|
209
420
|
const approvalParsed = (() => { try { return new URL(approvalUrl); } catch { return null; } })();
|
|
210
|
-
|
|
421
|
+
const serverParsed = new URL(normalized);
|
|
422
|
+
if (
|
|
423
|
+
!approvalParsed
|
|
424
|
+
|| !['http:', 'https:'].includes(approvalParsed.protocol)
|
|
425
|
+
|| approvalParsed.origin !== serverParsed.origin
|
|
426
|
+
|| approvalParsed.username
|
|
427
|
+
|| approvalParsed.password
|
|
428
|
+
) {
|
|
211
429
|
throw new Error('Invalid approval URL returned by server.');
|
|
212
430
|
}
|
|
213
431
|
await setStorage({
|
|
@@ -226,14 +444,17 @@ async function claimPairing() {
|
|
|
226
444
|
if (!serverUrl || !pairingId || !pairingSecret) {
|
|
227
445
|
throw new Error('No pending pairing request.');
|
|
228
446
|
}
|
|
447
|
+
const normalizedServerUrl = normalizeServerUrl(serverUrl);
|
|
229
448
|
const nameToUse = String(extensionName || 'Chrome Extension').trim() || 'Chrome Extension';
|
|
230
|
-
const response = await
|
|
449
|
+
const { response, payload } = await fetchJsonWithTimeout(`${normalizedServerUrl}/api/browser-extension/pairing/${encodeURIComponent(pairingId)}/claim`, {
|
|
231
450
|
method: 'POST',
|
|
232
451
|
headers: { 'content-type': 'application/json' },
|
|
233
452
|
body: JSON.stringify({ pairingSecret, extensionName: nameToUse }),
|
|
234
453
|
});
|
|
235
|
-
const payload = await response.json().catch(() => ({}));
|
|
236
454
|
if (!response.ok) throw new Error(payload.error || `Claim failed: ${response.status}`);
|
|
455
|
+
if (!payload.token || !payload.tokenId) {
|
|
456
|
+
throw new Error('NeoAgent server returned an incomplete pairing claim.');
|
|
457
|
+
}
|
|
237
458
|
await setStorage({
|
|
238
459
|
token: payload.token,
|
|
239
460
|
tokenId: payload.tokenId,
|
|
@@ -246,20 +467,29 @@ async function claimPairing() {
|
|
|
246
467
|
|
|
247
468
|
async function disconnect() {
|
|
248
469
|
clearReconnectTimer();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
470
|
+
reconnectAttempt = 0;
|
|
471
|
+
const disconnectedEpoch = ++connectionEpoch;
|
|
472
|
+
connectPromise = null;
|
|
473
|
+
const previousSocket = socket;
|
|
253
474
|
socket = null;
|
|
475
|
+
if (previousSocket) {
|
|
476
|
+
try { previousSocket.close(); } catch {}
|
|
477
|
+
}
|
|
478
|
+
for (const controller of activeCommandControllers.values()) {
|
|
479
|
+
controller.abort(new Error('NeoAgent browser connection was disconnected.'));
|
|
480
|
+
}
|
|
481
|
+
activeCommandControllers.clear();
|
|
482
|
+
rejectPendingUrlValidations(new Error('NeoAgent browser connection was disconnected.'));
|
|
254
483
|
await removeStorage(['token', 'tokenId', 'pairingId', 'pairingSecret', 'approvalUrl']);
|
|
255
|
-
await updateStatus('not_paired');
|
|
484
|
+
await updateStatus('not_paired', disconnectedEpoch);
|
|
256
485
|
}
|
|
257
486
|
|
|
258
487
|
async function checkForUpdates(preferredServerUrl) {
|
|
259
488
|
const serverUrl = await resolveServerUrl(preferredServerUrl);
|
|
260
489
|
if (!serverUrl) throw new Error('NeoAgent server URL required.');
|
|
261
|
-
const response = await
|
|
262
|
-
|
|
490
|
+
const { response, payload: latest } = await fetchJsonWithTimeout(
|
|
491
|
+
`${serverUrl}/api/browser-extension/latest`,
|
|
492
|
+
);
|
|
263
493
|
if (!response.ok) throw new Error(latest.error || `Update check failed: ${response.status}`);
|
|
264
494
|
const manifest = chrome.runtime.getManifest();
|
|
265
495
|
const currentVersion = manifest.version;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const DEFAULT_EXTENSION_HTTP_TIMEOUT_MS = 10000;
|
|
2
|
+
export const DEFAULT_MAX_SERVER_RESPONSE_BYTES = 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
function abortError(signal, fallback = 'NeoAgent server request was aborted.') {
|
|
5
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
6
|
+
const error = new Error(String(signal?.reason || fallback));
|
|
7
|
+
error.name = 'AbortError';
|
|
8
|
+
error.code = 'ABORT_ERR';
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function throwIfAborted(signal) {
|
|
13
|
+
if (signal?.aborted) throw abortError(signal);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function responseTooLargeError(maxResponseBytes) {
|
|
17
|
+
const error = new Error(
|
|
18
|
+
`NeoAgent server response exceeded the ${maxResponseBytes}-byte safety limit.`,
|
|
19
|
+
);
|
|
20
|
+
error.code = 'EXTENSION_RESPONSE_TOO_LARGE';
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function waitForAbortable(promise, signal) {
|
|
25
|
+
throwIfAborted(signal);
|
|
26
|
+
if (!signal) return Promise.resolve(promise);
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const onAbort = () => reject(abortError(signal));
|
|
29
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => {
|
|
31
|
+
signal.removeEventListener('abort', onAbort);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readJsonResponse(response, options = {}) {
|
|
37
|
+
const signal = options.signal || null;
|
|
38
|
+
const maxResponseBytes = Math.max(
|
|
39
|
+
1,
|
|
40
|
+
Number(options.maxResponseBytes) || DEFAULT_MAX_SERVER_RESPONSE_BYTES,
|
|
41
|
+
);
|
|
42
|
+
throwIfAborted(signal);
|
|
43
|
+
const contentLength = Number(response?.headers?.get?.('content-length'));
|
|
44
|
+
if (Number.isFinite(contentLength) && contentLength > maxResponseBytes) {
|
|
45
|
+
await Promise.resolve(response?.body?.cancel?.()).catch(() => {});
|
|
46
|
+
throw responseTooLargeError(maxResponseBytes);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const reader = response?.body?.getReader?.();
|
|
50
|
+
let text = '';
|
|
51
|
+
if (!reader) {
|
|
52
|
+
text = await waitForAbortable(response.text(), signal);
|
|
53
|
+
if (new TextEncoder().encode(text).byteLength > maxResponseBytes) {
|
|
54
|
+
throw responseTooLargeError(maxResponseBytes);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const decoder = new TextDecoder();
|
|
58
|
+
let totalBytes = 0;
|
|
59
|
+
const cancelReader = () => {
|
|
60
|
+
try {
|
|
61
|
+
Promise.resolve(reader.cancel(signal?.reason)).catch(() => {});
|
|
62
|
+
} catch {}
|
|
63
|
+
};
|
|
64
|
+
signal?.addEventListener('abort', cancelReader, { once: true });
|
|
65
|
+
try {
|
|
66
|
+
while (true) {
|
|
67
|
+
const { done, value } = await waitForAbortable(reader.read(), signal);
|
|
68
|
+
if (done) break;
|
|
69
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value || []);
|
|
70
|
+
totalBytes += chunk.byteLength;
|
|
71
|
+
if (totalBytes > maxResponseBytes) {
|
|
72
|
+
await Promise.resolve(reader.cancel()).catch(() => {});
|
|
73
|
+
throw responseTooLargeError(maxResponseBytes);
|
|
74
|
+
}
|
|
75
|
+
text += decoder.decode(chunk, { stream: true });
|
|
76
|
+
}
|
|
77
|
+
text += decoder.decode();
|
|
78
|
+
} finally {
|
|
79
|
+
signal?.removeEventListener('abort', cancelReader);
|
|
80
|
+
reader.releaseLock?.();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!text.trim()) return {};
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(text);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error('NeoAgent server returned invalid JSON.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function fetchJsonWithTimeout(url, options = {}, config = {}) {
|
|
93
|
+
const timeoutMs = Math.max(
|
|
94
|
+
1,
|
|
95
|
+
Number(config.timeoutMs) || DEFAULT_EXTENSION_HTTP_TIMEOUT_MS,
|
|
96
|
+
);
|
|
97
|
+
const maxResponseBytes = Math.max(
|
|
98
|
+
1,
|
|
99
|
+
Number(config.maxResponseBytes) || DEFAULT_MAX_SERVER_RESPONSE_BYTES,
|
|
100
|
+
);
|
|
101
|
+
const fetchImpl = typeof config.fetchImpl === 'function' ? config.fetchImpl : fetch;
|
|
102
|
+
const callerSignal = options.signal || null;
|
|
103
|
+
throwIfAborted(callerSignal);
|
|
104
|
+
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const onCallerAbort = () => controller.abort(callerSignal.reason);
|
|
107
|
+
callerSignal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
const error = new Error(`NeoAgent server request timed out after ${timeoutMs}ms.`);
|
|
110
|
+
error.code = 'EXTENSION_HTTP_TIMEOUT';
|
|
111
|
+
controller.abort(error);
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
|
|
114
|
+
const { signal: _signal, ...fetchOptions } = options;
|
|
115
|
+
try {
|
|
116
|
+
const response = await waitForAbortable(
|
|
117
|
+
fetchImpl(url, {
|
|
118
|
+
...fetchOptions,
|
|
119
|
+
signal: controller.signal,
|
|
120
|
+
}),
|
|
121
|
+
controller.signal,
|
|
122
|
+
);
|
|
123
|
+
const payload = await readJsonResponse(response, {
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
maxResponseBytes,
|
|
126
|
+
});
|
|
127
|
+
return { response, payload };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (callerSignal?.aborted) throw abortError(callerSignal);
|
|
130
|
+
if (controller.signal.aborted) throw abortError(controller.signal);
|
|
131
|
+
throw error;
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
callerSignal?.removeEventListener('abort', onCallerAbort);
|
|
135
|
+
}
|
|
136
|
+
}
|