neoagent 2.3.1-beta.70 → 2.3.1-beta.72

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.
@@ -134,6 +134,8 @@ Runtime profile and backend selection are stored in user settings, not normally
134
134
 
135
135
  Production policy can require the VM backend. In that case, set a strong `NEOAGENT_VM_GUEST_TOKEN` of at least 32 characters and avoid placeholder values.
136
136
 
137
+ The VM backend requires QEMU on the host machine. It runs an x86_64 Ubuntu guest so the Android emulator can run in the same isolated runtime as the browser. On macOS, install QEMU with `brew install qemu`. On Ubuntu or Debian, install `qemu-system` and `qemu-utils` from apt before starting NeoAgent.
138
+
137
139
  The app exposes two browser backend choices: VM and Chrome extension. VM uses the local isolated runtime. Chrome extension uses the paired extension connection on the remote machine instead of the server-local browser. To install only the extension on a remote machine, open NeoAgent, download `/api/browser-extension/download`, unzip it, load the folder through `chrome://extensions` with Developer mode enabled, then pair after logging in to NeoAgent. Unpacked Chrome extensions cannot replace themselves automatically; use the extension popup's update check to compare against the server bundle, then download and reload the latest ZIP when needed.
138
140
 
139
141
  ## Secrets Guidance
@@ -5,10 +5,27 @@ NeoAgent installs as a Node CLI and runs a self-hosted server with a bundled Flu
5
5
  ## Requirements
6
6
 
7
7
  - Node.js 20 or newer.
8
+ - QEMU for VM-backed browser and Android runs.
8
9
  - A reachable server URL if you want OAuth callbacks, mobile access, or messaging webhooks.
9
10
  - At least one hosted AI provider API key, unless you only use local Ollama.
10
11
  - Android Studio or a Flutter Android toolchain if you build the Android client yourself.
11
12
 
13
+ ### QEMU Installation
14
+
15
+ NeoAgent uses a per-user x86_64 VM for browser and Android execution. Install QEMU before starting the service:
16
+
17
+ ```bash
18
+ # macOS
19
+ brew install qemu
20
+
21
+ # Ubuntu / Debian
22
+ sudo apt-get update
23
+ sudo apt-get install -y qemu-system qemu-utils
24
+ ```
25
+
26
+ The first VM boot also downloads the Ubuntu base image and seeds the guest runtime automatically.
27
+ That guest bootstrap installs the browser and Android runtime dependencies it needs, including Java and the emulator support packages.
28
+
12
29
  ## Install
13
30
 
14
31
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.70",
3
+ "version": "2.3.1-beta.72",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
package/runtime/paths.js CHANGED
@@ -135,7 +135,7 @@ function generateSecret(bytes = 32) {
135
135
  return crypto.randomBytes(bytes).toString('hex');
136
136
  }
137
137
 
