@supermachine/core 0.5.3 → 0.5.5
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/README.md +35 -0
- package/examples/06-mount-workspace.mjs +97 -0
- package/examples/README.md +2 -1
- package/index.d.ts +9 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -182,6 +182,40 @@ const image = await Image.build({
|
|
|
182
182
|
|
|
183
183
|
Host file contents are folded into the snapshot's input-hash, so editing the host file invalidates the cache and rebakes automatically.
|
|
184
184
|
|
|
185
|
+
### Pattern 5: Live host directory mount (virtio-fs DAX)
|
|
186
|
+
|
|
187
|
+
For dev loops, code-mounted-from-host workflows, and "run my project" sandboxes, you want a *live* host directory inside the guest — edits on the host show up in the guest immediately, and the guest can read/write back. This is what `mounts` does: a virtio-fs share with DAX (zero-copy reads through the host page cache, kqueue-driven cache invalidation when host files change).
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const image = await Image.build({
|
|
191
|
+
ref: "node:20-alpine",
|
|
192
|
+
mounts: [
|
|
193
|
+
{
|
|
194
|
+
hostPath: "/Users/me/my-app", // host source tree
|
|
195
|
+
guestTag: "workspace", // guest mounts via this tag
|
|
196
|
+
symlinks: "opaque", // see below
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const vm = await (await image.pool()).acquire();
|
|
202
|
+
// Inside the guest:
|
|
203
|
+
// mount -t virtiofs workspace /work
|
|
204
|
+
// cd /work && npm install
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**`symlinks` policy** picks the trust posture for the mount. Workspace tools (npm workspaces, pnpm, yarn berry) symlink prolifically — `node_modules/<member> → ../packages/<member>` — so symlink creation has to work. But symlinks are also the classic escape vector if you're running untrusted code. The policy lets integrators pick:
|
|
208
|
+
|
|
209
|
+
| Value | Guest can create symlinks? | External host symlinks visible/traversable? | When |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `"deny"` | no (`EPERM`) | no (`EACCES` at LOOKUP) | Paranoid mounts: pure file content, no metadata surprises |
|
|
212
|
+
| `"opaque"` *(default)* | yes — targets stored verbatim, host never resolves them | no | **Safe multi-tenant default**: npm/pnpm/yarn all work; a hostile guest can plant `escape → /etc/passwd` but the host never follows it |
|
|
213
|
+
| `"follow"` | yes | yes | Trusted single-tenant: dev trees that symlink into Homebrew/`~/.cache`/etc. |
|
|
214
|
+
|
|
215
|
+
The `"opaque"` default exists because of how `symlink(2)` works: the target is opaque bytes stored on the symlink inode — POSIX never validates it. Resolution happens *in the guest's kernel* against the guest's own namespace, so a target like `/etc/passwd` resolves to the guest's `/etc/passwd`, not the host's. The host serves `readlink()` (returns those bytes) and refuses to follow them. That's the security model: guest can store arbitrary bytes, host never canonicalizes guest-supplied paths.
|
|
216
|
+
|
|
217
|
+
A runnable example is in [`examples/06-mount-workspace.mjs`](./examples/06-mount-workspace.mjs).
|
|
218
|
+
|
|
185
219
|
---
|
|
186
220
|
|
|
187
221
|
## Full API
|
|
@@ -217,6 +251,7 @@ class Image {
|
|
|
217
251
|
| `guestPort` | `number` | 80 | Listener port (used with `listenerRequired`) |
|
|
218
252
|
| `listenerRequired` | `boolean` | `false` | Wait for workload's listener before snapshotting |
|
|
219
253
|
| `extraFiles` | `ExtraFile[]` | — | Stage host files into guest at fixed paths |
|
|
254
|
+
| `mounts` | `MountSpec[]` | — | Live host dir mounts via virtio-fs DAX. See Pattern 5. |
|
|
220
255
|
| `warmupTag` | `string` | `"default"` | Cache key for the warmup; bump to invalidate |
|
|
221
256
|
| `warmup` | `(vm: WarmupVm) => Promise<void>` | — | Post-bake state pre-population |
|
|
222
257
|
| `snapshotsDir` | `string` | `~/.local/supermachine-snapshots` | Where to write the snapshot |
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// 06-mount-workspace.mjs — mount a host directory into the guest via
|
|
2
|
+
// virtio-fs DAX, then run an `npm install`-style symlink workflow
|
|
3
|
+
// from inside the VM.
|
|
4
|
+
//
|
|
5
|
+
// Demonstrates:
|
|
6
|
+
// - `mounts: [{ hostPath, guestTag, symlinks }]` on Image.build
|
|
7
|
+
// - The per-mount `symlinks` policy: "deny" | "opaque" | "follow"
|
|
8
|
+
// - The npm-workspace symlink pattern (node_modules/<name> →
|
|
9
|
+
// ../packages/<name>) working end-to-end through virtio-fs
|
|
10
|
+
//
|
|
11
|
+
// The mount surfaces host files in the guest with zero-copy DAX
|
|
12
|
+
// reads and kqueue-driven cache invalidation when the host file
|
|
13
|
+
// changes — edit a file on the host, the guest sees the new bytes
|
|
14
|
+
// on the next read with no manual re-mount.
|
|
15
|
+
//
|
|
16
|
+
// `symlinks: "opaque"` is the safe default for guest-writable mounts
|
|
17
|
+
// in a multi-tenant setting: guest can create symlinks (workspace
|
|
18
|
+
// tools need this), targets are stored verbatim (POSIX symlink(2)),
|
|
19
|
+
// and the host never resolves them. A guest planting
|
|
20
|
+
// `escape -> /etc/passwd` gets a symlink whose readlink returns
|
|
21
|
+
// "/etc/passwd" bytes — but the host won't follow it. Path
|
|
22
|
+
// resolution happens in the guest's kernel against the guest's
|
|
23
|
+
// own namespace.
|
|
24
|
+
//
|
|
25
|
+
// Run:
|
|
26
|
+
// node examples/06-mount-workspace.mjs
|
|
27
|
+
|
|
28
|
+
import { Image } from "@supermachine/core";
|
|
29
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readlinkSync } from "node:fs";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
|
|
33
|
+
// 1. Build a host-side workspace tree that mimics what npm creates:
|
|
34
|
+
// project/
|
|
35
|
+
// packages/alpha/index.js "ALPHA"
|
|
36
|
+
// packages/beta/index.js "BETA"
|
|
37
|
+
// Inside the guest we'll add node_modules/<name> symlinks
|
|
38
|
+
// pointing at ../packages/<name>.
|
|
39
|
+
const project = mkdtempSync(join(tmpdir(), "sm-workspace-"));
|
|
40
|
+
mkdirSync(join(project, "packages", "alpha"), { recursive: true });
|
|
41
|
+
mkdirSync(join(project, "packages", "beta"), { recursive: true });
|
|
42
|
+
writeFileSync(join(project, "packages", "alpha", "index.js"), "ALPHA");
|
|
43
|
+
writeFileSync(join(project, "packages", "beta", "index.js"), "BETA");
|
|
44
|
+
|
|
45
|
+
// 2. Bake an image with the project mounted at guest tag "workspace".
|
|
46
|
+
// `symlinks: "opaque"` is the default but spelled out here for
|
|
47
|
+
// documentation value.
|
|
48
|
+
const img = await Image.build({
|
|
49
|
+
ref: "alpine:latest",
|
|
50
|
+
name: "mount-workspace-demo",
|
|
51
|
+
memoryMib: 256,
|
|
52
|
+
cmd: ["/bin/sh", "-c", "sleep infinity"],
|
|
53
|
+
mounts: [
|
|
54
|
+
{
|
|
55
|
+
hostPath: project,
|
|
56
|
+
guestTag: "workspace",
|
|
57
|
+
symlinks: "opaque",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const pool = await img.pool({ min: 1, max: 1, restoreOnRelease: false });
|
|
63
|
+
const vm = await pool.acquire();
|
|
64
|
+
|
|
65
|
+
// 3. Inside the guest: mount the virtio-fs share, create the
|
|
66
|
+
// workspace symlinks, resolve through them.
|
|
67
|
+
const result = await vm.exec({
|
|
68
|
+
argv: [
|
|
69
|
+
"sh",
|
|
70
|
+
"-c",
|
|
71
|
+
[
|
|
72
|
+
"mkdir -p /work && mount -t virtiofs workspace /work",
|
|
73
|
+
"mkdir -p /work/node_modules",
|
|
74
|
+
"ln -s ../packages/alpha /work/node_modules/alpha",
|
|
75
|
+
"ln -s ../packages/beta /work/node_modules/beta",
|
|
76
|
+
"echo === readlink ===",
|
|
77
|
+
"readlink /work/node_modules/alpha",
|
|
78
|
+
"readlink /work/node_modules/beta",
|
|
79
|
+
"echo === resolve through ===",
|
|
80
|
+
"cat /work/node_modules/alpha/index.js",
|
|
81
|
+
"cat /work/node_modules/beta/index.js",
|
|
82
|
+
].join(" && "),
|
|
83
|
+
],
|
|
84
|
+
timeoutMs: 10_000,
|
|
85
|
+
});
|
|
86
|
+
console.log(result.stdout.toString());
|
|
87
|
+
|
|
88
|
+
// 4. Host-side: confirm the symlinks landed on disk as real
|
|
89
|
+
// symlinks (not regular files), with the bytes the guest asked
|
|
90
|
+
// for. This is the "store-target-verbatim" guarantee in action.
|
|
91
|
+
console.log("=== host-side symlink targets ===");
|
|
92
|
+
console.log("alpha →", readlinkSync(join(project, "node_modules", "alpha")));
|
|
93
|
+
console.log("beta →", readlinkSync(join(project, "node_modules", "beta")));
|
|
94
|
+
|
|
95
|
+
await vm.release();
|
|
96
|
+
await pool.shutdown();
|
|
97
|
+
rmSync(project, { recursive: true, force: true });
|
package/examples/README.md
CHANGED
|
@@ -11,6 +11,7 @@ with the package installed.
|
|
|
11
11
|
| `03-snapshot-then-restore.mjs` | `vm.writeFile` / `readFile`, mid-flight `vm.snapshot()`, `Image.fromSnapshot` |
|
|
12
12
|
| `04-expose-tcp.mjs` | `listenerRequired: true` bakes, `vm.exposeTcp()`, `TcpForwarder` |
|
|
13
13
|
| `05-spawn-streaming.mjs` | `vm.spawn()` → `ExecChild` (writeStdin / readStdout / wait) |
|
|
14
|
+
| `06-mount-workspace.mjs` | `mounts` + `symlinks` policy: live host dir into guest, npm-workspace symlink pattern |
|
|
14
15
|
|
|
15
16
|
## Requirements
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ node --input-type=module -e \
|
|
|
33
34
|
"$(sed 's|@supermachine/core|./index.js|' examples/01-hello.mjs)"
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
For a quick smoke test that all
|
|
37
|
+
For a quick smoke test that all examples work end-to-end:
|
|
37
38
|
|
|
38
39
|
```sh
|
|
39
40
|
for f in examples/*.mjs; do
|
package/index.d.ts
CHANGED
|
@@ -39,6 +39,15 @@ export interface MountSpec {
|
|
|
39
39
|
hostPath: string
|
|
40
40
|
/** virtiofs tag the guest mounts by. Max 35 bytes. */
|
|
41
41
|
guestTag: string
|
|
42
|
+
/**
|
|
43
|
+
* Symlink policy: `"deny"` | `"opaque"` | `"follow"`. Default
|
|
44
|
+
* `"opaque"` — guest may create symlinks (npm/pnpm/yarn rely on
|
|
45
|
+
* them) but host-side symlinks pointing outside the mount root
|
|
46
|
+
* are rejected. Use `"deny"` for paranoid mounts (no symlinks or
|
|
47
|
+
* hard links at all) or `"follow"` for trusted single-tenant
|
|
48
|
+
* workloads (pre-0.5.5 `allowExternalSymlinks: true` behaviour).
|
|
49
|
+
*/
|
|
50
|
+
symlinks?: string
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supermachine/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Run any OCI/Docker image as a hardware-isolated microVM. Node/Bun/Deno binding for the supermachine Rust crate.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"arm64"
|
|
23
23
|
],
|
|
24
24
|
"optionalDependencies": {
|
|
25
|
-
"@supermachine/core-darwin-arm64": "0.5.
|
|
25
|
+
"@supermachine/core-darwin-arm64": "0.5.5"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build:rust": "TYPE_DEF_TMP_PATH=$(pwd)/scripts/.napi_type_defs.tmp cargo build --release -p supermachine-napi && cargo build --release --bin supermachine-worker -p supermachine",
|