neoagent 2.3.1-beta.85 → 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.
@@ -26,6 +26,7 @@ const VIEWPORTS = [
26
26
 
27
27
  function resolveBrowserExecutablePath() {
28
28
  const explicitPath =
29
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ||
29
30
  process.env.PUPPETEER_EXECUTABLE_PATH ||
30
31
  process.env.CHROME_BIN ||
31
32
  process.env.CHROMIUM_BIN;
@@ -34,7 +35,6 @@ function resolveBrowserExecutablePath() {
34
35
 
35
36
  const bundledCandidates = [
36
37
  () => require('playwright-chromium').chromium.executablePath(),
37
- () => require('playwright').chromium.executablePath(),
38
38
  ];
39
39
  for (const resolveBundled of bundledCandidates) {
40
40
  try {
@@ -76,22 +76,11 @@ function resolveBrowserExecutablePath() {
76
76
  return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
77
77
  }
78
78
 
79
- function resolveFirefoxExecutablePath() {
80
- try {
81
- const bundledPath = require('playwright').firefox.executablePath();
82
- return bundledPath && fs.existsSync(bundledPath) ? bundledPath : null;
83
- } catch {
84
- return null;
85
- }
86
- }
87
-
88
79
  function installPlaywrightBrowserBinary(browserName) {
89
- const packageRoot = path.dirname(require.resolve('playwright/package.json'));
80
+ const packageRoot = path.dirname(require.resolve('playwright-chromium/package.json'));
90
81
  const cliPath = path.join(packageRoot, 'cli.js');
91
82
  return new Promise((resolve, reject) => {
92
- const args = browserName === 'chromium'
93
- ? [cliPath, 'install', '--no-shell', 'chromium']
94
- : [cliPath, 'install', browserName];
83
+ const args = [cliPath, 'install', '--no-shell', browserName];
95
84
  const child = spawn(process.execPath, args, {
96
85
  stdio: ['ignore', 'pipe', 'pipe'],
97
86
  });
@@ -161,13 +150,29 @@ function normalizeWaitUntil(waitUntil) {
161
150
  return 'domcontentloaded';
162
151
  }
163
152
 
153
+ function clearChromiumSingletonLocks(profileDir) {
154
+ const lockEntries = [
155
+ 'SingletonLock',
156
+ 'SingletonSocket',
157
+ 'SingletonCookie',
158
+ 'SingletonStartupLock',
159
+ 'DevToolsActivePort',
160
+ ];
161
+ for (const entry of lockEntries) {
162
+ const targetPath = path.join(profileDir, entry);
163
+ try {
164
+ fs.rmSync(targetPath, { force: true, recursive: true });
165
+ } catch {}
166
+ }
167
+ }
168
+
164
169
  class BrowserController {
165
170
  constructor(options = {}) {
166
171
  this.io = options.io || null;
167
172
  this.userId = options.userId != null ? String(options.userId) : null;
168
173
  this.artifactStore = options.artifactStore || null;
169
174
  this.runtimeBackend = options.runtimeBackend || 'host';
170
- this.engine = this.runtimeBackend === 'vm' && process.platform === 'linux' ? 'firefox' : 'chromium';
175
+ this.engine = 'chromium';
171
176
  this.browser = null;
172
177
  this.context = null;
173
178
  this.page = null;
@@ -327,9 +332,7 @@ class BrowserController {
327
332
  this._userAgent = USER_AGENTS[rand(0, USER_AGENTS.length - 1)];
328
333
  this._viewport = VIEWPORTS[rand(0, VIEWPORTS.length - 1)];
329
334
 
330
- let executablePath = this.engine === 'firefox'
331
- ? resolveFirefoxExecutablePath()
332
- : resolveBrowserExecutablePath();
335
+ let executablePath = resolveBrowserExecutablePath();
333
336
  if (!executablePath) {
334
337
  if (!this.browserBinaryInstallPromise) {
335
338
  this.browserBinaryInstallPromise = installPlaywrightBrowserBinary(this.engine);
@@ -339,9 +342,7 @@ class BrowserController {
339
342
  } finally {
340
343
  this.browserBinaryInstallPromise = null;
341
344
  }
342
- executablePath = this.engine === 'firefox'
343
- ? resolveFirefoxExecutablePath()
344
- : resolveBrowserExecutablePath();
345
+ executablePath = resolveBrowserExecutablePath();
345
346
  }
346
347
 
347
348
  if (!executablePath) {
@@ -353,53 +354,35 @@ class BrowserController {
353
354
  ...(this.displayValue ? { DISPLAY: this.displayValue } : {}),
354
355
  };
355
356
 
356
- if (this.engine === 'firefox') {
357
- const { firefox } = require('playwright');
358
- this.context = await firefox.launchPersistentContext(this.profileDir, {
359
- headless: false,
360
- executablePath,
361
- env: launchEnv,
362
- viewport: this._viewport,
363
- userAgent: this._userAgent,
364
- locale: 'en-US',
365
- extraHTTPHeaders: {
366
- 'Accept-Language': 'en-US,en;q=0.9',
367
- },
368
- firefoxUserPrefs: {
369
- 'browser.shell.checkDefaultBrowser': false,
370
- 'browser.startup.homepage': 'about:blank',
371
- },
372
- });
373
- this.browser = typeof this.context.browser === 'function' ? this.context.browser() : null;
374
- this.page = this.context.pages()[0] || await this.context.newPage();
375
- } else {
376
- const puppeteer = require('puppeteer-core');
377
- this.browser = await puppeteer.launch({
378
- headless: false,
379
- executablePath,
380
- userDataDir: this.profileDir,
381
- env: launchEnv,
382
- args: [
383
- '--no-sandbox',
384
- '--disable-setuid-sandbox',
385
- '--disable-dev-shm-usage',
386
- '--disable-crash-reporter',
387
- '--disable-background-networking',
388
- '--disable-component-update',
389
- '--disable-blink-features=AutomationControlled',
390
- '--disable-infobars',
391
- '--no-first-run',
392
- '--no-default-browser-check',
393
- '--disable-gpu',
394
- '--lang=en-US,en',
395
- `--window-size=${this._viewport.width},${this._viewport.height}`,
396
- ],
397
- defaultViewport: this._viewport,
398
- ignoreDefaultArgs: ['--enable-automation'],
399
- timeout: 120000,
400
- });
401
- this.page = await this.browser.newPage();
402
- }
357
+ const launchArgs = [
358
+ '--no-sandbox',
359
+ '--disable-setuid-sandbox',
360
+ '--disable-dev-shm-usage',
361
+ '--disable-crash-reporter',
362
+ '--disable-background-networking',
363
+ '--disable-component-update',
364
+ '--disable-blink-features=AutomationControlled',
365
+ '--disable-infobars',
366
+ '--no-first-run',
367
+ '--no-default-browser-check',
368
+ '--disable-gpu',
369
+ '--lang=en-US,en',
370
+ `--window-size=${this._viewport.width},${this._viewport.height}`,
371
+ ];
372
+
373
+ const playwright = require('playwright-chromium');
374
+ clearChromiumSingletonLocks(this.profileDir);
375
+ this.context = await playwright.chromium.launchPersistentContext(this.profileDir, {
376
+ headless: false,
377
+ executablePath,
378
+ env: launchEnv,
379
+ args: launchArgs,
380
+ viewport: this._viewport,
381
+ ignoreHTTPSErrors: false,
382
+ timeout: 120000,
383
+ });
384
+ this.browser = typeof this.context.browser === 'function' ? this.context.browser() : null;
385
+ this.page = this.context.pages()[0] || await this.context.newPage();
403
386
  await this._applyStealthToPage(this.page);
404
387
  })();
405
388
 
@@ -23,6 +23,18 @@ function assertPathInside(baseDir, candidatePath, label) {
23
23
  return resolvedCandidate;
24
24
  }
25
25
 
26
+ function isPidAlive(pid) {
27
+ if (!Number.isInteger(pid) || pid <= 0) {
28
+ return false;
29
+ }
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
26
38
  class RuntimeHttpClient {
27
39
  constructor(baseUrl, token = '', options = {}) {
28
40
  this.baseUrl = String(baseUrl || '').replace(/\/+$/, '');
@@ -341,6 +353,7 @@ class VmAndroidProvider {
341
353
  class LocalVmExecutionBackend {
342
354
  constructor(options = {}) {
343
355
  this.vmManager = options.vmManager;
356
+ this.runtimeProfile = options.runtimeProfile === 'android' ? 'android' : 'browser_cli';
344
357
  this.token = options.token || process.env.NEOAGENT_VM_GUEST_TOKEN || '';
345
358
  this.artifactStore = options.artifactStore || null;
346
359
  this.lastActivity = new Map();
@@ -364,12 +377,12 @@ class LocalVmExecutionBackend {
364
377
  const now = Date.now();
365
378
  for (const [userId, lastUsed] of this.lastActivity.entries()) {
366
379
  if (now - lastUsed > IDLE_TIMEOUT_MS) {
367
- console.log(`[Runtime] User ${userId} runtime idle for ${Math.round((now - lastUsed) / 1000)}s, shutting down VM.`);
380
+ console.log(`[Runtime:${this.runtimeProfile}] User ${userId} runtime idle for ${Math.round((now - lastUsed) / 1000)}s, shutting down VM.`);
368
381
  this.lastActivity.delete(userId);
369
382
  try {
370
383
  await this.vmManager?.killVm?.(userId);
371
384
  } catch (err) {
372
- console.error(`[Runtime] Failed to shut down idle VM for user ${userId}:`, err.message);
385
+ console.error(`[Runtime:${this.runtimeProfile}] Failed to shut down idle VM for user ${userId}:`, err.message);
373
386
  }
374
387
  }
375
388
  }
@@ -391,7 +404,7 @@ class LocalVmExecutionBackend {
391
404
  checkLiveness: () => {
392
405
  const key = String(userId || '').trim();
393
406
  const session = this.vmManager.instances.get(key);
394
- return session && session.process && !session.process.killed && session.process.exitCode === null;
407
+ return Boolean(session && session.process && isPidAlive(session.process.pid));
395
408
  },
396
409
  });
397
410
  } catch (error) {
@@ -6,15 +6,24 @@ const { DATA_DIR } = require('../../../runtime/paths');
6
6
  const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
7
7
  const GUEST_BOOTSTRAP_ROOT = path.join(VM_ROOT, 'guest-bootstrap');
8
8
  const REPO_ROOT = path.resolve(__dirname, '../../..');
9
- const GUEST_PAYLOAD_ENTRIES = Object.freeze([
10
- { source: 'server/guest-agent.package.json', target: 'package.json' },
11
- { source: 'runtime/env.js', target: 'runtime/env.js' },
12
- { source: 'runtime/paths.js', target: 'runtime/paths.js' },
13
- { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
14
- { source: 'server/services/cli', target: 'server/services/cli' },
15
- { source: 'server/services/browser', target: 'server/services/browser' },
16
- { source: 'server/services/android', target: 'server/services/android' },
17
- ]);
9
+ const GUEST_PAYLOAD_PROFILES = Object.freeze({
10
+ browser_cli: [
11
+ { source: 'server/guest-agent.browser.package.json', target: 'package.json' },
12
+ { source: 'runtime/env.js', target: 'runtime/env.js' },
13
+ { source: 'runtime/paths.js', target: 'runtime/paths.js' },
14
+ { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
15
+ { source: 'server/services/cli', target: 'server/services/cli' },
16
+ { source: 'server/services/browser', target: 'server/services/browser' },
17
+ ],
18
+ android: [
19
+ { source: 'server/guest-agent.android.package.json', target: 'package.json' },
20
+ { source: 'runtime/env.js', target: 'runtime/env.js' },
21
+ { source: 'runtime/paths.js', target: 'runtime/paths.js' },
22
+ { source: 'server/guest_agent.js', target: 'server/guest_agent.js' },
23
+ { source: 'server/services/cli', target: 'server/services/cli' },
24
+ { source: 'server/services/android', target: 'server/services/android' },
25
+ ],
26
+ });
18
27
 
19
28
  fs.mkdirSync(GUEST_BOOTSTRAP_ROOT, { recursive: true });
20
29
 
@@ -22,15 +31,20 @@ function encodeGuestToken(value) {
22
31
  return Buffer.from(String(value || ''), 'utf8').toString('base64');
23
32
  }
24
33
 
25
- function createGuestPayloadArchive(seedDir) {
34
+ function normalizeRuntimeProfile(runtimeProfile) {
35
+ return runtimeProfile === 'android' ? 'android' : 'browser_cli';
36
+ }
37
+
38
+ function createGuestPayloadArchive(seedDir, runtimeProfile = 'browser_cli') {
26
39
  const seedRoot = path.dirname(seedDir);
27
40
  const stagingRoot = path.join(seedRoot, 'guest-payload');
28
41
  const archivePath = path.join(seedRoot, 'guest-payload.tar.gz');
42
+ const payloadEntries = GUEST_PAYLOAD_PROFILES[normalizeRuntimeProfile(runtimeProfile)];
29
43
  fs.rmSync(stagingRoot, { recursive: true, force: true });
30
44
  fs.rmSync(archivePath, { force: true });
31
45
  fs.mkdirSync(stagingRoot, { recursive: true });
32
46
 
33
- for (const entry of GUEST_PAYLOAD_ENTRIES) {
47
+ for (const entry of payloadEntries) {
34
48
  const sourcePath = path.join(REPO_ROOT, entry.source);
35
49
  const targetPath = path.join(stagingRoot, entry.target);
36
50
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
@@ -59,10 +73,17 @@ function createCloudInitScript({
59
73
  guestToken,
60
74
  guestPayloadPath = '/var/lib/neoagent/guest-payload.tar.gz',
61
75
  guestAgentPort = 8421,
76
+ runtimeProfile = 'browser_cli',
62
77
  }) {
78
+ const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
79
+ const includeBrowser = normalizedProfile === 'browser_cli';
80
+ const guestUtilityPackages = includeBrowser
81
+ ? 'curl ca-certificates gnupg git rsync unzip xvfb dbus-x11'
82
+ : 'curl ca-certificates gnupg git rsync unzip dbus-x11 adb';
63
83
  const guestTokenB64 = encodeGuestToken(guestToken);
64
84
  const envFile = '/etc/neoagent/neoagent.env';
65
85
  const appDir = '/opt/neoagent';
86
+ const playwrightBrowsersPath = `${appDir}/.playwright-browsers`;
66
87
  const bootstrapMarker = '/var/lib/neoagent/bootstrap-complete';
67
88
  const browserReadyMarker = '/var/lib/neoagent/browser-runtime-ready';
68
89
  const browserDepsMarker = '/var/lib/neoagent/browser-deps-installed';
@@ -74,6 +95,7 @@ function createCloudInitScript({
74
95
  '',
75
96
  'export DEBIAN_FRONTEND=noninteractive',
76
97
  `APP_DIR=${JSON.stringify(appDir)}`,
98
+ `PLAYWRIGHT_BROWSERS_PATH=${JSON.stringify(playwrightBrowsersPath)}`,
77
99
  `BOOTSTRAP_MARKER=${JSON.stringify(bootstrapMarker)}`,
78
100
  `BROWSER_READY_MARKER=${JSON.stringify(browserReadyMarker)}`,
79
101
  `BROWSER_DEPS_MARKER=${JSON.stringify(browserDepsMarker)}`,
@@ -123,53 +145,47 @@ function createCloudInitScript({
123
145
  '',
124
146
  `printf '%s\n' ${JSON.stringify(`NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`)} > "$ENV_FILE"`,
125
147
  `printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`)} >> "$ENV_FILE"`,
148
+ `printf '%s\n' ${JSON.stringify(`NEOAGENT_GUEST_PROFILE=${normalizedProfile}`)} >> "$ENV_FILE"`,
126
149
  'chmod 0600 "$ENV_FILE"',
127
150
  '',
128
151
  'cd "$APP_DIR"',
129
152
  'export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1',
153
+ 'export PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH"',
130
154
  'if [ ! -d node_modules ] || [ ! -f node_modules/.neoagent-bootstrap-stamp ] || [ package.json -nt node_modules/.neoagent-bootstrap-stamp ]; then',
131
155
  ' echo "Installing npm dependencies..."',
132
- ' retry_cmd npm cache clean --force',
133
- ' retry_cmd npm install --omit=dev --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
156
+ ' retry_cmd npm install --omit=dev --ignore-scripts --prefer-offline --no-audit --no-fund || { echo "Error: npm install failed." >&2; exit 1; }',
134
157
  ' mkdir -p node_modules',
135
158
  ' date > node_modules/.neoagent-bootstrap-stamp',
136
159
  'fi',
137
160
  '',
138
- 'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
139
- ' echo "Updating package lists..."',
140
- ' retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
141
- '',
142
- ' echo "Installing browser runtime dependencies..."',
143
- ' retry_cmd apt-get install -y --no-install-recommends \\',
144
- ' curl ca-certificates gnupg git rsync unzip \\',
145
- ' dbus-x11 \\',
146
- ' xvfb \\',
147
- ' libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libcups2 \\',
148
- ' libx11-xcb1 libgtk-3-0 libnss3 libnspr4 libxcomposite1 libxdamage1 \\',
149
- ' libxrandr2 libxkbcommon0 libasound2t64 libgbm1 libdrm2 libdbus-1-3 \\',
150
- ' libpango-1.0-0 libpangocairo-1.0-0 libxshmfence1 || echo "Warning: Some browser dependencies failed to install."',
151
- ' touch "$BROWSER_DEPS_MARKER"',
152
- 'else',
153
- ' echo "Browser runtime dependencies already installed; skipping apt install."',
154
- 'fi',
155
161
  '',
156
- 'systemctl daemon-reload',
157
- 'systemctl enable neoagent-guest-agent.service || true',
158
- 'if ! systemctl is-active --quiet neoagent-guest-agent.service; then',
159
- ' systemctl start neoagent-guest-agent.service || true',
160
- 'fi',
161
- 'echo "NeoAgent guest agent is available; continuing browser runtime provisioning..."',
162
+ 'echo "Ensuring guest runtime utilities..."',
163
+ 'retry_cmd apt-get update || echo "Warning: apt-get update failed, proceeding with cached lists."',
164
+ `retry_cmd apt-get install -y --no-install-recommends ${guestUtilityPackages} || { echo "Error: Failed to install required guest runtime utilities." >&2; exit 1; }`,
162
165
  '',
163
- 'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
164
- 'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.firefox-installed"',
165
- 'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
166
- 'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
167
- ' echo "Installing Playwright browsers..."',
168
- ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install firefox || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
169
- ' date > "$PLAYWRIGHT_STAMP"',
170
- 'fi',
166
+ 'echo "NeoAgent guest runtime payload is ready."',
171
167
  '',
172
- 'touch "$BROWSER_READY_MARKER"',
168
+ ...(includeBrowser
169
+ ? [
170
+ 'echo "Continuing browser runtime provisioning..."',
171
+ 'PLAYWRIGHT_BROWSERS_PATH="$APP_DIR/.playwright-browsers"',
172
+ 'PLAYWRIGHT_STAMP="$PLAYWRIGHT_BROWSERS_PATH/.chromium-installed"',
173
+ 'mkdir -p "$PLAYWRIGHT_BROWSERS_PATH"',
174
+ 'if [ ! -f "$BROWSER_DEPS_MARKER" ]; then',
175
+ ' echo "Installing Playwright browser dependencies..."',
176
+ ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install-deps chromium || { echo "Error: Playwright dependency install failed." >&2; exit 1; }',
177
+ ' touch "$BROWSER_DEPS_MARKER"',
178
+ 'fi',
179
+ 'if [ ! -f "$PLAYWRIGHT_STAMP" ] || [ package.json -nt "$PLAYWRIGHT_STAMP" ]; then',
180
+ ' echo "Installing Playwright browsers..."',
181
+ ' PLAYWRIGHT_BROWSERS_PATH="$PLAYWRIGHT_BROWSERS_PATH" retry_cmd npx playwright install chromium || { echo "Error: Playwright browser install failed." >&2; exit 1; }',
182
+ ' date > "$PLAYWRIGHT_STAMP"',
183
+ 'fi',
184
+ 'touch "$BROWSER_READY_MARKER"',
185
+ ]
186
+ : [
187
+ 'rm -f "$BROWSER_READY_MARKER"',
188
+ ]),
173
189
  'touch "$BOOTSTRAP_MARKER"',
174
190
  'echo "NeoAgent guest bootstrap completed."',
175
191
  '',
@@ -178,16 +194,109 @@ function createCloudInitScript({
178
194
 
179
195
  function createCloudInitUserData({
180
196
  guestToken,
181
- guestPayloadBase64,
197
+ guestPayloadBase64 = '',
182
198
  guestAgentPort = 8421,
199
+ runtimeMode = 'template',
200
+ runtimeProfile = 'browser_cli',
183
201
  }) {
202
+ const normalizedProfile = normalizeRuntimeProfile(runtimeProfile);
203
+ const includeBrowser = normalizedProfile === 'browser_cli';
204
+ const guestAgentInnerCommand = includeBrowser
205
+ ? 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && env DISPLAY=:99 PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console'
206
+ : 'set -a; . /etc/neoagent/neoagent.env; set +a; cd /opt/neoagent && /usr/bin/env node server/guest_agent.js 2>&1 | tee -a /var/log/neoagent-guest-agent.log >/dev/console';
207
+ const guestAgentLaunchCommand = `nohup /bin/sh -lc ${JSON.stringify(guestAgentInnerCommand)} </dev/null >/dev/null 2>&1 &`;
184
208
  const guestTokenB64 = encodeGuestToken(guestToken);
185
209
  const bootstrapScript = createCloudInitScript({
186
210
  guestToken,
187
211
  guestPayloadPath: '/var/lib/neoagent/guest-payload.tar.gz',
188
212
  guestAgentPort,
213
+ runtimeProfile: normalizedProfile,
189
214
  });
190
215
 
216
+ if (runtimeMode === 'user') {
217
+ return [
218
+ '#cloud-config',
219
+ 'package_update: false',
220
+ 'write_files:',
221
+ ' - path: /etc/neoagent/neoagent.env',
222
+ " permissions: '0600'",
223
+ ' owner: root:root',
224
+ ' content: |',
225
+ ` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
226
+ ` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
227
+ ` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
228
+ ...(includeBrowser
229
+ ? [
230
+ ' - path: /etc/systemd/system/neoagent-xvfb.service',
231
+ " permissions: '0644'",
232
+ ' owner: root:root',
233
+ ' content: |',
234
+ ' [Unit]',
235
+ ' Description=NeoAgent virtual display',
236
+ ' After=network-online.target',
237
+ ' Wants=network-online.target',
238
+ '',
239
+ ' [Service]',
240
+ ' Type=simple',
241
+ ' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
242
+ ' Restart=always',
243
+ ' RestartSec=2',
244
+ ' StandardOutput=journal+console',
245
+ ' StandardError=journal+console',
246
+ '',
247
+ ' [Install]',
248
+ ' WantedBy=multi-user.target',
249
+ ]
250
+ : []),
251
+ ' - path: /etc/systemd/system/neoagent-guest-agent.service',
252
+ " permissions: '0644'",
253
+ ' owner: root:root',
254
+ ' content: |',
255
+ ' [Unit]',
256
+ ' Description=NeoAgent guest agent',
257
+ ' After=network-online.target',
258
+ ...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
259
+ ' ConditionPathExists=/etc/neoagent/neoagent.env',
260
+ ' Wants=network-online.target',
261
+ '',
262
+ ' [Service]',
263
+ ' Type=simple',
264
+ ' EnvironmentFile=/etc/neoagent/neoagent.env',
265
+ ' ExecStartPre=/bin/mkdir -p /var/lib/neoagent',
266
+ ...(includeBrowser
267
+ ? [
268
+ ' ExecStartPre=/usr/bin/touch /var/lib/neoagent/browser-runtime-ready',
269
+ ' ExecStartPre=/bin/sh -lc \'for _ in $(seq 1 30); do [ -S /tmp/.X11-unix/X99 ] && exit 0; sleep 1; done; exit 1\'',
270
+ ' Environment=DISPLAY=:99',
271
+ ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
272
+ ]
273
+ : [
274
+ ' ExecStartPre=/bin/sh -lc \'rm -f /var/lib/neoagent/browser-runtime-ready || true\'',
275
+ ]),
276
+ ' ExecStartPre=/usr/bin/touch /var/lib/neoagent/bootstrap-complete',
277
+ ' WorkingDirectory=/opt/neoagent',
278
+ ' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
279
+ ' Restart=always',
280
+ ' RestartSec=5',
281
+ ' StandardOutput=journal+console',
282
+ ' StandardError=journal+console',
283
+ '',
284
+ ' [Install]',
285
+ ' WantedBy=multi-user.target',
286
+ 'runcmd:',
287
+ ' - [bash, -lc, "systemctl daemon-reload"]',
288
+ ...(includeBrowser
289
+ ? [
290
+ ' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
291
+ ' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
292
+ ]
293
+ : []),
294
+ ' - [bash, -lc, "systemctl enable neoagent-guest-agent.service"]',
295
+ ' - [bash, -lc, "systemctl start --no-block neoagent-guest-agent.service"]',
296
+ '',
297
+ ].join('\n');
298
+ }
299
+
191
300
  return [
192
301
  '#cloud-config',
193
302
  'package_update: false',
@@ -195,9 +304,10 @@ function createCloudInitUserData({
195
304
  ' - path: /etc/neoagent/neoagent.env',
196
305
  " permissions: '0600'",
197
306
  ' owner: root:root',
198
- ' content: |',
307
+ ' content: |',
199
308
  ` NEOAGENT_VM_GUEST_TOKEN_B64=${guestTokenB64}`,
200
309
  ` NEOAGENT_GUEST_AGENT_PORT=${guestAgentPort}`,
310
+ ` NEOAGENT_GUEST_PROFILE=${normalizedProfile}`,
201
311
  ' - path: /var/lib/neoagent/guest-payload.tar.gz',
202
312
  " permissions: '0644'",
203
313
  ' owner: root:root',
@@ -216,12 +326,22 @@ function createCloudInitUserData({
216
326
  ' [Unit]',
217
327
  ' Description=NeoAgent guest agent',
218
328
  ' After=network-online.target',
329
+ ' After=cloud-final.service',
330
+ ' After=neoagent-guest-bootstrap.service',
331
+ ...(includeBrowser ? [' After=neoagent-xvfb.service'] : []),
332
+ ' ConditionPathExists=/etc/neoagent/neoagent.env',
333
+ ...(includeBrowser ? [' Requires=neoagent-xvfb.service'] : []),
219
334
  ' Wants=network-online.target',
220
335
  '',
221
336
  ' [Service]',
222
337
  ' Type=simple',
223
338
  ' EnvironmentFile=/etc/neoagent/neoagent.env',
224
- ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
339
+ ...(includeBrowser
340
+ ? [
341
+ ' Environment=DISPLAY=:99',
342
+ ' Environment=PLAYWRIGHT_BROWSERS_PATH=/opt/neoagent/.playwright-browsers',
343
+ ]
344
+ : []),
225
345
  ' WorkingDirectory=/opt/neoagent',
226
346
  ' ExecStart=/usr/bin/env node /opt/neoagent/server/guest_agent.js',
227
347
  ' Restart=always',
@@ -231,6 +351,29 @@ function createCloudInitUserData({
231
351
  '',
232
352
  ' [Install]',
233
353
  ' WantedBy=multi-user.target',
354
+ ...(includeBrowser
355
+ ? [
356
+ ' - path: /etc/systemd/system/neoagent-xvfb.service',
357
+ " permissions: '0644'",
358
+ ' owner: root:root',
359
+ ' content: |',
360
+ ' [Unit]',
361
+ ' Description=NeoAgent virtual display',
362
+ ' After=network-online.target',
363
+ ' Wants=network-online.target',
364
+ '',
365
+ ' [Service]',
366
+ ' Type=simple',
367
+ ' ExecStart=/usr/bin/Xvfb :99 -screen 0 1440x900x24 -ac -nolisten tcp',
368
+ ' Restart=always',
369
+ ' RestartSec=2',
370
+ ' StandardOutput=journal+console',
371
+ ' StandardError=journal+console',
372
+ '',
373
+ ' [Install]',
374
+ ' WantedBy=multi-user.target',
375
+ ]
376
+ : []),
234
377
  ' - path: /etc/systemd/system/neoagent-guest-bootstrap.service',
235
378
  " permissions: '0644'",
236
379
  ' owner: root:root',
@@ -249,8 +392,14 @@ function createCloudInitUserData({
249
392
  ' WantedBy=multi-user.target',
250
393
  'runcmd:',
251
394
  ' - [bash, -lc, "systemctl daemon-reload"]',
252
- ' - [bash, -lc, "systemctl enable neoagent-guest-bootstrap.service"]',
253
- ' - [bash, -lc, "systemctl start neoagent-guest-bootstrap.service"]',
395
+ ...(includeBrowser
396
+ ? [
397
+ ' - [bash, -lc, "systemctl enable neoagent-xvfb.service"]',
398
+ ' - [bash, -lc, "systemctl start neoagent-xvfb.service"]',
399
+ ]
400
+ : []),
401
+ ' - [bash, -lc, "/usr/local/bin/neoagent-guest-bootstrap.sh"]',
402
+ ` - [bash, -lc, ${JSON.stringify(guestAgentLaunchCommand)}]`,
254
403
  '',
255
404
  ].join('\n');
256
405
  }
@@ -263,6 +412,21 @@ function createCloudInitMetaData({ instanceId, localHostName }) {
263
412
  ].join('\n');
264
413
  }
265
414
 
415
+ function resolveCloudInitIdentity(userRoot) {
416
+ const relativePath = path.relative(VM_ROOT, path.resolve(userRoot || ''));
417
+ const normalized = relativePath
418
+ .split(path.sep)
419
+ .filter(Boolean)
420
+ .map((segment) => segment.replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 32))
421
+ .filter(Boolean)
422
+ .join('-');
423
+ const scope = normalized || 'default';
424
+ return {
425
+ instanceId: `neoagent-${scope}`,
426
+ localHostName: `neoagent-${scope}`,
427
+ };
428
+ }
429
+
266
430
  function commandExists(command) {
267
431
  const probe = spawnSync(
268
432
  process.platform === 'win32' ? 'where' : 'bash',
@@ -413,6 +577,8 @@ function ensureGuestBootstrapSeed({
413
577
  guestToken,
414
578
  guestAgentPort = 8421,
415
579
  guestArch = 'x64',
580
+ runtimeMode = 'template',
581
+ runtimeProfile = 'browser_cli',
416
582
  }) {
417
583
  const seedRoot = path.join(userRoot, 'cloud-init');
418
584
  const seedDir = path.join(seedRoot, 'seed');
@@ -423,17 +589,18 @@ function ensureGuestBootstrapSeed({
423
589
  const userDataPath = path.join(seedDir, 'user-data');
424
590
  const metaDataPath = path.join(seedDir, 'meta-data');
425
591
  const startupNshPath = path.join(seedDir, 'startup.nsh');
426
- const guestPayloadArchivePath = createGuestPayloadArchive(seedDir);
427
- const guestPayloadBase64 = fs.readFileSync(guestPayloadArchivePath).toString('base64');
592
+ const guestPayloadBase64 = runtimeMode === 'user'
593
+ ? ''
594
+ : fs.readFileSync(createGuestPayloadArchive(seedDir, runtimeProfile)).toString('base64');
428
595
  const userData = createCloudInitUserData({
429
596
  guestToken,
430
597
  guestPayloadBase64,
431
598
  guestAgentPort,
599
+ runtimeMode,
600
+ runtimeProfile,
432
601
  });
433
- const metaData = createCloudInitMetaData({
434
- instanceId: `neoagent-${path.basename(userRoot)}`,
435
- localHostName: `neoagent-${path.basename(userRoot)}`,
436
- });
602
+ const identity = resolveCloudInitIdentity(userRoot);
603
+ const metaData = createCloudInitMetaData(identity);
437
604
  const startupNsh = guestArch === 'arm64'
438
605
  ? [
439
606
  '@echo -off',
@@ -480,6 +647,7 @@ function ensureGuestBootstrapSeed({
480
647
  module.exports = {
481
648
  createCloudInitMetaData,
482
649
  createCloudInitUserData,
650
+ createCloudInitScript,
483
651
  createSeedIso,
484
652
  ensureGuestBootstrapSeed,
485
653
  GUEST_BOOTSTRAP_ROOT,