138
- function getDefaultVmBaseImageUrl(arch = process.arch) {
138
+ function getDefaultVmBaseImageUrl(arch = 'x64') {
139
139
  return arch === 'arm64' ? DEFAULT_VM_BASE_IMAGE_URLS.arm64 : DEFAULT_VM_BASE_IMAGE_URLS.x64;
140
140
  }
141
141
 
@@ -173,8 +173,9 @@ function ensureSecureRuntimeEnv({ envFile = ENV_FILE, env = process.env, logger
173
173
  }
174
174
 
175
175
  let vmBaseImageUrl = String(env.NEOAGENT_VM_BASE_IMAGE_URL || parsed.get('NEOAGENT_VM_BASE_IMAGE_URL') || '').trim();
176
- if (!vmBaseImageUrl) {
177
- vmBaseImageUrl = getDefaultVmBaseImageUrl();
176
+ const preferredVmBaseImageUrl = getDefaultVmBaseImageUrl();
177
+ if (!vmBaseImageUrl || /arm64|aarch64/i.test(vmBaseImageUrl)) {
178
+ vmBaseImageUrl = preferredVmBaseImageUrl;
178
179
  env.NEOAGENT_VM_BASE_IMAGE_URL = vmBaseImageUrl;
179
180
  upsertEnvValue(envFile, 'NEOAGENT_VM_BASE_IMAGE_URL', vmBaseImageUrl);
180
181
  changes.push('NEOAGENT_VM_BASE_IMAGE_URL');
@@ -0,0 +1,8 @@
1
+ # NeoAgent Guest Agent Dependencies
2
+
3
+ This guest runtime uses both browser automation packages for distinct roles:
4
+
5
+ - puppeteer-core: used by server/services/browser/controller.js to drive the browser.
6
+ - playwright-chromium: supplies the bundled Chromium binary and installer used by resolveBrowserExecutablePath() and installPlaywrightChromiumBinary().
7
+
8
+ Keeping both avoids bundling a second browser downloader while still using Puppeteer's API surface.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "neoagent-guest-agent",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services (uses puppeteer-core with playwright-chromium browser binaries)",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "dependencies": {
10
+ "express": "^4.21.2",
11
+ "node-pty": "^1.0.0",
12
+ "playwright-chromium": "^1.59.1",
13
+ "proper-lockfile": "^4.1.2",
14
+ "puppeteer-core": "^24.40.0"
15
+ }
16
+ }
@@ -10,7 +10,19 @@ const { AndroidController } = require('./services/android/controller');
10
10
  const { RUNTIME_HOME } = require('../runtime/paths');
11
11
 
12
12
  const PORT = Number(process.env.NEOAGENT_GUEST_AGENT_PORT || 8421);
13
- const AUTH_TOKEN = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
13
+ function resolveGuestToken() {
14
+ const raw = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
15
+ if (raw) return raw;
16
+ const b64 = String(process.env.NEOAGENT_VM_GUEST_TOKEN_B64 || '').trim();
17
+ if (!b64) return '';
18
+ try {
19
+ return Buffer.from(b64, 'base64').toString('utf8').trim();
20
+ } catch {
21
+ return '';
22
+ }
23
+ }
24
+
25
+ const AUTH_TOKEN = resolveGuestToken();
14
26
  const FILE_ROOT = path.join(RUNTIME_HOME, 'guest-agent-files');
15
27
  const MAX_APK_STREAM_BYTES = Number(process.env.NEOAGENT_GUEST_MAX_APK_STREAM_BYTES || 512 * 1024 * 1024);
16
28
 
package/server/index.js CHANGED
@@ -210,8 +210,17 @@ function closeHttpServer(server, sockets, timeoutMs = 5000) {
210
210
  });
211
211
  }
212
212
 
213
+ function normalizeShutdownExitCode(value) {
214
+ const code = Number(value);
215
+ if (Number.isFinite(code)) {
216
+ return code;
217
+ }
218
+ return 1;
219
+ }
220
+
213
221
  async function shutdown(exitCode = 0) {
214
- shutdownExitCode = Math.max(shutdownExitCode, exitCode);
222
+ const normalizedExitCode = normalizeShutdownExitCode(exitCode);
223
+ shutdownExitCode = Math.max(shutdownExitCode, normalizedExitCode);
215
224
  if (shuttingDown) return;
216
225
  shuttingDown = true;
217
226
 
@@ -224,7 +233,7 @@ async function shutdown(exitCode = 0) {
224
233
  ]);
225
234
 
226
235
  db.close();
227
- process.exit(shutdownExitCode);
236
+ process.exit(normalizeShutdownExitCode(shutdownExitCode));
228
237
  }
229
238
 
