neoagent 2.3.1-beta.84 → 2.3.1-beta.85

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.
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const crypto = require('crypto');
2
3
  const http = require('http');
3
4
  const https = require('https');
4
5
  const net = require('net');
@@ -10,21 +11,16 @@ const { ensureGuestBootstrapSeed } = require('./guest_bootstrap');
10
11
 
11
12
  const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
12
13
  const BASE_IMAGE_CACHE_ROOT = path.join(VM_ROOT, 'base-images');
13
- const REPO_ROOT = path.resolve(__dirname, '../../../');
14
- const HOST_SHARE_ROOT = path.join(VM_ROOT, 'host-share');
14
+ const TEMPLATE_ROOT = path.join(VM_ROOT, 'templates');
15
15
  fs.mkdirSync(VM_ROOT, { recursive: true });
16
16
  fs.mkdirSync(BASE_IMAGE_CACHE_ROOT, { recursive: true });
17
+ fs.mkdirSync(TEMPLATE_ROOT, { recursive: true });
17
18
 
18
19
  const DEFAULT_UBUNTU_BASE_IMAGE_URLS = Object.freeze({
19
20
  x64: 'https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img',
20
21
  arm64: 'https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img',
21
22
  });
22
23
 
23
- const HOST_SHARE_LINKS = [
24
- { name: 'server', source: path.join(REPO_ROOT, 'server') },
25
- { name: 'runtime', source: path.join(REPO_ROOT, 'runtime') },
26
- ];
27
-
28
24
  const QEMU_SHARE_ROOT_CANDIDATES = [
29
25
  path.resolve(process.execPath, '..', '..', 'share', 'qemu'),
30
26
  path.resolve(process.execPath, '..', '..', '..', 'share', 'qemu'),
@@ -34,10 +30,7 @@ const QEMU_SHARE_ROOT_CANDIDATES = [
34
30
  ];
35
31
 
36
32
  function guestArchForHost() {
37
- if (process.arch === 'arm64' || process.arch === 'x64') {
38
- return process.arch;
39
- }
40
- return 'x64';
33
+ return process.arch === 'arm64' ? 'arm64' : 'x64';
41
34
  }
42
35
 
43
36
  function resolveQemuBinary({ arch = guestArchForHost(), platform = process.platform } = {}) {
@@ -70,6 +63,26 @@ function isHttpUrl(value) {
70
63
  return /^https?:\/\//i.test(candidate);
71
64
  }
72
65
 
66
+ function generateGuestToken() {
67
+ return crypto.randomBytes(32).toString('hex');
68
+ }
69
+
70
+ function resolveGuestToken(userRoot) {
71
+ const tokenPath = path.join(userRoot, 'guest-token.txt');
72
+ try {
73
+ const existing = String(fs.readFileSync(tokenPath, 'utf8') || '').trim();
74
+ if (existing.length >= 32) {
75
+ return existing;
76
+ }
77
+ } catch {}
78
+
79
+ const candidate = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
80
+ const token = candidate.length >= 32 ? candidate : generateGuestToken();
81
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true });
82
+ fs.writeFileSync(tokenPath, `${token}\n`, { mode: 0o600 });
83
+ return token;
84
+ }
85
+
73
86
  function downloadFile(sourceUrl, destinationPath, redirectCount = 0) {
74
87
  return new Promise((resolve, reject) => {
75
88
  if (redirectCount > 5) {
@@ -167,37 +180,6 @@ function resolveQemuShareRoot() {
167
180
  return null;
168
181
  }
169
182
 
170
- function ensureHostShareRoot() {
171
- fs.mkdirSync(HOST_SHARE_ROOT, { recursive: true });
172
-
173
- for (const entry of HOST_SHARE_LINKS) {
174
- const sourcePath = path.resolve(entry.source);
175
- if (!fs.existsSync(sourcePath)) {
176
- throw new Error(`Host share source is missing: ${sourcePath}`);
177
- }
178
-
179
- const linkPath = path.join(HOST_SHARE_ROOT, entry.name);
180
- let needsLink = true;
181
- if (fs.existsSync(linkPath)) {
182
- try {
183
- const resolved = fs.realpathSync.native ? fs.realpathSync.native(linkPath) : fs.realpathSync(linkPath);
184
- needsLink = resolved !== sourcePath;
185
- } catch {
186
- needsLink = true;
187
- }
188
- if (needsLink) {
189
- fs.rmSync(linkPath, { recursive: true, force: true });
190
- }
191
- }
192
-
193
- if (needsLink) {
194
- fs.symlinkSync(sourcePath, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
195
- }
196
- }
197
-
198
- return HOST_SHARE_ROOT;
199
- }
200
-
201
183
  function resolveAarch64FirmwarePaths() {
202
184
  const shareRoot = resolveQemuShareRoot();
203
185
  if (!shareRoot) {
@@ -261,8 +243,6 @@ function buildQemuArgs({
261
243
  cpus = 2,
262
244
  arch = guestArchForHost(),
263
245
  platform = process.platform,
264
- hostShareRoot = null,
265
- hostDataRoot = null,
266
246
  seedPath = null,
267
247
  seedIsRaw = false,
268
248
  consoleLogPath = null,
@@ -281,42 +261,29 @@ function buildQemuArgs({
281
261
  } else {
282
262
  args.push('-machine', `q35,accel=${accel}`);
283
263
  if (platform !== 'win32') {
284
- args.push('-cpu', process.arch === arch ? 'host' : 'max');
264
+ args.push('-cpu', process.arch === arch ? 'host' : 'qemu64');
285
265
  }
286
266
  }
287
267
 
268
+ const isMmio = arch === 'arm64';
269
+ const blkDev = isMmio ? 'virtio-blk-device' : 'virtio-blk-pci';
270
+ const netDev = isMmio ? 'virtio-net-device' : 'virtio-net-pci';
271
+ const p9Dev = isMmio ? 'virtio-9p-device' : 'virtio-9p-pci';
272
+
288
273
  // OS disk — always first boot candidate
289
274
  args.push(
290
275
  '-drive', `if=none,id=os,file=${imagePath},format=qcow2`,
291
- '-device', 'virtio-blk-pci,drive=os,bootindex=1',
276
+ '-device', `${blkDev},drive=os,bootindex=1`,
292
277
  '-netdev', `user,id=net0,hostfwd=tcp:127.0.0.1:${sshPort}-:22,hostfwd=tcp:127.0.0.1:${agentPort}-:8421`,
293
- '-device', 'virtio-net-pci,netdev=net0',
278
+ '-device', `${netDev},netdev=net0`,
294
279
  );
295
280
 
296
- if (hostShareRoot) {
297
- const id = 'fsdev-host';
298
- const deviceType = arch === 'arm64' ? 'virtio-9p-pci,disable-legacy=on,disable-modern=off,romfile=' : 'virtio-9p-pci';
299
- args.push(
300
- '-fsdev', `local,path=${hostShareRoot},id=${id},security_model=none,readonly=on`,
301
- '-device', `${deviceType},fsdev=${id},mount_tag=neoagent-host`,
302
- );
303
- }
304
-
305
- if (hostDataRoot) {
306
- const id = 'fsdev-data';
307
- const deviceType = arch === 'arm64' ? 'virtio-9p-pci,disable-legacy=on,disable-modern=off,romfile=' : 'virtio-9p-pci';
308
- args.push(
309
- '-fsdev', `local,path=${hostDataRoot},id=${id},security_model=none`,
310
- '-device', `${deviceType},fsdev=${id},mount_tag=neoagent-data`,
311
- );
312
- }
313
-
314
281
  if (seedPath) {
315
282
  if (seedIsRaw) {
316
283
  // Raw FAT image — attach as a plain virtio block device
317
284
  args.push(
318
285
  '-drive', `if=none,id=cidata,file=${seedPath},format=raw,readonly=on`,
319
- '-device', 'virtio-blk-pci,drive=cidata',
286
+ '-device', `${blkDev},drive=cidata`,
320
287
  );
321
288
  } else if (arch === 'arm64') {
322
289
  // ARM virt machine has no IDE controller; use virtio-scsi for the seed ISO
@@ -388,6 +355,135 @@ function allocatePort() {
388
355
  });
389
356
  }
390
357
 
358
+ function sleep(ms) {
359
+ return new Promise((resolve) => setTimeout(resolve, ms));
360
+ }
361
+
362
+ async function waitForPath(targetPath, timeoutMs, intervalMs = 1000) {
363
+ const startedAt = Date.now();
364
+ while (Date.now() - startedAt < timeoutMs) {
365
+ if (fs.existsSync(targetPath)) {
366
+ return true;
367
+ }
368
+ await sleep(intervalMs);
369
+ }
370
+ return fs.existsSync(targetPath);
371
+ }
372
+
373
+ function writeLockMetadata(lockDir) {
374
+ try {
375
+ fs.writeFileSync(
376
+ path.join(lockDir, 'owner.json'),
377
+ JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
378
+ 'utf8',
379
+ );
380
+ } catch {}
381
+ }
382
+
383
+ function readLockMetadata(lockDir) {
384
+ try {
385
+ return JSON.parse(fs.readFileSync(path.join(lockDir, 'owner.json'), 'utf8'));
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+
391
+ function isPidAlive(pid) {
392
+ if (!Number.isInteger(pid) || pid <= 0) {
393
+ return false;
394
+ }
395
+ try {
396
+ process.kill(pid, 0);
397
+ return true;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+
403
+ async function requestGuestAgent(baseUrl, token, pathname, body, options = {}) {
404
+ const controller = new AbortController();
405
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs || 5000));
406
+ const timer = setTimeout(() => {
407
+ controller.abort(new Error(`Request timed out after ${timeoutMs} ms.`));
408
+ }, timeoutMs);
409
+ try {
410
+ const response = await fetch(`${String(baseUrl || '').replace(/\/+$/, '')}${pathname}`, {
411
+ method: body === undefined ? 'GET' : 'POST',
412
+ headers: {
413
+ 'content-type': 'application/json',
414
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
415
+ },
416
+ body: body === undefined ? undefined : JSON.stringify(body),
417
+ signal: controller.signal,
418
+ });
419
+ const contentType = response.headers.get('content-type') || '';
420
+ const payload = contentType.includes('application/json')
421
+ ? await response.json().catch(() => ({}))
422
+ : { text: await response.text().catch(() => '') };
423
+ if (!response.ok) {
424
+ const detail = payload?.error || payload?.text || `Runtime request failed: ${response.status}`;
425
+ throw new Error(detail);
426
+ }
427
+ return payload;
428
+ } finally {
429
+ clearTimeout(timer);
430
+ }
431
+ }
432
+
433
+ async function waitForGuestAgentHealth(baseUrl, token, options = {}) {
434
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs || 5 * 60 * 1000));
435
+ const intervalMs = Math.max(250, Number(options.intervalMs || 1000));
436
+ const checkLiveness = options.checkLiveness || (() => true);
437
+ const startedAt = Date.now();
438
+ let lastError = null;
439
+
440
+ while (Date.now() - startedAt < timeoutMs) {
441
+ if (!checkLiveness()) {
442
+ throw new Error('Guest runtime process exited unexpectedly during bootstrap.');
443
+ }
444
+ try {
445
+ const health = await requestGuestAgent(baseUrl, token, '/health', undefined, { timeoutMs: 2000 });
446
+ if (health?.status === 'ok') {
447
+ return health;
448
+ }
449
+ lastError = new Error('Guest agent health check returned a non-ok status.');
450
+ } catch (error) {
451
+ lastError = error;
452
+ }
453
+ await sleep(intervalMs);
454
+ }
455
+
456
+ throw new Error(`Timed out waiting for guest agent health: ${lastError?.message || 'unknown error'}`);
457
+ }
458
+
459
+ async function waitForGuestMarker(baseUrl, token, markerPath, options = {}) {
460
+ const timeoutMs = Math.max(1000, Number(options.timeoutMs || 15 * 60 * 1000));
461
+ const intervalMs = Math.max(250, Number(options.intervalMs || 2000));
462
+ const checkLiveness = options.checkLiveness || (() => true);
463
+ const startedAt = Date.now();
464
+ let lastError = null;
465
+
466
+ while (Date.now() - startedAt < timeoutMs) {
467
+ if (!checkLiveness()) {
468
+ throw new Error('Guest runtime process exited unexpectedly while waiting for guest marker.');
469
+ }
470
+ try {
471
+ const result = await requestGuestAgent(baseUrl, token, '/exec', {
472
+ command: `test -f ${JSON.stringify(String(markerPath || ''))} && printf ready || printf pending`,
473
+ timeout: 15000,
474
+ }, { timeoutMs: 20000 });
475
+ if (String(result?.stdout || '').trim() === 'ready') {
476
+ return true;
477
+ }
478
+ } catch (error) {
479
+ lastError = error;
480
+ }
481
+ await sleep(intervalMs);
482
+ }
483
+
484
+ throw new Error(`Timed out waiting for guest marker ${markerPath}: ${lastError?.message || 'unknown error'}`);
485
+ }
486
+
391
487
  function ensureUserVmDisk(userRoot, baseImagePath) {
392
488
  fs.mkdirSync(userRoot, { recursive: true });
393
489
  const diskPath = path.join(userRoot, 'disk.qcow2');
@@ -468,7 +564,13 @@ class QemuVmManager {
468
564
  this.cpus = Number(options.cpus || process.env.NEOAGENT_VM_CPUS || 2);
469
565
  this.instances = new Map();
470
566
  this.baseImagePromise = null;
567
+ this.runtimeTemplatePromise = null;
471
568
  fs.mkdirSync(this.rootDir, { recursive: true });
569
+ setTimeout(() => {
570
+ this.ensureRuntimeTemplateAvailable().catch((error) => {
571
+ console.warn(`[VM] Background runtime template warmup failed: ${error.message}`);
572
+ });
573
+ }, 0);
472
574
  }
473
575
 
474
576
  getBaseImageCachePath() {
@@ -516,6 +618,174 @@ class QemuVmManager {
516
618
  return this.baseImagePromise;
517
619
  }
518
620
 
621
+ getRuntimeTemplateRoot() {
622
+ return path.join(TEMPLATE_ROOT, this.guestArch);
623
+ }
624
+
625
+ getRuntimeTemplateDiskPath() {
626
+ return path.join(this.getRuntimeTemplateRoot(), 'disk.qcow2');
627
+ }
628
+
629
+ getRuntimeTemplateLockDir() {
630
+ return `${this.getRuntimeTemplateRoot()}.lock`;
631
+ }
632
+
633
+ getRuntimeTemplateReadyMarker() {
634
+ return '/var/lib/neoagent/browser-runtime-ready';
635
+ }
636
+
637
+ async ensureRuntimeTemplateAvailable() {
638
+ const readyDiskPath = this.getRuntimeTemplateDiskPath();
639
+ const readySentinelPath = path.join(this.getRuntimeTemplateRoot(), '.runtime-template-ready');
640
+ if (fs.existsSync(readyDiskPath) && fs.existsSync(readySentinelPath)) {
641
+ return readyDiskPath;
642
+ }
643
+ if (!this.runtimeTemplatePromise) {
644
+ this.runtimeTemplatePromise = this.#ensureRuntimeTemplateAvailableWithLock().finally(() => {
645
+ this.runtimeTemplatePromise = null;
646
+ });
647
+ }
648
+ return this.runtimeTemplatePromise;
649
+ }
650
+
651
+ async #ensureRuntimeTemplateAvailableWithLock() {
652
+ const readyDiskPath = this.getRuntimeTemplateDiskPath();
653
+ const readySentinelPath = path.join(this.getRuntimeTemplateRoot(), '.runtime-template-ready');
654
+ const lockDir = this.getRuntimeTemplateLockDir();
655
+ const acquireStartedAt = Date.now();
656
+
657
+ while (true) {
658
+ if (fs.existsSync(readyDiskPath) && fs.existsSync(readySentinelPath)) {
659
+ return readyDiskPath;
660
+ }
661
+
662
+ try {
663
+ fs.mkdirSync(lockDir, { recursive: false });
664
+ writeLockMetadata(lockDir);
665
+ try {
666
+ if (fs.existsSync(readyDiskPath) && fs.existsSync(readySentinelPath)) {
667
+ return readyDiskPath;
668
+ }
669
+ return await this.#buildRuntimeTemplate();
670
+ } finally {
671
+ fs.rmSync(lockDir, { recursive: true, force: true });
672
+ }
673
+ } catch (error) {
674
+ if (error?.code !== 'EEXIST') {
675
+ throw error;
676
+ }
677
+ }
678
+
679
+ const lockStats = fs.existsSync(lockDir) ? fs.statSync(lockDir) : null;
680
+ const lockMetadata = readLockMetadata(lockDir);
681
+ const lockAgeMs = lockStats ? Date.now() - lockStats.mtimeMs : 0;
682
+ const staleLock = lockAgeMs > 45 * 60 * 1000 || (lockMetadata?.pid && !isPidAlive(lockMetadata.pid));
683
+ if (staleLock) {
684
+ fs.rmSync(lockDir, { recursive: true, force: true });
685
+ continue;
686
+ }
687
+
688
+ if (Date.now() - acquireStartedAt > 30 * 60 * 1000) {
689
+ throw new Error('Timed out waiting for the shared runtime template build lock.');
690
+ }
691
+
692
+ await sleep(2000);
693
+ }
694
+ }
695
+
696
+ async #buildRuntimeTemplate() {
697
+ const templateRoot = this.getRuntimeTemplateRoot();
698
+ const templateDiskPath = this.getRuntimeTemplateDiskPath();
699
+ const readyMarkerPath = this.getRuntimeTemplateReadyMarker();
700
+ const readySentinelPath = path.join(templateRoot, '.runtime-template-ready');
701
+
702
+ fs.rmSync(templateRoot, { recursive: true, force: true });
703
+ fs.mkdirSync(templateRoot, { recursive: true });
704
+
705
+ const baseImagePath = await this.ensureBaseImageAvailable();
706
+ const diskPath = ensureUserVmDisk(templateRoot, baseImagePath);
707
+ const guestToken = resolveGuestToken(templateRoot);
708
+ const bootstrap = ensureGuestBootstrapSeed({
709
+ userRoot: templateRoot,
710
+ guestToken,
711
+ guestArch: this.guestArch,
712
+ });
713
+ const consoleLogPath = path.join(templateRoot, 'console.log');
714
+ const firmware = this.guestArch === 'arm64'
715
+ ? resolveAarch64FirmwarePaths()
716
+ : resolveX86_64FirmwarePaths();
717
+ const firmwareVarsPath = firmware ? path.join(templateRoot, 'uefi-vars.fd') : null;
718
+ if (firmware && !fs.existsSync(firmwareVarsPath)) {
719
+ fs.copyFileSync(firmware.varsTemplatePath, firmwareVarsPath);
720
+ }
721
+ const agentPort = await allocatePort();
722
+ const sshPort = await allocatePort();
723
+ const qemuBinary = resolveQemuBinary({ arch: this.guestArch });
724
+ const qemuBinaryPath = resolveCommandPath(qemuBinary) || qemuBinary;
725
+ const args = buildQemuArgs({
726
+ imagePath: diskPath,
727
+ sshPort,
728
+ agentPort,
729
+ memoryMb: this.memoryMb,
730
+ cpus: this.cpus,
731
+ arch: this.guestArch,
732
+ seedPath: bootstrap.seedImagePath || bootstrap.isoPath,
733
+ seedIsRaw: Boolean(bootstrap.seedImagePath && bootstrap.seedImagePath.endsWith('.img')),
734
+ consoleLogPath,
735
+ firmwareCodePath: firmware?.codePath || null,
736
+ firmwareVarsPath,
737
+ });
738
+
739
+ console.log(`[VM] Building runtime template for ${this.guestArch}: ${qemuBinaryPath} ${args.join(' ')}`);
740
+ const child = spawn(qemuBinaryPath, args, {
741
+ cwd: templateRoot,
742
+ detached: process.platform !== 'win32',
743
+ stdio: ['ignore', 'ignore', 'pipe'],
744
+ });
745
+
746
+ let stderrText = '';
747
+ child.stderr.on('data', (chunk) => {
748
+ stderrText += chunk.toString('utf8');
749
+ });
750
+
751
+ const baseUrl = `http://127.0.0.1:${agentPort}`;
752
+ const checkLiveness = () => !child.killed && child.exitCode === null;
753
+ try {
754
+ await waitForGuestAgentHealth(baseUrl, guestToken, {
755
+ timeoutMs: 30 * 60 * 1000,
756
+ intervalMs: 1000,
757
+ checkLiveness,
758
+ });
759
+ await waitForGuestMarker(baseUrl, guestToken, readyMarkerPath, {
760
+ timeoutMs: 45 * 60 * 1000,
761
+ intervalMs: 2000,
762
+ checkLiveness,
763
+ });
764
+ fs.writeFileSync(readySentinelPath, `${new Date().toISOString()}\n`, 'utf8');
765
+ } finally {
766
+ try {
767
+ if (process.platform === 'win32') {
768
+ spawnSync('taskkill', ['/F', '/T', '/PID', child.pid]);
769
+ } else {
770
+ process.kill(-child.pid, 'SIGKILL');
771
+ }
772
+ } catch {
773
+ try {
774
+ child.kill('SIGKILL');
775
+ } catch {}
776
+ }
777
+ }
778
+
779
+ if (!fs.existsSync(templateDiskPath)) {
780
+ throw new Error('Runtime template disk was not created.');
781
+ }
782
+ return templateDiskPath;
783
+ }
784
+
785
+ async ensureRuntimeImageAvailable() {
786
+ return this.ensureRuntimeTemplateAvailable();
787
+ }
788
+
519
789
  isConfigured() {
520
790
  return this.getReadiness().ready;
521
791
  }
@@ -564,18 +834,14 @@ class QemuVmManager {
564
834
  }
565
835
 
566
836
  const userRoot = path.join(this.rootDir, key, this.guestArch);
567
- const baseImagePath = await this.ensureBaseImageAvailable();
837
+ const baseImagePath = await this.ensureRuntimeImageAvailable();
568
838
  const diskPath = ensureUserVmDisk(userRoot, baseImagePath);
569
- const guestToken = String(process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
570
- if (!guestToken) {
571
- throw new Error('NEOAGENT_VM_GUEST_TOKEN is required to bootstrap the guest runtime.');
572
- }
839
+ const guestToken = resolveGuestToken(userRoot);
573
840
  const bootstrap = ensureGuestBootstrapSeed({
574
841
  userRoot,
575
842
  guestToken,
576
843
  guestArch: this.guestArch,
577
844
  });
578
- const guestDataRoot = path.join(userRoot, 'guest-data');
579
845
  const consoleLogPath = path.join(userRoot, 'console.log');
580
846
  const firmware = this.guestArch === 'arm64'
581
847
  ? resolveAarch64FirmwarePaths()
@@ -597,12 +863,10 @@ class QemuVmManager {
597
863
  throw new Error(`Failed to copy firmware vars template: ${detail}`);
598
864
  }
599
865
  }
600
- fs.mkdirSync(guestDataRoot, { recursive: true });
601
866
  const agentPort = await allocatePort();
602
867
  const sshPort = await allocatePort();
603
868
  const qemuBinary = resolveQemuBinary({ arch: this.guestArch });
604
869
  const qemuBinaryPath = resolveCommandPath(qemuBinary) || qemuBinary;
605
- const hostShareRoot = ensureHostShareRoot();
606
870
  const args = buildQemuArgs({
607
871
  imagePath: diskPath,
608
872
  sshPort,
@@ -610,8 +874,6 @@ class QemuVmManager {
610
874
  memoryMb: this.memoryMb,
611
875
  cpus: this.cpus,
612
876
  arch: this.guestArch,
613
- hostShareRoot,
614
- hostDataRoot: guestDataRoot,
615
877
  seedPath: bootstrap.seedImagePath || bootstrap.isoPath,
616
878
  seedIsRaw: Boolean(bootstrap.seedImagePath && bootstrap.seedImagePath.endsWith('.img')),
617
879
  consoleLogPath,
@@ -656,7 +918,10 @@ class QemuVmManager {
656
918
  }
657
919
  });
658
920
  child.on('exit', () => {
659
- this.instances.delete(key);
921
+ const current = this.instances.get(key);
922
+ if (current?.process === child) {
923
+ this.instances.delete(key);
924
+ }
660
925
  });
661
926
 
662
927
  const session = {
@@ -667,6 +932,7 @@ class QemuVmManager {
667
932
  guestArch: this.guestArch,
668
933
  userRoot,
669
934
  diskPath,
935
+ guestToken,
670
936
  agentPort,
671
937
  sshPort,
672
938
  baseUrl: `http://127.0.0.1:${agentPort}`,
@@ -2,32 +2,10 @@
2
2
 
3
3
  const { getDeploymentPolicy } = require('../../utils/deployment');
4
4
 
5
- function validateGuestToken(token) {
6
- const value = String(token || '').trim();
7
- if (!value) {
8
- return { valid: false, reason: 'NEOAGENT_VM_GUEST_TOKEN is missing.' };
9
- }
10
- if (value.length < 32) {
11
- return { valid: false, reason: 'NEOAGENT_VM_GUEST_TOKEN must be at least 32 characters long.' };
12
- }
13
- if (/^(change|replace|set|your|example|sample|placeholder|token|secret)[-_a-z0-9]*$/i.test(value)) {
14
- return { valid: false, reason: 'NEOAGENT_VM_GUEST_TOKEN looks like a placeholder value.' };
15
- }
16
- if (/change-this-guest-token-before-prod/i.test(value)) {
17
- return { valid: false, reason: 'NEOAGENT_VM_GUEST_TOKEN is using the insecure example placeholder.' };
18
- }
19
- if (/^(.)\1+$/.test(value)) {
20
- return { valid: false, reason: 'NEOAGENT_VM_GUEST_TOKEN must not be a repeated single character.' };
21
- }
22
- return { valid: true, reason: null };
23
- }
24
-
25
5
  function getRuntimeValidation(runtimeManager) {
26
6
  const policy = getDeploymentPolicy();
27
7
  const nodeEnvIsProd = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'prod';
28
8
  const vmReadiness = runtimeManager?.vmBackend?.vmManager?.getReadiness?.() || null;
29
- const guestToken = String(runtimeManager?.vmBackend?.token || process.env.NEOAGENT_VM_GUEST_TOKEN || '').trim();
30
- const guestTokenValidation = validateGuestToken(guestToken);
31
9
  const issues = [];
32
10
 
33
11
  if (policy.profile === 'prod' || nodeEnvIsProd) {
@@ -43,16 +21,13 @@ function getRuntimeValidation(runtimeManager) {
43
21
  }
44
22
  }
45
23
  }
46
- if (!guestTokenValidation.valid) {
47
- issues.push(`prod profile requires a secure NEOAGENT_VM_GUEST_TOKEN. ${guestTokenValidation.reason}`);
48
- }
49
24
  }
50
25
 
51
26
  return {
52
27
  ready: issues.length === 0,
53
28
  issues,
54
29
  vm: vmReadiness,
55
- guestTokenConfigured: guestTokenValidation.valid,
30
+ guestTokenConfigured: true,
56
31
  policy,
57
32
  };
58
33
  }
@@ -68,5 +43,4 @@ function assertRuntimeValidation(runtimeManager) {
68
43
  module.exports = {
69
44
  assertRuntimeValidation,
70
45
  getRuntimeValidation,
71
- validateGuestToken,
72
46
  };