Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
125723a
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
e4cbcad
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
c43060f
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
0604b72
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
5099871
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
12a734a
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
e20bb93
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
c5e1502
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
f832490
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
726869a
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
13d8631
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
eda1537
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
4f0ce6d
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
8d2473c
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
055655d
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
4906f63
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
de16c45
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
3466de3
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
41a6c20
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
6d9dd41
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
4a5caa7
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
819bd2c
feat(sandbox): a local process runtime for Sandboxes, LAYERFS_SANDBOX…
jiangkoumo Oct 22, 2026
13a791a
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
13361ba
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
fed0249
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
603b561
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
1f2307e
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
4a66f35
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
cba6af0
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
f71df4a
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
0466af2
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
9e0b6a0
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
fd35180
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
0cf539a
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
e88bd55
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
0f156e3
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
3fa366c
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
d53d4fb
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
d463067
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
8c22d17
fix(sandbox): address the first review of the local runtime (#402)
jiangkoumo Oct 22, 2026
5f83fab
Merge PR #402 head 8c22d1754 (fork feat/sandbox-local-backend) into m…
jiangkoumo Oct 22, 2026
e63af4c
feat(nfs): a loopback NFSv3 export of one mounted Workspace (layerfs-…
jiangkoumo Oct 22, 2026
757e448
feat(daemon): NFS as a kernel mount entry beside FUSE (LAYERFS_KERNEL…
jiangkoumo Oct 22, 2026
6c8ca0e
feat(sandbox): local Sandboxes on macOS and the SDK's native route (L…
jiangkoumo Oct 22, 2026
1deb780
feat(store-service): open the Store on macOS for the native route; bu…
jiangkoumo Oct 22, 2026
5169c30
feat(dsh): the LayerFS Host on the native macOS route (sandbox: nativ…
jiangkoumo Oct 22, 2026
94f15b7
Merge upstream main 1aa56ece7 into feat/macos-native-on-main
jiangkoumo Oct 23, 2026
833acf7
Merge upstream main e8f129c1b into feat/macos-native-on-main
jiangkoumo Oct 23, 2026
ada323c
Merge upstream main 4497b0cbf (v1.1.0, #428) into PR #402 (local port)
jiangkoumo Oct 24, 2026
da213ce
Merge PR #402 local port (ada323c90, with upstream main 4497b0cbf v1.…
jiangkoumo Oct 24, 2026
009931c
layerfs-nfs: macOS mount tolerates slow answers; LOOKUP carries the d…
jiangkoumo Oct 24, 2026
915b622
Merge upstream main c77281e49 (#434 Ruff format, #440) into feat/maco…
jiangkoumo Oct 24, 2026
0cc863a
Merge upstream main ad78914e3 (#449, per-Store overlay name) into PR …
jiangkoumo Oct 25, 2026
4deeefb
Merge upstream main ad78914e3 (#449, per-Store overlay name) into fea…
jiangkoumo Oct 25, 2026
a700af8
layerfs-nfs: keep AppleDouble files in the export's memory instead of…
jiangkoumo Oct 25, 2026
4c1648e
fix(native): bound NFS helper reaping and detach before daemon stop
jiangkoumo Oct 25, 2026
b3f8f80
fix(binding): release Sandbox ownership while unmount waits
jiangkoumo Oct 26, 2026
d3a2a8c
fix(binding): use libc::O_NOFOLLOW for the unmount lease
jiangkoumo Oct 26, 2026
766e67e
fix(sandbox): detach local mounts before stopping the daemon
jiangkoumo Oct 26, 2026
3e427a3
Merge upstream main caebd268d (D8 three-layer split, #465) into PR #4…
jiangkoumo Oct 27, 2026
8b11778
style: rustfmt
jiangkoumo Oct 27, 2026
1931032
Merge upstream main caebd268d (D8 three-layer split, #465) into PR #4…
jiangkoumo Oct 27, 2026
eb21fbb
test(node): cover local Sandbox records through the built addon (#402)
jiangkoumo Oct 27, 2026
7fcfdfb
Merge port/d8-402 8b17038f2 (#402 node tests through the built addon)…
jiangkoumo Oct 27, 2026
8b17038
style: rustfmt
jiangkoumo Oct 27, 2026
1dcd1fb
test(node): native Sandbox records and the unmount lease regression a…
jiangkoumo Oct 27, 2026
a6b53a9
fix(tools): place layerfs-nfs in the engine layer of the boundary map…
jiangkoumo Oct 27, 2026
bf50a5f
docs(node): list native_local in the binding test guide (#402)
jiangkoumo Oct 27, 2026
befe2a9
Merge port/d8-402 bf50a5f70 (#402 binding test guide) into PR #427 (l…
jiangkoumo Oct 27, 2026
9478190
docs(node): native records in the binding test guide (#427)
jiangkoumo Oct 27, 2026
eb0d6d6
Merge upstream main 582bc786f into feat/macos-native-on-main (#427)
jiangkoumo Oct 28, 2026
d55acc1
refactor(node): native is the only local deployment; local is no long…
jiangkoumo Oct 28, 2026
559bff5
feat(sandbox): define provider lifecycle and shared command types
jiangkoumo Oct 28, 2026
8ce4697
feat(sandbox): adapt native macOS runtime to provider traits
jiangkoumo Oct 28, 2026
59c0da2
refactor(api): share provider daemon startup with native macOS
jiangkoumo Oct 28, 2026
c82a903
feat(sandbox): add native Linux provider over the shared local runtime
jiangkoumo Oct 28, 2026
7a6952c
feat(node): restore Linux native deployment through the provider pipe…
jiangkoumo Oct 28, 2026
e5004d6
feat(sandbox): add the Docker provider behind the shared runtime port
jiangkoumo Oct 28, 2026
2e4e3cf
test(node): answer the Docker version probe in the Engine fixture (H25)
jiangkoumo Oct 28, 2026
abed25d
merge: bring in the H25 Engine fixture fix (2e4e3cfec)
jiangkoumo Oct 28, 2026
7922044
feat(sandbox): add Docker options and record variants to provider tra…
jiangkoumo Oct 28, 2026
70ec59e
feat(api): unify SandboxApi behind the runtime provider route (H26)
jiangkoumo Oct 28, 2026
8cffb6b
feat(node): route Docker through DockerProvider and remove deprecated…
jiangkoumo Oct 28, 2026
3147916
Merge upstream main 35d36575c (#500 stalled unmount recovery) into PR…
jiangkoumo Oct 29, 2026
ab6ce70
Merge PR #427 head 3147916ef (upstream main 35d36575c) into B1
jiangkoumo Oct 29, 2026
2518769
test(sandbox): wait for local daemon reaping (H29)
jiangkoumo Oct 29, 2026
5e5486f
Merge PR #427 head 251876979 (H29 local_runtime reaping test fix) int…
jiangkoumo Oct 29, 2026
f4c8167
refactor(sandbox): dispatch concrete runtimes and split native backen…
jiangkoumo Oct 30, 2026
2c2ff0a
feat(sandbox): add Linux namespaces backend and sandboxed helper
jiangkoumo Oct 30, 2026
bdc341a
fix(node): report why a namespaces record cannot run on this host
jiangkoumo Oct 30, 2026
dee8869
feat(runtime): place a LayerFS home in one Linux VM (#515)
jiangkoumo Oct 31, 2026
27d92c5
feat(sandbox): unpack docker-archive Sandbox rootfs and overlay upper…
jiangkoumo Oct 31, 2026
06ae574
feat(runtime): place each Store in one Linux VM (#515)
jiangkoumo Oct 31, 2026
9c8c1e0
merge h38-oci-rootfs into h39-vm-image
jiangkoumo Oct 31, 2026
5d9dc29
feat(runtime): place each VM's Sandbox root on the bundle image (#515)
jiangkoumo Oct 31, 2026
9aa10e6
feat(runtime): add vm:ssh adapter for user-provided Linux hosts (#515)
jiangkoumo Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ remains a separate MEMORY/OFF/EXCLUSIVE database file.
prefixed names, such as dsh's `DSH_*`, maps them itself. Linux still makes
Store calls in the caller's process, and the worker's rules above (loopback,
secret, one attempt, no replay) are unchanged.
- **VM placement (`@layerfs/runtime`, owner 2026-10-31, #515).** A LayerFS
home may be placed in Linux VMs on this machine instead of in the host's
Docker: `LAYERFS_PLACEMENT=vm:<provider>` (or the runtime's `placement`
setting), with `local` the default and unchanged. **One Linux VM per Store**
(V1): a project's VM holds that project's Store on its own ext4 disk, its
Sandboxes are Linux namespaces inside it
(`LAYERFS_SANDBOX_BACKEND=namespaces`, documented in
[the agent guide](api/docs/agent.md#namespace-sandboxes-linux)), and its
worker runs the real SDK as root with its own secret and its own
loopback-published port; the host reaches only each VM's worker, with one
attempt per call and no replay, the same rules as the macOS and Windows
routes. Store calls therefore leave the caller's process on Linux too, under
`vm:` placement only, and a process routes them per project
(`useWorker((project) => endpoint)`, or that project's
`LAYERFS_WORKER_ENDPOINT`). `project.init` makes the project's VM and
`project.remove` deletes it with its Store disk; a bare VM delete is refused
while the project exists, because that Store is on that disk only and no
remote Store push exists (#506). Implemented and verified providers:
`microsandbox` (`msb`) and `apple-container` (`container` 1.6.1).
`@layerfs/runtime`'s guide and `api/runtime/src/platform/vm/` hold the details.
- **One global database, one writer, short holds (owner decision 2026-10-18).**
All committed state lives in one SQLite Store file; no sharding, second
database, concurrent-writer engine or objects outside it. Writes go one at a
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 44 additions & 4 deletions api/agent/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ export const BACKEND = /^(backend|runtime)\./;

/** Runs one operation once and returns its value or throws its refusal;
* `options` are the client's settings, whose `trace` selects its trace file;
* `transport` is the client's binding (default: the one a new client would use). */
export async function invoke(op, args, options, transport = binding()) {
* `transport` is the client's binding (default: the one a new client of
* `args.project` would use). */
export async function invoke(op, args, options, transport = binding(args.project)) {
return outcome(await traced(options, op, args, (scope) => {
if (BACKEND.test(op)) throw new LayerFSError("UnknownMethod", "backend and runtime methods are @layerfs/backend's and @layerfs/runtime's", null);
return transport.call(op, observed(args, scope));
}));
}

/** One call on the binding's backend table (`backend.*`, `runtime.*`). */
export async function invokeBackend(op, args, options, transport = binding()) {
export async function invokeBackend(op, args, options, transport = binding(args?.project)) {
return outcome(await traced(options, op, args, (scope) => transport.backendCall(op, observed(args, scope))));
}

Expand Down Expand Up @@ -60,9 +61,19 @@ export function sandboxArg(sandbox) {

/** Deployment settings from the client options, else the environment
* (LAYERFS_<NAME>). A Workspace's Sandbox needs
* every one but the container prefix and daemon host. */
* every one but the container prefix and daemon host. With `sandbox`
* (LAYERFS_SANDBOX) `native` (Linux or macOS), a Sandbox is a host daemon:
* Linux uses FUSE with a root daemon and nonroot commands (501:20);
* macOS uses loopback NFS and this user. It needs the daemon and
* `localDir` (LAYERFS_LOCAL_DIR) and no Docker setting. Unset or `docker` is
* Docker; any other value is refused. */
export function deployment(options = {}) {
const env = (name) => process.env[`LAYERFS_${name}`] || undefined;
const sandbox = options.sandbox ?? env("SANDBOX");
if (sandbox === "native") return nativeDeployment(options, env, sandbox);
if (sandbox !== undefined && sandbox !== "docker") {
throw new LayerFSError("InvalidInput", `deploy.sandbox (LAYERFS_SANDBOX) is docker or native (Linux or macOS), not ${sandbox}`, null);
}
const deploy = {
docker: options.docker ?? env("DOCKER") ?? "/var/run/docker.sock",
image: options.image ?? env("IMAGE"),
Expand All @@ -80,6 +91,35 @@ export function deployment(options = {}) {
return deploy;
}

/** A native Sandbox's settings: the daemon, the directory its Sandboxes
* live in, the optional name prefix, and the backend inside that directory
* (`sandboxBackend`, LAYERFS_SANDBOX_BACKEND: `native` when unset, or
* `namespaces` on Linux). The namespaces backend gives each Sandbox a root
* filesystem from an image, so it also needs `image` (LAYERFS_IMAGE, the
* same digest Docker deploys) and `imageArchive` (LAYERFS_IMAGE_ARCHIVE, the
* docker-archive the bundle carries). */
function nativeDeployment(options, env, sandbox) {
const deploy = {
sandbox,
daemon: options.daemon ?? env("DAEMON"),
localDir: options.localDir ?? env("LOCAL_DIR"),
containerPrefix: options.containerPrefix ?? env("CONTAINER_PREFIX"),
};
const backend = options.sandboxBackend ?? env("SANDBOX_BACKEND");
if (backend !== undefined) deploy.sandboxBackend = backend;
const required = [["daemon", "DAEMON"], ["localDir", "LOCAL_DIR"]];
if (backend === "namespaces") {
deploy.image = options.image ?? env("IMAGE");
deploy.imageArchive = options.imageArchive ?? env("IMAGE_ARCHIVE");
required.push(["image", "IMAGE"], ["imageArchive", "IMAGE_ARCHIVE"]);
}
for (const [name, setting] of required) {
if (!deploy[name]) throw new LayerFSError("InvalidInput", `deploy.${name} is not configured (LAYERFS_${setting})`, null);
}
if (!deploy.containerPrefix) delete deploy.containerPrefix;
return deploy;
}

/** Refuses every call on a closed client (or one no factory opened). */
export function live(client) {
if (client?.[STATE]?.closed !== false) {
Expand Down
19 changes: 16 additions & 3 deletions api/agent/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,15 @@ export class LayerFS {
* settings read LAYERFS_HOME, _DOCKER, _IMAGE, _VOLUME and _DAEMON; a
* Sandbox this client starts runs _IMAGE
* on _VOLUME. LAYERFS_CONTAINER_PREFIX (default `layerfs-`) names every
* container it starts `<prefix><project>-<n>`.
* container it starts `<prefix><project>-<n>`. On Linux or macOS,
* LAYERFS_SANDBOX=native starts each Sandbox as a host daemon in a directory
* under LAYERFS_LOCAL_DIR. Linux requires root for FUSE and runs commands
* as 501:20; macOS uses loopback NFS and the caller's identity. No Docker
* setting is read; native provides no isolation from the host.
* LAYERFS_SANDBOX_BACKEND picks the backend inside that directory: `native`
* (unset) or, on Linux, `namespaces` (namespaces, cgroups, nonroot commands).
* It is read for every call, so a Sandbox made with one backend is never
* reopened as another.
* LAYERFS_DAEMON_HOST is the host this process
* dials for a daemon's port, published on the Docker host's 127.0.0.1 only;
* set it when the client runs in a container (`host.docker.internal` on
Expand Down Expand Up @@ -129,13 +137,18 @@ export class LayerFS {
volume: env("VOLUME"),
daemon: env("DAEMON"),
...(env("CONTAINER_PREFIX") ? { containerPrefix: env("CONTAINER_PREFIX") } : {}),
...(env("SANDBOX") ? { sandbox: env("SANDBOX") } : {}),
...(env("SANDBOX_BACKEND") ? { sandboxBackend: env("SANDBOX_BACKEND") } : {}),
...(env("LOCAL_DIR") ? { localDir: env("LOCAL_DIR") } : {}),
...(env("IMAGE_ARCHIVE") ? { imageArchive: env("IMAGE_ARCHIVE") } : {}),
...(env("DAEMON_HOST") ? { daemonHost: env("DAEMON_HOST") } : {}),
},
storePolicy: env("STORE_POLICY"),
storePath: env("STORE_PATH"),
};
// not open until a factory has opened its Store; the binding is chosen now, once
this[STATE] = { closed: null, transport: binding() };
// not open until a factory has opened its Store; the binding is chosen now, once,
// for this client's project (a VM placement has one worker per project)
this[STATE] = { closed: null, transport: binding(this.projectName) };
this.fs = bind(this, fsMethods);
this.project = bind(this, { list_branches, list_sandboxes });
this.branch = bind(this, { list_commits, diff_commits, diff_branches, fork, merge, publish });
Expand Down
35 changes: 23 additions & 12 deletions api/agent/src/transport/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
//
// The worker route is open to any integration that names an endpoint
// (AGENTS.md, owner 2026-10-24): @layerfs/runtime starts the worker on macOS
// and Windows, where a process cannot open the Store itself. LayerFS names no
// integration here.
// and Windows, where a process cannot open the Store itself, and in a
// `vm:<provider>` home, where each project's Store is in that project's VM
// (one endpoint per project). LayerFS names no integration here.
import { LayerFSError } from "../errors.js";
import { inprocess } from "./inprocess.js";
import { workerClient } from "./worker_client.js";

/** The worker `useWorker` handed over: { endpoint }, else null. */
/** The worker `useWorker` handed over: { endpoint }, { resolve } or null. */
let handed = null;
let local = null;
/** Worker clients by endpoint file. */
Expand All @@ -24,18 +25,28 @@ const workers = new Map();
export const setting = (name) => process.env[`LAYERFS_${name}`] || undefined;

/** Internal, for @layerfs/runtime: clients made from now on in this process
* use the worker at `endpoint` (an endpoint file). Takes precedence over the
* environment. One worker per process: another endpoint is refused. */
* use the worker at `endpoint` (an endpoint file), or, when `endpoint` is a
* function, the worker that function names for a client's project — one VM
* per Store has one endpoint each. Takes precedence over the environment.
* One worker per process: another endpoint is refused, and so is a second
* resolver. */
export function useWorker(endpoint) {
if (typeof endpoint !== "string" || !endpoint) throw new LayerFSError("InvalidInput", "a worker endpoint file is required", null);
if (handed && handed.endpoint !== endpoint) throw new LayerFSError("InvalidInput", `this process already uses the worker at ${handed.endpoint}`, null);
handed = { endpoint };
const next = typeof endpoint === "function" ? { resolve: endpoint }
: typeof endpoint === "string" && endpoint ? { endpoint }
: null;
if (!next) throw new LayerFSError("InvalidInput", "a worker endpoint file is required", null);
if (handed && (handed.endpoint !== next.endpoint || handed.resolve !== next.resolve)) {
throw new LayerFSError("InvalidInput", `this process already uses the worker at ${handed.endpoint ?? "one endpoint per project"}`, null);
}
handed = next;
}

/** The binding a client made now uses: the handed-over worker, else
* LAYERFS_WORKER_ENDPOINT's, else the addon in this process. */
export function binding() {
const endpoint = handed?.endpoint ?? setting("WORKER_ENDPOINT");
/** The binding a client made now uses: the handed-over worker (by project, or
* the one endpoint), else LAYERFS_WORKER_ENDPOINT's, else the addon in this
* process. A call without a project (an in-process backend call) uses the
* environment's endpoint, never a project's resolver. */
export function binding(project) {
const endpoint = handed?.resolve && typeof project === "string" ? handed.resolve(project) : handed?.endpoint ?? setting("WORKER_ENDPOINT");
if (!endpoint) return (local ??= inprocess());
let client = workers.get(endpoint);
if (!client) workers.set(endpoint, (client = workerClient(endpoint)));
Expand Down
143 changes: 143 additions & 0 deletions api/agent/test/native_deployment.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Which runtime a Sandbox uses. No real addon, Store or Docker (the
// stand-in binding answers the client's calls):
// node api/agent/test/native_deployment.mjs
// LAYERFS_SANDBOX unset or `docker` gives exactly the Docker deployment it
// always gave; `native` (macOS) needs only the daemon and LAYERFS_LOCAL_DIR
// (no Docker setting); any other value, the former `local` included, is
// refused rather than taken as either.
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { deployment } from "../src/client.js";

const FAKE = fileURLToPath(new URL("./fixture/fake-native.cjs", import.meta.url));
const native = createRequire(import.meta.url)(FAKE);

const clear = () => {
for (const name of Object.keys(process.env)) {
if (name.startsWith("LAYERFS_") || name.startsWith("DSH_LAYERFS_")) delete process.env[name];
}
process.env.LAYERFS_NATIVE = FAKE;
};
const docker = { docker: "/run/docker.sock", image: `sha256:${"a".repeat(64)}`, volume: "lfs-v", daemon: "/opt/layerfs-daemon" };
const setDocker = () => {
process.env.LAYERFS_DOCKER = docker.docker;
process.env.LAYERFS_IMAGE = docker.image;
process.env.LAYERFS_VOLUME = docker.volume;
process.env.LAYERFS_DAEMON = docker.daemon;
};
const store = () => {
process.env.LAYERFS_HOME = "/private/local-home";
process.env.LAYERFS_STORE_PATH = "/fake/store.sqlite";
};

// unset: the Docker deployment, field for field
clear();
setDocker();
assert.deepEqual(deployment(), docker);
// `docker` is the same deployment
process.env.LAYERFS_SANDBOX = "docker";
assert.deepEqual(deployment(), docker);
// the Docker settings are still required there
delete process.env.LAYERFS_IMAGE;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && /LAYERFS_IMAGE/.test(e.message));

// native: daemon + localDir, no Docker setting read or required
clear();
process.env.LAYERFS_SANDBOX = "native";
process.env.LAYERFS_DAEMON = "/opt/layerfs-daemon";
process.env.LAYERFS_LOCAL_DIR = "/Users/me/lfs";
assert.deepEqual(deployment(), { sandbox: "native", daemon: "/opt/layerfs-daemon", localDir: "/Users/me/lfs" });
// an image (sandbox.create's option) does not change a native deployment
assert.deepEqual(deployment({ image: docker.image }), { sandbox: "native", daemon: "/opt/layerfs-daemon", localDir: "/Users/me/lfs" });
// the backend inside the directory is native unless named; namespaces is
// passed through (the binding refuses it off Linux, and an unknown value)
assert.equal(deployment().sandboxBackend, undefined);
const archive = `/bundle/images/${"a".repeat(64)}.tar`;
process.env.LAYERFS_SANDBOX_BACKEND = "namespaces";
process.env.LAYERFS_IMAGE = docker.image;
process.env.LAYERFS_IMAGE_ARCHIVE = archive;
// a namespaces Sandbox's root is an image: the same digest Docker deploys
// and the docker-archive the bundle carries
assert.deepEqual(deployment(), {
sandbox: "native",
daemon: "/opt/layerfs-daemon",
localDir: "/Users/me/lfs",
sandboxBackend: "namespaces",
image: docker.image,
imageArchive: archive,
});
// each of those is named when it is missing
delete process.env.LAYERFS_IMAGE;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && /\(LAYERFS_IMAGE\)/.test(e.message));
process.env.LAYERFS_IMAGE = docker.image;
delete process.env.LAYERFS_IMAGE_ARCHIVE;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && /\(LAYERFS_IMAGE_ARCHIVE\)/.test(e.message));
process.env.LAYERFS_IMAGE_ARCHIVE = archive;
// the native backend inside the same directory reads neither of them
assert.deepEqual(deployment({ sandboxBackend: "native" }), {
sandbox: "native",
daemon: "/opt/layerfs-daemon",
localDir: "/Users/me/lfs",
sandboxBackend: "native",
});
delete process.env.LAYERFS_SANDBOX_BACKEND;
delete process.env.LAYERFS_IMAGE;
delete process.env.LAYERFS_IMAGE_ARCHIVE;
// the container prefix names native Sandbox directories too
process.env.LAYERFS_CONTAINER_PREFIX = "ci-";
assert.equal(deployment().containerPrefix, "ci-");
delete process.env.LAYERFS_CONTAINER_PREFIX;
// each required setting is named when missing
delete process.env.LAYERFS_LOCAL_DIR;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && /LAYERFS_LOCAL_DIR/.test(e.message));
process.env.LAYERFS_LOCAL_DIR = "/Users/me/lfs";
delete process.env.LAYERFS_DAEMON;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && /LAYERFS_DAEMON/.test(e.message));
process.env.LAYERFS_DAEMON = "/opt/layerfs-daemon";
// a DSH_LAYERFS_NFS_ name is not mapped (an integration maps its own names)
process.env.DSH_LAYERFS_NFS_MOUNT = "/bin/true";
deployment();
assert.equal(process.env.LAYERFS_NFS_MOUNT, undefined);
// since #428 the SDK reads LAYERFS_ names only: a DSH_LAYERFS_ twin selects nothing
clear();
setDocker();
process.env.DSH_LAYERFS_SANDBOX = "native";
process.env.DSH_LAYERFS_LOCAL_DIR = "/l";
assert.deepEqual(deployment(), docker);

// anything else, the former `local` included, is refused, never silently Docker
for (const typo of ["local", "Native", "nativ", "NATIVE", "podman"]) {
clear();
setDocker();
process.env.LAYERFS_SANDBOX = typo;
assert.throws(() => deployment(), (e) => e.code === "InvalidInput" && e.message.includes(`not ${typo}`));
}

// a client reads LAYERFS_SANDBOX and LAYERFS_LOCAL_DIR once, and
// sandbox.create sends the native deployment (no Docker setting)
clear();
store();
process.env.LAYERFS_SANDBOX = "native";
process.env.LAYERFS_DAEMON = "/opt/layerfs-daemon";
process.env.LAYERFS_LOCAL_DIR = "/Users/me/lfs";
const { LayerFS } = await import("../src/index.js");
const client = await LayerFS.open("website");
assert.equal(client.options.deploy.sandbox, "native");
assert.equal(client.options.deploy.localDir, "/Users/me/lfs");
const before = native.deploys.length;
await client.sandbox.create();
assert.deepEqual(native.deploys.slice(before), [{ sandbox: "native", daemon: "/opt/layerfs-daemon", localDir: "/Users/me/lfs" }]);
// an image given to sandbox.create does not turn it into a Docker deployment
await client.sandbox.create({ image: docker.image });
assert.deepEqual(native.deploys.at(-1), { sandbox: "native", daemon: "/opt/layerfs-daemon", localDir: "/Users/me/lfs" });
await client.close();
// unset: the client carries no new key
clear();
store();
setDocker();
const plain = await LayerFS.open("website");
assert.equal("sandbox" in plain.options.deploy, false);
assert.equal("localDir" in plain.options.deploy, false);
await plain.close();
console.log("native_deployment: ok");
Loading
Loading