neoagent 2.3.1-beta.71 → 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.
- package/docs/configuration.md +2 -0
- package/docs/getting-started.md +17 -0
- package/package.json +1 -1
- package/runtime/paths.js +4 -3
- package/server/guest-agent.README.md +8 -0
- package/server/guest-agent.package.json +16 -0
- package/server/guest_agent.js +13 -1
- package/server/index.js +11 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/routes/android.js +30 -1
- package/server/routes/browser.js +29 -0
- package/server/services/android/android_bootstrap_worker.js +47 -0
- package/server/services/android/controller.js +297 -57
- package/server/services/browser/controller.js +65 -4
- package/server/services/cli/executor.js +36 -1
- package/server/services/runtime/backends/local-vm.js +97 -20
- package/server/services/runtime/guest_bootstrap.js +450 -0
- package/server/services/runtime/manager.js +11 -0
- package/server/services/runtime/qemu.js +273 -16
package/docs/configuration.md
CHANGED
|
@@ -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
|
package/docs/getting-started.md
CHANGED
|
@@ -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
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 =
|
|
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
|
-
|
|
177
|
-
|
|
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
|
+
}
|
package/server/guest_agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
e5ba48ddcff36fe434ca4a61fcdffaa6
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"42d3d75a56efe1a2e9902f52dc8006099c45d9
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
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("
|
|
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("
|
|
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,"
|
|
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("
|
|
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
|
package/server/routes/android.js
CHANGED
|
@@ -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',
|
|
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 || {})));
|
package/server/routes/browser.js
CHANGED
|
@@ -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
|
+
});
|