230
239
  httpServer.listen(PORT, () => {
@@ -1 +1 @@
1
- 5938e78ce8065f2bdbeea2a8ccd118fb
1
+ e5ba48ddcff36fe434ca4a61fcdffaa6
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "2845487408" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2369394308" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -127230,7 +127230,7 @@ r===$&&A.b()
127230
127230
  o.push(A.id(p,A.iS(!1,new A.a3(B.tE,A.dZ(new A.cU(B.h8,new A.a5p(r,p),p),p,p),p),!1,B.I,!0),p,p,0,0,0,p))}r=!1
127231
127231
  if(!s.ay)if(!s.ch){r=s.e
127232
127232
  r===$&&A.b()
127233
- r=B.b.A("mp10yphj-a1afcb2").length!==0&&r.b}if(r){r=s.d
127233
+ r=B.b.A("mp2b0p01-89c158b").length!==0&&r.b}if(r){r=s.d
127234
127234
  r===$&&A.b()
127235
127235
  r=r.V&&!r.a0?84:0
127236
127236
  q=s.e
@@ -131894,7 +131894,7 @@ $S:324}
131894
131894
  A.Y0.prototype={}
131895
131895
  A.R0.prototype={
131896
131896
  mJ(a){var s=this
131897
- if(B.b.A("mp10yphj-a1afcb2").length===0||s.a!=null)return
131897
+ if(B.b.A("mp2b0p01-89c158b").length===0||s.a!=null)return
131898
131898
  s.zY()
131899
131899
  s.a=A.pN(B.Pr,new A.b3g(s))},
131900
131900
  zY(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
@@ -131912,7 +131912,7 @@ if(!t.f.b(k)){s=1
131912
131912
  break}i=J.Z(k,"buildId")
131913
131913
  h=i==null?null:B.b.A(J.r(i))
131914
131914
  j=h==null?"":h
131915
- if(J.bi(j)===0||J.c(j,"mp10yphj-a1afcb2")){s=1
131915
+ if(J.bi(j)===0||J.c(j,"mp2b0p01-89c158b")){s=1
131916
131916
  break}n.b=!0
131917
131917
  n.J()
131918
131918
  p=2
@@ -131929,7 +131929,7 @@ case 2:return A.i(o.at(-1),r)}})
131929
131929
  return A.k($async$zY,r)},
