From fe4a70dcc5c565c5f5b56cddc663f7f275c1b3dc Mon Sep 17 00:00:00 2001 From: enixCode <58286681+enixCode@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:02:03 +0200 Subject: [PATCH] docs: align docs with code, unify ecosystem wording - command -> entrypoint in all examples (the RunRequest field is entrypoint) - network: 'none' -> networks: ['none'] (field is networks: string[]) - ecosystem table: light-run and light-process are shipped, not planned - RunState: add timeout and extract fields; clarify reapOrphans volume reaping - artefact -> artifact; add ecosystem cross-links on the overview page build with cc --- README.md | 10 ++++++---- website/content/docs/detached.mdx | 4 ++-- website/content/docs/extract.mdx | 6 +++--- website/content/docs/gvisor-kata.mdx | 6 +++--- website/content/docs/index.mdx | 2 ++ website/content/docs/quickstart.mdx | 4 ++-- website/content/docs/security.mdx | 8 ++++---- 7 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 69a8eae..f367d2c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ | Project | Responsibility | Status | | --------------- | ---------------------------------------------- | ------------ | | `light-runner` | Spawn one container, return exit code + files | **this repo** | -| `light-run` | CLI + HTTP wrapper around `light-runner` | planned | -| `light-process` | DAG orchestration, retries, fan-out | planned | +| `light-run` | CLI + HTTP wrapper around `light-runner` | [shipped](https://enixcode.github.io/light-run/) | +| `light-process` | DAG orchestration, retries, fan-out | [shipped](https://enixcode.github.io/light-process/) | Each layer is an independent npm package. Use `light-runner` alone when you just need to run code in a sandbox; pick the higher layers when you want scheduling or an HTTP surface. @@ -264,6 +264,8 @@ interface RunState { image: string; workdir: string; entrypoint?: string; + timeout?: number; + extract?: ExtractSpec[]; startedAt: string; finishedAt?: string; status: 'running' | 'exited' | 'cancelled' | 'failed'; @@ -333,13 +335,13 @@ Failures talk to you in a structured way, and stale resources do not pile up. - `VOLUME_CREATE_FAILED` - per-run volume could not be created - `CONTAINER_START_FAILED` - container could not be created or started - `SEED_FAILED` - host folder could not be streamed into the volume - - `EXTRACT_FAILED` - artefact streaming out of the container failed + - `EXTRACT_FAILED` - artifact streaming out of the container failed - `BUILD_FAILED` - `run[]` setup build failed (transport or non-zero RUN step) - `INVALID_RUN_STEP` - a `run[]` entry violated the validation rules - `NETWORK_CONNECT_FAILED` - an extra `networks[]` entry could not be attached (e.g. the network does not exist) Each error carries an optional `dockerOp` (which docker call was in flight) and `containerId` (when known) for log correlation. -- **Orphan reaping.** `DockerRunner.reapOrphans()` lists every `light-runner-*` container and volume tagged with the library's label, then removes any that have been idle or exited longer than `LIGHT_RUNNER_REAP_AGE_MS` (default 5 min). Returns `{ containers, volumes }` counts. Safe to call from a cron, a process-shutdown hook, or a sibling watchdog. +- **Orphan reaping.** `DockerRunner.reapOrphans()` lists every `light-runner-*` container and volume tagged with the library's label. Containers idle or exited longer than `LIGHT_RUNNER_REAP_AGE_MS` (default 5 min) are removed; their associated volumes are removed unconditionally (the daemon refuses to remove a volume that is still in use by a running container). Returns `{ containers, volumes }` counts. Safe to call from a cron, a process-shutdown hook, or a sibling watchdog. - **Volume sweeping.** `DockerRunner.cleanupOrphanVolumes()` removes `light-runner-*` volumes, skipping any created within the last `LIGHT_RUNNER_REAP_AGE_MS` (default 5 min) so it cannot yank a volume out from under a concurrent run that has not yet mounted it. Returns the count removed. - **State-file GC.** Every run writes one JSON file per run id under the state dir (both attached and detached since v0.15). `DockerRunner.cleanupOldStates(maxBytes?)` caps that directory: once its total size exceeds `maxBytes` (default `LIGHT_RUNNER_STATE_MAX_BYTES`, or 50 MiB) it deletes terminal states (`exited` / `cancelled` / `failed`) oldest-first until back under budget. `running` states are never removed. Returns the count deleted. This is distinct from `cleanupOrphanStates()`, which only reconciles `running` -> `failed` and deletes nothing. Synchronous (pure filesystem); call it on boot or a schedule. - **Attached vs detached state.** Attached (synchronous) runs are persisted purely for observability (`listStates()`) and post-mortem reconciliation: they cannot be resumed live (`AutoRemove` tears the container down on exit, and `onLog` output is not replayed on `attach`). If an attached launcher dies mid-run, the container is auto-removed and the state stays `running` until `cleanupOrphanStates()` marks it `failed`. diff --git a/website/content/docs/detached.mdx b/website/content/docs/detached.mdx index fec33e4..8338923 100644 --- a/website/content/docs/detached.mdx +++ b/website/content/docs/detached.mdx @@ -19,7 +19,7 @@ const runner = new DockerRunner(); const execution = runner.run({ image: 'python:3.12-alpine', - command: 'python long_job.py', + entrypoint: 'python long_job.py', dir: './job', timeout: 6 * 60 * 60 * 1000, // 6 hours extract: [{ from: '/app/result.json', to: './out' }], @@ -74,7 +74,7 @@ Every detached run writes one JSON file under `~/.light-runner/state/.json` "volume": "light-runner-abc123def456", "image": "python:3.12-alpine", "workdir": "/app", - "command": "python long_job.py", + "entrypoint": "python long_job.py", "timeout": 21600000, "extract": [{"from": "/app/result.json", "to": "./out"}], "startedAt": "2026-04-27T10:15:00.000Z", diff --git a/website/content/docs/extract.mdx b/website/content/docs/extract.mdx index f704280..f1c4648 100644 --- a/website/content/docs/extract.mdx +++ b/website/content/docs/extract.mdx @@ -9,7 +9,7 @@ title: "Extract files" ```ts const execution = runner.run({ image: 'node:lts-alpine', - command: 'node build.js', + entrypoint: 'node build.js', dir: './project', extract: [ { from: '/app/dist', to: './out' }, // folder, recursive @@ -87,7 +87,7 @@ Have the container write a JSON file, extract it, parse it on the host: const result = await runner.run({ image: 'python:3.12-alpine', - command: 'python main.py', + entrypoint: 'python main.py', dir: './solver', extract: [{ from: '/app/result.json', to: './out' }], }).result; @@ -95,7 +95,7 @@ const result = await runner.run({ const data = JSON.parse(fs.readFileSync('./out/result.json', 'utf8')); ``` -### Pull a build artefact +### Pull a build artifact ```ts extract: [ diff --git a/website/content/docs/gvisor-kata.mdx b/website/content/docs/gvisor-kata.mdx index 8eea7f0..94ffa69 100644 --- a/website/content/docs/gvisor-kata.mdx +++ b/website/content/docs/gvisor-kata.mdx @@ -108,7 +108,7 @@ A quick sanity check that the runtime actually changed: ```ts const result = await runner.run({ image: 'alpine:3.19', - command: 'cat /proc/self/maps | head -5', + entrypoint: 'cat /proc/self/maps | head -5', // gVisor-mapped binaries look very different from runc-mapped ones }).result; ``` @@ -118,7 +118,7 @@ Or look for the gVisor signature: ```ts const result = await runner.run({ image: 'alpine:3.19', - command: 'dmesg | grep -i gvisor || echo "not gvisor"', + entrypoint: 'dmesg | grep -i gvisor || echo "not gvisor"', }).result; ``` @@ -131,4 +131,4 @@ Under `runsc` you will see `gVisor` strings in `dmesg`. Under `runc`, you will s | Maximum speed, you trust the code | `runc` (default) | | Hard barrier against kernel exploits, willing to pay 10-30% I/O | `runsc` | | Full VM isolation, separate kernel | `kata` (un-validated, at your own risk) | -| Air-gapped network on top of any of the above | `network: 'none'` on the request | +| Air-gapped network on top of any of the above | `networks: ['none']` on the request | diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx index b3f5374..ce67aba 100644 --- a/website/content/docs/index.mdx +++ b/website/content/docs/index.mdx @@ -34,3 +34,5 @@ console.log(result.exitCode, result.extracted); ``` Start with the [Quick start](/docs/quickstart), then explore [Extract files](/docs/extract), [Detached runs](/docs/detached), the [Security model](/docs/security), and [gVisor & Kata](/docs/gvisor-kata). + +**See also:** [light-run](https://enixcode.github.io/light-run/) — HTTP wrapper around light-runner, and [light-process](https://enixcode.github.io/light-process/) — DAG orchestration layer. diff --git a/website/content/docs/quickstart.mdx b/website/content/docs/quickstart.mdx index 00a5573..126c7a5 100644 --- a/website/content/docs/quickstart.mdx +++ b/website/content/docs/quickstart.mdx @@ -25,7 +25,7 @@ const runner = new DockerRunner({ memory: '512m', cpus: '1' }); const execution = runner.run({ image: 'python:3.12-alpine', - command: 'python main.py', + entrypoint: 'python main.py', dir: './my-project', input: { task: 'compute', n: 20 }, timeout: 30_000, @@ -51,7 +51,7 @@ result.extracted // [{ from, to, status, bytes? }, ...] if extract was set ## Where to go next -- [Extract files](./extract) — how to pull artefacts out of the container after a run +- [Extract files](./extract) — how to pull artifacts out of the container after a run - [Detached runs](./detached) — long-running jobs that survive a host restart - [Security model](./security) — what the sandbox protects against and what it does not - [gVisor & Kata](./gvisor-kata) — adding a stronger runtime for hostile code diff --git a/website/content/docs/security.mdx b/website/content/docs/security.mdx index 2e43694..0f6dc23 100644 --- a/website/content/docs/security.mdx +++ b/website/content/docs/security.mdx @@ -52,7 +52,7 @@ A `setuid` binary inside the container **cannot elevate above the user it starts Default network: a **dedicated isolated bridge** (`light-runner-isolated`) with **inter-container traffic disabled** (`com.docker.network.bridge.enable_icc: false`). Outbound internet works; sibling runs on the same bridge cannot see each other. -For air-gapped runs, set `network: 'none'` in the request — the container has no network interface at all. +For air-gapped runs, set `networks: ['none']` in the request — the container has no network interface at all. ### Filesystem protections @@ -77,7 +77,7 @@ For genuinely hostile code (anonymous user-submitted source, AI-agent-generated - **`input`** (stdin). Ephemeral. Not in metadata, not in `docker inspect`. Your container reads it via `sys.stdin.read()` / `process.stdin`. - **A bind mount to `/run/secrets/`**. Docker-native file-based secrets pattern (compose has a `secrets:` block for this). Not managed by light-runner — the consumer wires it via the host config or via a parent compose definition. -Avoid putting API keys in `env`. Avoid putting them in `command`. Avoid putting them in `dir` (they would be tarred into the seed archive and visible to anyone with access to the volume). +Avoid putting API keys in `env`. Avoid putting them in `entrypoint`. Avoid putting them in `dir` (they would be tarred into the seed archive and visible to anyone with access to the volume). ## Hardening recipes @@ -87,9 +87,9 @@ Avoid putting API keys in `env`. Avoid putting them in `command`. Avoid putting const runner = new DockerRunner(); runner.run({ image: 'python:3.12-alpine', - command: 'python untrusted.py', + entrypoint: 'python untrusted.py', dir: './sandbox', - network: 'none', + networks: ['none'], timeout: 30_000, extract: [{ from: '/app/output.json', to: './out' }], });