@layerfs/runtime
The runtime opens on a LayerFS home, not on a project, so it works before the first project exists, after one is removed and while the worker is down. Agents' Bash calls use thelayerfs command, the checkout's api/agent/cli/ folder over @layerfs/agent; the runtime has no command of its own (its older lfs command is retired). Link it from the checkout like the other packages:npm install "file:$LAYERFS_REPO/api/runtime" "file:$LAYERFS_REPO/api/backend".
import { LayerFSRuntime } from "@layerfs/runtime";
const runtime = await LayerFSRuntime.open({ home, image, bundle }); // a LayerFS home, not a project
try {
const { missing } = await runtime.status(); // settings still missing, if any
const job = await runtime.project.start_init("shop", "/src/shop", { timeout: 600_000 });
console.log(await runtime.project.get_init(job.id)); // { step, scanned, totals, imported, … }
for await (const event of runtime.events.subscribe({ project: "shop" })) {
console.log(event.actor, event.method, event.ms); // one line per call, from every process
}
} finally {
await runtime.close();
}
LayerFSRuntime | Answers |
|---|
LayerFSRuntime.open(settings) | A runtime on one LayerFS home. A missing setting doesn't refuse: status().missing lists it |
runtime | Answers |
|---|
runtime.status() | { platform, missing, docker, worker, disk, sweep }; sweep is the open-time sweep: null, { state: "running" } or { state: "done" | "failed", at, … } |
runtime.close() | Waits for the open-time sweep, then closes its project clients; Workspaces stay mounted. On Windows it drains and ends the worker it started |
runtime.project | Answers |
|---|
runtime.project.list() | [{ name, volume, image, source, created, initialized }] |
runtime.project.start_init(name, folder, { image, timeout }?) | A job. It checks the name, folder, image and timeout first, then counts the source, makes the volume and runs one Init |
runtime.project.get_init(job) | { step, scanned, totals, imported, project, error, ended } |
runtime.project.get_state(name, since?) | One project for a host's view: its history, Sandboxes and their Workspaces under one version; { kind: "unchanged" } when since is still current |
runtime.project.remove(name, { confirm, stopSandboxes }) | Deletes the project's volume and home entry. confirm must equal the name; Busy while a container uses the volume unless stopSandboxes |
runtime.worker | Answers |
|---|
runtime.worker.start() | macOS and Windows: starts or reuses the LayerFS home's worker, and stops no other container. Linux has no worker: refused InvalidInput |
runtime.worker.restart({ confirm: "restart" }) | Restarts the worker. It cuts off every project's Store calls, so it needs the confirmation |
runtime.sandbox | Answers |
|---|
runtime.sandbox.reclaim(name) | { reclaimed: [ids] }: the project's Sandboxes that closed themselves |
runtime.sandbox.list_leftovers() | Containers of this home that belong to no project, or were left stopped |
runtime.sandbox.clean_leftovers(names) | Removes the named leftovers, each once |
runtime.sandbox.list() | [{ name, project, sandbox, state, running, startedAt, finishedAt, image, ports, scratch, owned }]: this home's Sandbox containers, running or not; scratch is the bytes commands wrote outside every Workspace. It changes nothing |
runtime.sandbox.sweep() | { reclaimed: { project: [ids] }, failed: [{ project, code, message }] }: reclaim for every project of the home with a stopped Sandbox. open runs it once unless sweep: false |
runtime.volume | Answers |
|---|
runtime.volume.list() | [{ name, size, containers, owner, project, live, removable }]: every Docker volume, with owner "this", "other" or "unknown" |
runtime.volume.remove(names) | { results: [{ name, ok, message? }] }: removes only volumes list marks removable (this home's, no project's, used by no container), one docker volume rm each |
runtime.events | Answers |
|---|
runtime.events.subscribe({ project, since }?) | An async iterator of trace lines from every process in this home, each with its actor |
start_init passes timeout unchanged to LayerFS.init, so an omitted one gets its 300 s. A failed Init removes the volume and home entry it made; an uncertain one (Unknown, or any failure on Windows) keeps everything and is recorded in init-failures.jsonl in stateDir. Init labels the project's volume layerfs.role=store, layerfs.home andlayerfs.project, which is how volume.list tells whose it is.
Settings
A missing setting doesn't refuse open; status().missing lists it.
| Setting | Meaning |
|---|
home | Required: the LayerFS home, an absolute folder |
image | The Sandbox image digest, sha256:… |
bundle | A sealed Linux build folder; macOS and Windows need it for the worker |
daemon | The Linux daemon; default the bundle's |
docker, volumes | Docker Engine socket, and on Linux Docker's volumes folder |
containerPrefix | Container name prefix; default layerfs- |
traceDir, stateDir | Trace files and the runtime's own records; default traces/ and .runtime/ in the home |
sweep | Default true: open runs one sandbox.sweep in the background, reported by status().sweep |
wsl | Windows: { distribution, user, node, home } of the WSL2 side |
| Platform | How the runtime runs |
|---|
| Linux | Store calls run in this process; there is no worker. Init installs the Store under Docker's volumes folder, so the process must be able to write it |
| macOS | The worker is one container per home, <prefix>worker (label layerfs.role=worker), running the bundle's real SDK. Init runs in a container of its own with the source folder mounted read-only. The worker's secret reaches docker run through its environment, never its command line |
| Windows | The worker is a Node process in a WSL2 distribution; Init runs in the worker. close() drains and ends it |
The worker route is open to any integration. On macOS and Windows the runtime hands its worker's endpoint to@layerfs/agent (useWorker, without changing the environment), so every client made afteropen in its process uses the worker; a client made before keeps its in-process binding. One worker per process: a second runtime of another home is refused. Another process, such as an agent's command, uses the worker when its LAYERFS_WORKER_ENDPOINT names the endpoint file, <home>/worker/endpoint.json; the integration that runs it sets that. The worker checks every file of the bundle's manifest.jsonbefore it listens.
@layerfs/backend
The backend wraps an open agent client. store.* calls read the project's Store and need no Sandbox;daemon.* calls need the Sandbox running, else NoSuchSandbox. Background processes run through the Sandbox's ordinary command launcher. Thelayerfs-sdk-observe skill covers these calls and the call trace.
import { LayerFS } from "@layerfs/agent";
import { LayerFSBackend } from "@layerfs/backend";
const layerfs = await LayerFS.open("shop");
const backend = LayerFSBackend.of(layerfs); // opens nothing of its own
const history = await backend.store.project.get_history(); // Branches, newest Commits, Layers, version
const storage = await backend.store.project.get_storage();
const proc = await backend.daemon.process.start(ws, "npm run dev"); // a background process in a Workspace
await backend.daemon.process.cancel(proc.process);
LayerFSBackend | Answers |
|---|
LayerFSBackend.of(layerfs) | A backend over an open @layerfs/agent client. It opens nothing and is refused once the client is closed |
backend.store.project | Answers |
|---|
backend.store.project.get_history(since?) | Branches, newest Commits, Layers and a version, for a history view |
backend.store.project.get_storage() | The Store's sizes: logical, canonical, stored, metadata, file, free and their ratios |
backend.store.project.get_comparable(ref) | { commit, root, exact }: the Commit with the same files as a Layer, so it can be diffed; or NotComparable |
backend.store.project.list_overwritten(layer, pageSize?, pagePos?) | What a publish overwrote on main |
backend.store.branch | Answers |
|---|
backend.store.branch.list_commit_storage(branch, pageSize?, pagePos?) | Each Commit's new bytes |
backend.store.fs | Answers |
|---|
backend.store.fs.get_storage(ref, path) | One file's sizes |
backend.store.fs.list_revisions(ref, path, pageSize?, pagePos?) | The Commits that changed one file |
backend.daemon.workspace | Answers |
|---|
backend.daemon.workspace.status(ws) | { base, baseLayer, branchHead, headMovedSinceMount, activity, overlay, branch } |
backend.daemon.workspace.list_changes(ws, pageSize?, pagePos?) | What changed in a mounted Workspace since its mount |
backend.daemon.workspace.get_storage(ws) | The Workspace overlay's sizes |
backend.daemon.process | Answers |
|---|
backend.daemon.process.start(ws, command) | Starts a background process, such as a dev server, and answers its id |
backend.daemon.process.list(pageSize?, pagePos?) | Every Sandbox's background processes, each with its sandbox |
backend.daemon.process.cancel(process) | Stops one background process |
backend.daemon.sandbox | Answers |
|---|
backend.daemon.sandbox.get_log(sandbox, { lines }?) | { sandbox, container, text, bytes }: the daemon's last lines (1 to 2000, at most 64 KiB) |