neoagent 2.3.1-beta.85 → 2.3.1-beta.87

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 (55) hide show
  1. package/docs/capabilities.md +2 -0
  2. package/flutter_app/android/app/src/main/AndroidManifest.xml +14 -0
  3. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +84 -0
  4. package/flutter_app/lib/main_chat.dart +156 -2
  5. package/flutter_app/lib/main_controller.dart +137 -10
  6. package/flutter_app/lib/main_models.dart +69 -0
  7. package/flutter_app/lib/main_operations.dart +248 -0
  8. package/flutter_app/lib/main_runtime.dart +11 -2
  9. package/flutter_app/lib/main_settings.dart +173 -176
  10. package/flutter_app/lib/main_shared.dart +78 -0
  11. package/flutter_app/lib/src/app_launch_bridge.dart +39 -10
  12. package/flutter_app/lib/src/backend_client.dart +28 -0
  13. package/package.json +1 -1
  14. package/server/guest-agent.android.package.json +13 -0
  15. package/server/guest-agent.browser.package.json +14 -0
  16. package/server/guest_agent.js +61 -44
  17. package/server/http/routes.js +1 -0
  18. package/server/public/.last_build_id +1 -1
  19. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  20. package/server/public/flutter_bootstrap.js +1 -1
  21. package/server/public/main.dart.js +69936 -69277
  22. package/server/routes/android.js +2 -11
  23. package/server/routes/browser.js +2 -2
  24. package/server/routes/memory.js +90 -0
  25. package/server/routes/social_video.js +62 -0
  26. package/server/services/ai/capabilityHealth.js +6 -14
  27. package/server/services/ai/systemPrompt.js +1 -0
  28. package/server/services/ai/toolResult.js +20 -0
  29. package/server/services/ai/tools.js +29 -0
  30. package/server/services/android/android_bootstrap_worker.js +2 -2
  31. package/server/services/android/controller.js +528 -132
  32. package/server/services/browser/controller.js +51 -68
  33. package/server/services/manager.js +15 -0
  34. package/server/services/memory/llm_transfer.js +217 -0
  35. package/server/services/runtime/backends/local-vm.js +16 -3
  36. package/server/services/runtime/guest_bootstrap.js +224 -56
  37. package/server/services/runtime/manager.js +53 -15
  38. package/server/services/runtime/qemu.js +149 -24
  39. package/server/services/runtime/settings.js +9 -14
  40. package/server/services/runtime/validation.js +10 -11
  41. package/server/services/social_video/adapters/base.js +26 -0
  42. package/server/services/social_video/adapters/index.js +27 -0
  43. package/server/services/social_video/adapters/instagram.js +17 -0
  44. package/server/services/social_video/adapters/tiktok.js +17 -0
  45. package/server/services/social_video/adapters/x.js +17 -0
  46. package/server/services/social_video/adapters/youtube.js +17 -0
  47. package/server/services/social_video/captions.js +187 -0
  48. package/server/services/social_video/frame.js +42 -0
  49. package/server/services/social_video/index.js +7 -0
  50. package/server/services/social_video/metadata.js +63 -0
  51. package/server/services/social_video/result.js +63 -0
  52. package/server/services/social_video/service.js +576 -0
  53. package/server/services/social_video/url.js +83 -0
  54. package/server/utils/deployment.js +4 -4
  55. package/server/guest-agent.package.json +0 -15
@@ -6,15 +6,24 @@ const { DATA_DIR } = require('../../../runtime/paths');
6
6
  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
