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.
- package/package.json +1 -1
- package/server/guest-agent.android.package.json +13 -0
- package/server/guest-agent.browser.package.json +14 -0
- package/server/guest_agent.js +61 -44
- 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 +528 -132
- package/server/services/browser/controller.js +51 -68
- package/server/services/runtime/backends/local-vm.js +16 -3
- package/server/services/runtime/guest_bootstrap.js +224 -56
- package/server/services/runtime/manager.js +53 -15
- package/server/services/runtime/qemu.js +149 -24
- package/server/services/runtime/settings.js +9 -14
- package/server/services/runtime/validation.js +10 -11
- package/server/utils/deployment.js +4 -4
- package/server/guest-agent.package.json +0 -15
|
@@ -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
|
|
|
@@ -9,12 +9,9 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
9
9
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
10
10
|
const { ensureGuestBootstrapSeed } = require('./guest_bootstrap');
|
|
11
11
|
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
12
13
|
const VM_ROOT = path.join(DATA_DIR, 'runtime-vms');
|
|
13
|
-
const BASE_IMAGE_CACHE_ROOT = path.join(VM_ROOT, 'base-images');
|
|
14
|
-
const TEMPLATE_ROOT = path.join(VM_ROOT, 'templates');
|
|
15
14
|
fs.mkdirSync(VM_ROOT, { recursive: true });
|
|
16
|
-
fs.mkdirSync(BASE_IMAGE_CACHE_ROOT, { recursive: true });
|
|
17
|
-
fs.mkdirSync(TEMPLATE_ROOT, { recursive: true });
|
|
18
15
|
|
|
19
16
|
const DEFAULT_UBUNTU_BASE_IMAGE_URLS = Object.freeze({
|
|
20
17
|
x64: 'https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img',
|
|
@@ -67,6 +64,47 @@ function generateGuestToken() {
|
|
|
67
64
|
return crypto.randomBytes(32).toString('hex');
|
|
68
65
|
}
|
|
69
66
|
|
|
67
|
+
function computeRuntimeTemplateSignature(guestArch, runtimeProfile = 'browser_cli') {
|
|
68
|
+
const hash = crypto.createHash('sha256');
|
|
69
|
+
const normalizedProfile = runtimeProfile === 'android' ? 'android' : 'browser_cli';
|
|
70
|
+
const trackedFiles = normalizedProfile === 'android'
|
|
71
|
+
? [
|
|
72
|
+
'server/guest-agent.android.package.json',
|
|
73
|
+
'server/guest_agent.js',
|
|
74
|
+
'server/services/android/controller.js',
|
|
75
|
+
'server/services/cli/executor.js',
|
|
76
|
+
'server/services/runtime/guest_bootstrap.js',
|
|
77
|
+
'runtime/env.js',
|
|
78
|
+
'runtime/paths.js',
|
|
79
|
+
]
|
|
80
|
+
: [
|
|
81
|
+
'server/guest-agent.browser.package.json',
|
|
82
|
+
'server/guest_agent.js',
|
|
83
|
+
'server/services/browser/controller.js',
|
|
84
|
+
'server/services/cli/executor.js',
|
|
85
|
+
'server/services/runtime/guest_bootstrap.js',
|
|
86
|
+
'runtime/env.js',
|
|
87
|
+
'runtime/paths.js',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
hash.update(String(guestArch || 'x64'));
|
|
91
|
+
hash.update('\0');
|
|
92
|
+
hash.update(normalizedProfile);
|
|
93
|
+
for (const relativePath of trackedFiles) {
|
|
94
|
+
const filePath = path.join(REPO_ROOT, relativePath);
|
|
95
|
+
hash.update('\0');
|
|
96
|
+
hash.update(relativePath);
|
|
97
|
+
hash.update('\0');
|
|
98
|
+
try {
|
|
99
|
+
hash.update(fs.readFileSync(filePath));
|
|
100
|
+
} catch (error) {
|
|
101
|
+
hash.update(`missing:${error?.code || 'unknown'}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return hash.digest('hex');
|
|
106
|
+
}
|
|
107
|
+
|
|
70
108
|
function resolveGuestToken(userRoot) {
|
|
71
109
|
const tokenPath = path.join(userRoot, 'guest-token.txt');
|
|
72
110
|
try {
|
|
@@ -551,9 +589,22 @@ function formatReadinessIssues(readiness) {
|
|
|
551
589
|
return issues.length > 0 ? issues : ['VM runtime is unavailable on this host.'];
|
|
552
590
|
}
|
|
553
591
|
|
|
592
|
+
function readTemplateReadyMetadata(readySentinelPath) {
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(fs.readFileSync(readySentinelPath, 'utf8'));
|
|
595
|
+
if (parsed && typeof parsed === 'object') {
|
|
596
|
+
return parsed;
|
|
597
|
+
}
|
|
598
|
+
} catch {}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
554
602
|
class QemuVmManager {
|
|
555
603
|
constructor(options = {}) {
|
|
556
|
-
this.
|
|
604
|
+
this.runtimeProfile = options.runtimeProfile === 'android' ? 'android' : 'browser_cli';
|
|
605
|
+
this.rootDir = path.resolve(options.rootDir || path.join(VM_ROOT, this.runtimeProfile));
|
|
606
|
+
this.baseImageCacheRoot = path.resolve(options.baseImageCacheRoot || path.join(this.rootDir, 'base-images'));
|
|
607
|
+
this.templateRootDir = path.resolve(options.templateRootDir || path.join(this.rootDir, 'templates'));
|
|
557
608
|
this.baseImagePath = options.baseImagePath || process.env.NEOAGENT_VM_BASE_IMAGE || '';
|
|
558
609
|
this.guestArch = options.guestArch || guestArchForHost();
|
|
559
610
|
this.baseImageUrl = normalizeBaseImageUrlForArch(
|
|
@@ -565,12 +616,17 @@ class QemuVmManager {
|
|
|
565
616
|
this.instances = new Map();
|
|
566
617
|
this.baseImagePromise = null;
|
|
567
618
|
this.runtimeTemplatePromise = null;
|
|
619
|
+
this.warmupEnabled = options.warmup === true;
|
|
568
620
|
fs.mkdirSync(this.rootDir, { recursive: true });
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
621
|
+
fs.mkdirSync(this.baseImageCacheRoot, { recursive: true });
|
|
622
|
+
fs.mkdirSync(this.templateRootDir, { recursive: true });
|
|
623
|
+
if (this.warmupEnabled) {
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
this.ensureRuntimeTemplateAvailable().catch((error) => {
|
|
626
|
+
console.warn(`[VM:${this.runtimeProfile}] Background runtime template warmup failed: ${error.message}`);
|
|
627
|
+
});
|
|
628
|
+
}, 0);
|
|
629
|
+
}
|
|
574
630
|
}
|
|
575
631
|
|
|
576
632
|
getBaseImageCachePath() {
|
|
@@ -579,7 +635,7 @@ class QemuVmManager {
|
|
|
579
635
|
}
|
|
580
636
|
const parsed = new URL(this.baseImageUrl);
|
|
581
637
|
const filename = path.basename(parsed.pathname || '') || `${this.guestArch}-base.img`;
|
|
582
|
-
return path.join(
|
|
638
|
+
return path.join(this.baseImageCacheRoot, filename);
|
|
583
639
|
}
|
|
584
640
|
|
|
585
641
|
resolveBaseImagePath() {
|
|
@@ -619,7 +675,7 @@ class QemuVmManager {
|
|
|
619
675
|
}
|
|
620
676
|
|
|
621
677
|
getRuntimeTemplateRoot() {
|
|
622
|
-
return path.join(
|
|
678
|
+
return path.join(this.templateRootDir, this.guestArch);
|
|
623
679
|
}
|
|
624
680
|
|
|
625
681
|
getRuntimeTemplateDiskPath() {
|
|
@@ -631,13 +687,24 @@ class QemuVmManager {
|
|
|
631
687
|
}
|
|
632
688
|
|
|
633
689
|
getRuntimeTemplateReadyMarker() {
|
|
634
|
-
return '
|
|
690
|
+
return this.runtimeProfile === 'android'
|
|
691
|
+
? '/var/lib/neoagent/bootstrap-complete'
|
|
692
|
+
: '/var/lib/neoagent/browser-runtime-ready';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
getRuntimeTemplateSignature() {
|
|
696
|
+
return computeRuntimeTemplateSignature(this.guestArch, this.runtimeProfile);
|
|
635
697
|
}
|
|
636
698
|
|
|
637
699
|
async ensureRuntimeTemplateAvailable() {
|
|
638
700
|
const readyDiskPath = this.getRuntimeTemplateDiskPath();
|
|
639
701
|
const readySentinelPath = path.join(this.getRuntimeTemplateRoot(), '.runtime-template-ready');
|
|
640
|
-
|
|
702
|
+
const readyMetadata = readTemplateReadyMetadata(readySentinelPath);
|
|
703
|
+
if (
|
|
704
|
+
fs.existsSync(readyDiskPath)
|
|
705
|
+
&& readyMetadata
|
|
706
|
+
&& readyMetadata.signature === this.getRuntimeTemplateSignature()
|
|
707
|
+
) {
|
|
641
708
|
return readyDiskPath;
|
|
642
709
|
}
|
|
643
710
|
if (!this.runtimeTemplatePromise) {
|
|
@@ -653,9 +720,15 @@ class QemuVmManager {
|
|
|
653
720
|
const readySentinelPath = path.join(this.getRuntimeTemplateRoot(), '.runtime-template-ready');
|
|
654
721
|
const lockDir = this.getRuntimeTemplateLockDir();
|
|
655
722
|
const acquireStartedAt = Date.now();
|
|
723
|
+
const expectedSignature = this.getRuntimeTemplateSignature();
|
|
656
724
|
|
|
657
725
|
while (true) {
|
|
658
|
-
|
|
726
|
+
const readyMetadata = readTemplateReadyMetadata(readySentinelPath);
|
|
727
|
+
if (
|
|
728
|
+
fs.existsSync(readyDiskPath)
|
|
729
|
+
&& readyMetadata
|
|
730
|
+
&& readyMetadata.signature === expectedSignature
|
|
731
|
+
) {
|
|
659
732
|
return readyDiskPath;
|
|
660
733
|
}
|
|
661
734
|
|
|
@@ -698,8 +771,8 @@ class QemuVmManager {
|
|
|
698
771
|
const templateDiskPath = this.getRuntimeTemplateDiskPath();
|
|
699
772
|
const readyMarkerPath = this.getRuntimeTemplateReadyMarker();
|
|
700
773
|
const readySentinelPath = path.join(templateRoot, '.runtime-template-ready');
|
|
774
|
+
const templateSignature = this.getRuntimeTemplateSignature();
|
|
701
775
|
|
|
702
|
-
fs.rmSync(templateRoot, { recursive: true, force: true });
|
|
703
776
|
fs.mkdirSync(templateRoot, { recursive: true });
|
|
704
777
|
|
|
705
778
|
const baseImagePath = await this.ensureBaseImageAvailable();
|
|
@@ -709,6 +782,8 @@ class QemuVmManager {
|
|
|
709
782
|
userRoot: templateRoot,
|
|
710
783
|
guestToken,
|
|
711
784
|
guestArch: this.guestArch,
|
|
785
|
+
runtimeMode: 'template',
|
|
786
|
+
runtimeProfile: this.runtimeProfile,
|
|
712
787
|
});
|
|
713
788
|
const consoleLogPath = path.join(templateRoot, 'console.log');
|
|
714
789
|
const firmware = this.guestArch === 'arm64'
|
|
@@ -736,7 +811,7 @@ class QemuVmManager {
|
|
|
736
811
|
firmwareVarsPath,
|
|
737
812
|
});
|
|
738
813
|
|
|
739
|
-
console.log(`[VM] Building runtime template for ${this.guestArch}: ${qemuBinaryPath} ${args.join(' ')}`);
|
|
814
|
+
console.log(`[VM:${this.runtimeProfile}] Building runtime template for ${this.guestArch}: ${qemuBinaryPath} ${args.join(' ')}`);
|
|
740
815
|
const child = spawn(qemuBinaryPath, args, {
|
|
741
816
|
cwd: templateRoot,
|
|
742
817
|
detached: process.platform !== 'win32',
|
|
@@ -749,7 +824,7 @@ class QemuVmManager {
|
|
|
749
824
|
});
|
|
750
825
|
|
|
751
826
|
const baseUrl = `http://127.0.0.1:${agentPort}`;
|
|
752
|
-
const checkLiveness = () =>
|
|
827
|
+
const checkLiveness = () => isPidAlive(child.pid);
|
|
753
828
|
try {
|
|
754
829
|
await waitForGuestAgentHealth(baseUrl, guestToken, {
|
|
755
830
|
timeoutMs: 30 * 60 * 1000,
|
|
@@ -761,7 +836,31 @@ class QemuVmManager {
|
|
|
761
836
|
intervalMs: 2000,
|
|
762
837
|
checkLiveness,
|
|
763
838
|
});
|
|
764
|
-
|
|
839
|
+
try {
|
|
840
|
+
await requestGuestAgent(baseUrl, guestToken, '/exec', {
|
|
841
|
+
command: [
|
|
842
|
+
'cloud-init clean --logs --seed --machine-id || true',
|
|
843
|
+
'rm -rf /var/lib/cloud/instances/* /var/lib/cloud/seed/* || true',
|
|
844
|
+
'rm -f /var/lib/neoagent/bootstrap-complete /var/lib/neoagent/browser-runtime-ready || true',
|
|
845
|
+
'rm -f /var/lib/systemd/random-seed || true',
|
|
846
|
+
'truncate -s 0 /etc/machine-id || true',
|
|
847
|
+
'sync',
|
|
848
|
+
].join('; '),
|
|
849
|
+
timeout: 120000,
|
|
850
|
+
}, { timeoutMs: 120000 });
|
|
851
|
+
} catch (cleanupError) {
|
|
852
|
+
console.warn(`[VM:${this.runtimeProfile}] Template cleanup after bootstrap failed: ${cleanupError.message}`);
|
|
853
|
+
}
|
|
854
|
+
fs.writeFileSync(
|
|
855
|
+
readySentinelPath,
|
|
856
|
+
JSON.stringify({
|
|
857
|
+
signature: templateSignature,
|
|
858
|
+
runtimeProfile: this.runtimeProfile,
|
|
859
|
+
guestArch: this.guestArch,
|
|
860
|
+
builtAt: new Date().toISOString(),
|
|
861
|
+
}, null, 2),
|
|
862
|
+
'utf8',
|
|
863
|
+
);
|
|
765
864
|
} finally {
|
|
766
865
|
try {
|
|
767
866
|
if (process.platform === 'win32') {
|
|
@@ -841,6 +940,8 @@ class QemuVmManager {
|
|
|
841
940
|
userRoot,
|
|
842
941
|
guestToken,
|
|
843
942
|
guestArch: this.guestArch,
|
|
943
|
+
runtimeMode: 'user',
|
|
944
|
+
runtimeProfile: this.runtimeProfile,
|
|
844
945
|
});
|
|
845
946
|
const consoleLogPath = path.join(userRoot, 'console.log');
|
|
846
947
|
const firmware = this.guestArch === 'arm64'
|
|
@@ -881,7 +982,7 @@ class QemuVmManager {
|
|
|
881
982
|
firmwareVarsPath,
|
|
882
983
|
});
|
|
883
984
|
|
|
884
|
-
console.log(`[VM] Starting QEMU for user ${key} (${this.guestArch}): ${qemuBinaryPath} ${args.join(' ')}`);
|
|
985
|
+
console.log(`[VM:${this.runtimeProfile}] Starting QEMU for user ${key} (${this.guestArch}): ${qemuBinaryPath} ${args.join(' ')}`);
|
|
885
986
|
const child = spawn(qemuBinaryPath, args, {
|
|
886
987
|
cwd: userRoot,
|
|
887
988
|
detached: process.platform !== 'win32',
|
|
@@ -926,6 +1027,7 @@ class QemuVmManager {
|
|
|
926
1027
|
|
|
927
1028
|
const session = {
|
|
928
1029
|
userId: key,
|
|
1030
|
+
runtimeProfile: this.runtimeProfile,
|
|
929
1031
|
process: child,
|
|
930
1032
|
qemuBinary,
|
|
931
1033
|
qemuArgs: args,
|
|
@@ -953,12 +1055,36 @@ class QemuVmManager {
|
|
|
953
1055
|
if (!session) return;
|
|
954
1056
|
|
|
955
1057
|
try {
|
|
1058
|
+
try {
|
|
1059
|
+
await requestGuestAgent(session.baseUrl, session.guestToken, '/browser/close', {}, { timeoutMs: 10000 });
|
|
1060
|
+
} catch {}
|
|
1061
|
+
try {
|
|
1062
|
+
await requestGuestAgent(session.baseUrl, session.guestToken, '/android/stop', {}, { timeoutMs: 10000 });
|
|
1063
|
+
} catch {}
|
|
1064
|
+
try {
|
|
1065
|
+
await requestGuestAgent(session.baseUrl, session.guestToken, '/exec', {
|
|
1066
|
+
command: 'sync || true',
|
|
1067
|
+
timeout: 15000,
|
|
1068
|
+
}, { timeoutMs: 20000 });
|
|
1069
|
+
} catch {}
|
|
1070
|
+
|
|
956
1071
|
if (process.platform === 'win32') {
|
|
957
|
-
spawnSync('taskkill', ['/
|
|
1072
|
+
spawnSync('taskkill', ['/T', '/PID', session.process.pid]);
|
|
958
1073
|
} else {
|
|
959
|
-
process.kill(-session.process.pid, '
|
|
1074
|
+
process.kill(-session.process.pid, 'SIGTERM');
|
|
1075
|
+
}
|
|
1076
|
+
const shutdownStartedAt = Date.now();
|
|
1077
|
+
while (isPidAlive(session.process.pid) && Date.now() - shutdownStartedAt < 10000) {
|
|
1078
|
+
await sleep(250);
|
|
1079
|
+
}
|
|
1080
|
+
if (isPidAlive(session.process.pid)) {
|
|
1081
|
+
if (process.platform === 'win32') {
|
|
1082
|
+
spawnSync('taskkill', ['/F', '/T', '/PID', session.process.pid]);
|
|
1083
|
+
} else {
|
|
1084
|
+
process.kill(-session.process.pid, 'SIGKILL');
|
|
1085
|
+
}
|
|
960
1086
|
}
|
|
961
|
-
} catch
|
|
1087
|
+
} catch {
|
|
962
1088
|
try {
|
|
963
1089
|
session.process.kill('SIGKILL');
|
|
964
1090
|
} catch {}
|
|
@@ -978,7 +1104,6 @@ class QemuVmManager {
|
|
|
978
1104
|
}
|
|
979
1105
|
|
|
980
1106
|
module.exports = {
|
|
981
|
-
BASE_IMAGE_CACHE_ROOT,
|
|
982
1107
|
DEFAULT_UBUNTU_BASE_IMAGE_URLS,
|
|
983
1108
|
QemuVmManager,
|
|
984
1109
|
VM_ROOT,
|
|
@@ -19,8 +19,8 @@ function getEffectiveDefaults() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const BASE_FALLBACK_SETTINGS = Object.freeze({
|
|
22
|
-
runtime_profile: '
|
|
23
|
-
runtime_backend: '
|
|
22
|
+
runtime_profile: 'secure-vm',
|
|
23
|
+
runtime_backend: 'vm',
|
|
24
24
|
browser_backend: 'vm',
|
|
25
25
|
android_backend: 'host',
|
|
26
26
|
mcp_backend: 'host-remote',
|
|
@@ -36,15 +36,10 @@ function normalizeChoice(value, allowed, fallback) {
|
|
|
36
36
|
function deriveDefaultsForProfile(profile) {
|
|
37
37
|
switch (profile) {
|
|
38
38
|
case 'secure-vm':
|
|
39
|
-
return {
|
|
40
|
-
runtime_backend: 'vm',
|
|
41
|
-
browser_backend: 'vm',
|
|
42
|
-
android_backend: 'vm',
|
|
43
|
-
};
|
|
44
39
|
case 'trusted-host':
|
|
45
40
|
default:
|
|
46
41
|
return {
|
|
47
|
-
runtime_backend: '
|
|
42
|
+
runtime_backend: 'vm',
|
|
48
43
|
browser_backend: 'vm',
|
|
49
44
|
android_backend: 'host',
|
|
50
45
|
};
|
|
@@ -56,14 +51,14 @@ function normalizeRuntimeSettings(raw = {}) {
|
|
|
56
51
|
const defaults = getEffectiveDefaults();
|
|
57
52
|
const profile = normalizeChoice(raw.runtime_profile, ['secure-vm', 'trusted-host'], defaults.runtime_profile);
|
|
58
53
|
const derived = deriveDefaultsForProfile(profile);
|
|
59
|
-
const runtimeBackend = normalizeChoice(raw.runtime_backend, ['
|
|
54
|
+
const runtimeBackend = normalizeChoice(raw.runtime_backend, ['vm'], derived.runtime_backend);
|
|
60
55
|
const browserBackend = normalizeChoice(raw.browser_backend, ['vm', 'extension'], derived.browser_backend);
|
|
61
56
|
const androidBackend = normalizeChoice(raw.android_backend, ['host', 'vm'], derived.android_backend);
|
|
62
57
|
return {
|
|
63
|
-
runtime_profile: profile,
|
|
64
|
-
runtime_backend:
|
|
58
|
+
runtime_profile: profile === 'trusted-host' ? 'secure-vm' : profile,
|
|
59
|
+
runtime_backend: runtimeBackend,
|
|
65
60
|
browser_backend: browserBackend === 'extension' ? 'extension' : 'vm',
|
|
66
|
-
android_backend:
|
|
61
|
+
android_backend: androidBackend === 'vm' ? 'host' : androidBackend,
|
|
67
62
|
mcp_backend: 'host-remote',
|
|
68
63
|
};
|
|
69
64
|
}
|
|
@@ -107,8 +102,8 @@ function validateRuntimeSettings(raw = {}) {
|
|
|
107
102
|
if (settings.browser_backend !== 'vm' && settings.browser_backend !== 'extension') {
|
|
108
103
|
issues.push('This deployment requires the VM browser backend or a paired browser extension backend.');
|
|
109
104
|
}
|
|
110
|
-
if (settings.android_backend !== '
|
|
111
|
-
issues.push('This deployment requires the
|
|
105
|
+
if (settings.android_backend !== 'host') {
|
|
106
|
+
issues.push('This deployment requires the host Android backend.');
|
|
112
107
|
}
|
|
113
108
|
}
|
|
114
109
|
|
|
@@ -5,20 +5,19 @@ const { getDeploymentPolicy } = require('../../utils/deployment');
|
|
|
5
5
|
function getRuntimeValidation(runtimeManager) {
|
|
6
6
|
const policy = getDeploymentPolicy();
|
|
7
7
|
const nodeEnvIsProd = String(process.env.NODE_ENV || '').trim().toLowerCase() === 'prod';
|
|
8
|
-
const
|
|
8
|
+
const browserVmReadiness = runtimeManager?.browserBackend?.vmManager?.getReadiness?.() || null;
|
|
9
|
+
const vmReadiness = browserVmReadiness || null;
|
|
9
10
|
const issues = [];
|
|
10
11
|
|
|
11
12
|
if (policy.profile === 'prod' || nodeEnvIsProd) {
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
issues.push('prod profile requires a VM base image or a downloadable base image URL.');
|
|
21
|
-
}
|
|
13
|
+
if (!browserVmReadiness) {
|
|
14
|
+
issues.push('prod profile requires a working local VM runtime for browser/CLI.');
|
|
15
|
+
} else if (!browserVmReadiness.ready) {
|
|
16
|
+
if (!browserVmReadiness.qemuAvailable) {
|
|
17
|
+
issues.push(`prod profile requires QEMU (${browserVmReadiness.qemuBinary}) to be installed for browser/CLI.`);
|
|
18
|
+
}
|
|
19
|
+
if (!browserVmReadiness.baseImageExists && !browserVmReadiness.downloadConfigured) {
|
|
20
|
+
issues.push('prod profile requires a VM base image or a downloadable base image URL for browser/CLI.');
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -65,13 +65,13 @@ function getDeploymentPolicy(env = process.env) {
|
|
|
65
65
|
allowSelfUpdate: mode !== DEPLOYMENT_MODE_MANAGED,
|
|
66
66
|
registrationOpen: isProdProfile,
|
|
67
67
|
runtimeDefaults: {
|
|
68
|
-
runtime_profile:
|
|
69
|
-
runtime_backend:
|
|
68
|
+
runtime_profile: 'secure-vm',
|
|
69
|
+
runtime_backend: 'vm',
|
|
70
70
|
browser_backend: 'vm',
|
|
71
|
-
android_backend:
|
|
71
|
+
android_backend: 'host',
|
|
72
72
|
mcp_backend: 'host-remote',
|
|
73
73
|
},
|
|
74
|
-
allowHostRuntime:
|
|
74
|
+
allowHostRuntime: false,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "neoagent-guest-agent",
|
|
3
|
-
"private": true,
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Minimal guest runtime for NeoAgent VM browser, CLI, and Android services",
|
|
6
|
-
"engines": {
|
|
7
|
-
"node": ">=20"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"express": "^4.21.2",
|
|
11
|
-
"playwright": "^1.59.1",
|
|
12
|
-
"proper-lockfile": "^4.1.2",
|
|
13
|
-
"puppeteer-core": "^24.40.0"
|
|
14
|
-
}
|
|
15
|
-
}
|