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,197 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../../db/database');
4
+ const { resolveAgentId } = require('../../agents/manager');
5
+ const { getConnectionAccessMode } = require('../access');
6
+ const {
7
+ deleteProviderConfig,
8
+ getProviderConfig,
9
+ setProviderConfig,
10
+ } = require('../provider_config_store');
11
+ const { encryptValue } = require('../secrets');
12
+ const { BITWARDEN_APP, BITWARDEN_PROVIDER_KEY } = require('./constants');
13
+ const { buildBitwardenSnapshot } = require('./snapshot');
14
+
15
+ const DEFAULT_SERVER_URL = 'https://vault.bitwarden.com';
16
+
17
+ function text(value) {
18
+ return String(value || '').trim();
19
+ }
20
+
21
+ function normalizeServerUrl(value) {
22
+ const url = new URL(text(value) || DEFAULT_SERVER_URL);
23
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
24
+ throw new Error('Bitwarden server must be an HTTPS URL without embedded credentials.');
25
+ }
26
+ return url.toString().replace(/\/+$/, '');
27
+ }
28
+
29
+ function parseConfig(raw, existing = {}) {
30
+ const source = raw && typeof raw === 'object' ? raw : {};
31
+ return {
32
+ serverUrl: normalizeServerUrl(source.serverUrl || existing.serverUrl || DEFAULT_SERVER_URL),
33
+ email: text(source.email) || text(existing.email),
34
+ clientId: text(source.clientId) || text(existing.clientId),
35
+ clientSecret: text(source.clientSecret) || text(existing.clientSecret),
36
+ idleTimeoutMinutes: Math.max(5, Math.min(120, Number(
37
+ source.idleTimeoutMinutes || existing.idleTimeoutMinutes || 30,
38
+ ))),
39
+ };
40
+ }
41
+
42
+ function loadConnection(userId, agentId) {
43
+ return db.prepare(
44
+ `SELECT * FROM integration_connections
45
+ WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND app_key = ?
46
+ ORDER BY updated_at DESC, id DESC LIMIT 1`,
47
+ ).get(userId, agentId, BITWARDEN_PROVIDER_KEY, BITWARDEN_APP.id) || null;
48
+ }
49
+
50
+ function publicConfig(config, connection, status = {}) {
51
+ return {
52
+ serverUrl: config.serverUrl || DEFAULT_SERVER_URL,
53
+ email: config.email || '',
54
+ idleTimeoutMinutes: config.idleTimeoutMinutes || 30,
55
+ hasClientId: Boolean(config.clientId),
56
+ hasClientSecret: Boolean(config.clientSecret),
57
+ configured: Boolean(config.email && config.clientId && config.clientSecret),
58
+ accountCount: connection?.status === 'connected' ? 1 : 0,
59
+ hasConnectedAccount: connection?.status === 'connected',
60
+ connectionId: connection?.id || null,
61
+ cliAvailable: status.cliAvailable !== false,
62
+ unlocked: Boolean(status.unlocked),
63
+ lastUsedAt: status.lastUsedAt || null,
64
+ };
65
+ }
66
+
67
+ function upsertConnection(userId, agentId, config) {
68
+ const existing = loadConnection(userId, agentId);
69
+ const metadata = JSON.stringify({
70
+ access_mode: getConnectionAccessMode(existing),
71
+ idleTimeoutMinutes: config.idleTimeoutMinutes,
72
+ });
73
+ db.prepare(
74
+ `INSERT INTO integration_connections (
75
+ user_id, agent_id, provider_key, app_key, status, account_email,
76
+ scopes_json, credentials_json, metadata_json, last_connected_at, updated_at
77
+ ) VALUES (?, ?, ?, ?, 'connected', ?, ?, ?, ?, datetime('now'), datetime('now'))
78
+ ON CONFLICT(user_id, agent_id, provider_key, app_key, account_email) DO UPDATE SET
79
+ status = excluded.status,
80
+ credentials_json = excluded.credentials_json,
81
+ metadata_json = excluded.metadata_json,
82
+ last_connected_at = excluded.last_connected_at,
83
+ updated_at = excluded.updated_at`,
84
+ ).run(
85
+ userId,
86
+ agentId,
87
+ BITWARDEN_PROVIDER_KEY,
88
+ BITWARDEN_APP.id,
89
+ config.email,
90
+ JSON.stringify(['vault:read_selected']),
91
+ encryptValue(JSON.stringify(config)),
92
+ metadata,
93
+ );
94
+ if (existing && existing.account_email !== config.email) {
95
+ db.prepare('DELETE FROM integration_connections WHERE id = ?').run(existing.id);
96
+ }
97
+ return loadConnection(userId, agentId);
98
+ }
99
+
100
+ function createBitwardenProvider(options = {}) {
101
+ const app = options.app || null;
102
+ const cli = () => app?.locals?.bitwardenCli;
103
+ const broker = () => app?.locals?.credentialBroker;
104
+
105
+ return {
106
+ key: BITWARDEN_PROVIDER_KEY,
107
+ label: 'Bitwarden',
108
+ description: 'Fill browser logins and authenticate bounded API requests while keeping secrets hidden from the AI.',
109
+ icon: 'password',
110
+ apps: [BITWARDEN_APP],
111
+ connectPrompt: 'Add a Bitwarden API key, then unlock the vault when credentials are needed. The master password and vault secrets are never sent to the AI.',
112
+ supportsMultipleAccounts: false,
113
+ connectionMethod: 'user_config',
114
+ getApp(appId) {
115
+ return text(appId) === BITWARDEN_APP.id ? BITWARDEN_APP : null;
116
+ },
117
+ getToolAppId() {
118
+ return null;
119
+ },
120
+ getToolDefinitions() {
121
+ return [];
122
+ },
123
+ supportsTool() {
124
+ return false;
125
+ },
126
+ getEnvStatus(context = {}) {
127
+ const userId = Number(context.userId);
128
+ const agentId = Number.isInteger(userId) && userId > 0
129
+ ? resolveAgentId(userId, context.agentId || null)
130
+ : null;
131
+ const config = agentId ? parseConfig(getProviderConfig(userId, BITWARDEN_PROVIDER_KEY, agentId)) : {};
132
+ const missing = ['email', 'clientId', 'clientSecret'].filter((key) => !config[key]);
133
+ return {
134
+ configured: missing.length === 0,
135
+ missing,
136
+ summary: missing.length === 0
137
+ ? 'Bitwarden is configured. Unlock it only when a credential operation is needed.'
138
+ : 'Add a Bitwarden personal API key in Official Integrations.',
139
+ setupMode: 'user',
140
+ };
141
+ },
142
+ buildSnapshot(connectionRows, context = {}) {
143
+ return buildBitwardenSnapshot(this, connectionRows, {
144
+ ...context,
145
+ credentialBindingsSummary: context.userId
146
+ ? broker()?.summarizeBindings(context.userId, context.agentId)
147
+ : 'No credential bindings are configured.',
148
+ });
149
+ },
150
+ summarizeForModel(snapshot) {
151
+ if (!snapshot?.connection?.connected) {
152
+ return 'Bitwarden: not configured. The user can connect it in Official Integrations.';
153
+ }
154
+ const bindings = snapshot.credentialBindingsSummary || 'No credential bindings are configured.';
155
+ return `Bitwarden credential broker: ${bindings} Secret values are never available to the model.`;
156
+ },
157
+ getUserConfig({ userId, agentId }) {
158
+ const normalizedUserId = Number(userId);
159
+ const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
160
+ const config = parseConfig(getProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId));
161
+ return publicConfig(config, loadConnection(normalizedUserId, scopedAgentId), cli()?.getStatus(normalizedUserId, scopedAgentId));
162
+ },
163
+ async saveUserConfig({ userId, agentId, config, signal }) {
164
+ const normalizedUserId = Number(userId);
165
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
166
+ throw new Error('A valid user is required to save Bitwarden configuration.');
167
+ }
168
+ const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
169
+ const existing = getProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId);
170
+ const parsed = parseConfig(config, existing);
171
+ if (!parsed.email || !parsed.clientId || !parsed.clientSecret) {
172
+ throw new Error('Bitwarden email, client ID, and client secret are required.');
173
+ }
174
+ if (!cli()) throw new Error('Bitwarden credential service is unavailable.');
175
+ await cli().configure(normalizedUserId, scopedAgentId, parsed, { signal });
176
+ setProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, parsed, scopedAgentId);
177
+ const connection = upsertConnection(normalizedUserId, scopedAgentId, parsed);
178
+ return publicConfig(parsed, connection, cli().getStatus(normalizedUserId, scopedAgentId));
179
+ },
180
+ async clearUserConfig({ userId, agentId }) {
181
+ const normalizedUserId = Number(userId);
182
+ const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
183
+ await cli()?.logout(normalizedUserId, scopedAgentId);
184
+ deleteProviderConfig(normalizedUserId, BITWARDEN_PROVIDER_KEY, scopedAgentId);
185
+ db.prepare(
186
+ 'DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?',
187
+ ).run(normalizedUserId, scopedAgentId, BITWARDEN_PROVIDER_KEY);
188
+ return { cleared: true };
189
+ },
190
+ };
191
+ }
192
+
193
+ module.exports = {
194
+ createBitwardenProvider,
195
+ normalizeServerUrl,
196
+ parseConfig,
197
+ };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const { getConnectionAccessMode } = require('../access');
4
+ const { BITWARDEN_APP } = require('./constants');
5
+
6
+ function summarizeAccount(row, env) {
7
+ if (!row) {
8
+ return {
9
+ id: null,
10
+ status: env.configured ? 'not_connected' : 'env_not_configured',
11
+ connected: false,
12
+ accountEmail: null,
13
+ lastConnectedAt: null,
14
+ accessMode: 'read_write',
15
+ };
16
+ }
17
+ return {
18
+ id: row.id,
19
+ status: row.status,
20
+ connected: row.status === 'connected',
21
+ accountEmail: row.account_email || null,
22
+ lastConnectedAt: row.last_connected_at || null,
23
+ accessMode: getConnectionAccessMode(row),
24
+ };
25
+ }
26
+
27
+ function buildBitwardenSnapshot(provider, connectionRows, context = {}) {
28
+ const env = provider.getEnvStatus(context);
29
+ const row = (Array.isArray(connectionRows) ? connectionRows : [])
30
+ .filter((candidate) => candidate.app_key === BITWARDEN_APP.id)
31
+ .sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')))[0] || null;
32
+ const account = summarizeAccount(row, env);
33
+ const accounts = row ? [account] : [];
34
+ const connection = {
35
+ status: !env.configured ? 'env_not_configured' : account.status,
36
+ connected: account.connected,
37
+ accountEmail: account.accountEmail,
38
+ accountCount: account.connected ? 1 : 0,
39
+ appCount: account.connected ? 1 : 0,
40
+ lastConnectedAt: account.lastConnectedAt,
41
+ };
42
+ return {
43
+ id: provider.key,
44
+ label: provider.label,
45
+ description: provider.description,
46
+ icon: provider.icon,
47
+ apps: [{
48
+ ...BITWARDEN_APP,
49
+ accounts,
50
+ connection,
51
+ availableToolCount: account.connected ? 2 : 0,
52
+ }],
53
+ env,
54
+ connection,
55
+ availableToolCount: account.connected ? 2 : 0,
56
+ credentialBindingsSummary: context.credentialBindingsSummary || 'No credential bindings are configured.',
57
+ connectPrompt: provider.connectPrompt,
58
+ supportsMultipleAccounts: false,
59
+ connectionMethod: 'user_config',
60
+ };
61
+ }
62
+
63
+ module.exports = {
64
+ buildBitwardenSnapshot,
65
+ };
@@ -59,6 +59,7 @@ class IntegrationManager {
59
59
  constructor(options = {}) {
60
60
  this.app = options.app || null;
61
61
  this.registry = createIntegrationRegistry({
62
+ app: this.app,
62
63
  io: this.app?.locals?.io || null,
63
64
  });
64
65
  this.connectionExecutionQueues = new Map();
@@ -4,6 +4,7 @@ const { createFigmaProvider } = require('./figma/provider');
4
4
  const { createGoogleWorkspaceProvider } = require('./google/provider');
5
5
  const { createGithubProvider } = require('./github/provider');
6
6
  const { createHomeAssistantProvider } = require('./home_assistant/provider');
7
+ const { createBitwardenProvider } = require('./bitwarden/provider');
7
8
  const { createTrelloProvider } = require('./trello/provider');
8
9
  const { createMicrosoftProvider } = require('./microsoft/provider');
9
10
  const { createNotionProvider } = require('./notion/provider');
@@ -28,6 +29,7 @@ function createIntegrationRegistry(options = {}) {
28
29
  createNeoArchiveProvider(),
29
30
  createNeoRecallProvider(),
30
31
  createHomeAssistantProvider(),
32
+ createBitwardenProvider(options),
31
33
  createWhatsAppPersonalProvider(options),
32
34
  ];
33
35
  const byKey = new Map(providers.map((provider) => [provider.key, provider]));
@@ -34,6 +34,8 @@ const { DesktopCompanionRegistry } = require('./desktop/registry');
34
34
  const { DesktopProvider } = require('./desktop/provider');
35
35
  const { TimelineService } = require('./timeline/service');
36
36
  const { WearableService } = require('./wearable/service');
37
+ const { BitwardenCli } = require('./credentials/bitwarden_cli');
38
+ const { CredentialBroker } = require('./credentials/broker');
37
39
  const { getRuntimeValidation } = require('./runtime/validation');
38
40
  const {
39
41
  getErrorMessage,
@@ -142,6 +144,17 @@ function createIntegrationManager(app) {
142
144
  return integrationManager;
143
145
  }
144
146
 
147
+ function createCredentialBroker(app) {
148
+ const bitwardenCli = registerLocal(app, 'bitwardenCli', new BitwardenCli());
149
+ const credentialBroker = registerLocal(
150
+ app,
151
+ 'credentialBroker',
152
+ new CredentialBroker({ bitwarden: bitwardenCli }),
153
+ );
154
+ logServiceReady('Credential broker ready');
155
+ return credentialBroker;
156
+ }
157
+
145
158
  function createMemoryIngestionService(app, { memoryManager, integrationManager }) {
146
159
  const memoryIngestionService = registerLocal(
147
160
  app,
@@ -469,10 +482,12 @@ async function startServices(app, io) {
469
482
  const memoryManager = createMemoryManager(app);
470
483
  const mcpClient = createMcpClient(app);
471
484
  createAuthProviderManager(app);
485
+ const credentialBroker = createCredentialBroker(app);
472
486
  const integrationManager = createIntegrationManager(app);
473
487
  createMemoryIngestionService(app, { memoryManager, integrationManager });
474
488
  const browserController = createBrowserController(app, artifactStore);
475
489
  const runtimeManager = createRuntimeManager(app);
490
+ credentialBroker.setRuntimeManager(runtimeManager);
476
491
  const runtimeValidation = getRuntimeValidation(runtimeManager);
477
492
  registerLocal(app, 'runtimeValidation', runtimeValidation);
478
493
  if (!runtimeValidation.ready) {
@@ -753,6 +768,14 @@ async function stopServices(app) {
753
768
  );
754
769
  }
755
770
 
771
+ if (app.locals.bitwardenCli) {
772
+ tasks.push(
773
+ app.locals.bitwardenCli.shutdown().catch((err) => {
774
+ console.error('[Bitwarden] Shutdown error:', getErrorMessage(err));
775
+ }),
776
+ );
777
+ }
778
+
756
779
  if (app.locals.widgetService) {
757
780
  const widgetService = app.locals.widgetService;
758
781
  const cleanupMethod = ['shutdown', 'close', 'stop', 'dispose'].find(
@@ -389,6 +389,15 @@ class VmBrowserProvider {
389
389
  async launch(options = {}) { return this.#request('POST', '/browser/launch', options, options); }
390
390
  async closeBrowser(options = {}) { return this.#request('POST', '/browser/close', undefined, options); }
391
391
  async fill(selector, value, options = {}) { return this.type(selector, value, options); }
392
+ async fillCredential(input, options = {}) {
393
+ return this.#request('POST', '/browser/credential-fill', input, options);
394
+ }
395
+ async submitProtectedCredential(protectedFillId, options = {}) {
396
+ return this.#request('POST', '/browser/credential-submit', { protectedFillId }, options);
397
+ }
398
+ async cancelProtectedCredential(protectedFillId, options = {}) {
399
+ return this.#request('POST', '/browser/credential-cancel', { protectedFillId }, options);
400
+ }
392
401
  async extractContent(options = {}) { return this.#request('POST', '/browser/extract', options, options); }
393
402
  async executeJS(code, options = {}) { return this.evaluate(code, options); }
394
403
  async getPageInfo(options = {}) {
@@ -417,7 +426,10 @@ class VmBrowserProvider {
417
426
  class LocalVmExecutionBackend {
418
427
  constructor(options = {}) {
419
428
  this.vmManager = options.vmManager;
420
- this.runtimeProfile = options.runtimeProfile === 'android' ? 'android' : 'browser_cli';
429
+ const runtimeProfile = String(options.runtimeProfile || '').trim();
430
+ this.runtimeProfile = ['android', 'browser', 'cli', 'browser_cli'].includes(runtimeProfile)
431
+ ? runtimeProfile
432
+ : 'browser_cli';
421
433
  this.token = options.token || process.env.NEOAGENT_VM_GUEST_TOKEN || '';
422
434
  this.artifactStore = options.artifactStore || null;
423
435
  this.lastActivity = new Map();
@@ -7,6 +7,20 @@ const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
7
7
  const GUEST_BOOTSTRAP_ROOT = path.join(VM_ROOT, 'guest-bootstrap');
8
8
  const REPO_ROOT = path.resolve(__dirname, '../../..');
9
9
  const GUEST_PAYLOAD_PROFILES = Object.freeze({
10
+ browser: [
11
+ { source: 'server/guest-agent.browser.package.json', target: 'package.json' },
12
+ { source: 'runtime/env.js', target: 'runtime/env.js' },
13
+ { source: 'runtime/paths.js', target: 'runtime/paths.js' },
14
+ { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
15
+ { source: 'server/services/browser', target: 'server/services/browser' },
16
+ ],
17
+ cli: [
18
+ { source: 'server/guest-agent.cli.package.json', target: 'package.json' },
19
+ { source: 'runtime/env.js', target: 'runtime/env.js' },
20
+ { source: 'runtime/paths.js', target: 'runtime/paths.js' },
21
+ { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
22
+ { source: 'server/services/cli', target: 'server/services/cli' },
23
+ ],
10
24
  browser_cli: [
11
25
  { source: 'server/guest-agent.browser.package.json', target: 'package.json' },
12
26
  { source: 'runtime/env.js', target: 'runtime/env.js' },
@@ -32,7 +46,10 @@ function encodeGuestToken(value) {
32
46
  }
33
47
 
34
48
  function normalizeRuntimeProfile(runtimeProfile) {
35
- return runtimeProfile === 'android' ? 'android' : 'browser_cli';
49
+ const normalized = String(runtimeProfile || '').trim();
50
+ return ['android', 'browser', 'cli', 'browser_cli'].includes(normalized)
51
+ ? normalized
52
+ : 'browser_cli';
36
53
  }
37
54
 
38
55
  // Copy the guest runtime source files for a profile into `stagingRoot`, producing
@@ -84,10 +101,12 @@ function createCloudInitScript({
84
101
  runtimeProfile = 'browser_cli',
85
102
  }) {
86
103
  const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
87
- const includeBrowser = normalizedProfile === 'browser_cli';
104
+ const includeBrowser = ['browser', 'browser_cli'].includes(normalizedProfile);
88
105
  const guestUtilityPackages = includeBrowser
89
106
  ? 'curl ca-certificates gnupg git rsync unzip xvfb dbus-x11'
90
- : 'curl ca-certificates gnupg git rsync unzip dbus-x11 adb';
107
+ : normalizedProfile === 'android'
108
+ ? 'curl ca-certificates gnupg git rsync unzip dbus-x11 adb'
109
+ : 'curl ca-certificates gnupg git rsync unzip';
91
110
  const guestTokenB64 = encodeGuestToken(guestToken);
92
111
  const envFile = '/etc/neoagent/neoagent.env';
93
112
  const appDir = '/opt/neoagent';
@@ -208,7 +227,7 @@ function createCloudInitUserData({
208
227
  runtimeProfile = 'browser_cli',
209
228
  }) {
210
229
  const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
211
- const includeBrowser = normalizedProfile === 'browser_cli';
230
+ const includeBrowser = ['browser', 'browser_cli'].includes(normalizedProfile);
212
231
  const guestAgentInnerCommand = includeBrowser
213
232
  ? 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && env DISPLAY=:99 PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console'
214
233
  : 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console';
@@ -21,11 +21,12 @@ const BUILD_TIMEOUT_MS = Number(process.env.NEOAGENT_GUEST_IMAGE_BUILD_TIMEOUT_M
21
21
  // container start. Copy package.json first so the dependency layer stays cached
22
22
  // across source-only changes.
23
23
  function dockerfileFor(profile) {
24
- if (normalizeRuntimeProfile(profile) === 'android') {
24
+ const normalizedProfile = normalizeRuntimeProfile(profile);
25
+ if (normalizedProfile === 'android' || normalizedProfile === 'cli') {
25
26
  return [
26
27
  `FROM ${BASE_IMAGE}`,
27
28
  'ENV NODE_ENV=production',
28
- 'ENV NEOAGENT_GUEST_PROFILE=android',
29
+ `ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
29
30
  'WORKDIR /opt/neoagent',
30
31
  'COPY package.json ./',
31
32
  'RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force',
@@ -42,7 +43,7 @@ function dockerfileFor(profile) {
42
43
  `FROM ${BASE_IMAGE}`,
43
44
  'ENV NODE_ENV=production',
44
45
  'ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright',
45
- 'ENV NEOAGENT_GUEST_PROFILE=browser_cli',
46
+ `ENV NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
46
47
  'WORKDIR /opt/neoagent',
47
48
  'COPY package.json ./',
48
49
  // Install deps without running package postinstall scripts, then fetch the
@@ -10,6 +10,8 @@ const { DesktopProvider } = require('../desktop/provider');
10
10
  // Resource defaults for Docker VMs (overridable via env).
11
11
  const DEFAULT_VM_MEMORY_MB = Number(process.env.NEOAGENT_VM_MEMORY_MB ?? 2048);
12
12
  const DEFAULT_VM_CPUS = Number(process.env.NEOAGENT_VM_CPUS ?? 2);
13
+ const DEFAULT_CLI_VM_MEMORY_MB = Number(process.env.NEOAGENT_CLI_VM_MEMORY_MB ?? 1024);
14
+ const DEFAULT_CLI_VM_CPUS = Number(process.env.NEOAGENT_CLI_VM_CPUS ?? 1);
13
15
 
14
16
  class RuntimeManager {
15
17
  constructor(options = {}) {
@@ -19,15 +21,25 @@ class RuntimeManager {
19
21
  this.androidControllers = new Map();
20
22
 
21
23
  const browserVmManager = options.browserVmManager || new DockerVMManager({
22
- runtimeProfile: 'browser_cli',
24
+ runtimeProfile: 'browser',
23
25
  memoryMb: DEFAULT_VM_MEMORY_MB,
24
26
  cpus: DEFAULT_VM_CPUS,
25
27
  });
26
28
  this.browserBackend = new LocalVmExecutionBackend({
27
- runtimeProfile: 'browser_cli',
29
+ runtimeProfile: 'browser',
28
30
  vmManager: browserVmManager,
29
31
  artifactStore: options.artifactStore,
30
32
  });
33
+ const cliVmManager = options.cliVmManager || new DockerVMManager({
34
+ runtimeProfile: 'cli',
35
+ memoryMb: DEFAULT_CLI_VM_MEMORY_MB,
36
+ cpus: DEFAULT_CLI_VM_CPUS,
37
+ });
38
+ this.cliBackend = new LocalVmExecutionBackend({
39
+ runtimeProfile: 'cli',
40
+ vmManager: cliVmManager,
41
+ artifactStore: options.artifactStore,
42
+ });
31
43
 
32
44
  this.artifactStore = options.artifactStore || null;
33
45
 
@@ -77,25 +89,25 @@ class RuntimeManager {
77
89
 
78
90
  resolveBackend(userId, requested) {
79
91
  void userId;
80
- void requested;
81
- return this.browserBackend;
92
+ return requested === 'browser' ? this.browserBackend : this.cliBackend;
82
93
  }
83
94
 
84
95
  async executeCommand(userId, command, options = {}) {
85
- const backend = this.resolveBackend(userId, 'browser_cli');
96
+ const backend = this.resolveBackend(userId, 'cli');
86
97
  return backend.executeCommand(userId, command, options);
87
98
  }
88
99
 
89
100
  hasVmForUser(userId, capability = 'browser') {
90
- return Boolean(this.browserBackend?.vmManager?.hasVm?.(userId));
101
+ const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
102
+ return Boolean(backend?.vmManager?.hasVm?.(userId));
91
103
  }
92
104
 
93
105
  async killCommand(userId, pid, reason = 'aborted') {
94
- return this.browserBackend.killCommand(userId, pid, reason);
106
+ return this.cliBackend.killCommand(userId, pid, reason);
95
107
  }
96
108
 
97
109
  async getCommandExecutorForUser(userId) {
98
- return this.browserBackend.getCommandExecutorForUser(userId);
110
+ return this.cliBackend.getCommandExecutorForUser(userId);
99
111
  }
100
112
 
101
113
  async getBrowserProviderForUser(userId, options = {}) {
@@ -124,7 +136,7 @@ class RuntimeManager {
124
136
  kill: () => Promise.resolve(false),
125
137
  };
126
138
  }
127
- const executor = await this.browserBackend.getCommandExecutorForUser(userId);
139
+ const executor = await this.cliBackend.getCommandExecutorForUser(userId);
128
140
  return { ...executor, backend: 'vm' };
129
141
  }
130
142
 
@@ -156,15 +168,17 @@ class RuntimeManager {
156
168
  }
157
169
 
158
170
  async isGuestAgentReadyForUser(userId, timeoutMs = 1000, capability = 'browser') {
159
- if (typeof this.browserBackend?.isGuestAgentReadyForUser !== 'function') {
171
+ const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
172
+ if (typeof backend?.isGuestAgentReadyForUser !== 'function') {
160
173
  return false;
161
174
  }
162
- return this.browserBackend.isGuestAgentReadyForUser(userId, timeoutMs);
175
+ return backend.isGuestAgentReadyForUser(userId, timeoutMs);
163
176
  }
164
177
 
165
178
  async shutdown() {
166
179
  const tasks = [
167
180
  this.browserBackend?.shutdown?.(),
181
+ this.cliBackend?.shutdown?.(),
168
182
  ...Array.from(this.androidControllers.values(), (controller) => controller?.close?.()),
169
183
  ];
170
184
  this.androidControllers.clear();
@@ -6,14 +6,14 @@ function getRuntimeValidation(runtimeManager) {
6
6
  const policy = getDeploymentPolicy();
7
7
  const nodeEnvIsProd = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'prod';
8
8
  const browserVmReadiness = runtimeManager?.browserBackend?.vmManager?.getReadiness?.() || null;
9
- const vmReadiness = browserVmReadiness || null;
9
+ const cliVmReadiness = runtimeManager?.cliBackend?.vmManager?.getReadiness?.() || null;
10
10
  const issues = [];
11
11
 
12
12
  if (policy.profile === 'prod' || nodeEnvIsProd) {
13
- if (!browserVmReadiness) {
14
- issues.push('prod profile requires a working container runtime for browser/CLI.');
15
- } else if (!browserVmReadiness.dockerAvailable) {
16
- issues.push('prod profile requires Docker to be installed and running for the browser/CLI runtime.');
13
+ if (!browserVmReadiness || !cliVmReadiness) {
14
+ issues.push('prod profile requires working isolated container runtimes for browser and CLI.');
15
+ } else if (!browserVmReadiness.dockerAvailable || !cliVmReadiness.dockerAvailable) {
16
+ issues.push('prod profile requires Docker to be installed and running for the browser and CLI runtimes.');
17
17
  }
18
18
  }
19
19
 
@@ -21,7 +21,8 @@ function getRuntimeValidation(runtimeManager) {
21
21
  ready: issues.length === 0,
22
22
  issues,
23
23
  vm: {
24
- browser: vmReadiness,
24
+ browser: browserVmReadiness,
25
+ cli: cliVmReadiness,
25
26
  android: null,
26
27
  },
27
28
  guestTokenConfigured: true,
@@ -18,6 +18,7 @@ const TOOL_CATEGORIES = {
18
18
  'desktop_observe',
19
19
  ],
20
20
  browser_privileged: ['browser_evaluate'],
21
+ credential_use: ['credential_fill_browser', 'credential_http_request'],
21
22
  network_write: ['http_request'],
22
23
  skill_mutation: [
23
24
  'create_skill',
@@ -78,6 +79,10 @@ const BUILT_IN_TOOLS = new Set([
78
79
  'browser_extract',
79
80
  'browser_screenshot',
80
81
  'browser_evaluate',
82
+ 'credential_fill_browser',
83
+ 'credential_submit_browser',
84
+ 'credential_cancel_browser',
85
+ 'credential_http_request',
81
86
  'desktop_list_devices',
82
87
  'desktop_select_device',
83
88
  'desktop_observe',
@@ -166,6 +171,7 @@ const DEFAULT_POLICY = {
166
171
  android_privileged: 'require_approval',
167
172
  desktop_control: 'require_approval',
168
173
  browser_privileged: 'require_approval',
174
+ credential_use: 'require_approval',
169
175
  network_write: 'require_approval',
170
176
  skill_mutation: 'deny',
171
177
  external: 'require_approval',