neoagent 3.2.1-beta.0 → 3.2.1-beta.10
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/docs/agent-run-lifecycle.md +10 -0
- 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 +111 -20
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +78 -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 +145 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +4 -4
- 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 +11 -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 +73083 -72209
- package/server/routes/admin.js +1 -1
- package/server/routes/agents.js +35 -2
- 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 +107 -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/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 +496 -166
- 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 +121 -5
- package/server/services/ai/loop/conversation_loop.js +620 -340
- package/server/services/ai/loop/lifecycle.js +108 -0
- 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 +48 -56
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +28 -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 +4 -4
- 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 +6 -5
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +12 -7
- package/server/services/ai/providers/ollama.js +114 -86
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +41 -7
- package/server/services/ai/providers/openaiCodex.js +13 -4
- package/server/services/ai/providers/openrouter.js +31 -9
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +19 -12
- package/server/services/ai/taskAnalysis.js +58 -10
- package/server/services/ai/terminal_reply.js +18 -0
- package/server/services/ai/toolEvidence.js +350 -29
- package/server/services/ai/tools.js +190 -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 +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 +52 -19
- package/server/services/messaging/automation.js +85 -10
- package/server/services/messaging/formatting_guides.js +7 -4
- 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 +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 +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/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/retry.js +107 -0
|
@@ -7,6 +7,22 @@ const FORCE_KILL_GRACE_MS = 5000;
|
|
|
7
7
|
const MAX_STDOUT_CHARS = 50000;
|
|
8
8
|
const MAX_STDERR_CHARS = 10000;
|
|
9
9
|
|
|
10
|
+
function abortedResult(command, cwd, startedAt = Date.now()) {
|
|
11
|
+
return {
|
|
12
|
+
exitCode: null,
|
|
13
|
+
stdout: '',
|
|
14
|
+
stderr: 'Command aborted before it started.',
|
|
15
|
+
killed: true,
|
|
16
|
+
timedOut: false,
|
|
17
|
+
aborted: true,
|
|
18
|
+
signal: null,
|
|
19
|
+
durationMs: Date.now() - startedAt,
|
|
20
|
+
pid: null,
|
|
21
|
+
command,
|
|
22
|
+
cwd,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
function resolveDefaultShell() {
|
|
11
27
|
const candidates = [
|
|
12
28
|
process.env.SHELL,
|
|
@@ -63,6 +79,22 @@ function terminateProcess(proc, signal = 'SIGTERM') {
|
|
|
63
79
|
proc.kill?.(signal) || proc.kill?.();
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
function terminateWithEscalation(proc, isActive) {
|
|
83
|
+
terminateProcess(proc, 'SIGTERM');
|
|
84
|
+
if (proc.__neoagentForceKillTimer) return;
|
|
85
|
+
proc.__neoagentForceKillTimer = setTimeout(() => {
|
|
86
|
+
if (isActive()) terminateProcess(proc, 'SIGKILL');
|
|
87
|
+
}, FORCE_KILL_GRACE_MS);
|
|
88
|
+
proc.__neoagentForceKillTimer.unref?.();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clearForceKill(proc) {
|
|
92
|
+
if (proc?.__neoagentForceKillTimer) {
|
|
93
|
+
clearTimeout(proc.__neoagentForceKillTimer);
|
|
94
|
+
proc.__neoagentForceKillTimer = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function shellSupportsPipefail(shellPath) {
|
|
67
99
|
const normalized = String(shellPath || '').trim().toLowerCase();
|
|
68
100
|
return /(?:^|\/)(?:bash|zsh|ksh|mksh|yash)$/.test(normalized);
|
|
@@ -111,6 +143,7 @@ class CLIExecutor {
|
|
|
111
143
|
const cwd = options.cwd || process.env.HOME;
|
|
112
144
|
const timeout = clampTimeout(options.timeout, DEFAULT_TIMEOUT_MS);
|
|
113
145
|
const stdinInput = options.stdinInput;
|
|
146
|
+
if (options.signal?.aborted) return abortedResult(command, cwd);
|
|
114
147
|
|
|
115
148
|
return new Promise((resolve) => {
|
|
116
149
|
let stdout = '';
|
|
@@ -131,6 +164,13 @@ class CLIExecutor {
|
|
|
131
164
|
const pid = proc.pid;
|
|
132
165
|
this.activeProcesses.set(pid, proc);
|
|
133
166
|
options.onSpawn?.(pid);
|
|
167
|
+
const onAbort = () => {
|
|
168
|
+
killed = true;
|
|
169
|
+
proc.__neoagentKilled = true;
|
|
170
|
+
proc.__neoagentKillReason = 'aborted';
|
|
171
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
172
|
+
};
|
|
173
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
134
174
|
|
|
135
175
|
proc.stdout.on('data', (data) => {
|
|
136
176
|
stdout += data.toString();
|
|
@@ -156,14 +196,14 @@ class CLIExecutor {
|
|
|
156
196
|
timedOut = true;
|
|
157
197
|
proc.__neoagentKilled = true;
|
|
158
198
|
proc.__neoagentKillReason = 'timeout';
|
|
159
|
-
|
|
160
|
-
setTimeout(() => {
|
|
161
|
-
if (!proc.killed) terminateProcess(proc, 'SIGKILL');
|
|
162
|
-
}, FORCE_KILL_GRACE_MS);
|
|
199
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
163
200
|
}, timeout);
|
|
201
|
+
timer.unref?.();
|
|
164
202
|
|
|
165
203
|
proc.on('close', (code, signal) => {
|
|
166
204
|
clearTimeout(timer);
|
|
205
|
+
clearForceKill(proc);
|
|
206
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
167
207
|
this.activeProcesses.delete(pid);
|
|
168
208
|
const durationMs = Date.now() - startedAt;
|
|
169
209
|
|
|
@@ -173,6 +213,7 @@ class CLIExecutor {
|
|
|
173
213
|
stderr: truncateOutput(stderr.trim(), MAX_STDERR_CHARS),
|
|
174
214
|
killed: killed || proc.__neoagentKilled === true,
|
|
175
215
|
timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
|
|
216
|
+
aborted: proc.__neoagentKillReason === 'aborted',
|
|
176
217
|
signal: signal || null,
|
|
177
218
|
durationMs,
|
|
178
219
|
pid,
|
|
@@ -183,6 +224,8 @@ class CLIExecutor {
|
|
|
183
224
|
|
|
184
225
|
proc.on('error', (err) => {
|
|
185
226
|
clearTimeout(timer);
|
|
227
|
+
clearForceKill(proc);
|
|
228
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
186
229
|
this.activeProcesses.delete(pid);
|
|
187
230
|
resolve({
|
|
188
231
|
exitCode: -1,
|
|
@@ -204,6 +247,7 @@ class CLIExecutor {
|
|
|
204
247
|
async executeInteractive(command, inputs = [], options = {}) {
|
|
205
248
|
const cwd = options.cwd || process.env.HOME;
|
|
206
249
|
const timeout = clampTimeout(options.timeout, DEFAULT_INTERACTIVE_TIMEOUT_MS);
|
|
250
|
+
if (options.signal?.aborted) return abortedResult(command, cwd);
|
|
207
251
|
|
|
208
252
|
return new Promise((resolve) => {
|
|
209
253
|
let output = '';
|
|
@@ -231,6 +275,13 @@ class CLIExecutor {
|
|
|
231
275
|
const pid = proc.pid;
|
|
232
276
|
this.activeProcesses.set(pid, proc);
|
|
233
277
|
options.onSpawn?.(pid);
|
|
278
|
+
const onAbort = () => {
|
|
279
|
+
killed = true;
|
|
280
|
+
proc.__neoagentKilled = true;
|
|
281
|
+
proc.__neoagentKillReason = 'aborted';
|
|
282
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
283
|
+
};
|
|
284
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
234
285
|
|
|
235
286
|
proc.onData((data) => {
|
|
236
287
|
output += data;
|
|
@@ -256,11 +307,14 @@ class CLIExecutor {
|
|
|
256
307
|
timedOut = true;
|
|
257
308
|
proc.__neoagentKilled = true;
|
|
258
309
|
proc.__neoagentKillReason = 'timeout';
|
|
259
|
-
proc.
|
|
310
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
260
311
|
}, timeout);
|
|
312
|
+
timer.unref?.();
|
|
261
313
|
|
|
262
314
|
proc.onExit(({ exitCode, signal }) => {
|
|
263
315
|
clearTimeout(timer);
|
|
316
|
+
clearForceKill(proc);
|
|
317
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
264
318
|
this.activeProcesses.delete(pid);
|
|
265
319
|
|
|
266
320
|
const cleanOutput = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').trim();
|
|
@@ -270,6 +324,7 @@ class CLIExecutor {
|
|
|
270
324
|
stderr: '',
|
|
271
325
|
killed: killed || proc.__neoagentKilled === true,
|
|
272
326
|
timedOut: timedOut || proc.__neoagentKillReason === 'timeout',
|
|
327
|
+
aborted: proc.__neoagentKillReason === 'aborted',
|
|
273
328
|
signal: typeof signal === 'number' ? String(signal) : signal || null,
|
|
274
329
|
durationMs: Date.now() - startedAt,
|
|
275
330
|
pid,
|
|
@@ -286,8 +341,7 @@ class CLIExecutor {
|
|
|
286
341
|
if (proc) {
|
|
287
342
|
proc.__neoagentKilled = true;
|
|
288
343
|
proc.__neoagentKillReason = reason;
|
|
289
|
-
|
|
290
|
-
this.activeProcesses.delete(pid);
|
|
344
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
291
345
|
return true;
|
|
292
346
|
}
|
|
293
347
|
return false;
|
|
@@ -301,9 +355,8 @@ class CLIExecutor {
|
|
|
301
355
|
for (const [pid, proc] of this.activeProcesses) {
|
|
302
356
|
proc.__neoagentKilled = true;
|
|
303
357
|
proc.__neoagentKillReason = reason;
|
|
304
|
-
|
|
358
|
+
terminateWithEscalation(proc, () => this.activeProcesses.get(pid) === proc);
|
|
305
359
|
}
|
|
306
|
-
this.activeProcesses.clear();
|
|
307
360
|
}
|
|
308
361
|
}
|
|
309
362
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const { DATA_DIR } = require('../../../runtime/paths');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_IDLE_TIMEOUT_MINUTES = 30;
|
|
9
|
+
const MIN_IDLE_TIMEOUT_MINUTES = 5;
|
|
10
|
+
const MAX_IDLE_TIMEOUT_MINUTES = 120;
|
|
11
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
function resolveCliScript() {
|
|
14
|
+
try {
|
|
15
|
+
return require.resolve('@bitwarden/cli/build/bw.js');
|
|
16
|
+
} catch {
|
|
17
|
+
const error = new Error('Bitwarden CLI is not installed. Reinstall NeoAgent dependencies and try again.');
|
|
18
|
+
error.code = 'BITWARDEN_CLI_MISSING';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeIdleTimeoutMinutes(value) {
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
if (!Number.isFinite(parsed)) return DEFAULT_IDLE_TIMEOUT_MINUTES;
|
|
26
|
+
return Math.max(MIN_IDLE_TIMEOUT_MINUTES, Math.min(MAX_IDLE_TIMEOUT_MINUTES, Math.round(parsed)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scopeKey(userId, agentId) {
|
|
30
|
+
return `${String(userId)}:${String(agentId)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appDataDirectory(userId, agentId) {
|
|
34
|
+
const safeUser = String(userId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
35
|
+
const safeAgent = String(agentId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
36
|
+
return path.join(DATA_DIR, 'integrations', 'bitwarden', safeUser, safeAgent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runProcess(command, args, options = {}) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const child = spawn(command, args, {
|
|
42
|
+
cwd: options.cwd,
|
|
43
|
+
env: options.env,
|
|
44
|
+
shell: false,
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
const stdout = [];
|
|
49
|
+
const stderr = [];
|
|
50
|
+
let stdoutBytes = 0;
|
|
51
|
+
let stderrBytes = 0;
|
|
52
|
+
let settled = false;
|
|
53
|
+
const timeoutMs = Math.max(1000, Number(options.timeoutMs || 60_000));
|
|
54
|
+
let timer = null;
|
|
55
|
+
const finish = (callback, value) => {
|
|
56
|
+
if (settled) return;
|
|
57
|
+
settled = true;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
60
|
+
callback(value);
|
|
61
|
+
};
|
|
62
|
+
const onAbort = () => {
|
|
63
|
+
child.kill('SIGKILL');
|
|
64
|
+
const error = new Error('Bitwarden operation was aborted.');
|
|
65
|
+
error.name = 'AbortError';
|
|
66
|
+
error.code = 'ABORT_ERR';
|
|
67
|
+
finish(reject, error);
|
|
68
|
+
};
|
|
69
|
+
timer = setTimeout(() => {
|
|
70
|
+
child.kill('SIGKILL');
|
|
71
|
+
const error = new Error('Bitwarden CLI timed out.');
|
|
72
|
+
error.code = 'BITWARDEN_CLI_TIMEOUT';
|
|
73
|
+
finish(reject, error);
|
|
74
|
+
}, timeoutMs);
|
|
75
|
+
if (options.signal?.aborted) {
|
|
76
|
+
onAbort();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
80
|
+
|
|
81
|
+
child.stdout.on('data', (chunk) => {
|
|
82
|
+
stdoutBytes += chunk.length;
|
|
83
|
+
if (stdoutBytes <= MAX_OUTPUT_BYTES) stdout.push(chunk);
|
|
84
|
+
});
|
|
85
|
+
child.stderr.on('data', (chunk) => {
|
|
86
|
+
stderrBytes += chunk.length;
|
|
87
|
+
if (stderrBytes <= MAX_OUTPUT_BYTES) stderr.push(chunk);
|
|
88
|
+
});
|
|
89
|
+
child.on('error', (error) => finish(reject, error));
|
|
90
|
+
child.on('close', (code) => {
|
|
91
|
+
if (code !== 0) {
|
|
92
|
+
const error = new Error('Bitwarden rejected the operation.');
|
|
93
|
+
error.code = 'BITWARDEN_CLI_FAILED';
|
|
94
|
+
error.exitCode = code;
|
|
95
|
+
error.stderrPresent = stderrBytes > 0;
|
|
96
|
+
finish(reject, error);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (stdoutBytes > MAX_OUTPUT_BYTES || stderrBytes > MAX_OUTPUT_BYTES) {
|
|
100
|
+
const error = new Error('Bitwarden CLI returned too much data.');
|
|
101
|
+
error.code = 'BITWARDEN_CLI_OUTPUT_TOO_LARGE';
|
|
102
|
+
finish(reject, error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
finish(resolve, Buffer.concat(stdout).toString('utf8').trim());
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class BitwardenCli {
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
this.runner = options.runner || runProcess;
|
|
113
|
+
this.cliScript = options.cliScript || null;
|
|
114
|
+
this.sessions = new Map();
|
|
115
|
+
this.timer = setInterval(() => this.lockExpired().catch(() => {}), 30_000);
|
|
116
|
+
this.timer.unref?.();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#session(userId, agentId) {
|
|
120
|
+
return this.sessions.get(scopeKey(userId, agentId)) || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#touch(userId, agentId) {
|
|
124
|
+
const session = this.#session(userId, agentId);
|
|
125
|
+
if (session) session.lastUsedAt = Date.now();
|
|
126
|
+
return session;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#environment(userId, agentId, extra = {}) {
|
|
130
|
+
const directory = appDataDirectory(userId, agentId);
|
|
131
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
132
|
+
try { fs.chmodSync(directory, 0o700); } catch {}
|
|
133
|
+
const inherited = {};
|
|
134
|
+
for (const name of [
|
|
135
|
+
'PATH',
|
|
136
|
+
'HOME',
|
|
137
|
+
'TMPDIR',
|
|
138
|
+
'TMP',
|
|
139
|
+
'TEMP',
|
|
140
|
+
'LANG',
|
|
141
|
+
'LC_ALL',
|
|
142
|
+
'HTTPS_PROXY',
|
|
143
|
+
'HTTP_PROXY',
|
|
144
|
+
'NO_PROXY',
|
|
145
|
+
'NODE_EXTRA_CA_CERTS',
|
|
146
|
+
]) {
|
|
147
|
+
if (process.env[name]) inherited[name] = process.env[name];
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...inherited,
|
|
151
|
+
BITWARDENCLI_APPDATA_DIR: directory,
|
|
152
|
+
...extra,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async #run(userId, agentId, args, options = {}) {
|
|
157
|
+
const script = this.cliScript || resolveCliScript();
|
|
158
|
+
return this.runner(process.execPath, [script, ...args], {
|
|
159
|
+
...options,
|
|
160
|
+
env: this.#environment(userId, agentId, options.env || {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getStatus(userId, agentId) {
|
|
165
|
+
const session = this.#session(userId, agentId);
|
|
166
|
+
return {
|
|
167
|
+
cliAvailable: (() => {
|
|
168
|
+
try {
|
|
169
|
+
this.cliScript || resolveCliScript();
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
})(),
|
|
175
|
+
unlocked: Boolean(session?.sessionKey),
|
|
176
|
+
idleTimeoutMinutes: session?.idleTimeoutMinutes || DEFAULT_IDLE_TIMEOUT_MINUTES,
|
|
177
|
+
lastUsedAt: session?.lastUsedAt ? new Date(session.lastUsedAt).toISOString() : null,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async configure(userId, agentId, config, options = {}) {
|
|
182
|
+
const serverUrl = String(config.serverUrl || 'https://vault.bitwarden.com').trim().replace(/\/+$/, '');
|
|
183
|
+
await this.logout(userId, agentId);
|
|
184
|
+
await this.#run(userId, agentId, ['config', 'server', serverUrl], options);
|
|
185
|
+
await this.#run(userId, agentId, ['login', '--apikey'], {
|
|
186
|
+
...options,
|
|
187
|
+
env: {
|
|
188
|
+
BW_CLIENTID: String(config.clientId || ''),
|
|
189
|
+
BW_CLIENTSECRET: String(config.clientSecret || ''),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return { configured: true };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async unlock(userId, agentId, masterPassword, idleTimeoutMinutes, options = {}) {
|
|
196
|
+
const password = String(masterPassword || '');
|
|
197
|
+
if (!password) {
|
|
198
|
+
const error = new Error('Bitwarden master password is required.');
|
|
199
|
+
error.code = 'BITWARDEN_MASTER_PASSWORD_REQUIRED';
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
const sessionKey = await this.#run(userId, agentId, [
|
|
203
|
+
'unlock',
|
|
204
|
+
'--passwordenv',
|
|
205
|
+
'NEOAGENT_BITWARDEN_MASTER_PASSWORD',
|
|
206
|
+
'--raw',
|
|
207
|
+
], {
|
|
208
|
+
...options,
|
|
209
|
+
env: { NEOAGENT_BITWARDEN_MASTER_PASSWORD: password },
|
|
210
|
+
});
|
|
211
|
+
if (!sessionKey) {
|
|
212
|
+
const error = new Error('Bitwarden did not return an unlock session.');
|
|
213
|
+
error.code = 'BITWARDEN_UNLOCK_FAILED';
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
this.sessions.set(scopeKey(userId, agentId), {
|
|
217
|
+
sessionKey,
|
|
218
|
+
lastUsedAt: Date.now(),
|
|
219
|
+
idleTimeoutMinutes: normalizeIdleTimeoutMinutes(idleTimeoutMinutes),
|
|
220
|
+
});
|
|
221
|
+
return this.getStatus(userId, agentId);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
requireSession(userId, agentId) {
|
|
225
|
+
const session = this.#touch(userId, agentId);
|
|
226
|
+
if (!session?.sessionKey) {
|
|
227
|
+
const error = new Error('Bitwarden is locked. Unlock it in Official Integrations.');
|
|
228
|
+
error.code = 'BITWARDEN_LOCKED';
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
return session.sessionKey;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async sync(userId, agentId, options = {}) {
|
|
235
|
+
const sessionKey = this.requireSession(userId, agentId);
|
|
236
|
+
await this.#run(userId, agentId, ['sync'], {
|
|
237
|
+
...options,
|
|
238
|
+
env: { BW_SESSION: sessionKey },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async listItems(userId, agentId, options = {}) {
|
|
243
|
+
const sessionKey = this.requireSession(userId, agentId);
|
|
244
|
+
const raw = await this.#run(userId, agentId, ['list', 'items'], {
|
|
245
|
+
...options,
|
|
246
|
+
env: { BW_SESSION: sessionKey },
|
|
247
|
+
});
|
|
248
|
+
const items = JSON.parse(raw || '[]');
|
|
249
|
+
return Array.isArray(items) ? items : [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async getItem(userId, agentId, itemId, options = {}) {
|
|
253
|
+
const sessionKey = this.requireSession(userId, agentId);
|
|
254
|
+
const raw = await this.#run(userId, agentId, ['get', 'item', String(itemId)], {
|
|
255
|
+
...options,
|
|
256
|
+
env: { BW_SESSION: sessionKey },
|
|
257
|
+
});
|
|
258
|
+
const item = JSON.parse(raw || '{}');
|
|
259
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
260
|
+
throw new Error('Bitwarden returned an invalid item.');
|
|
261
|
+
}
|
|
262
|
+
return item;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async lock(userId, agentId) {
|
|
266
|
+
const key = scopeKey(userId, agentId);
|
|
267
|
+
const session = this.sessions.get(key);
|
|
268
|
+
this.sessions.delete(key);
|
|
269
|
+
if (!session?.sessionKey) return { locked: true };
|
|
270
|
+
try {
|
|
271
|
+
await this.#run(userId, agentId, ['lock'], {
|
|
272
|
+
env: { BW_SESSION: session.sessionKey },
|
|
273
|
+
timeoutMs: 15_000,
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
// The in-memory decryption key has already been discarded.
|
|
277
|
+
}
|
|
278
|
+
return { locked: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async logout(userId, agentId) {
|
|
282
|
+
await this.lock(userId, agentId);
|
|
283
|
+
try {
|
|
284
|
+
await this.#run(userId, agentId, ['logout'], { timeoutMs: 15_000 });
|
|
285
|
+
} catch {
|
|
286
|
+
// Treat an already logged-out CLI as disconnected.
|
|
287
|
+
}
|
|
288
|
+
fs.rmSync(appDataDirectory(userId, agentId), { recursive: true, force: true });
|
|
289
|
+
return { loggedOut: true };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async lockExpired() {
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
const expired = [];
|
|
295
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
296
|
+
if (now - session.lastUsedAt >= session.idleTimeoutMinutes * 60_000) {
|
|
297
|
+
expired.push(key);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await Promise.allSettled(expired.map((key) => {
|
|
301
|
+
const separator = key.indexOf(':');
|
|
302
|
+
return this.lock(key.slice(0, separator), key.slice(separator + 1));
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async shutdown() {
|
|
307
|
+
clearInterval(this.timer);
|
|
308
|
+
const scopes = Array.from(this.sessions.keys());
|
|
309
|
+
await Promise.allSettled(scopes.map((key) => {
|
|
310
|
+
const separator = key.indexOf(':');
|
|
311
|
+
return this.lock(key.slice(0, separator), key.slice(separator + 1));
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
BitwardenCli,
|
|
318
|
+
DEFAULT_IDLE_TIMEOUT_MINUTES,
|
|
319
|
+
appDataDirectory,
|
|
320
|
+
normalizeIdleTimeoutMinutes,
|
|
321
|
+
runProcess,
|
|
322
|
+
};
|