neoagent 2.3.1-beta.84 → 2.3.1-beta.86
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/package.json +1 -1
- package/runtime/paths.js +6 -6
- package/server/guest-agent.android.package.json +13 -0
- package/server/guest-agent.browser.package.json +14 -0
- package/server/guest_agent.js +61 -51
- 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 +2 -11
- package/server/routes/browser.js +2 -2
- package/server/services/ai/capabilityHealth.js +6 -14
- package/server/services/android/android_bootstrap_worker.js +2 -2
- package/server/services/android/controller.js +529 -133
- package/server/services/browser/controller.js +187 -42
- package/server/services/runtime/backends/local-vm.js +62 -33
- package/server/services/runtime/guest_bootstrap.js +287 -113
- package/server/services/runtime/manager.js +53 -15
- package/server/services/runtime/qemu.js +477 -86
- package/server/services/runtime/settings.js +9 -14
- package/server/services/runtime/validation.js +11 -38
- package/server/utils/deployment.js +4 -4
- package/server/guest-agent.package.json +0 -16
|
@@ -5,6 +5,25 @@ const { DATA_DIR } = require('../../../runtime/paths');
|
|
|
5
5
|
|
|
6
6
|
const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
|
|
7
7
|
const GUEST_BOOTSTRAP_ROOT = path.join(VM_ROOT, 'guest-bootstrap');
|
|
8
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
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
|
+
});
|
|
8
27
|
|
|
9
28
|
fs.mkdirSync(GUEST_BOOTSTRAP_ROOT, { recursive: true });
|
|
10
29
|
|
|
@@ -12,16 +31,62 @@ function encodeGuestToken(value) {
|
|
|
12
31
|
return Buffer.from(String(value || ''), 'utf8').toString('base64');
|
|
13
32
|
}
|
|
14
33
|
|
|
34
|
+
function normalizeRuntimeProfile(runtimeProfile) {
|
|
35
|
+
return runtimeProfile === 'android' ? 'android' : 'browser_cli';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createGuestPayloadArchive(seedDir, runtimeProfile = 'browser_cli') {
|
|
39
|
+
const seedRoot = path.dirname(seedDir);
|
|
40
|
+
const stagingRoot = path.join(seedRoot, 'guest-payload');
|
|
41
|
+
const archivePath = path.join(seedRoot, 'guest-payload.tar.gz');
|
|
42
|
+
const payloadEntries = GUEST_PAYLOAD_PROFILES[normalizeRuntimeProfile(runtimeProfile)];
|
|
43
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
44
|
+
fs.rmSync(archivePath, { force: true });
|
|
45
|
+
fs.mkdirSync(stagingRoot, { recursive: true });
|
|
46
|
+
|
|
47
|
+
for (const entry of payloadEntries) {
|
|
48
|
+
const sourcePath = path.join(REPO_ROOT, entry.source);
|
|
49
|
+
const targetPath = path.join(stagingRoot, entry.target);
|
|
50
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
51
|
+
if (fs.statSync(sourcePath).isDirectory()) {
|
|
52
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true, force: true });
|
|
53
|
+
} else {
|
|
54
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const tarResult = spawnSync('tar', ['-czf', archivePath, '-C', stagingRoot, '.'], {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
if (tarResult.status !== 0 || !fs.existsSync(archivePath)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
String(tarResult.stderr || tarResult.stdout || tarResult.error?.message || 'Failed to create guest payload archive.')
|
|
65
|
+
.trim(),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
69
|
+
return archivePath;
|
|
70
|
+
}
|
|
71
|
+
|
|
15
72
|
function createCloudInitScript({
|
|
16
73
|
guestToken,
|
|
17
|
-
|
|
18
|
-
hostDataMount = '/mnt/neoagent-data',
|
|
74
|
+
guestPayloadPath = '/var/lib/neoagent/guest-payload.tar.gz',
|
|
19
75
|
guestAgentPort = 8421,
|
|
76
|
+
runtimeProfile = 'browser_cli',
|
|
20
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';
|
|
21
83
|
const guestTokenB64 = encodeGuestToken(guestToken);
|
|
22
84
|
const envFile = '/etc/neoagent/neoagent.env';
|
|
23
85
|
const appDir = '/opt/neoagent';
|
|
86
|
+
const playwrightBrowsersPath = `${appDir}/.playwright-browsers`;
|
|
24
87
|
const bootstrapMarker = '/var/lib/neoagent/bootstrap-complete';
|
|
88
|
+
const browserReadyMarker = '/var/lib/neoagent/browser-runtime-ready';
|
|
89
|
+
const browserDepsMarker = '/var/lib/neoagent/browser-deps-installed';
|
|
25
90
|
const nodeSourceSetupUrl = 'https://deb.nodesource.com/setup_20.x';
|
|
26
91
|
|
|
27
92
|
return [
|
|
@@ -29,53 +94,21 @@ function createCloudInitScript({
|
|
|
29
94
|
'set -uo pipefail', // Removed -e to handle non-critical failures gracefully
|
|
30
95
|
'',
|
|
31
96
|
'export DEBIAN_FRONTEND=noninteractive',
|
|
32
|
-
`HOST_SHARE_MOUNT=${JSON.stringify(hostShareMount)}`,
|
|
33
|
-
`HOST_DATA_MOUNT=${JSON.stringify(hostDataMount)}`,
|
|
34
|
-
'HOST_SHARE_TAG=neoagent-host',
|
|
35
|
-
'HOST_SHARE_TAG_FALLBACK=neoagent-host-pci',
|
|
36
|
-
'HOST_DATA_TAG=neoagent-data',
|
|
37
|
-
'HOST_DATA_TAG_FALLBACK=neoagent-data-pci',
|
|
38
97
|
`APP_DIR=${JSON.stringify(appDir)}`,
|
|
98
|
+
`PLAYWRIGHT_BROWSERS_PATH=${JSON.stringify(playwrightBrowsersPath)}`,
|
|
39
99
|
`BOOTSTRAP_MARKER=${JSON.stringify(bootstrapMarker)}`,
|
|
100
|
+
`BROWSER_READY_MARKER=${JSON.stringify(browserReadyMarker)}`,
|
|
101
|
+
`BROWSER_DEPS_MARKER=${JSON.stringify(browserDepsMarker)}`,
|
|
40
102
|
`ENV_FILE=${JSON.stringify(envFile)}`,
|
|
103
|
+
`GUEST_PAYLOAD_PATH=${JSON.stringify(guestPayloadPath)}`,
|
|
41
104
|
'',
|
|
42
|
-
'mkdir -p /etc/neoagent /var/lib/neoagent "$
|
|
43
|
-
'',
|
|
44
|
-
'# Ensure the 9p virtio filesystem driver is loaded',
|
|
45
|
-
'modprobe 9p 2>/dev/null || true',
|
|
46
|
-
'modprobe 9pnet_virtio 2>/dev/null || true',
|
|
47
|
-
'',
|
|
48
|
-
'function mount_9p_tag() {',
|
|
49
|
-
' local tag="$1"',
|
|
50
|
-
' local target="$2"',
|
|
51
|
-
' local mode="$3"',
|
|
52
|
-
' mount -t 9p -o "trans=virtio,version=9p2000.L,msize=131072,${mode}" "$tag" "$target" >/dev/null 2>&1',
|
|
53
|
-
'}',
|
|
54
|
-
'',
|
|
55
|
-
'if ! mount_9p_tag "$HOST_SHARE_TAG" "$HOST_SHARE_MOUNT" ro; then',
|
|
56
|
-
' if mount_9p_tag "$HOST_SHARE_TAG_FALLBACK" "$HOST_SHARE_MOUNT" ro; then',
|
|
57
|
-
' HOST_SHARE_TAG="$HOST_SHARE_TAG_FALLBACK"',
|
|
58
|
-
' fi',
|
|
59
|
-
'fi',
|
|
60
|
-
'if ! mount_9p_tag "$HOST_DATA_TAG" "$HOST_DATA_MOUNT" rw; then',
|
|
61
|
-
' if mount_9p_tag "$HOST_DATA_TAG_FALLBACK" "$HOST_DATA_MOUNT" rw; then',
|
|
62
|
-
' HOST_DATA_TAG="$HOST_DATA_TAG_FALLBACK"',
|
|
63
|
-
' fi',
|
|
64
|
-
'fi',
|
|
65
|
-
'',
|
|
66
|
-
'if ! grep -qs "${HOST_SHARE_MOUNT}" /etc/fstab; then',
|
|
67
|
-
' echo "${HOST_SHARE_TAG} ${HOST_SHARE_MOUNT} 9p trans=virtio,version=9p2000.L,msize=262144,ro 0 0" >> /etc/fstab',
|
|
68
|
-
'fi',
|
|
69
|
-
'if ! grep -qs "${HOST_DATA_MOUNT}" /etc/fstab; then',
|
|
70
|
-
' echo "${HOST_DATA_TAG} ${HOST_DATA_MOUNT} 9p trans=virtio,version=9p2000.L,msize=262144,rw 0 0" >> /etc/fstab',
|
|
71
|
-
'fi',
|
|
72
|
-
'',
|
|
73
|
-
'mount -a >/dev/null 2>&1 || true',
|
|
105
|
+
'mkdir -p /etc/neoagent /var/lib/neoagent "$APP_DIR"',
|
|
74
106
|
'',
|
|
75
|
-
'# Redirect logs to
|
|
76
|
-
'LOG_FILE="
|
|
107
|
+
'# Redirect logs to a guest-local file and console',
|
|
108
|
+
'LOG_FILE="/var/log/neoagent-bootstrap.log"',
|
|
77
109
|
'exec > >(tee -a "$LOG_FILE" >/dev/console) 2>&1',
|
|
78
110
|
'echo "NeoAgent guest bootstrap starting..."',
|
|
111
|
+
'rm -f "$BOOTSTRAP_MARKER" "$BROWSER_READY_MARKER"',
|
|
79
112
|
'',
|
|
80
113
|
'function retry_cmd() {',
|
|
81
114
|
' local n=1',
|
|
@@ -95,49 +128,14 @@ function createCloudInitScript({
|
|
|
95
128
|
' done',
|
|
96
129
|
'}',
|
|
97
130
|
'',
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'',
|
|
101
|
-
'echo "Installing dependencies..."',
|
|
102
|
-
'retry_cmd apt-get install -y --no-install-recommends \\',
|
|
103
|
-
' curl ca-certificates gnupg openjdk-17-jre-headless git rsync build-essential \\',
|
|
104
|
-
' python3 unzip libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \\',
|
|
105
|
-
' libx11-xcb1 libgtk-3-0 libnss3 libnspr4 libxcomposite1 libxdamage1 \\',
|
|
106
|
-
' libxrandr2 libxkbcommon0 libasound2t64 libgbm1 libdrm2 libdbus-1-3 \\',
|
|
107
|
-
' libpango-1.0-0 libpangocairo-1.0-0 libxshmfence1 || echo "Warning: Some dependencies failed to install."',
|
|
108
|
-
'',
|
|
109
|
-
'if [ -d "$HOST_SHARE_MOUNT" ]; then',
|
|
110
|
-
' echo "Syncing guest agent sources..."',
|
|
111
|
-
' SYNC_PATHS=(',
|
|
112
|
-
' server/guest-agent.package.json:package.json',
|
|
113
|
-
' runtime/env.js',
|
|
114
|
-
' runtime/paths.js',
|
|
115
|
-
' server/guest_agent.js',
|
|
116
|
-
' server/services/cli',
|
|
117
|
-
' server/services/browser',
|
|
118
|
-
' server/services/android',
|
|
119
|
-
' )',
|
|
120
|
-
' for relPath in "${SYNC_PATHS[@]}"; do',
|
|
121
|
-
' sourceRelPath="${relPath%%:*}"',
|
|
122
|
-
' targetRelPath="${relPath##*:}"',
|
|
123
|
-
' sourcePath="$HOST_SHARE_MOUNT/$sourceRelPath"',
|
|
124
|
-
' targetPath="$APP_DIR/$targetRelPath"',
|
|
125
|
-
' if [ -e "$sourcePath" ]; then',
|
|
126
|
-
' mkdir -p "$(dirname "$targetPath")"',
|
|
127
|
-
' if [ -d "$sourcePath" ]; then',
|
|
128
|
-
' mkdir -p "$targetPath"',
|
|
129
|
-
' rsync -a --delete "$sourcePath"/ "$targetPath"/',
|
|
130
|
-
' else',
|
|
131
|
-
' rsync -a "$sourcePath" "$targetPath"',
|
|
132
|
-
' fi',
|
|
133
|
-
' else',
|
|
134
|
-
' echo "Warning: Optional source path missing: $relPath"',
|
|
135
|
-
' fi',
|
|
136
|
-
' done',
|
|
137
|
-
'else',
|
|
138
|
-
' echo "Error: Host repo share is not available. Bootstrap cannot continue." >&2',
|
|
131
|
+
'if [ ! -f "$GUEST_PAYLOAD_PATH" ]; then',
|
|
132
|
+
' echo "Error: Guest payload archive is missing at $GUEST_PAYLOAD_PATH." >&2',
|
|
139
133
|
' exit 1',
|
|
140
134
|
'fi',
|
|
135
|
+
'echo "Extracting guest runtime payload..."',
|
|
136
|
+
'rm -rf "$APP_DIR"',
|
|
137
|
+
'mkdir -p "$APP_DIR"',
|
|
138
|
+
'tar -xzf "$GUEST_PAYLOAD_PATH" -C "$APP_DIR" || { echo "Error: Failed to extract guest runtime payload." >&2; exit 1; }',
|
|
141
139
|
'',
|
|
142
140
|
'if ! command -v node >/dev/null 2>&1 || ! node -e "process.exit(Number(process.versions.node.split(\'.\')[0]) >= 20 ? 0 : 1)"; then',
|
|
143
141
|
' echo "Installing Node.js..."',
|
|
@@ -147,31 +145,47 @@ function createCloudInitScript({
|
|
|
147
145
|
'',
|
|
148
146
|
`printf '%s\n' ${JSON.stringify(`NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`)} > "$ENV_FILE"`,
|
|
149
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"`,
|
|
150
149
|
'chmod 0600 "$ENV_FILE"',
|
|
151
150
|
'',
|
|
152
151
|
'cd "$APP_DIR"',
|
|
152
|
+
'export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
|
|
153
|
+
'export PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH"',
|
|
153
154
|
'if [ ! -d node_modules ] || [ ! -f node_modules/.neoagent-bootstrap-stamp ] || [ package.json -nt node_modules/.neoagent-bootstrap-stamp ]; then',
|
|
154
155
|
' echo "Installing npm dependencies..."',
|
|
155
|
-
'
|
|
156
|
-
' retry_cmd npm install --omit=dev --no-audit --no-fund || echo "Warning: npm install failed."',
|
|
156
|
+
' retry_cmd npm install --omit=dev --ignore-scripts --prefer-offline --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
|
|
157
157
|
' mkdir -p node_modules',
|
|
158
158
|
' date > node_modules/.neoagent-bootstrap-stamp',
|
|
159
159
|
'fi',
|
|
160
160
|
'',
|
|
161
|
-
'# Install Playwright browser binaries',
|
|
162
|
-
'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
|
|
163
|
-
'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.chromium-installed"',
|
|
164
|
-
'if [ ! -f "$PLAYWRIGHT_STAMP" ]; then',
|
|
165
|
-
' echo "Installing Playwright browsers..."',
|
|
166
|
-
' mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
|
|
167
|
-
' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" npx playwright install chromium --with-deps || \\',
|
|
168
|
-
' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" node ./node_modules/playwright-chromium/install.js || true',
|
|
169
|
-
' date > "$PLAYWRIGHT_STAMP"',
|
|
170
|
-
'fi',
|
|
171
161
|
'',
|
|
172
|
-
'
|
|
173
|
-
'
|
|
174
|
-
|
|
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; }`,
|
|
165
|
+
'',
|
|
166
|
+
'echo "NeoAgent guest runtime payload is ready."',
|
|
167
|
+
'',
|
|
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
|
+
]),
|
|
175
189
|
'touch "$BOOTSTRAP_MARKER"',
|
|
176
190
|
'echo "NeoAgent guest bootstrap completed."',
|
|
177
191
|
'',
|
|
@@ -180,18 +194,109 @@ function createCloudInitScript({
|
|
|
180
194
|
|
|
181
195
|
function createCloudInitUserData({
|
|
182
196
|
guestToken,
|
|
183
|
-
|
|
184
|
-
hostDataMount = '/mnt/neoagent-data',
|
|
197
|
+
guestPayloadBase64 = '',
|
|
185
198
|
guestAgentPort = 8421,
|
|
199
|
+
runtimeMode = 'template',
|
|
200
|
+
runtimeProfile = 'browser_cli',
|
|
186
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 &`;
|
|
187
208
|
const guestTokenB64 = encodeGuestToken(guestToken);
|
|
188
209
|
const bootstrapScript = createCloudInitScript({
|
|
189
210
|
guestToken,
|
|
190
|
-
|
|
191
|
-
hostDataMount,
|
|
211
|
+
guestPayloadPath: '/var/lib/neoagent/guest-payload.tar.gz',
|
|
192
212
|
guestAgentPort,
|
|
213
|
+
runtimeProfile: normalizedProfile,
|
|
193
214
|
});
|
|
194
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
|
+
|
|
195
300
|
return [
|
|
196
301
|
'#cloud-config',
|
|
197
302
|
'package_update: false',
|
|
@@ -199,9 +304,16 @@ function createCloudInitUserData({
|
|
|
199
304
|
' - path: /etc/neoagent/neoagent.env',
|
|
200
305
|
" permissions: '0600'",
|
|
201
306
|
' owner: root:root',
|
|
307
|
+
' content: |',
|
|
308
|
+
` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
|
|
309
|
+
` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
|
|
310
|
+
` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
|
|
311
|
+
' - path: /var/lib/neoagent/guest-payload.tar.gz',
|
|
312
|
+
" permissions: '0644'",
|
|
313
|
+
' owner: root:root',
|
|
314
|
+
" encoding: 'b64'",
|
|
202
315
|
' content: |',
|
|
203
|
-
|
|
204
|
-
` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
|
|
316
|
+
` ${guestPayloadBase64}`,
|
|
205
317
|
' - path: /usr/local/bin/neoagent-guest-bootstrap.sh',
|
|
206
318
|
" permissions: '0755'",
|
|
207
319
|
' owner: root:root',
|
|
@@ -214,12 +326,22 @@ function createCloudInitUserData({
|
|
|
214
326
|
' [Unit]',
|
|
215
327
|
' Description=NeoAgent guest agent',
|
|
216
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'] : []),
|
|
217
334
|
' Wants=network-online.target',
|
|
218
335
|
'',
|
|
219
336
|
' [Service]',
|
|
220
337
|
' Type=simple',
|
|
221
338
|
' EnvironmentFile=/etc/neoagent/neoagent.env',
|
|
222
|
-
|
|
339
|
+
...(includeBrowser
|
|
340
|
+
? [
|
|
341
|
+
' Environment=DISPLAY=:99',
|
|
342
|
+
' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
|
|
343
|
+
]
|
|
344
|
+
: []),
|
|
223
345
|
' WorkingDirectory=/opt/neoagent',
|
|
224
346
|
' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
|
|
225
347
|
' Restart=always',
|
|
@@ -229,6 +351,29 @@ function createCloudInitUserData({
|
|
|
229
351
|
'',
|
|
230
352
|
' [Install]',
|
|
231
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
|
+
: []),
|
|
232
377
|
' - path: /etc/systemd/system/neoagent-guest-bootstrap.service',
|
|
233
378
|
" permissions: '0644'",
|
|
234
379
|
' owner: root:root',
|
|
@@ -247,8 +392,14 @@ function createCloudInitUserData({
|
|
|
247
392
|
' WantedBy=multi-user.target',
|
|
248
393
|
'runcmd:',
|
|
249
394
|
' - [bash, -lc, "systemctl daemon-reload"]',
|
|
250
|
-
|
|
251
|
-
|
|
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)}]`,
|
|
252
403
|
'',
|
|
253
404
|
].join('\n');
|
|
254
405
|
}
|
|
@@ -261,6 +412,21 @@ function createCloudInitMetaData({ instanceId, localHostName }) {
|
|
|
261
412
|
].join('\n');
|
|
262
413
|
}
|
|
263
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
|
+
|
|
264
430
|
function commandExists(command) {
|
|
265
431
|
const probe = spawnSync(
|
|
266
432
|
process.platform === 'win32' ? 'where' : 'bash',
|
|
@@ -409,9 +575,10 @@ function createSeedIso(sourceDir, isoPath) {
|
|
|
409
575
|
function ensureGuestBootstrapSeed({
|
|
410
576
|
userRoot,
|
|
411
577
|
guestToken,
|
|
412
|
-
hostShareMount = '/mnt/neoagent-host',
|
|
413
578
|
guestAgentPort = 8421,
|
|
414
579
|
guestArch = 'x64',
|
|
580
|
+
runtimeMode = 'template',
|
|
581
|
+
runtimeProfile = 'browser_cli',
|
|
415
582
|
}) {
|
|
416
583
|
const seedRoot = path.join(userRoot, 'cloud-init');
|
|
417
584
|
const seedDir = path.join(seedRoot, 'seed');
|
|
@@ -422,11 +589,18 @@ function ensureGuestBootstrapSeed({
|
|
|
422
589
|
const userDataPath = path.join(seedDir, 'user-data');
|
|
423
590
|
const metaDataPath = path.join(seedDir, 'meta-data');
|
|
424
591
|
const startupNshPath = path.join(seedDir, 'startup.nsh');
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
592
|
+
const guestPayloadBase64 = runtimeMode === 'user'
|
|
593
|
+
? ''
|
|
594
|
+
: fs.readFileSync(createGuestPayloadArchive(seedDir, runtimeProfile)).toString('base64');
|
|
595
|
+
const userData = createCloudInitUserData({
|
|
596
|
+
guestToken,
|
|
597
|
+
guestPayloadBase64,
|
|
598
|
+
guestAgentPort,
|
|
599
|
+
runtimeMode,
|
|
600
|
+
runtimeProfile,
|
|
429
601
|
});
|
|
602
|
+
const identity = resolveCloudInitIdentity(userRoot);
|
|
603
|
+
const metaData = createCloudInitMetaData(identity);
|
|
430
604
|
const startupNsh = guestArch === 'arm64'
|
|
431
605
|
? [
|
|
432
606
|
'@echo -off',
|
|
@@ -467,13 +641,13 @@ function ensureGuestBootstrapSeed({
|
|
|
467
641
|
userDataPath,
|
|
468
642
|
metaDataPath,
|
|
469
643
|
startupNshPath,
|
|
470
|
-
hostShareMount,
|
|
471
644
|
};
|
|
472
645
|
}
|
|
473
646
|
|
|
474
647
|
module.exports = {
|
|
475
648
|
createCloudInitMetaData,
|
|
476
649
|
createCloudInitUserData,
|
|
650
|
+
createCloudInitScript,
|
|
477
651
|
createSeedIso,
|
|
478
652
|
ensureGuestBootstrapSeed,
|
|
479
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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,
|
|
47
|
+
const backend = this.resolveBackend(userId, 'browser_cli');
|
|
40
48
|
return backend.executeCommand(userId, command, options);
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
hasVmForUser(userId) {
|
|
44
|
-
|
|
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.
|
|
59
|
+
return this.browserBackend.killCommand(userId, pid, reason);
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
async getCommandExecutorForUser(userId) {
|
|
52
|
-
return this.
|
|
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.
|
|
71
|
+
return this.browserBackend.getBrowserProviderForUser(userId);
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
async getAndroidProviderForUser(userId) {
|
|
64
|
-
|
|
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 (
|
|
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.
|
|
105
|
+
return this.browserBackend.isGuestAgentReadyForUser(userId, timeoutMs);
|
|
72
106
|
}
|
|
73
107
|
|
|
74
108
|
async shutdown() {
|
|
75
|
-
await Promise.allSettled([
|
|
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
|
|