- const GUEST_PAYLOAD_ENTRIES = Object.freeze([
10
- { source: 'server/guest-agent.package.json', target: 'package.json' },
11
- { source: 'runtime/env.js', target: 'runtime/env.js' },
12
- { source: 'runtime/paths.js', target: 'runtime/paths.js' },
13
- { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
14
- { source: 'server/services/cli', target: 'server/services/cli' },
15
- { source: 'server/services/browser', target: 'server/services/browser' },
16
- { source: 'server/services/android', target: 'server/services/android' },
17
- ]);
9
+ const GUEST_PAYLOAD_PROFILES = Object.freeze({
10
+ browser_cli: [
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/cli', target: 'server/services/cli' },
16
+ { source: 'server/services/browser', target: 'server/services/browser' },
17
+ ],
18
+ android: [
19
+ { source: 'server/guest-agent.android.package.json', target: 'package.json' },
20
+ { source: 'runtime/env.js', target: 'runtime/env.js' },
21
+ { source: 'runtime/paths.js', target: 'runtime/paths.js' },
22
+ { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
23
+ { source: 'server/services/cli', target: 'server/services/cli' },
24
+ { source: 'server/services/android', target: 'server/services/android' },
25
+ ],
26
+ });
18
27
 
19
28
  fs.mkdirSync(GUEST_BOOTSTRAP_ROOT, { recursive: true });
20
29
 
@@ -22,15 +31,20 @@ function encodeGuestToken(value) {
22
31
  return Buffer.from(String(value || ''), 'utf8').toString('base64');
23
32
  }
24
33
 
25
- function createGuestPayloadArchive(seedDir) {
34
+ function normalizeRuntimeProfile(runtimeProfile) {
35
+ return runtimeProfile === 'android' ? 'android' : 'browser_cli';
36
+ }
37
+
38
+ function createGuestPayloadArchive(seedDir, runtimeProfile = 'browser_cli') {
26
39
  const seedRoot = path.dirname(seedDir);
27
40
  const stagingRoot = path.join(seedRoot, 'guest-payload');
28
41
  const archivePath = path.join(seedRoot, 'guest-payload.tar.gz');
42
+ const payloadEntries = GUEST_PAYLOAD_PROFILES[normalizeRuntimeProfile(runtimeProfile)];
29
43
  fs.rmSync(stagingRoot, { recursive: true, force: true });
30
44
  fs.rmSync(archivePath, { force: true });
31
45
  fs.mkdirSync(stagingRoot, { recursive: true });
32
46
 
33
- for (const entry of GUEST_PAYLOAD_ENTRIES) {
47
+ for (const entry of payloadEntries) {
34
48
  const sourcePath = path.join(REPO_ROOT, entry.source);
35
49
  const targetPath = path.join(stagingRoot, entry.target);
36
50
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
@@ -59,10 +73,17 @@ function createCloudInitScript({
59
73
  guestToken,
60
74
  guestPayloadPath = '/var/lib/neoagent/guest-payload.tar.gz',
61
75
  guestAgentPort = 8421,
76
+ runtimeProfile = 'browser_cli',
62
77
  }) {
78
+ const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
79
+ const includeBrowser = normalizedProfile === 'browser_cli';
80
+ const guestUtilityPackages = includeBrowser
81
+ ? 'curl ca-certificates gnupg git rsync unzip xvfb dbus-x11'
82
+ : 'curl ca-certificates gnupg git rsync unzip dbus-x11 adb';
63
83
  const guestTokenB64 = encodeGuestToken(guestToken);
64
84
  const envFile = '/etc/neoagent/neoagent.env';
65
85
  const appDir = '/opt/neoagent';
86
+ const playwrightBrowsersPath = `${appDir}/.playwright-browsers`;
66
87
  const bootstrapMarker = '/var/lib/neoagent/bootstrap-complete';
67
88
  const browserReadyMarker = '/var/lib/neoagent/browser-runtime-ready';
68
89
  const browserDepsMarker = '/var/lib/neoagent/browser-deps-installed';
@@ -74,6 +95,7 @@ function createCloudInitScript({
74
95
  '',
75
96
  'export DEBIAN_FRONTEND=noninteractive',
76
97
  `APP_DIR=${JSON.stringify(appDir)}`,
98
+ `PLAYWRIGHT_BROWSERS_PATH=${JSON.stringify(playwrightBrowsersPath)}`,
77
99
  `BOOTSTRAP_MARKER=${JSON.stringify(bootstrapMarker)}`,
78
100
  `BROWSER_READY_MARKER=${JSON.stringify(browserReadyMarker)}`,
79
101
  `BROWSER_DEPS_MARKER=${JSON.stringify(browserDepsMarker)}`,
@@ -123,53 +145,47 @@ function createCloudInitScript({
123
145
  '',
124
146
  `printf '%s\n' ${JSON.stringify(`NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`)} > "$ENV_FILE"`,
125
147
  `printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`)} >> "$ENV_FILE"`,
148
+ `printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_PROFILE=${normalizedProfile}`)} >> "$ENV_FILE"`,
126
149
  'chmod 0600 "$ENV_FILE"',
127
150
  '',
128
151
  'cd "$APP_DIR"',
129
152
  'export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
153
+ 'export PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH"',
130
154
  'if [ ! -d node_modules ] || [ ! -f node_modules/.neoagent-bootstrap-stamp ] || [ package.json -nt node_modules/.neoagent-bootstrap-stamp ]; then',
131
155
  ' echo "Installing npm dependencies..."',
132
- ' retry_cmd npm cache clean --force',
133
- ' retry_cmd npm install --omit=dev --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
156
+ ' retry_cmd npm install --omit=dev --ignore-scripts --prefer-offline --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
134
157
  ' mkdir -p node_modules',
135
158
  ' date > node_modules/.neoagent-bootstrap-stamp',
136
159
  'fi',
137
160
  '',
138
- 'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
139
- ' echo "Updating package lists..."',
140
- ' retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
141
- '',
142
- ' echo "Installing browser runtime dependencies..."',
143
- ' retry_cmd apt-get install -y --no-install-recommends \\',
144
- ' curl ca-certificates gnupg git rsync unzip \\',
145
- ' dbus-x11 \\',
146
- ' xvfb \\',
147
- ' libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \\',
148
- ' libx11-xcb1 libgtk-3-0 libnss3 libnspr4 libxcomposite1 libxdamage1 \\',
149
- ' libxrandr2 libxkbcommon0 libasound2t64 libgbm1 libdrm2 libdbus-1-3 \\',
150
- ' libpango-1.0-0 libpangocairo-1.0-0 libxshmfence1 || echo "Warning: Some browser dependencies failed to install."',
151
- ' touch "$BROWSER_DEPS_MARKER"',
152
- 'else',
153
- ' echo "Browser runtime dependencies already installed; skipping apt install."',
154
- 'fi',
155
161
  '',
156
- 'systemctl daemon-reload',
157
- 'systemctl enable neoagent-guest-agent.service || true',
158
- 'if ! systemctl is-active --quiet neoagent-guest-agent.service; then',
159
- ' systemctl start neoagent-guest-agent.service || true',
160
- 'fi',
161
- 'echo "NeoAgent guest agent is available; continuing browser runtime provisioning..."',
162
+ 'echo "Ensuring guest runtime utilities..."',
163
+ 'retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
164
+ `retry_cmd apt-get install -y --no-install-recommends ${guestUtilityPackages} || { echo "Error: Failed to install required guest runtime utilities." >&2; exit 1; }`,
162
165
  '',
163
- 'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
164
- 'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.firefox-installed"',
165
- 'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
166
- 'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
167
- ' echo "Installing Playwright browsers..."',
168
- ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install firefox || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
169
- ' date > "$PLAYWRIGHT_STAMP"',
170
- 'fi',
166
+ 'echo "NeoAgent guest runtime payload is ready."',
171
167
  '',
172
- 'touch "$BROWSER_READY_MARKER"',
168
+ ...(includeBrowser
169
+ ? [
170
+ 'echo "Continuing browser runtime provisioning..."',
171
+ 'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
172
+ 'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.chromium-installed"',
173
+ 'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
174
+ 'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
175
+ ' echo "Installing Playwright browser dependencies..."',
176
+ ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install-deps chromium || { echo "Error: Playwright dependency install failed." >&2; exit 1; }',
177
+ ' touch "$BROWSER_DEPS_MARKER"',
178
+ 'fi',
179
+ 'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
180
+ ' echo "Installing Playwright browsers..."',
181
+ ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install chromium || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
182
+ ' date > "$PLAYWRIGHT_STAMP"',
183
+ 'fi',
184
+ 'touch "$BROWSER_READY_MARKER"',
185
+ ]
186
+ : [
187
+ 'rm -f "$BROWSER_READY_MARKER"',
188
+ ]),
173
189
  'touch "$BOOTSTRAP_MARKER"',
174
190
  'echo "NeoAgent guest bootstrap completed."',
175
191
  '',
@@ -178,16 +194,109 @@ function createCloudInitScript({
178
194
 
179
195
  function createCloudInitUserData({
180
196
  guestToken,
181
- guestPayloadBase64,
197
+ guestPayloadBase64 = '',
182
198
  guestAgentPort = 8421,
199
+ runtimeMode = 'template',
200
+ runtimeProfile = 'browser_cli',
183
201
  }) {
202
+ const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
203
+ const includeBrowser = normalizedProfile === 'browser_cli';
204
+ const guestAgentInnerCommand = includeBrowser
205
+ ? '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'
206
+ : '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';
207
+ const guestAgentLaunchCommand = `nohup /bin/sh -lc ${JSON.stringify(guestAgentInnerCommand)} </dev/null >/dev/null 2>&1 &`;
184
208
  const guestTokenB64 = encodeGuestToken(guestToken);
185
209
  const bootstrapScript = createCloudInitScript({
186
210
  guestToken,
187
211
  guestPayloadPath: '/var/lib/neoagent/guest-payload.tar.gz',
188
212
  guestAgentPort,
213
+ runtimeProfile: normalizedProfile,
189
214
  });
190
215
 
216
+ if (runtimeMode === 'user') {
217
+ return [
218
+ '#cloud-config',
219
+ 'package_update: false',
220
+ 'write_files:',
221
+ ' - path: /etc/neoagent/neoagent.env',
222
+ " permissions: '0600'",
223
+ ' owner: root:root',
224
+ ' content: |',
225
+ ` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
226
+ ` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
227
+ ` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
228
+ ...(includeBrowser
229
+ ? [
230
+ ' - path: /etc/systemd/system/neoagent-xvfb.service',
231
+ " permissions: '0644'",
232
+ ' owner: root:root',
233
+ ' content: |',
234
+ ' [Unit]',
235
+ ' Description=NeoAgent virtual display',
236
+ ' After=network-online.target',
237
+ ' Wants=network-online.target',
238
+ '',
239
+ ' [Service]',
240
+ ' Type=simple',
241
+ ' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
242
+ ' Restart=always',
243
+ ' RestartSec=2',
244
+ ' StandardOutput=journal+console',
245
+ ' StandardError=journal+console',
246
+ '',
247
+ ' [Install]',
248
+ ' WantedBy=multi-user.target',
249
+ ]
250
+ : []),
251
+ ' - path: /etc/systemd/system/neoagent-guest-agent.service',
252
+ " permissions: '0644'",
253
+ ' owner: root:root',
254
+ ' content: |',
255
+ ' [Unit]',
256
+ ' Description=NeoAgent guest agent',
257
+ ' After=network-online.target',
258
+ ...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
259
+ ' ConditionPathExists=/etc/neoagent/neoagent.env',
260
+ ' Wants=network-online.target',
261
+ '',
262
+ ' [Service]',
263
+ ' Type=simple',
264
+ ' EnvironmentFile=/etc/neoagent/neoagent.env',
265
+ ' ExecStartPre=/bin/mkdir -p /var/lib/neoagent',
266
+ ...(includeBrowser
267
+ ? [
268
+ ' ExecStartPre=/usr/bin/touch /var/lib/neoagent/browser-runtime-ready',
269
+ ' ExecStartPre=/bin/sh -lc \'for _ in $(seq 1 30); do [ -S /tmp/.X11-unix/X99 ] && exit 0; sleep 1; done; exit 1\'',
270
+ ' Environment=DISPLAY=:99',
271
+ ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
272
+ ]
273
+ : [
274
+ ' ExecStartPre=/bin/sh -lc \'rm -f /var/lib/neoagent/browser-runtime-ready || true\'',
275
+ ]),
276
+ ' ExecStartPre=/usr/bin/touch /var/lib/neoagent/bootstrap-complete',
277
+ ' WorkingDirectory=/opt/neoagent',
278
+ ' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
279
+ ' Restart=always',
280
+ ' RestartSec=5',
281
+ ' StandardOutput=journal+console',
282
+ ' StandardError=journal+console',
283
+ '',
284
+ ' [Install]',
285
+ ' WantedBy=multi-user.target',
286
+ 'runcmd:',
287
+ ' - [bash, -lc, "systemctl daemon-reload"]',
288
+ ...(includeBrowser
289
+ ? [
290
+ ' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
291
+ ' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
292
+ ]
293
+ : []),
294
+ ' - [bash, -lc, "systemctl enable neoagent-guest-agent.service"]',
295
+ ' - [bash, -lc, "systemctl start --no-block neoagent-guest-agent.service"]',
296
+ '',
297
+ ].join('\n');
298
+ }
299
+
191
300
  return [
192
301
  '#cloud-config',
193
302
  'package_update: false',
@@ -195,9 +304,10 @@ function createCloudInitUserData({
195
304
  ' - path: /etc/neoagent/neoagent.env',
196
305
  " permissions: '0600'",
197
306
  ' owner: root:root',
198
- ' content: |',
307
+ ' content: |',
199
308
  ` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
200
309
  ` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
310
+ ` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
201
311
  ' - path: /var/lib/neoagent/guest-payload.tar.gz',
202
312
  " permissions: '0644'",
203
313
  ' owner: root:root',
@@ -216,12 +326,22 @@ function createCloudInitUserData({
216
326
  ' [Unit]',
217
327
  ' Description=NeoAgent guest agent',
218
328
  ' After=network-online.target',
329
+ ' After=cloud-final.service',
330
+ ' After=neoagent-guest-bootstrap.service',
331
+ ...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
332
+ ' ConditionPathExists=/etc/neoagent/neoagent.env',
333
+ ...(includeBrowser ? [' Requires=neoagent-xvfb.service'] : []),
219
334
  ' Wants=network-online.target',
220
335
  '',
221
336
  ' [Service]',
222
337
  ' Type=simple',
223
338
  ' EnvironmentFile=/etc/neoagent/neoagent.env',
224
- ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
339
+ ...(includeBrowser
340
+ ? [
341
+ ' Environment=DISPLAY=:99',
342
+ ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
343
+ ]
344
+ : []),
225
345
  ' WorkingDirectory=/opt/neoagent',
226
346
  ' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
227
347
  ' Restart=always',
@@ -231,6 +351,29 @@ function createCloudInitUserData({
231
351
  '',
232
352
  ' [Install]',
233
353
  ' WantedBy=multi-user.target',
354
+ ...(includeBrowser
355
+ ? [
356
+ ' - path: /etc/systemd/system/neoagent-xvfb.service',
357
+ " permissions: '0644'",
358
+ ' owner: root:root',
359
+ ' content: |',
360
+ ' [Unit]',
361
+ ' Description=NeoAgent virtual display',
362
+ ' After=network-online.target',
363
+ ' Wants=network-online.target',
364
+ '',
365
+ ' [Service]',
366
+ ' Type=simple',
367
+ ' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
368
+ ' Restart=always',
369
+ ' RestartSec=2',
370
+ ' StandardOutput=journal+console',
371
+ ' StandardError=journal+console',
372
+ '',
373
+ ' [Install]',
374
+ ' WantedBy=multi-user.target',
375
+ ]
376
+ : []),
234
377
  ' - path: /etc/systemd/system/neoagent-guest-bootstrap.service',
235
378
  " permissions: '0644'",
236
379
  ' owner: root:root',
@@ -249,8 +392,14 @@ function createCloudInitUserData({
249
392
  ' WantedBy=multi-user.target',
250
393
  'runcmd:',
251
394
  ' - [bash, -lc, "systemctl daemon-reload"]',
252
- ' - [bash, -lc, "systemctl enable neoagent-guest-bootstrap.service"]',
253
- ' - [bash, -lc, "systemctl start neoagent-guest-bootstrap.service"]',
395
+ ...(includeBrowser
396
+ ? [
397
+ ' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
398
+ ' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
399
+ ]
400
+ : []),
401
+ ' - [bash, -lc, "/usr/local/bin/neoagent-guest-bootstrap.sh"]',
402
+ ` - [bash, -lc, ${JSON.stringify(guestAgentLaunchCommand)}]`,
254
403
  '',
255
404
  ].join('\n');
256
405
  }
@@ -263,6 +412,21 @@ function createCloudInitMetaData({ instanceId, localHostName }) {
263
412
  ].join('\n');
264
413
  }
265
414
 
415
+ function resolveCloudInitIdentity(userRoot) {
416
+ const relativePath = path.relative(VM_ROOT, path.resolve(userRoot || ''));
417
+ const normalized = relativePath
418
+ .split(path.sep)
419
+ .filter(Boolean)
420
+ .map((segment) => segment.replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 32))
421
+ .filter(Boolean)
422
+ .join('-');
423
+ const scope = normalized || 'default';
424
+ return {
425
+ instanceId: `neoagent-${scope}`,
426
+ localHostName: `neoagent-${scope}`,
427
+ };
428
+ }
429
+
266
430
  function commandExists(command) {
267
431
  const probe = spawnSync(
268
432
  process.platform === 'win32' ? 'where' : 'bash',
@@ -413,6 +577,8 @@ function ensureGuestBootstrapSeed({
413
577
  guestToken,
414
578
  guestAgentPort = 8421,
415
579
  guestArch = 'x64',
580
+ runtimeMode = 'template',
581
+ runtimeProfile = 'browser_cli',
416
582
  }) {
417
583
  const seedRoot = path.join(userRoot, 'cloud-init');
418
584
  const seedDir = path.join(seedRoot, 'seed');
@@ -423,17 +589,18 @@ function ensureGuestBootstrapSeed({
423
589
  const userDataPath = path.join(seedDir, 'user-data');
424
590
  const metaDataPath = path.join(seedDir, 'meta-data');
425
591
  const startupNshPath = path.join(seedDir, 'startup.nsh');
426
- const guestPayloadArchivePath = createGuestPayloadArchive(seedDir);
427
- const guestPayloadBase64 = fs.readFileSync(guestPayloadArchivePath).toString('base64');
592
+ const guestPayloadBase64 = runtimeMode === 'user'
593
+ ? ''
594
+ : fs.readFileSync(createGuestPayloadArchive(seedDir, runtimeProfile)).toString('base64');
428
595
  const userData = createCloudInitUserData({
429
596
  guestToken,
430
597
  guestPayloadBase64,
431
598
  guestAgentPort,
599
+ runtimeMode,
600
+ runtimeProfile,
432
601
  });
433
- const metaData = createCloudInitMetaData({
434
- instanceId: `neoagent-${path.basename(userRoot)}`,
435
- localHostName: `neoagent-${path.basename(userRoot)}`,
436
- });
602
+ const identity = resolveCloudInitIdentity(userRoot);
603
+ const metaData = createCloudInitMetaData(identity);
437
604
  const startupNsh = guestArch === 'arm64'
438
605
  ? [
439
606
  '@echo -off',
@@ -480,6 +647,7 @@ function ensureGuestBootstrapSeed({
480
647
  module.exports = {
481
648
  createCloudInitMetaData,
482
649
  createCloudInitUserData,
650
+ createCloudInitScript,
483
651
  createSeedIso,
484
652
  ensureGuestBootstrapSeed,
485
653
  GUEST_BOOTSTRAP_ROOT,
@@ -2,14 +2,23 @@ const { LocalVmExecutionBackend } = require('./backends/local-vm');
2
2
  const { QemuVmManager } = require('./qemu');
3
3
  const { getRuntimeSettings } = require('./settings');
4
4
  const { ExtensionBrowserProvider } = require('../browser/extension/provider');
5
+ const { AndroidController } = require('../android/controller');
5
6
 
6
7
  class RuntimeManager {
7
8
  constructor(options = {}) {
8
9
  this.browserExtensionRegistry = options.browserExtensionRegistry || null;
9
- this.vmBackend = new LocalVmExecutionBackend({
10
- vmManager: options.vmManager || new QemuVmManager(),
10
+ const browserVmManager = options.browserVmManager || new QemuVmManager({
11
+ runtimeProfile: 'browser_cli',
12
+ memoryMb: 2048,
13
+ cpus: 2,
14
+ warmup: false,
15
+ });
16
+ this.browserBackend = new LocalVmExecutionBackend({
17
+ runtimeProfile: 'browser_cli',
18
+ vmManager: browserVmManager,
11
19
  artifactStore: options.artifactStore,
12
20
  });
21
+ this.androidControllers = new Map();
13
22
  this.getExtensionBrowserProvider = options.getExtensionBrowserProvider || ((userId) => new ExtensionBrowserProvider({
14
23
  registry: options.browserExtensionRegistry,
15
24
  artifactStore: options.artifactStore,
@@ -31,25 +40,27 @@ class RuntimeManager {
31
40
 
32
41
  resolveBackend(userId, requested) {
33
42
  void userId;
34
- void requested;
35
- return this.vmBackend;
43
+ return this.browserBackend;
36
44
  }
37
45
 
38
46
  async executeCommand(userId, command, options = {}) {
39
- const backend = this.resolveBackend(userId, options.backend);
47
+ const backend = this.resolveBackend(userId, 'browser_cli');
40
48
  return backend.executeCommand(userId, command, options);
41
49
  }
42
50
 
43
- hasVmForUser(userId) {
44
- return Boolean(this.vmBackend?.vmManager?.hasVm?.(userId));
51
+ hasVmForUser(userId, capability = 'browser') {
52
+ if (capability === 'android') {
53
+ return Boolean(this.androidControllers.get(String(userId || '').trim()));
54
+ }
55
+ return Boolean(this.browserBackend?.vmManager?.hasVm?.(userId));
45
56
  }
46
57
 
47
58
  async killCommand(userId, pid, reason = 'aborted') {
48
- return this.vmBackend.killCommand(userId, pid, reason);
59
+ return this.browserBackend.killCommand(userId, pid, reason);
49
60
  }
50
61
 
51
62
  async getCommandExecutorForUser(userId) {
52
- return this.vmBackend.getCommandExecutorForUser(userId);
63
+ return this.browserBackend.getCommandExecutorForUser(userId);
53
64
  }
54
65
 
55
66
  async getBrowserProviderForUser(userId) {
@@ -57,22 +68,49 @@ class RuntimeManager {
57
68
  if (settings.browser_backend === 'extension' && this.hasActiveExtensionBrowser(userId)) {
58
69
  return this.getExtensionBrowserProvider(userId);
59
70
  }
60
- return this.vmBackend.getBrowserProviderForUser(userId);
71
+ return this.browserBackend.getBrowserProviderForUser(userId);
61
72
  }
62
73
 
63
74
  async getAndroidProviderForUser(userId) {
64
- return this.vmBackend.getAndroidProviderForUser(userId);
75
+ const key = String(userId || '').trim();
76
+ if (!key) {
77
+ throw new Error('Android provider requires a user ID.');
78
+ }
79
+ if (!this.androidControllers.has(key)) {
80
+ this.androidControllers.set(key, new AndroidController({
81
+ userId: key,
82
+ runtimeBackend: 'host',
83
+ artifactStore: null,
84
+ }));
85
+ }
86
+ return this.androidControllers.get(key);
65
87
  }
66
88
 
67
- async isGuestAgentReadyForUser(userId, timeoutMs = 1000) {
68
- if (typeof this.vmBackend?.isGuestAgentReadyForUser !== 'function') {
89
+ async isGuestAgentReadyForUser(userId, timeoutMs = 1000, capability = 'browser') {
90
+ if (capability === 'android') {
91
+ const controller = this.androidControllers.get(String(userId || '').trim());
92
+ if (!controller || typeof controller.getStatus !== 'function') {
93
+ return false;
94
+ }
95
+ try {
96
+ const status = await controller.getStatus();
97
+ return Boolean(status?.bootstrapped || status?.serial || status?.starting);
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+ if (typeof this.browserBackend?.isGuestAgentReadyForUser !== 'function') {
69
103
  return false;
70
104
  }
71
- return this.vmBackend.isGuestAgentReadyForUser(userId, timeoutMs);
105
+ return this.browserBackend.isGuestAgentReadyForUser(userId, timeoutMs);
72
106
  }
73
107
 
74
108
  async shutdown() {
75
- await Promise.allSettled([this.vmBackend.shutdown()]);
109
+ await Promise.allSettled([
110
+ this.browserBackend?.shutdown?.(),
111
+ ...[...this.androidControllers.values()].map((controller) => controller?.stopEmulator?.().catch?.(() => {})),
112
+ ]);
113
+ this.androidControllers.clear();
76
114
  }
77
115
  }
78
116