neoagent 3.2.1-beta.4 → 3.2.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/extensions/chrome-browser/protocol.mjs +143 -1
  2. package/flutter_app/lib/main_controller.dart +82 -0
  3. package/flutter_app/lib/main_integrations.dart +607 -8
  4. package/flutter_app/lib/main_security.dart +266 -112
  5. package/flutter_app/lib/src/backend_client.dart +78 -0
  6. package/lib/schema_migrations.js +48 -0
  7. package/package.json +10 -2
  8. package/server/guest-agent.cli.package.json +13 -0
  9. package/server/guest_agent.js +33 -10
  10. package/server/public/.last_build_id +1 -1
  11. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  12. package/server/public/flutter_bootstrap.js +1 -1
  13. package/server/public/main.dart.js +69495 -68619
  14. package/server/routes/integrations.js +102 -0
  15. package/server/services/ai/systemPrompt.js +2 -2
  16. package/server/services/ai/tools.js +77 -0
  17. package/server/services/browser/controller.js +107 -0
  18. package/server/services/browser/extension/protocol.js +3 -0
  19. package/server/services/browser/extension/provider.js +12 -0
  20. package/server/services/credentials/bitwarden_cli.js +322 -0
  21. package/server/services/credentials/broker.js +594 -0
  22. package/server/services/integrations/bitwarden/constants.js +14 -0
  23. package/server/services/integrations/bitwarden/provider.js +197 -0
  24. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  25. package/server/services/integrations/manager.js +1 -0
  26. package/server/services/integrations/registry.js +2 -0
  27. package/server/services/manager.js +23 -0
  28. package/server/services/runtime/backends/local-vm.js +13 -1
  29. package/server/services/runtime/guest_bootstrap.js +23 -4
  30. package/server/services/runtime/guest_image.js +4 -3
  31. package/server/services/runtime/manager.js +25 -11
  32. package/server/services/runtime/validation.js +7 -6
  33. package/server/services/security/tool_categories.js +6 -0
@@ -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
+ };