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.
@@ -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
- hostShareMount,
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 "$HOST_SHARE_MOUNT" "$HOST_DATA_MOUNT" "$APP_DIR"',
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 both host-writable share and console',
76
- 'LOG_FILE="${HOST_DATA_MOUNT}/bootstrap.log"',
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
- 'echo "Updating package lists..."',
99
- 'retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
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
- ' export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
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
- 'systemctl daemon-reload',
173
- 'systemctl enable neoagent-guest-agent.service || true',
174
- 'systemctl restart neoagent-guest-agent.service || true',
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
- hostShareMount = '/mnt/neoagent-host',
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
- hostShareMount,
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
- ` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
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
- ' 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
+ : []),
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
- ' - [bash, -lc, "systemctl enable neoagent-guest-bootstrap.service"]',
251
- ' - [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)}]`,
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 userData = createCloudInitUserData({ guestToken, hostShareMount, guestAgentPort });
426
- const metaData = createCloudInitMetaData({
427
- instanceId: `neoagent-${path.basename(userRoot)}`,
428
- localHostName: `neoagent-${path.basename(userRoot)}`,
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
- 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