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.
- package/extensions/chrome-browser/protocol.mjs +143 -1
- package/flutter_app/lib/main_controller.dart +82 -0
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/src/backend_client.dart +78 -0
- package/lib/schema_migrations.js +48 -0
- package/package.json +10 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +33 -10
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +69495 -68619
- package/server/routes/integrations.js +102 -0
- package/server/services/ai/systemPrompt.js +2 -2
- package/server/services/ai/tools.js +77 -0
- package/server/services/browser/controller.js +107 -0
- package/server/services/browser/extension/protocol.js +3 -0
- package/server/services/browser/extension/provider.js +12 -0
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- 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/manager.js +1 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/manager.js +23 -0
- package/server/services/runtime/backends/local-vm.js +13 -1
- package/server/services/runtime/guest_bootstrap.js +23 -4
- package/server/services/runtime/guest_image.js +4 -3
- package/server/services/runtime/manager.js +25 -11
- package/server/services/runtime/validation.js +7 -6
- 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
|
+
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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, '
|
|
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
|
-
|
|
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.
|
|
106
|
+
return this.cliBackend.killCommand(userId, pid, reason);
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
async getCommandExecutorForUser(userId) {
|
|
98
|
-
return this.
|
|
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.
|
|
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
|
-
|
|
171
|
+
const backend = capability === 'browser' ? this.browserBackend : this.cliBackend;
|
|
172
|
+
if (typeof backend?.isGuestAgentReadyForUser !== 'function') {
|
|
160
173
|
return false;
|
|
161
174
|
}
|
|
162
|
-
return
|
|
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
|
|
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
|
|
15
|
-
} else if (!browserVmReadiness.dockerAvailable) {
|
|
16
|
-
issues.push('prod profile requires Docker to be installed and running for the browser
|
|
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:
|
|
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',
|