131930
131930
  v_(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
131931
131931
  var $async$v_=A.h(function(a2,a3){if(a2===1){o.push(a3)
131932
- s=p}for(;;)switch(s){case 0:if(B.b.A("mp10yphj-a1afcb2").length===0||n.c){s=1
131932
+ s=p}for(;;)switch(s){case 0:if(B.b.A("mp2b0p01-89c158b").length===0||n.c){s=1
131933
131933
  break}n.c=!0
131934
131934
  n.J()
131935
131935
  p=4
@@ -6,6 +6,7 @@ const router = express.Router();
6
6
  const { DATA_DIR } = require('../../runtime/paths');
7
7
  const { requireAuth } = require('../middleware/auth');
8
8
  const { sanitizeError } = require('../utils/security');
9
+ const { getRuntimeValidation } = require('../services/runtime/validation');
9
10
 
10
11
  router.use(requireAuth);
11
12
 
@@ -50,6 +51,18 @@ async function getAndroidController(req) {
50
51
  throw new Error('Android controller is unavailable. VM runtime is required.');
51
52
  }
52
53
 
54
+ function getAndroidStatusSnapshot(req) {
55
+ const runtimeValidation = getRuntimeValidation(req.app?.locals?.runtimeManager);
56
+ const ready = Boolean(runtimeValidation?.ready);
57
+ return {
58
+ bootstrapped: false,
59
+ canBootstrap: ready,
60
+ devices: [],
61
+ lastStartError: ready ? null : (runtimeValidation?.issues?.[0] || 'VM runtime is not ready.'),
62
+ runtimeReady: ready,
63
+ };
64
+ }
65
+
53
66
  function handleAndroidAction(action) {
54
67
  return async (req, res) => {
55
68
  try {
@@ -62,7 +75,23 @@ function handleAndroidAction(action) {
62
75
  };
63
76
  }
64
77
 
65
- router.get('/status', handleAndroidAction((controller) => controller.getStatus()));
78
+ router.get('/status', async (req, res) => {
79
+ try {
80
+ const runtimeManager = req.app?.locals?.runtimeManager;
81
+ if (!runtimeManager?.hasVmForUser?.(req.session?.userId)) {
82
+ res.json(getAndroidStatusSnapshot(req));
83
+ return;
84
+ }
85
+ if (!await runtimeManager?.isGuestAgentReadyForUser?.(req.session?.userId, 1000)) {
86
+ res.json(getAndroidStatusSnapshot(req));
87
+ return;
88
+ }
89
+ const controller = await getAndroidController(req);
90
+ res.json(await controller.getStatus());
91
+ } catch (err) {
92
+ res.status(500).json({ error: sanitizeError(err) });
93
+ }
94
+ });
66
95
 
67
96
  router.post('/start', handleAndroidAction((controller, req) =>
68
97
  controller.requestStartEmulator(req.body || {})));
@@ -2,6 +2,7 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { requireAuth } = require('../middleware/auth');
4
4
  const { sanitizeError } = require('../utils/security');
5
+ const { getRuntimeValidation } = require('../services/runtime/validation');
5
6
 
6
7
  router.use(requireAuth);
7
8
 
@@ -16,9 +17,33 @@ async function getBrowserController(req) {
16
17
  throw new Error('Browser controller is unavailable. VM runtime is required.');
17
18
  }
18
19
 
20
+ function getBrowserStatusSnapshot(req) {
21
+ const runtimeValidation = getRuntimeValidation(req.app?.locals?.runtimeManager);
22
+ const ready = Boolean(runtimeValidation?.ready);
23
+ return {
24
+ launched: false,
25
+ pages: 0,
26
+ headless: true,
27
+ pageInfo: null,
28
+ bootstrapped: false,
29
+ canBootstrap: ready,
30
+ runtimeReady: ready,
31
+ lastStartError: ready ? null : (runtimeValidation?.issues?.[0] || 'VM runtime is not ready.'),
32
+ };
33
+ }
34
+
19
35
  // Get browser status
20
36
  router.get('/status', async (req, res) => {
21
37
  try {
38
+ const runtimeManager = req.app?.locals?.runtimeManager;
39
+ if (!runtimeManager?.hasVmForUser?.(req.session?.userId)) {
40
+ res.json(getBrowserStatusSnapshot(req));
41
+ return;
42
+ }
43
+ if (!await runtimeManager?.isGuestAgentReadyForUser?.(req.session?.userId, 1000)) {
44
+ res.json(getBrowserStatusSnapshot(req));
45
+ return;
46
+ }
22
47
  const bc = await getBrowserController(req);
23
48
  const pageInfo = await bc.getPageInfo();
24
49
  res.json({
@@ -26,6 +51,10 @@ router.get('/status', async (req, res) => {
26
51
  pages: await Promise.resolve(bc.getPageCount()),
27
52
  headless: bc.headless,
28
53
  pageInfo,
54
+ bootstrapped: true,
55
+ canBootstrap: true,
56
+ runtimeReady: true,
57
+ lastStartError: null,
29
58
  });
30
59
  } catch (err) {
31
60
  res.status(500).json({ error: sanitizeError(err) });
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const { AndroidController } = require('./controller');
4
+
5
+ function parseBoolean(value) {
6
+ const normalized = String(value || '').trim().toLowerCase();
7
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
8
+ }
9
+
10
+ async function main() {
11
+ const controller = new AndroidController({
12
+ userId: process.env.NEOAGENT_ANDROID_BOOTSTRAP_USER_ID || null,
13
+ runtimeBackend: 'vm',
14
+ });
15
+ const headless = parseBoolean(process.env.NEOAGENT_ANDROID_BOOTSTRAP_HEADLESS);
16
+ const timeoutMs = Math.max(120000, Number(process.env.NEOAGENT_ANDROID_BOOTSTRAP_TIMEOUT_MS) || 240000);
17
+
18
+ try {
19
+ await controller.bootstrapEmulator({ headless, timeoutMs });
20
+ } catch (error) {
21
+ try {
22
+ await controller.markBootstrapFailure(error);
23
+ } catch (markError) {
24
+ try {
25
+ console.error('[Android] Failed to record bootstrap failure:', markError?.message || markError);
26
+ } catch {}
27
+ }
28
+ try {
29
+ console.error('[Android] Bootstrap worker failed:', error?.message || error);
30
+ } catch {}
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ process.on('unhandledRejection', (error) => {
36
+ try {
37
+ console.error('[Android] UnhandledRejection in bootstrap worker:', error?.message || error);
38
+ } catch {}
39
+ process.exit(1);
40
+ });
41
+
42
+ main().catch((error) => {
43
+ try {
44
+ console.error('[Android] Bootstrap worker crashed:', error?.message || error);
45
+ } catch {}
46
+ process.exit(1);
47
+ });