vmsan 0.1.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Angelo Recca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # vmsan
2
+
3
+ <!-- automd:badges color="yellow" license licenseSrc bundlephobia packagephobia -->
4
+
5
+ [![npm version](https://img.shields.io/npm/v/vmsan?color=yellow)](https://npmjs.com/package/vmsan)
6
+ [![npm downloads](https://img.shields.io/npm/dm/vmsan?color=yellow)](https://npm.chart.dev/vmsan)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/vmsan?color=yellow)](https://bundlephobia.com/package/vmsan)
8
+ [![install size](https://badgen.net/packagephobia/install/vmsan?color=yellow)](https://packagephobia.com/result?p=vmsan)
9
+ [![license](https://img.shields.io/github/license/angelorc/vmsan?color=yellow)](https://github.com/angelorc/vmsan/blob/main/LICENSE)
10
+
11
+ <!-- /automd -->
12
+
13
+ Firecracker microVM sandbox toolkit. Create, manage, and connect to isolated Firecracker microVMs from the command line.
14
+
15
+ ## Features
16
+
17
+ - Full VM lifecycle management (create, start, stop, remove)
18
+ - Network isolation with policy-based controls (allow-all, deny-all, custom domain/CIDR allowlists)
19
+ - Interactive shell access via WebSocket PTY
20
+ - File upload/download to running VMs
21
+ - Command execution with streaming output
22
+ - Multiple runtimes: `base`, `node22`, `python3.13`
23
+ - Docker image support via `--from-image`
24
+ - VM snapshots
25
+ - Structured JSON output for scripting
26
+
27
+ ## Prerequisites
28
+
29
+ - Linux (x86_64 or aarch64) with KVM support
30
+ - [Bun](https://bun.sh) >= 1.2
31
+ - [Go](https://go.dev) >= 1.22 (to build the in-VM agent)
32
+ - Root/sudo access (required for TAP device networking and jailer)
33
+ - `squashfs-tools` (for rootfs conversion during install)
34
+
35
+ ## Install
36
+
37
+ ### 1. Install Firecracker, kernel, and rootfs
38
+
39
+ ```bash
40
+ curl -fsSL https://raw.githubusercontent.com/angelorc/vmsan/main/install.sh | bash
41
+ ```
42
+
43
+ This downloads and installs into `~/.vmsan/`:
44
+
45
+ - Firecracker + Jailer (latest release)
46
+ - Linux kernel (vmlinux 6.1)
47
+ - Ubuntu 24.04 rootfs (converted from squashfs to ext4)
48
+
49
+ ### 2. Install vmsan CLI
50
+
51
+ <!-- automd:pm-install -->
52
+
53
+ ```sh
54
+ # ✨ Auto-detect
55
+ npx nypm install vmsan
56
+
57
+ # npm
58
+ npm install vmsan
59
+
60
+ # yarn
61
+ yarn add vmsan
62
+
63
+ # pnpm
64
+ pnpm add vmsan
65
+
66
+ # bun
67
+ bun install vmsan
68
+
69
+ # deno
70
+ deno install npm:vmsan
71
+ ```
72
+
73
+ <!-- /automd -->
74
+
75
+ ### 3. Build the in-VM agent
76
+
77
+ ```bash
78
+ cd agent
79
+ make install
80
+ cd ..
81
+ ```
82
+
83
+ ### Link globally (optional)
84
+
85
+ ```bash
86
+ bun link
87
+ ```
88
+
89
+ This makes the `vmsan` command available system-wide.
90
+
91
+ ## Usage
92
+
93
+ ```bash
94
+ # Create and start a VM
95
+ vmsan create --runtime node22 --memory 512 --cpus 2
96
+
97
+ # Create a VM from a Docker image
98
+ vmsan create --from-image node:22-alpine
99
+
100
+ # List all VMs
101
+ vmsan list
102
+
103
+ # Connect to a running VM shell
104
+ vmsan connect <vm-id>
105
+
106
+ # Upload a file to a VM
107
+ vmsan upload <vm-id> ./local-file.txt /remote/path/file.txt
108
+
109
+ # Download a file from a VM
110
+ vmsan download <vm-id> /remote/path/file.txt ./local-file.txt
111
+
112
+ # Stop a VM
113
+ vmsan stop <vm-id>
114
+
115
+ # Remove a VM
116
+ vmsan remove <vm-id>
117
+ ```
118
+
119
+ ### Global flags
120
+
121
+ ```
122
+ --json Output structured JSON
123
+ --verbose Show detailed debug output
124
+ ```
125
+
126
+ ### Commands
127
+
128
+ | Command | Alias | Description |
129
+ | ---------- | ----- | --------------------------------- |
130
+ | `create` | | Create and start a new microVM |
131
+ | `list` | `ls` | List all VMs |
132
+ | `start` | | Start a stopped VM |
133
+ | `stop` | | Stop a running VM |
134
+ | `remove` | `rm` | Remove a VM |
135
+ | `connect` | | Open an interactive shell to a VM |
136
+ | `upload` | | Upload files to a VM |
137
+ | `download` | | Download files from a VM |
138
+
139
+ ## Development
140
+
141
+ To use your local build instead of the installed one, link it:
142
+
143
+ ```bash
144
+ bun run build
145
+ ln -sf "$(pwd)/dist/bin/cli.mjs" ~/.vmsan/bin/vmsan
146
+ ```
147
+
148
+ ```bash
149
+ # Dev mode (watch)
150
+ bun run dev
151
+
152
+ # Run tests
153
+ bun run test
154
+
155
+ # Type check
156
+ bun run typecheck
157
+
158
+ # Lint
159
+ bun run lint
160
+
161
+ # Format
162
+ bun run fmt
163
+ ```
164
+
165
+ ## Project structure
166
+
167
+ ```
168
+ bin/ CLI entry point
169
+ src/
170
+ commands/ CLI subcommands
171
+ services/ Firecracker client, agent client, VM service
172
+ lib/ Utilities (jailer, networking, shell, logging)
173
+ errors/ Typed error system
174
+ generated/ Firecracker API type definitions
175
+ agent/ Go agent that runs inside the VM
176
+ ```
177
+
178
+ ## How it works
179
+
180
+ 1. **vmsan** uses [Firecracker](https://github.com/firecracker-microvm/firecracker) to create lightweight microVMs with a jailer for security isolation
181
+ 2. Each VM gets a TAP network device with its own `/30` subnet (`172.16.{slot}.0/30`)
182
+ 3. A Go-based **agent** runs inside the VM, exposing an HTTP API on port 9119 for command execution, file operations, and shell access
183
+ 4. The CLI communicates with the agent over the host-guest network to manage the VM
184
+
185
+ State is persisted in `~/.vmsan/`:
186
+
187
+ ```
188
+ ~/.vmsan/
189
+ vms/ VM state files (JSON)
190
+ jailer/ Chroot directories
191
+ bin/ Agent binary
192
+ kernels/ VM kernel images
193
+ rootfs/ Base root filesystems
194
+ registry/ Docker image rootfs cache
195
+ snapshots/ VM snapshots
196
+ ```
197
+
198
+ ## License
199
+
200
+ [MIT](./LICENSE)
201
+
202
+ <!-- automd:contributors author="angelorc" license="MIT" -->
203
+
204
+ Published under the [MIT](https://github.com/angelorc/vmsan/blob/main/LICENSE) license.
205
+ Made by [@angelorc](https://github.com/angelorc) and [community](https://github.com/angelorc/vmsan/graphs/contributors) 💛
206
+ <br><br>
207
+ <a href="https://github.com/angelorc/vmsan/graphs/contributors">
208
+ <img src="https://contrib.rocks/image?repo=angelorc/vmsan" />
209
+ </a>
210
+
211
+ <!-- /automd -->
212
+
213
+ <!-- automd:with-automd -->
214
+
215
+ ---
216
+
217
+ _🤖 auto updated with [automd](https://automd.unjs.io)_
218
+
219
+ <!-- /automd -->
@@ -0,0 +1,121 @@
1
+ import { createGzip } from "node:zlib";
2
+ import { Readable } from "node:stream";
3
+ import { pack } from "tar-stream";
4
+ var AgentClient = class {
5
+ constructor(baseUrl, token) {
6
+ this.baseUrl = baseUrl;
7
+ this.token = token;
8
+ }
9
+ async health() {
10
+ const res = await fetch(`${this.baseUrl}/health`);
11
+ if (!res.ok) throw new Error(`Agent health check failed: ${res.status}`);
12
+ return res.json();
13
+ }
14
+ async *run(params) {
15
+ const res = await fetch(`${this.baseUrl}/exec`, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${this.token}`
20
+ },
21
+ body: JSON.stringify(params)
22
+ });
23
+ if (!res.ok) {
24
+ const text = await res.text();
25
+ throw new Error(`Agent exec failed (${res.status}): ${text}`);
26
+ }
27
+ if (!res.body) throw new Error("Agent exec returned no body");
28
+ const reader = res.body.getReader();
29
+ const decoder = new TextDecoder();
30
+ let buffer = "";
31
+ while (true) {
32
+ const { done, value } = await reader.read();
33
+ if (done) break;
34
+ buffer += decoder.decode(value, { stream: true });
35
+ const lines = buffer.split("\n");
36
+ buffer = lines.pop() || "";
37
+ for (const line of lines) {
38
+ const trimmed = line.trim();
39
+ if (trimmed) yield JSON.parse(trimmed);
40
+ }
41
+ }
42
+ if (buffer.trim()) yield JSON.parse(buffer.trim());
43
+ }
44
+ async killCommand(cmdId, signal) {
45
+ const url = `${this.baseUrl}/exec/${cmdId}/kill`;
46
+ const res = await fetch(url, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Authorization: `Bearer ${this.token}`
51
+ },
52
+ body: signal ? JSON.stringify({ signal }) : void 0
53
+ });
54
+ if (!res.ok) {
55
+ const text = await res.text();
56
+ throw new Error(`Agent kill failed (${res.status}): ${text}`);
57
+ }
58
+ }
59
+ async writeFiles(files, extractDir) {
60
+ const tarPack = pack();
61
+ for (const file of files) tarPack.entry({ name: file.path }, file.content);
62
+ tarPack.finalize();
63
+ const gzipped = await tarToGzipBuffer(tarPack);
64
+ const headers = {
65
+ "Content-Type": "application/gzip",
66
+ Authorization: `Bearer ${this.token}`
67
+ };
68
+ if (extractDir) headers["X-Extract-Dir"] = extractDir;
69
+ const res = await fetch(`${this.baseUrl}/files/write`, {
70
+ method: "POST",
71
+ headers,
72
+ body: new Uint8Array(gzipped)
73
+ });
74
+ if (!res.ok) {
75
+ const text = await res.text();
76
+ throw new Error(`Agent writeFiles failed (${res.status}): ${text}`);
77
+ }
78
+ }
79
+ async listShellSessions() {
80
+ const res = await fetch(`${this.baseUrl}/shell/sessions`, { headers: { Authorization: `Bearer ${this.token}` } });
81
+ if (!res.ok) {
82
+ const text = await res.text();
83
+ throw new Error(`Agent listShellSessions failed (${res.status}): ${text}`);
84
+ }
85
+ return res.json();
86
+ }
87
+ async killShellSession(sessionId) {
88
+ const res = await fetch(`${this.baseUrl}/shell/sessions/${sessionId}/kill`, {
89
+ method: "POST",
90
+ headers: { Authorization: `Bearer ${this.token}` }
91
+ });
92
+ if (!res.ok) {
93
+ const text = await res.text();
94
+ throw new Error(`Agent killShellSession failed (${res.status}): ${text}`);
95
+ }
96
+ }
97
+ async readFile(path) {
98
+ const res = await fetch(`${this.baseUrl}/files/read`, {
99
+ method: "POST",
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ Authorization: `Bearer ${this.token}`
103
+ },
104
+ body: JSON.stringify({ path })
105
+ });
106
+ if (res.status === 404) return null;
107
+ if (!res.ok) {
108
+ const text = await res.text();
109
+ throw new Error(`Agent readFile failed (${res.status}): ${text}`);
110
+ }
111
+ return Buffer.from(await res.arrayBuffer());
112
+ }
113
+ };
114
+ async function tarToGzipBuffer(tarPack) {
115
+ const gzip = createGzip();
116
+ Readable.from(tarPack).pipe(gzip);
117
+ const chunks = [];
118
+ for await (const chunk of gzip) chunks.push(Buffer.from(chunk));
119
+ return Buffer.concat(chunks);
120
+ }
121
+ export { AgentClient as t };
@@ -0,0 +1,328 @@
1
+ import { o as safeKill, t as FileVmStateStore } from "./vm-state.mjs";
2
+ import { a as getVmPid, c as NetworkManager, i as getVmJailerPid } from "./environment.mjs";
3
+ import { dirname, join } from "node:path";
4
+ import { execFileSync, execSync } from "node:child_process";
5
+ import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
+ /**
7
+ * Template generators for the node22-demo runtime welcome page.
8
+ * All functions are pure and return string content ready to write to files.
9
+ */
10
+ function generateWelcomeHtml(vmId, ports) {
11
+ ports.map((p) => `<li>${p}</li>`).join("\n ");
12
+ return `<!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <title>vmsan VM ${vmId}</title>
18
+ <style>
19
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
22
+ background: #0f172a;
23
+ color: #e2e8f0;
24
+ min-height: 100vh;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ padding: 2rem;
29
+ }
30
+ .container { max-width: 640px; width: 100%; }
31
+ .header { text-align: center; margin-bottom: 2rem; }
32
+ .logo {
33
+ font-size: 2.5rem;
34
+ font-weight: 800;
35
+ background: linear-gradient(135deg, #f97316, #ef4444);
36
+ -webkit-background-clip: text;
37
+ -webkit-text-fill-color: transparent;
38
+ background-clip: text;
39
+ }
40
+ .subtitle { color: #94a3b8; margin-top: 0.5rem; font-size: 1.1rem; }
41
+ .card {
42
+ background: #1e293b;
43
+ border: 1px solid #334155;
44
+ border-radius: 12px;
45
+ padding: 1.5rem;
46
+ margin-bottom: 1.25rem;
47
+ }
48
+ .card h2 { font-size: 1rem; color: #f97316; margin-bottom: 0.75rem; }
49
+ .info-row { display: flex; justify-content: space-between; padding: 0.35rem 0; }
50
+ .info-label { color: #94a3b8; }
51
+ .info-value { font-family: monospace; color: #e2e8f0; }
52
+ ul { list-style: none; }
53
+ ul li { padding: 0.25rem 0; }
54
+ code {
55
+ background: #0f172a;
56
+ border: 1px solid #334155;
57
+ border-radius: 6px;
58
+ padding: 0.2rem 0.5rem;
59
+ font-size: 0.875rem;
60
+ color: #f97316;
61
+ }
62
+ .steps li { padding: 0.5rem 0; color: #cbd5e1; }
63
+ .steps li strong { color: #e2e8f0; }
64
+ .footer { text-align: center; color: #475569; font-size: 0.85rem; margin-top: 1.5rem; }
65
+ </style>
66
+ </head>
67
+ <body>
68
+ <div class="container">
69
+ <div class="header">
70
+ <div class="logo">vmsan</div>
71
+ <div class="subtitle">Your microVM is running</div>
72
+ </div>
73
+ <div class="card">
74
+ <h2>VM Info</h2>
75
+ <div class="info-row">
76
+ <span class="info-label">VM ID</span>
77
+ <span class="info-value">${vmId}</span>
78
+ </div>
79
+ <div class="info-row">
80
+ <span class="info-label">Runtime</span>
81
+ <span class="info-value">node22-demo</span>
82
+ </div>
83
+ <div class="info-row">
84
+ <span class="info-label">Published Ports</span>
85
+ <span class="info-value">${ports.join(", ")}</span>
86
+ </div>
87
+ </div>
88
+ <div class="card">
89
+ <h2>Next Steps</h2>
90
+ <ul class="steps">
91
+ <li><strong>Connect to the VM:</strong> <code>vmsan connect ${vmId}</code></li>
92
+ <li><strong>Deploy your app:</strong> Replace this page by stopping the welcome service and running your own server on the published port(s).</li>
93
+ <li><strong>Stop this page:</strong> <code>systemctl stop vmsan-welcome</code></li>
94
+ </ul>
95
+ </div>
96
+ <div class="footer">Powered by vmsan &middot; Firecracker microVMs</div>
97
+ </div>
98
+ </body>
99
+ </html>`;
100
+ }
101
+ function generateWelcomeServer(ports) {
102
+ return `"use strict";
103
+ const http = require("node:http");
104
+ const fs = require("node:fs");
105
+ const path = require("node:path");
106
+
107
+ const html = fs.readFileSync(path.join(__dirname, "index.html"), "utf-8");
108
+
109
+ const server = http.createServer((req, res) => {
110
+ res.writeHead(200, {
111
+ "Content-Type": "text/html; charset=utf-8",
112
+ "Cache-Control": "no-cache",
113
+ });
114
+ res.end(html);
115
+ });
116
+
117
+ ${ports.map((p) => `server.listen(${p}, "0.0.0.0", () => console.log("vmsan-welcome listening on 0.0.0.0:${p}"));`).join("\n")}
118
+ `;
119
+ }
120
+ function generateWelcomeService(ports) {
121
+ return `[Unit]
122
+ Description=${`vmsan welcome page on port(s) ${ports.join(", ")}`}
123
+ After=network.target
124
+
125
+ [Service]
126
+ Type=simple
127
+ ExecStart=/usr/local/bin/node /opt/vmsan/welcome/server.js
128
+ Restart=on-failure
129
+ RestartSec=2
130
+
131
+ [Install]
132
+ WantedBy=multi-user.target
133
+ `;
134
+ }
135
+ /**
136
+ * Template generators for the vmsan-agent systemd service.
137
+ * Follows the same pattern as welcome-page.ts.
138
+ */
139
+ function generateAgentService() {
140
+ return `[Unit]
141
+ Description=Vmsan VM Agent
142
+ After=network.target
143
+
144
+ [Service]
145
+ Type=simple
146
+ ExecStart=/usr/local/bin/vmsan-agent
147
+ EnvironmentFile=/etc/vmsan/agent.env
148
+ Restart=always
149
+ RestartSec=2
150
+
151
+ [Install]
152
+ WantedBy=multi-user.target
153
+ `;
154
+ }
155
+ function generateAgentEnv(token, port, vmId) {
156
+ return `VMSAN_AGENT_TOKEN=${token}
157
+ VMSAN_AGENT_PORT=${port}
158
+ VMSAN_VM_ID=${vmId}
159
+ `;
160
+ }
161
+ function detectCgroupVersion() {
162
+ try {
163
+ readFileSync("/sys/fs/cgroup/cgroup.controllers", "utf-8");
164
+ return 2;
165
+ } catch {
166
+ return 1;
167
+ }
168
+ }
169
+ var Jailer = class {
170
+ paths;
171
+ constructor(vmId, jailerBaseDir) {
172
+ this.vmId = vmId;
173
+ const chrootBase = jailerBaseDir;
174
+ const chrootDir = join(chrootBase, "firecracker", vmId);
175
+ const rootDir = join(chrootDir, "root");
176
+ const kernelDir = join(rootDir, "kernel");
177
+ const rootfsDir = join(rootDir, "rootfs");
178
+ const socketDir = join(rootDir, "run");
179
+ const snapshotDir = join(rootDir, "snapshot");
180
+ this.paths = {
181
+ chrootBase,
182
+ chrootDir,
183
+ rootDir,
184
+ kernelDir,
185
+ kernelPath: join(kernelDir, "vmlinux"),
186
+ rootfsDir,
187
+ rootfsPath: join(rootfsDir, "rootfs.ext4"),
188
+ socketDir,
189
+ socketPath: join(socketDir, "firecracker.socket"),
190
+ snapshotDir
191
+ };
192
+ }
193
+ prepare(config) {
194
+ const paths = this.paths;
195
+ mkdirSync(paths.kernelDir, { recursive: true });
196
+ mkdirSync(paths.rootfsDir, { recursive: true });
197
+ mkdirSync(paths.socketDir, { recursive: true });
198
+ if (!existsSync(paths.kernelPath)) linkSync(config.kernelSrc, paths.kernelPath);
199
+ copyFileSync(config.rootfsSrc, paths.rootfsPath);
200
+ if (typeof config.diskSizeGb === "number" && Number.isFinite(config.diskSizeGb)) {
201
+ const targetBytes = Math.trunc(config.diskSizeGb * 1024 * 1024 * 1024);
202
+ if (targetBytes > statSync(paths.rootfsPath).size) {
203
+ execSync(`truncate -s ${targetBytes} "${paths.rootfsPath}"`, { stdio: "pipe" });
204
+ execSync(`sudo e2fsck -fy "${paths.rootfsPath}"; [ $? -lt 4 ]`, { stdio: "pipe" });
205
+ execSync(`sudo resize2fs "${paths.rootfsPath}"`, { stdio: "pipe" });
206
+ execSync(`sudo tune2fs -m 0 "${paths.rootfsPath}"`, { stdio: "pipe" });
207
+ }
208
+ }
209
+ const tmpMount = join(paths.rootDir, "tmp-mount");
210
+ mkdirSync(tmpMount, { recursive: true });
211
+ try {
212
+ execSync(`sudo mount -o loop "${paths.rootfsPath}" "${tmpMount}"`, { stdio: "pipe" });
213
+ execSync(`rm -f "${tmpMount}/etc/resolv.conf" && ln -s /proc/net/pnp "${tmpMount}/etc/resolv.conf"`, { stdio: "pipe" });
214
+ if (config.welcomePage) {
215
+ const { vmId: welcomeVmId, ports: welcomePorts } = config.welcomePage;
216
+ const welcomeDir = join(tmpMount, "opt", "vmsan", "welcome");
217
+ mkdirSync(welcomeDir, { recursive: true });
218
+ writeFileSync(join(welcomeDir, "index.html"), generateWelcomeHtml(welcomeVmId, welcomePorts));
219
+ writeFileSync(join(welcomeDir, "server.js"), generateWelcomeServer(welcomePorts));
220
+ const systemdDir = join(tmpMount, "etc", "systemd", "system");
221
+ mkdirSync(systemdDir, { recursive: true });
222
+ writeFileSync(join(systemdDir, "vmsan-welcome.service"), generateWelcomeService(welcomePorts));
223
+ const wantsDir = join(systemdDir, "multi-user.target.wants");
224
+ mkdirSync(wantsDir, { recursive: true });
225
+ execSync(`ln -sf /etc/systemd/system/vmsan-welcome.service "${join(wantsDir, "vmsan-welcome.service")}"`, { stdio: "pipe" });
226
+ }
227
+ if (config.agent) {
228
+ const agentDst = join(tmpMount, "usr", "local", "bin", "vmsan-agent");
229
+ mkdirSync(join(tmpMount, "usr", "local", "bin"), { recursive: true });
230
+ copyFileSync(config.agent.binaryPath, agentDst);
231
+ execSync(`chmod 755 "${agentDst}"`, { stdio: "pipe" });
232
+ const envDir = join(tmpMount, "etc", "vmsan");
233
+ mkdirSync(envDir, { recursive: true });
234
+ writeFileSync(join(envDir, "agent.env"), generateAgentEnv(config.agent.token, config.agent.port, config.agent.vmId));
235
+ const systemdDir = join(tmpMount, "etc", "systemd", "system");
236
+ mkdirSync(systemdDir, { recursive: true });
237
+ writeFileSync(join(systemdDir, "vmsan-agent.service"), generateAgentService());
238
+ const wantsDir = join(systemdDir, "multi-user.target.wants");
239
+ mkdirSync(wantsDir, { recursive: true });
240
+ execSync(`ln -sf /etc/systemd/system/vmsan-agent.service "${join(wantsDir, "vmsan-agent.service")}"`, { stdio: "pipe" });
241
+ }
242
+ execSync(`sudo umount "${tmpMount}"`, { stdio: "pipe" });
243
+ } catch {
244
+ try {
245
+ execSync(`sudo umount "${tmpMount}" 2>/dev/null`, { stdio: "pipe" });
246
+ } catch {}
247
+ }
248
+ try {
249
+ execSync(`rm -rf "${tmpMount}"`, { stdio: "pipe" });
250
+ } catch {}
251
+ if (config.snapshot) {
252
+ mkdirSync(paths.snapshotDir, { recursive: true });
253
+ copyFileSync(config.snapshot.snapshotFile, join(paths.snapshotDir, "snapshot_file"));
254
+ copyFileSync(config.snapshot.memFile, join(paths.snapshotDir, "mem_file"));
255
+ }
256
+ return paths;
257
+ }
258
+ spawn(config) {
259
+ const uid = config.uid ?? 0;
260
+ const gid = config.gid ?? 0;
261
+ const args = [
262
+ config.jailerBin,
263
+ "--exec-file",
264
+ config.firecrackerBin,
265
+ "--id",
266
+ this.vmId,
267
+ "--uid",
268
+ String(uid),
269
+ "--gid",
270
+ String(gid),
271
+ "--chroot-base-dir",
272
+ config.chrootBase,
273
+ "--daemonize"
274
+ ];
275
+ if (config.newPidNs !== false) args.push("--new-pid-ns");
276
+ if (config.netns) args.push("--netns", `/var/run/netns/${config.netns}`);
277
+ if (config.cgroup) if (detectCgroupVersion() === 2) {
278
+ args.push("--cgroup-version", "2");
279
+ args.push("--cgroup", `cpu.max=${config.cgroup.cpuQuotaUs} ${config.cgroup.cpuPeriodUs}`);
280
+ args.push("--cgroup", `memory.max=${config.cgroup.memoryBytes}`);
281
+ } else {
282
+ args.push("--cgroup", `cpu.cfs_quota_us=${config.cgroup.cpuQuotaUs}`);
283
+ args.push("--cgroup", `cpu.cfs_period_us=${config.cgroup.cpuPeriodUs}`);
284
+ args.push("--cgroup", `memory.limit_in_bytes=${config.cgroup.memoryBytes}`);
285
+ }
286
+ args.push("--", "--api-sock", "run/firecracker.socket");
287
+ if (config.seccompFilter && existsSync(config.seccompFilter)) args.push("--seccomp-filter", config.seccompFilter);
288
+ else args.push("--no-seccomp");
289
+ execFileSync("sudo", args, { stdio: "pipe" });
290
+ }
291
+ };
292
+ function killOrphanVmProcess(vmId) {
293
+ const orphanPid = getVmPid(vmId);
294
+ const orphanJailerPid = getVmJailerPid(vmId);
295
+ if (orphanPid) safeKill(orphanPid, "SIGKILL");
296
+ if (orphanJailerPid) safeKill(orphanJailerPid, "SIGKILL");
297
+ }
298
+ function markVmAsError(vmId, error, paths) {
299
+ try {
300
+ new FileVmStateStore(paths.vmsDir).update(vmId, {
301
+ status: "error",
302
+ error: error instanceof Error ? error.message : String(error)
303
+ });
304
+ } catch {}
305
+ }
306
+ function cleanupNetwork(networkConfig) {
307
+ if (!networkConfig) return;
308
+ try {
309
+ NetworkManager.fromConfig(networkConfig).teardown();
310
+ } catch {}
311
+ }
312
+ function cleanupChroot(chrootDir) {
313
+ if (!chrootDir) return;
314
+ const vmJailerDir = dirname(chrootDir);
315
+ try {
316
+ rmSync(chrootDir, {
317
+ recursive: true,
318
+ force: true
319
+ });
320
+ } catch {}
321
+ try {
322
+ rmSync(vmJailerDir, {
323
+ recursive: true,
324
+ force: true
325
+ });
326
+ } catch {}
327
+ }
328
+ export { Jailer as a, markVmAsError as i, cleanupNetwork as n, detectCgroupVersion as o, killOrphanVmProcess as r, cleanupChroot as t };
@@ -0,0 +1,13 @@
1
+ import { l as agentTimeoutError } from "./errors.mjs";
2
+ async function waitForAgent(guestIp, port, timeoutMs = 6e4) {
3
+ const start = Date.now();
4
+ const url = `http://${guestIp}:${port}/health`;
5
+ while (Date.now() - start < timeoutMs) {
6
+ try {
7
+ if ((await fetch(url, { signal: AbortSignal.timeout(2e3) })).ok) return;
8
+ } catch {}
9
+ await new Promise((r) => setTimeout(r, 500));
10
+ }
11
+ throw agentTimeoutError(guestIp, timeoutMs);
12
+ }
13
+ export { waitForAgent as t };