diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96dfebd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +content-design/ +.claude/ diff --git a/.mintignore b/.mintignore index 9ee1503..c3465ec 100644 --- a/.mintignore +++ b/.mintignore @@ -4,4 +4,7 @@ # Draft content drafts/ -*.draft.mdx \ No newline at end of file +*.draft.mdx + +# Internal content-design workspace (also git-ignored) +content-design/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3020c71..c5749e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,10 +5,47 @@ This is the official documentation site for **BoxLite** — a local-first micro-VM sandbox for AI agents. Stateful, lightweight, hardware-level isolation, no daemon required. The site is built with [Mintlify](https://mintlify.com) and deployed automatically on push to `main`. - **BoxLite repo**: https://github.com/boxlite-ai/boxlite -- **BoxRun repo**: https://github.com/boxlite-ai/boxrun -- **Current versions**: BoxLite Python v0.5.11 (stable), Node.js v0.2.8, C v0.5.11; BoxRun latest +- **SDKs**: Python, Node.js, Rust, Go, C, plus a built-in REST API (`boxlite serve`) - **Platforms**: macOS (Apple Silicon), Linux (KVM), Windows (WSL2) +## Authoring Principles (must follow) + +These two rules override convenience. Documentation that violates them ships incorrect information and erodes trust faster than missing content does. + +### 1. Ground every claim in real code — never hallucinate + +The source of truth is the BoxLite repository at https://github.com/boxlite-ai/boxlite, **not** prior conversations, training data, or memory. Before writing or editing any factual statement (API name, parameter, default, return type, error class, platform support, performance number, behavior under edge conditions): + +- **Open the code first.** Verify against `sdks/{python,nodejs,rust,c,go}/`, `src/boxlite/`, `src/cli/`, `examples/`, the public README, and the latest tagged release notes. If the repo is not cloned locally, fetch the relevant file via `WebFetch` or `gh api repos/boxlite-ai/boxlite/contents/...`. +- **Cite specifically when behavior is non-obvious.** In your working notes (not necessarily in the published page) record the exact path you verified against, e.g. *"verified `disk_size_gb` default in `sdks/python/src/box_options.rs`"*. This makes review and future code-drift checks tractable. +- **Refuse to write what you cannot verify.** If you cannot find a feature in the code, do not document it — flag it for engineering instead. Conversely, if the code clearly does something the docs do not yet describe, raise that as a content gap, do not paper over it. +- **Watch for stale knowledge.** API surfaces evolve; assume any signature you "remember" is wrong until re-checked against the current default branch (or the version the docs target). Capability claims older than ~30 days deserve re-verification. +- **Mark uncertainty explicitly.** If a behavior is partially confirmed (e.g., works on macOS but Linux unchecked), say so in the page rather than generalizing. + +### 1a. Stay strictly in the user's SDK frame — never quote or link to internal source + +The published documentation describes BoxLite **as the user calls it** — SDK class and function names, options, return shapes, error strings. Internal implementation paths, Rust source files, line numbers, internal struct definitions, build scripts, and any artefact that lives below the SDK boundary do **not** belong in user-facing pages. + +- Do **not** write `src/boxlite/...`, `sdks/python/src/...`, `src/deps/...`, `wire.go:81`, internal trait or struct names that the SDK does not export, or links into `github.com/boxlite-ai/boxlite/blob/main/src/...`. +- Do **not** paste Rust struct definitions, `pub` keywords, `Vec<...>`, `Option<...>`, lifetime annotations, or any other syntax that only appears in the runtime's source. If a user-facing concept needs a structured shape, render it as a **field table** or as a snippet in the user's actual SDK language. +- It is fine to link to the SDK module path users `import` (`github.com/boxlite-ai/boxlite/sdks/go`, `pip install boxlite`, `npm install @boxlite-ai/boxlite`), to the BoxLite README, to the OpenAPI spec, and to the upstream `examples/` directory — those are user-facing artefacts. +- Code-grounding still applies: every fact must be verified against the real source. The verification footprint goes into the internal `content-design/architecture-design/stage2-validation-log.md` (gitignored), never into the published page. + +If a fact can only be expressed by pointing at internal source, it is the wrong fact for a user-facing page — either rephrase it in user terms, escalate it to engineering, or drop it. + +### 2. Every demo code block must be end-to-end validated + +A code block in the published docs is a contract: copy-paste, run, get the stated result. If a snippet has not been executed, it must not ship in a Tutorial or Quick Start. Do this before declaring a page complete: + +- **Install the real SDK** at the version the docs target (`pip install boxlite` / `npm install boxlite` / `cargo add boxlite` / `go get …` / link `libboxlite`). +- **Run the snippet end-to-end** in a clean environment that matches the page's stated prerequisites (OS, Python/Node version, KVM availability). Capture actual stdout/stderr; the page's expected output must match what you saw. +- **Verify the failure modes you describe.** If the page says "exit code 137 on OOM" or "raises `BoxTimeoutError`", trigger the path and confirm the observation. Do not paraphrase plausible-sounding behavior. +- **Test multi-language tabs symmetrically.** A snippet behind every Tab must have been run in that language — not transliterated from one tab into another and shipped untested. +- **Re-run on bumps.** When pinning to a new SDK version or after a code change in the BoxLite repo, re-run the affected snippets. The CHANGELOG is not enough — APIs change in patch releases. +- **If you cannot run it, do not ship it as runnable.** Either get the prerequisites and run it, or downgrade the page from Tutorial / Quick Start to a Concept page that frames the snippet as illustrative pseudocode (and label it as such). Never publish unverified code under a tutorial heading. + +When a code-grounded check or end-to-end run reveals a discrepancy with the existing docs, fix the docs in the same change — don't leave the contradiction in place "for later". + ## Tech Stack - **Mintlify** — documentation platform (config in `docs.json`) @@ -21,12 +58,13 @@ This is the official documentation site for **BoxLite** — a local-first micro- docs.json # Mintlify config: navigation, theme, colors, logo index.mdx # Home page faq.mdx # FAQ & troubleshooting -getting-started/ # Quickstart guides (Python, Node.js, Rust, C) +getting-started/ # Installation + quickstarts + core concepts + design principles +concepts/ # Primitive deep-dives (lifecycle, execution, filesystem, network, snapshot) +tutorials/ # Step-by-step task tutorials +guides/ # How-to guides (production patterns) + changelog +reference/ # SDK API reference (python/, nodejs/, rust/, c/, go/, rest/) architecture/ # Architecture, components, security, networking -reference/ # SDK API reference (python/, nodejs/, rust/, c/) -boxrun/ # BoxRun platform docs (CLI, Python SDK, REST API, config) -guides/ # How-to guides (build, examples, AI integration, etc.) -development/ # Internal docs (CLI, Rust style guide) +development/ # Internal docs (CLI, Rust style guide) — files exist but currently hidden from navigation snippets/ # Reusable MDX snippets (e.g., prerequisites.mdx) images/ # Static images (hero, screenshots) logo/ # Light/dark SVG logos @@ -55,9 +93,10 @@ mint update ### Navigation - All navigation is defined in `docs.json` under `navigation.tabs` -- 4 tabs: Documentation, SDK Reference, Guides, Development +- 1 user-facing tab **BoxLite** with 8 groups: Introduction, Get Started, Concepts, Tutorials, How-to Guides, SDK Reference, Architecture, Resources. (`development/*.mdx` files are still in the repo but intentionally not in navigation right now.) - **Never add a page to navigation without creating the file first** - **Never remove a page without checking for inbound links** +- Placeholder pages have `placeholder: true` in frontmatter; treat them as not-yet-shipped content (Phase 2 will fill the bodies) ### Writing Style - Active voice, second person ("you") @@ -93,12 +132,9 @@ Use these terms consistently across all documentation: | Term | Usage | |------|-------| | BoxLite | Local-first micro-VM sandbox (capital B, capital L) | -| BoxRun | Sandbox management platform (capital B, capital R) | | LiteBox | The VM instance type (capital L, capital B) | | box | Generic reference to a sandbox instance (lowercase) | -| SimpleBox / CodeBox / BrowserBox | Python/Node.js SDK box types | -| BoxHandle | BoxRun SDK handle to a specific box | -| BoxRunClient | BoxRun Python SDK client class | +| SimpleBox / CodeBox / BrowserBox / ComputerBox / InteractiveBox | Specialized box types (Python and Node.js SDKs only) | | Guest Agent | The agent running inside the VM | | Jailer | The security isolation component | | ShimController | Process lifecycle manager | diff --git a/concepts/execution.mdx b/concepts/execution.mdx new file mode 100644 index 0000000..0b8ee5b --- /dev/null +++ b/concepts/execution.mdx @@ -0,0 +1,109 @@ +--- +title: "Execution model" +sidebarTitle: "Execution" +description: "How exec, run, streaming, PTY, and timeouts work — the contract between your code and the guest." +icon: "terminal" +--- + +`exec` is the workhorse of every box. This page describes what actually happens when you call it, the difference between `exec` and `run`, and how stdin/stdout/stderr/timeouts/PTY interact. + +## `exec` is the primitive; `run` is sugar + +- `SimpleBox.exec(cmd, *args)` is the underlying call. It spawns a new process inside the guest and gives you control over its lifecycle. +- `CodeBox.run(code)` is a thin wrapper that calls `exec("python", "-c", code)`, with `install_package(...)` doing `exec("pip", "install", ...)` underneath. `CodeBox` extends `SimpleBox`, so `exec` is still available on a `CodeBox`. + +Picking between them is a matter of ergonomics, not capability — anything `run` does, `exec` can do verbatim. + +## Two return shapes + +Different SDK surfaces return different shapes by design: + +| Surface | Return type | Output | When to use | +|---|---|---|---| +| **Python `box.exec(...)`** | `Execution` handle | Streams via `execution.stdout()` / `stderr()`; collect or iterate | When you want streaming, kill, resize_tty | +| **Python `await execution.wait()`** | `ExecResult` | Buffered `stdout` / `stderr` strings, `exit_code` | When you just want the final outcome | +| **Node `SimpleBox.exec(...)`** | `ExecResult` (await once) | Buffered `stdout` / `stderr` strings, `exitCode` | The simple, one-shot case | +| **Node `JsBoxlite` lower-level** | `Execution` handle | Streams; `await execution.wait()` for `ExecResult` | When you need streams or signal control | +| **REST `POST .../exec`** | `{execution_id}` (immediate) | Stream via SSE on `.../output`; `exit` event carries `exit_code` | Always async on the wire | + +The two-shape design is intentional: the simple wrappers (`SimpleBox.exec` in Node, `CodeBox.run` in Python) buffer everything for a one-liner; the lower-level handles let you tail output in real time and kill the process. See [Reference](/reference/index) for each language's exact signature. + +## Streaming + +When you have an `Execution` handle, stdout and stderr are async iterators. Each yielded item is a chunk (line-buffered by the runtime), not a single character. + +```python +execution = await box.exec("python", "-u", "-c", + "import time; [print(i) or time.sleep(0.5) for i in range(5)]") +async for line in execution.stdout(): + print("got:", line, end="") +result = await execution.wait() +``` + +The REST equivalent is the SSE stream at `GET /v1/{prefix}/boxes/{box_id}/executions/{exec_id}/output` — events have `stdout` / `stderr` / `exit` types, and `stdout` / `stderr` payloads are base64-encoded. + +## stdin + +Each `Execution` exposes an stdin handle (`execution.stdin()` in Python, `execution.stdin()` in the Node low-level API). It is a write-only channel — feed bytes in, the guest sees them on file descriptor 0. + +This is what `InteractiveBox` uses to drive a shell session. + +## Timeouts and kill + +`exec` itself does not enforce a timeout. Cancelling the awaitable in your SDK only cancels your local future — the guest process keeps running. To actually stop a guest process, call: + +```python +await execution.kill() # default signal SIGKILL +await execution.kill("SIGTERM") # custom signal +``` + +Because of this, the canonical timeout pattern in production is: + +```python +try: + result = await asyncio.wait_for(execution.wait(), timeout=30) +except asyncio.TimeoutError: + await execution.kill() + raise +``` + +This pattern is documented end-to-end in [How-to guides → AI Agent Integration](/guides/ai-agent-integration#timeout-handling-and-zombie-prevention). + +## Exit codes + +`exit_code` is the guest process's exit status — straight from the kernel. The same conventions as Linux apply: + +| Exit code | Meaning | +|---|---| +| `0` | Success | +| `1`–`125` | Application-defined | +| `127` | Command not found | +| `128 + N` | Killed by signal `N` (`137` = `SIGKILL`, `143` = `SIGTERM`) | + +Exit code `137` from a memory-limited box almost always means the in-guest OOM killer fired. The box itself stays `running`; only the offending process dies. See [How-to guides → AI Agent Integration → Memory limits and OOM](/guides/ai-agent-integration#memory-limits-and-oom) for the recovery pattern. + +## PTY (`tty=True`) + +Pass `tty=True` to `exec` to allocate a pseudo-terminal. Two effects: + +1. The guest sees a real TTY on fds 0/1/2, so programs that switch behaviour based on `isatty(stdin)` (REPLs, `top`, colourised CLIs) will use the TTY path. +2. `execution.resize_tty(rows, cols)` becomes valid (both arguments are unsigned integers). Calling it on a non-TTY execution returns an error. + +`InteractiveBox` is the high-level wrapper around this — it sets up the TTY, hooks stdin/stdout, and gives you `wait()` to block until the session ends. + +## Working directory, env, user + +Available on every `exec`: + +- `cwd` — directory inside the guest. Defaults to the image's `WORKDIR` (often `/`). +- `env` — extra environment variables, merged on top of the box's own `env`. +- `user` — `"name"` or `"uid:gid"`. Without this, the guest entrypoint's default user runs the command. + +These are the levers you tune when integrating an AI agent that expects a specific account or working directory. + +## See also + +- [Lifecycle](/concepts/lifecycle) — `exec` triggers an implicit start when the box is `configured` or `stopped`. +- [Tutorials → Execute AI-generated code](/tutorials/code-execution) — the same primitives in task form. +- [Tutorials → Interactive terminal](/tutorials/interactive-terminal) — TTY-driven sessions. +- [Tutorials → Handle errors and debug](/tutorials/error-handling) — exit codes, exceptions, timeouts. diff --git a/concepts/filesystem.mdx b/concepts/filesystem.mdx new file mode 100644 index 0000000..f09279b --- /dev/null +++ b/concepts/filesystem.mdx @@ -0,0 +1,75 @@ +--- +title: "Filesystem" +sidebarTitle: "Filesystem" +description: "How files move in and out of a box, and how disks persist (or don't) across stop and restart." +icon: "folder" +--- + +A box has its own filesystem rooted at the image's rootfs. This page is about the three mechanisms you actually have for moving data across that boundary, and what survives a `stop()`. + +## Three ways data crosses the boundary + +| Mechanism | Direction | Granularity | When to use | +|---|---|---|---| +| `copy_into` / `copy_out` | Both | Files or directories | One-shot transfers; results retrieval; dropping in a script | +| Volume mount | Both, persistent while running | Path-level (host dir ↔ guest dir) | The host is the source of truth (a workspace, an artifact directory) | +| stdin / stdout | Both, in-process | Bytes | Inline data with no on-disk artefact | + +`copy_into` / `copy_out` are exposed on the box object in every SDK; volumes are configured at box creation via `BoxOptions.volumes`. See the per-language [SDK reference](/reference/index) for the exact signatures. + +## Ephemeral vs persistent disks + +Every box has a container disk built from the OCI image. Whether changes outlive the VM depends on the `disk_size_gb` option on `BoxOptions`: + +- **`disk_size_gb` not set (default)** — disk lives only as long as the box. `stop()` followed by `auto_remove=true` drops everything. This is the right default for ad-hoc execution. +- **`disk_size_gb = N`** — a persistent QCOW2 image is allocated for the container disk. `stop()` releases the VM but keeps the image; the next `start()` re-uses it. Files written under `/`, installed packages, and any in-place edits all survive. + +The on-disk layout per box, under the BoxLite home directory (`~/.boxlite/` by default): + +``` +~/.boxlite/boxes/{box_id}/ +├── disks/ +│ ├── disk.qcow2 # live container disk (COW child) +│ └── guest-rootfs.qcow2 # the kernel/init rootfs +└── snapshots/ + └── snap-N/disk.qcow2 # immutable per-snapshot disk image +``` + +QCOW2 means the disk is sparse and grows as you write. Allocating `disk_size_gb=10` does not actually consume 10 GB on the host — it's a ceiling, not an upfront cost. + +## Volume mounts + +`BoxOptions.volumes` takes a list of `{ host_path, guest_path, read_only }` entries. A volume is the right answer when: + +- The host directory is the canonical place where data lives (e.g., a build directory you want to inspect from your editor). +- You want to share data between multiple boxes without an intermediate copy. +- You want write-protected access — set `read_only=true`. + +Volumes are wired only while the box is `running`. They are not part of the snapshot, the clone, or the export — they are a host concern, not a box concern. + +## Picking between copy and volume + +The rule of thumb: + +- **Inputs are small and per-task** → `copy_into`. Cheap, isolated, no host coupling. +- **Inputs are big or shared** → volume. The host stays the source of truth; the box reads (or writes) in place. +- **Outputs are artefacts** → `copy_out` after `exec()` finishes — predictable, atomic, simple. +- **Outputs need to be tailed live** → write to a host-side volume, or use `exec` streaming if the data is line-oriented. + +## Cross-box sharing + +There is no built-in "shared filesystem" between boxes. The supported ways to share data are: + +1. A host-side volume mounted into both boxes (the simplest, fastest path). +2. `copy_out` from one, `copy_into` the other. +3. `clone()` if you want two boxes that start from the same point in time but then diverge — see [Snapshot, clone, export](/concepts/snapshot). + +## What does and doesn't roll back + +A snapshot captures only the **container disk** (`disk.qcow2`), not host volumes or in-flight writes from the guest's page cache. If you `restore()` a snapshot, anything you wrote on a mounted host volume is still there — that's a feature, not a bug. + +## See also + +- [Tutorials → Upload & download files](/tutorials/file-transfer) — task form, with all three methods. +- [Architecture → Networking & storage](/architecture/networking-storage) — implementation details (QCOW2, COW chains, GC). +- [Snapshot, clone, export](/concepts/snapshot) — what's captured and what isn't. diff --git a/concepts/lifecycle.mdx b/concepts/lifecycle.mdx new file mode 100644 index 0000000..9977257 --- /dev/null +++ b/concepts/lifecycle.mdx @@ -0,0 +1,83 @@ +--- +title: "Box lifecycle" +sidebarTitle: "Lifecycle" +description: "States a box moves through — from create to remove — and which operations are valid in each state." +icon: "circle-nodes" +--- + +A box has a small, well-defined state machine. Reading this page once will save you a lot of guessing about why an SDK call returned `InvalidState`. + +## States + +A box is always in exactly one of these states: + +| State | Meaning | +|---|---| +| `configured` | Persisted to the database; no VM process yet. The state every newly-created box starts in. | +| `running` | VM is alive and the in-guest agent is accepting commands. | +| `stopping` | A graceful shutdown is in progress (transient). | +| `stopped` | VM process terminated; the rootfs and any persistent disk are preserved. The box can be restarted. | +| `paused` | VM frozen via `SIGSTOP` (used internally during snapshot or export for point-in-time consistency). | +| `unknown` | The runtime can't determine the state — only seen during error recovery. | + +## Transitions + +The transitions BoxLite allows are: + +``` +configured ──────► running (start succeeds) +configured ──────► stopped (start fails) +running ──────► stopping ──► stopped +running ──────► stopped (crash) +running ◄──────► paused (snapshot/export pause and resume) +stopped ──────► running (restart) +unknown ──────► any (recovery) +``` + +Anything not in that list is rejected by the SDK with an `InvalidState` error. + +## Which operations are valid in which state + +Each public lifecycle method has a precondition table baked into the state enum: + +| Operation | Valid from | +|---|---| +| `start()` | `configured`, `stopped` | +| `stop()` | `running`, `paused` | +| `remove()` | `configured`, `stopped`, `unknown` | +| `exec()` | `configured`, `running`, `stopped` (for `configured` and `stopped`, an implicit `start()` runs first) | + +Implicit-start on `exec()` is the reason `async with boxlite.SimpleBox(...) as box: await box.exec(...)` works without an explicit `start()` — the wrapper relies on it. + +## Cross-process reattach + +A box's identity is its `box_id` (a ULID-style string the runtime hands you on `create`). Boxes are persisted in the runtime's local SQLite store, so any process running on the same host can reattach by id: + +```python +runtime = boxlite.Boxlite.default() +box = await runtime.get(box_id) # state preserved, no re-create +await box.exec("echo", "still here") +``` + +This works across process restarts and across users sharing the same `home_dir`. It's also how a long-running CLI client reconnects after a crash. + +## Auto-remove and detach + +`BoxOptions` exposes two flags that interact with the lifecycle: + +- `auto_remove` — defaults to `true`. When the box reaches `stopped` and `auto_remove` is set, the runtime also removes it. Most quickstart-style code relies on this behaviour. +- `detach` — defaults to `false`. When set, the box keeps running after the parent process exits; you must reattach by id and stop it explicitly. + +`auto_remove=true` together with `detach=true` is rejected at creation time with a configuration error — a detached box that vanishes when its parent dies cannot be managed. + +## Where each state shows up in the SDKs + +- Python and Node SDKs surface state via `box.info()` / `box.status` and `runtime.list()`. +- The REST API returns the lowercase string (`"configured"`, `"running"`, …) as the `status` field on `GET /v1/{prefix}/boxes/{box_id}`. +- The `paused` state is currently driven only by the runtime itself during snapshot/export. There is no public `pause()` API. + +## See also + +- [Tutorials → Quick Start](/getting-started/quickstart-python) for the happy path through `configured → running → stopped`. +- [Snapshot, clone, export](/concepts/snapshot) — uses the `paused` state internally. +- [Execution](/concepts/execution) — what `exec` actually does, including implicit start. diff --git a/concepts/network.mdx b/concepts/network.mdx new file mode 100644 index 0000000..5679c08 --- /dev/null +++ b/concepts/network.mdx @@ -0,0 +1,82 @@ +--- +title: "Network" +sidebarTitle: "Network" +description: "Outbound allowlists, port forwarding, and host-side secret injection — what the SDK exposes for shaping a box's network." +icon: "network-wired" +--- + +Networking in BoxLite has four moving parts: the bridge that gets traffic in and out, an outbound allowlist, inbound port forwarding, and host-side secret substitution. They compose, but each is small enough to understand on its own. + +## NetworkSpec + +`BoxOptions.network` accepts a network config with two fields: + +| Field | Values | Purpose | +|---|---|---| +| `mode` | `"enabled"` (default) \| `"disabled"` | Whether the guest has any network at all | +| `allow_net` | list of host patterns | Outbound destinations the guest may reach | + +`mode="disabled"` removes the guest interface entirely — the box has no network at all, host or otherwise. `mode="enabled"` is the default and is the only mode that respects `allow_net`. + +### Allowlist semantics + +`allow_net` is a list of host patterns the guest is allowed to reach for outbound traffic. Anything not on the list is dropped before it leaves the host. Patterns are matched against the destination host of HTTP(S) traffic and DNS. + +For an AI-agent box, the typical shape is: + +```python +boxlite.SimpleBox( + image="python:slim", + network=boxlite.NetworkConfig( + mode="enabled", + allow_net=["api.openai.com", "pypi.org", "files.pythonhosted.org"], + ), +) +``` + +If you want a box that genuinely cannot phone home, set `mode="disabled"` — but be aware this also blocks `pip install` and any image-pull-time package fetches. + +## Inbound — port forwarding + +`BoxOptions.ports` is a list of `(host_port, guest_port, protocol)` tuples (`protocol` ∈ `"tcp" | "udp"`). When the box is running, the host port is mapped to the guest port — exactly the model `docker run -p` uses, but on a real micro-VM. + +This is what makes [Tutorials → Run services with port forwarding](/tutorials/long-running-services) work: start a server inside the guest, expose its port, hit it from the host. + +Per-box network metrics are reported back via `BoxMetrics.network_bytes_sent` / `network_bytes_received` (visible on `GET /v1/{prefix}/boxes/{box_id}/metrics`). + +## Secrets — host-side substitution + +`BoxOptions.secrets` is the safe alternative to passing API keys in environment variables. Each entry has four fields: + +| Field | Purpose | +|---|---| +| `name` | Human-readable label, e.g. `"openai_api_key"`. | +| `hosts` | Host patterns where injection happens (e.g. `["api.openai.com"]`). | +| `placeholder` | The string the guest sees. Defaults to ``. | +| `value` | The real value. Never enters the VM. | + +BoxLite intercepts the guest's outbound HTTPS at the host boundary and substitutes the placeholder with the real value, only for matching hosts. Inside the guest you see — and your code logs — only the placeholder string. The real secret never crosses into the VM, so a guest compromise can't exfiltrate it directly. + +Secrets are also surfaced as environment variables. A secret named `openai` with placeholder `` becomes `BOXLITE_SECRET_OPENAI=` inside the box (uppercase, with non-alphanumeric characters in the name replaced by `_`). Guest code can read the env var and embed the placeholder anywhere — Authorization headers, JSON bodies, query strings — and the proxy will rewrite it on the way out. + +## No host-side privileges required + +BoxLite ships its own user-space network bridge, so the host never needs kernel-level networking changes — no TUN devices, no `sudo`, no daemon. Inbound and outbound traffic both pass through this bridge, which is why allowlists and port forwarding work uniformly across macOS, Linux (KVM), and Windows (WSL2). + +## Picking a configuration + +| Scenario | `mode` | `allow_net` | `ports` | `secrets` | +|---|---|---|---|---| +| Untrusted code, no network at all | `Disabled` | — | — | — | +| Untrusted code that needs `pip install` | `Enabled` | `["pypi.org", "files.pythonhosted.org"]` | — | — | +| Agent calling one API | `Enabled` | `["api.openai.com"]` | — | one `Secret` for the API key | +| Run a server, hit it from host | `Enabled` | (set if outbound is needed) | `[(host_port, guest_port, "tcp")]` | — | + +Each row maps directly onto a box-creation call — see the per-language [SDK reference](/reference/index) for the exact field names. + +## See also + +- [Tutorials → Run services with port forwarding](/tutorials/long-running-services) — task form for the inbound case. +- [Architecture → Networking & storage](/architecture/networking-storage) — host-side network bridge and storage internals. +- [Architecture → Security & isolation](/architecture/security) — how the network boundary fits the broader threat model. +- [How-to guides → AI Agent Integration](/guides/ai-agent-integration) — `network_enabled` interaction with security presets. diff --git a/concepts/snapshot.mdx b/concepts/snapshot.mdx new file mode 100644 index 0000000..702b63a --- /dev/null +++ b/concepts/snapshot.mdx @@ -0,0 +1,77 @@ +--- +title: "Snapshot, clone, export" +sidebarTitle: "Snapshot" +description: "Three different ways to capture box state, and what each one can and cannot do." +icon: "camera" +--- + +These are three operations that look similar from the outside but solve different problems. They share one important boundary: **all three are disk-only.** None of them captures memory, page cache, or in-flight I/O. + +## What is captured + +Every snapshot exposes the same shape across the SDKs and the REST API: + +| Field | Purpose | +|---|---| +| `id` | Server-generated identifier | +| `box_id` | The box this snapshot belongs to | +| `name` | Your label, unique per box (≤ 255 characters; `.` and `..` are rejected) | +| `created_at` | Unix timestamp of capture | +| `size_bytes` / `container_disk_bytes` | Disk footprint, in bytes | + +Behind the metadata, BoxLite keeps a single QCOW2 disk image per snapshot under the BoxLite home directory (`~/.boxlite/boxes/{box_id}/snapshots/`). That disk image is the immutable container disk at the moment of capture. Nothing else is captured. + +## What is not captured + +- **Process state.** Running PIDs are not preserved. After `restore()`, the box is brought up like a fresh start — your daemons, shell sessions, and exec'd commands are gone. +- **Memory pages.** No memory checkpoint exists today. `SnapshotOptions` is currently a forward-compatible empty struct in the public SDKs — the field is reserved for future memory-capture work. +- **Host volumes.** Volume mounts are a host concern; they aren't part of the snapshot, so writes to a volume between snapshot and restore are not undone. +- **Open network connections.** Sockets are dropped when the VM shuts down for the snapshot. + +If a workflow assumes "rewind to exactly what was running" — including a half-finished syscall — BoxLite cannot do that today. Plan for "rewind disk state, then re-run from a known prefix instead." + +## Snapshot vs clone vs export + +| Op | Captures | Result | Stays inside the runtime? | +|---|---|---|---| +| `snapshot(name)` | Container disk at point in time | A named entry under the source box | Yes | +| `clone()` | Container disk at point in time | A **new box** with its own `box_id`, ready to run | Yes | +| `export()` / `import()` | Container disk + box config metadata | A portable archive on disk | No (movable across hosts) | + +In other words: + +- **Snapshot** is "I want to roll this *same* box back to here later." It's the cheapest of the three because nothing is duplicated up front. +- **Clone** is "I want a second, independent box that starts from the same disk." Use this when you want to fork execution paths or stamp out N parallel copies. +- **Export/import** is "I want to ship this state to another machine," or archive it. The archive is a regular file you can rsync. + +The three options structs are placeholders today (`SnapshotOptions`, `CloneOptions`, `ExportOptions` are empty in the public SDKs); the operations themselves are real and shipping. + +## Restore is destructive on the source + +`restore(name)` rolls the box's *current* disk back to the snapshot's disk. It is destructive — the writes you've done since the snapshot are gone. Two practical consequences: + +1. The runtime refuses to remove a snapshot whose disk the current box still depends on. The error is explicit: `"current disk depends on this snapshot. Restore a different snapshot first."` +2. If you want to keep both timelines, take a fresh snapshot of the current state first, then restore. + +If you want both branches to remain live and runnable in parallel, use `clone` instead of `snapshot + restore`. + +## State machine interaction + +Snapshot, clone, and export all need a quiescent disk. The SDK briefly moves the box through the `paused` state for the duration of the operation, then returns it to `running`. This is one of the only places the `paused` state shows up in the public lifecycle (see [Lifecycle](/concepts/lifecycle)). + +## Where each operation surfaces + +| Surface | Snapshot create | Snapshot restore | Clone | Export / Import | +|---|---|---|---|---| +| Python | `await box.snapshot(name)` / `await box.restore(name)` | yes | `await box.clone()` | `await box.export(path)` / `runtime.import_box(path)` | +| Node.js | same shape | yes | `await box.clone()` | `await box.export(path)` | +| REST | `POST .../boxes/{id}/snapshots` | `POST .../snapshots/{name}/restore` | `POST .../clone` | `POST .../export`, `POST .../boxes/import` | +| CLI | `boxlite snapshot ...` | `boxlite restore ...` | `boxlite clone ...` | `boxlite export ...` / `boxlite import ...` | + +For a full end-to-end walkthrough, see [Tutorials → Snapshot, fork, restore](/tutorials/snapshot-fork-restore). + +## See also + +- [Lifecycle](/concepts/lifecycle) — the `paused` state that snapshots use under the hood. +- [Filesystem](/concepts/filesystem) — what's on `disk.qcow2` and what's outside it. +- [REST API reference → Snapshots](/reference/rest/index#snapshots-over-rest) — the wire format. diff --git a/docs.json b/docs.json index 59d0ac3..6ce66be 100644 --- a/docs.json +++ b/docs.json @@ -7,23 +7,38 @@ "light": "#FFFFFF", "dark": "#000000" }, - "favicon": "/favicon.svg", + "favicon": "/favicon.png", "navigation": { "tabs": [ { "tab": "BoxLite", "groups": [ { - "group": "Getting Started", + "group": "Introduction", "pages": [ "index", - "getting-started/index", + "getting-started/design-principles" + ] + }, + { + "group": "Get Started", + "pages": [ + "getting-started/install", "getting-started/quickstart-python", "getting-started/quickstart-nodejs", "getting-started/quickstart-rust", - "getting-started/quickstart-c", + "getting-started/quickstart-c" + ] + }, + { + "group": "Concepts", + "pages": [ "getting-started/core-concepts", - "getting-started/design-principles" + "concepts/lifecycle", + "concepts/execution", + "concepts/filesystem", + "concepts/network", + "concepts/snapshot" ] }, { @@ -39,11 +54,12 @@ "tutorials/interactive-terminal", "tutorials/run-any-language", "tutorials/long-running-services", + "tutorials/snapshot-fork-restore", "tutorials/error-handling" ] }, { - "group": "Guides", + "group": "How-to Guides", "pages": [ "guides/index", "guides/ai-agent-integration", @@ -95,6 +111,20 @@ "reference/c/memory-json-threading", "reference/c/errors-metrics" ] + }, + { + "group": "Go SDK", + "icon": "golang", + "pages": [ + "reference/go/index" + ] + }, + { + "group": "REST API", + "icon": "globe", + "pages": [ + "reference/rest/index" + ] } ] }, @@ -113,13 +143,6 @@ "faq", "guides/changelog" ] - }, - { - "group": "Development", - "pages": [ - "development/cli", - "development/rust-style" - ] } ] } diff --git a/faq.mdx b/faq.mdx index 318472a..b6459c3 100644 --- a/faq.mdx +++ b/faq.mdx @@ -756,7 +756,7 @@ Frequently asked questions and common issues with BoxLite. **Documentation:** - - [Getting Started](/getting-started/index) -- Quick onboarding + - [Getting Started](/getting-started/install) -- Quick onboarding - [How-to Guides](/guides/index) -- Practical guides - [SDK Reference](/reference/index) -- API and configuration reference - [Architecture](/architecture/index) -- How BoxLite works diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000..b74b0b4 Binary files /dev/null and b/favicon.png differ diff --git a/favicon.svg b/favicon.svg deleted file mode 100644 index 6c82b77..0000000 --- a/favicon.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - diff --git a/getting-started/core-concepts.mdx b/getting-started/core-concepts.mdx index 3b25b17..24c9278 100644 --- a/getting-started/core-concepts.mdx +++ b/getting-started/core-concepts.mdx @@ -81,131 +81,21 @@ async with boxlite.SimpleBox(image="python:slim") as box: **For BrowserBox and ComputerBox, technically yes but practically no.** You could use SimpleBox with the same images (e.g. `SimpleBox(image="mcr.microsoft.com/playwright:v1.58.0-jammy")`), but you'd have to manually set up port forwarding, start the browser server, wait for it to be ready, and connect — all things that BrowserBox handles in a single `connect()` call. The specialized APIs (`screenshot()`, `mouse_move()`, `playwright_endpoint()`, etc.) save significant work. -## Lifecycle +## Primitive deep-dives -Every box follows the same lifecycle: **create → use → stop**. +Box types are the *shape* of a sandbox. The five concept pages below cover the *primitives* every box exposes — read them in order if you want a complete mental model, or jump to whichever topic you're about to use. -### Context manager (recommended) +- [Lifecycle](/concepts/lifecycle) — the box state machine, valid transitions, and what `auto_remove` and `detach` actually do +- [Execution](/concepts/execution) — `exec` vs `run`, streaming output, stdin, timeouts, PTY, exit codes +- [Filesystem](/concepts/filesystem) — `copy_in` / `copy_out`, volume mounts, ephemeral vs persistent disks +- [Network](/concepts/network) — outbound allowlists, port forwarding, host-side secret injection +- [Snapshot](/concepts/snapshot) — snapshot vs clone vs export, and the disk-only boundary -The simplest pattern uses `async with`, which handles startup and cleanup automatically: +Anything related to install, resource sizing, or low-level architecture lives elsewhere: -```python -async with boxlite.SimpleBox(image="python:slim") as box: - await box.exec("echo", "hello") - # Box is automatically stopped when the block exits -``` - -### Manual lifecycle - -When you need more control (long-lived boxes, cross-function usage), manage the lifecycle explicitly: - - - - ```python - box = boxlite.SimpleBox(image="python:slim") - await box.start() - - try: - await box.exec("echo", "hello") - finally: - await box.shutdown() - ``` - - - ```typescript - const box = new SimpleBox({ image: 'python:slim' }); - - try { - const result = await box.exec('echo', 'hello'); - console.log(result.stdout); - } finally { - await box.stop(); - } - ``` - - - -### What happens at each stage - -| Stage | What happens | -|-------|-------------| -| **Create** | BoxLite pulls the OCI image (cached after first use), allocates resources, and boots a microVM. This typically takes under a second for cached images. | -| **Use** | Run commands with `exec()` (SimpleBox, CodeBox, InteractiveBox) or `run()` (CodeBox only). Use specialized methods for BrowserBox (`connect()`) and ComputerBox (`screenshot()`, `mouse_move()`, etc.). Files you create and packages you install persist within the box. Note: each `CodeBox.run()` call starts a new Python process, so Python variables and function definitions do not carry over between calls. | -| **Stop** | The VM shuts down and all resources are released. With `auto_remove=True` (the default), the box is fully cleaned up. | - -## Images - -BoxLite uses standard **OCI container images** — the same images you use with Docker. For box types that accept a custom image (SimpleBox, CodeBox, InteractiveBox), specify it when creating the box: - -```python -# Docker Hub images — use any image you want -boxlite.SimpleBox(image="python:slim") -boxlite.SimpleBox(image="alpine:latest") -boxlite.SimpleBox(image="ubuntu:22.04") -boxlite.SimpleBox(image="node:20-slim") - -# CodeBox defaults to python:slim — no image needed -boxlite.CodeBox() -``` - -BrowserBox and ComputerBox use **fixed images** (Playwright and webtop respectively) that can't be changed, because their convenience methods depend on specific software being pre-installed. - -The image is pulled on first use and cached locally. Subsequent starts reuse the cached image. - - - Use slim or minimal images (`python:slim`, `alpine:latest`) for faster boot times. Full images like `ubuntu:latest` work but are larger and slower to pull. - - -## Resource configuration - -Every box accepts CPU and memory parameters. The example below uses SimpleBox/CodeBox/InteractiveBox parameter names: - -```python -boxlite.SimpleBox( - image="python:slim", - cpus=2, # CPU cores (default: 1) - memory_mib=4096, # Memory in MiB (default: 2048) - disk_size_gb=10, # Persistent disk in GB (default: ephemeral) -) -``` - -| Parameter | Default | Range | Notes | -|-----------|---------|-------|-------| -| `cpus` | 1 | 1 to host CPU count | More CPUs help with parallel workloads | -| `memory_mib` | 2048 | 128–65536 | Increase for data-heavy workloads or large packages | -| `disk_size_gb` | None (ephemeral) | Any positive integer | Set only if you need data to survive a restart | - - -BrowserBox and ComputerBox use slightly different parameter names: `cpu` (singular) and `memory` (instead of `memory_mib`). See their [SDK reference](/reference/python/box-types) pages for details. - - -For most use cases, the defaults work well. Increase memory when running data-heavy code or installing large packages, and add CPUs for compute-intensive workloads. - -## Security model - -Each box is a real **microVM** — not a container. BoxLite provides multiple layers of isolation: - -- **Hardware virtualization** — KVM on Linux, Hypervisor.framework on macOS. Each box has its own kernel. -- **Jailer** — restricts the VM process with seccomp filters and cgroups. -- **Network isolation** — boxes get their own network namespace by default. - -You can tune security with `SecurityOptions` presets: - -```python -from boxlite.boxlite import SecurityOptions - -async with boxlite.SimpleBox( - image="python:slim", - security=SecurityOptions.maximum(), # Maximum isolation -) as box: - await box.exec("echo", "locked down") -``` - - -`SecurityOptions` is currently imported from `boxlite.boxlite`, not from the top-level `boxlite` module. Available presets: `development()`, `standard()`, `maximum()`. - - -For full details on the security architecture, see [Security](/architecture/security). +- Install + verify your platform → [Installation](/getting-started/install) +- Pick CPU / memory / disk for production agents → [How-to guides → AI Agent Integration](/guides/ai-agent-integration#recommended-configuration) +- The defense-in-depth security model → [Architecture → Security](/architecture/security) ## What's next? @@ -219,7 +109,7 @@ For full details on the security architecture, see [Security](/architecture/secu Full API reference for all box types. - - How BoxLite works under the hood — microVMs, the jailer, networking. + + Components, isolation layers, networking, and storage. diff --git a/getting-started/design-principles.mdx b/getting-started/design-principles.mdx index 8e94c4c..1896623 100644 --- a/getting-started/design-principles.mdx +++ b/getting-started/design-principles.mdx @@ -1,10 +1,11 @@ --- -title: "Design principles" -description: "The ideas behind BoxLite's architecture and API decisions." +title: "Why BoxLite" +sidebarTitle: "Why BoxLite" +description: "The ideas behind BoxLite — embeddable, stateful, snapshottable, hardware-isolated — and how it differs from other sandboxes." icon: "compass" --- -# Design principles +# Why BoxLite BoxLite is built on a simple belief: AI agents deserve sandboxes that are secure, stateful, and trivially easy to adopt. Every design decision we make traces back to four principles. @@ -77,3 +78,20 @@ Together they define what we think a sandbox for AI agents should be: something ``` $ pip install boxlite ``` + +--- + +## How BoxLite compares to other sandboxes + +A neutral, dimension-by-dimension look at where BoxLite sits in the landscape. We deliberately do not name specific products here — sandbox tools evolve fast, and the dimensions matter more than the names of the moment. + +| Dimension | Hosted cloud sandbox | Container-based local sandbox | Process-level sandbox | **BoxLite** | +|---|---|---|---|---| +| Deployment model | Cloud account required | Daemon or root often required | In-process | **Library import, no daemon, no root** | +| Isolation level | VM or strong container | Shared host kernel | Same process / user | **Hardware-virtualized micro-VM** | +| State persistence | Often ephemeral | Sometimes; image rebuild common | None | **Stateful by default, survives stop/restart** | +| Snapshot / fork | Varies, often paid feature | Rare | None | **First-class disk snapshots** | +| Multi-language SDK | Usually 1–2 languages | N/A | Language-bound | **Python, Node.js, Rust, Go, C** | +| Where it runs | Vendor cloud | Your machine + daemon | Your process | **Your machine, your code, no extra services** | + +BoxLite's wedge is the combination: VM-grade isolation, embedded SDK ergonomics, and stateful boxes — without depending on a hosted control plane. Hosted offerings remain a good fit when fleet management and remote scale are the primary needs. BoxLite is for the rest of the time, when you want sandboxing to behave like SQLite: local, embedded, and out of the way. diff --git a/getting-started/index.mdx b/getting-started/install.mdx similarity index 100% rename from getting-started/index.mdx rename to getting-started/install.mdx diff --git a/getting-started/quickstart-nodejs.mdx b/getting-started/quickstart-nodejs.mdx index c921ae7..c3c0238 100644 --- a/getting-started/quickstart-nodejs.mdx +++ b/getting-started/quickstart-nodejs.mdx @@ -13,7 +13,7 @@ npm install @boxlite-ai/boxlite **Requirements:** - Node.js 18 or later -- Platform with hardware virtualization (see [Installation](/getting-started/index#system-requirements)) +- Platform with hardware virtualization (see [Installation](/getting-started/install#system-requirements)) **Verify Installation:** diff --git a/guides/ai-agent-integration.mdx b/guides/ai-agent-integration.mdx index c5e69d6..2ed5b79 100644 --- a/guides/ai-agent-integration.mdx +++ b/guides/ai-agent-integration.mdx @@ -7,10 +7,6 @@ icon: "robot" This guide covers production patterns for deploying BoxLite as a sandboxed execution environment for AI agents. It assumes you've already worked through the [Tutorials](/tutorials/index) and know how to create boxes, run code, and transfer files. - - **Using BoxRun?** If your agent communicates via HTTP, [BoxRun's REST API](/boxrun/rest-api) or [Python SDK](/boxrun/python-sdk) may be a simpler integration path. BoxRun handles sandbox lifecycle, file upload/download, and SSE streaming out of the box. See the [AI agent patterns](/boxrun/python-sdk#ai-agent-patterns) section in the BoxRun SDK docs. - - ## Recommended Configuration ### Workload-Type Reference @@ -645,7 +641,5 @@ Putting it all together: security configuration, concurrent execution with timeo ## See also - [Tutorials](/tutorials/index) — Step-by-step walkthroughs for code execution, file transfer, LLM integration, and browser automation -- [BoxRun Python SDK — AI agent patterns](/boxrun/python-sdk#ai-agent-patterns) — Agent integration via REST API -- [BoxRun REST API](/boxrun/rest-api) — HTTP endpoints for sandbox management - [SDK Reference](/reference/index) — Full API reference for all BoxLite SDKs - [Architecture](/architecture/index) — How BoxLite isolation works diff --git a/guides/index.mdx b/guides/index.mdx index df8b0ed..01dce4d 100644 --- a/guides/index.mdx +++ b/guides/index.mdx @@ -1,11 +1,13 @@ --- -title: "Guides" +title: "How-to guides" sidebarTitle: "Overview" -description: "Practical how-to guides for building, configuring, and integrating BoxLite into your projects." +description: "Production-grade how-to guides for building, configuring, and integrating BoxLite — picking up where the tutorials leave off." icon: "book-open" --- -These guides cover advanced topics: building from source, AI agent integration patterns, image registry configuration, and debugging. Each guide is self-contained and can be read independently. +How-to guides solve a different problem from tutorials. **Tutorials teach you to complete a specific task** (run AI-generated code, automate a browser, transfer files). **How-to guides show you how to run BoxLite well in production** — handling concurrency, configuring security, choosing image registries, debugging the host sandbox. They assume you've already done the basics and want to go deeper. + +Each guide is self-contained and can be read independently. Looking for step-by-step task tutorials? See the [Tutorials](/tutorials/index) section for code execution, file transfer, LLM integration, and browser automation walkthroughs. @@ -56,5 +58,5 @@ These guides cover advanced topics: building from source, AI agent integration p The source README also covers networking, volume mounting, debugging, resource tuning, deployment patterns, and integration examples. These topics are woven into the guides above and the [Architecture](/architecture/index) and [SDK Reference](/reference/index) sections. - New to BoxLite? Start with the [Getting Started](/getting-started/index) guide, then work through the [Tutorials](/tutorials/index) before diving into these advanced guides. + New to BoxLite? Start with the [Getting Started](/getting-started/install) guide, then work through the [Tutorials](/tutorials/index) before diving into these advanced guides. diff --git a/index.mdx b/index.mdx index b4ab221..3e4b406 100644 --- a/index.mdx +++ b/index.mdx @@ -58,17 +58,6 @@ Runs on macOS (Apple Silicon), Linux (KVM), and Windows (WSL2). -## BoxRun — sandbox management - -**BoxRun** is the management layer for BoxLite sandboxes. Create, list, stop, and delete boxes through a CLI, REST API, Python SDK, or web dashboard — all in a single binary. - -```bash -curl -fsSL https://boxlite.ai/boxrun/install | sh -boxrun shell ubuntu -``` - -BoxLite creates and runs micro-VMs. BoxRun manages them — tracking state, persisting data, and exposing operations over HTTP and CLI. - ## SDKs and platforms | SDK | Install | @@ -90,7 +79,7 @@ BoxLite creates and runs micro-VMs. BoxRun manages them — tracking state, pers Execute code, connect LLMs, transfer files, and automate browsers — step-by-step. - + AI agent integration, security configuration, and production patterns. diff --git a/llms.txt b/llms.txt index 083b861..570a284 100644 --- a/llms.txt +++ b/llms.txt @@ -2,14 +2,21 @@ > BoxLite is a local-first micro-VM sandbox for AI agents — stateful, lightweight, hardware-level isolation, no daemon required. -## Getting Started +## Introduction - [Introduction](/index.mdx): BoxLite overview, features, and quick start -- [Installation](/getting-started/index.mdx): Install BoxLite for Python, Node.js, Rust, or C +- [Why BoxLite](/getting-started/design-principles.mdx): Design principles and how BoxLite compares to other sandboxes + +## Get Started + +- [Installation](/getting-started/install.mdx): Install BoxLite for Python, Node.js, Rust, or C - [Python Quick Start](/getting-started/quickstart-python.mdx): Get started with BoxLite in Python - [Node.js Quick Start](/getting-started/quickstart-nodejs.mdx): Get started with BoxLite in Node.js - [Rust Quick Start](/getting-started/quickstart-rust.mdx): Get started with BoxLite in Rust - [C Quick Start](/getting-started/quickstart-c.mdx): Get started with BoxLite in C + +## Concepts + - [Core Concepts](/getting-started/core-concepts.mdx): Boxes, images, execution model, and lifecycle ## Tutorials @@ -26,14 +33,13 @@ - [Run services with port forwarding](/tutorials/long-running-services.mdx): Start servers, forward ports, access from host - [Handle errors and debug](/tutorials/error-handling.mdx): Exit codes, exceptions, timeouts, streaming -## Guides +## How-to Guides -- [Guides Overview](/guides/index.mdx): Advanced patterns and configuration guides +- [How-to Guides Overview](/guides/index.mdx): Production patterns and configuration guides - [AI Agent Integration](/guides/ai-agent-integration.mdx): Concurrency, timeouts, security presets - [Building from Source](/guides/building-from-source.mdx): Compile BoxLite from source - [Image Registry Configuration](/guides/image-registry-configuration.mdx): Configure OCI image registries - [macOS Sandbox Debugging](/guides/macos-sandbox-debugging.mdx): Debug sandbox issues on macOS -- [Changelog](/guides/changelog.mdx): Release history ## SDK Reference @@ -74,15 +80,7 @@ - [Security & Isolation](/architecture/security.mdx): Security model and isolation guarantees - [Networking & Storage](/architecture/networking-storage.mdx): Network and storage configuration -## BoxRun - -- [BoxRun Overview](/boxrun/index.mdx): Managed sandbox platform -- [BoxRun Quick Start](/boxrun/quickstart.mdx): Get started with BoxRun -- [BoxRun CLI](/boxrun/cli.mdx): Command-line reference -- [BoxRun Python SDK](/boxrun/python-sdk.mdx): Python SDK for BoxRun -- [BoxRun REST API](/boxrun/rest-api.mdx): REST API reference -- [BoxRun Configuration](/boxrun/configuration.mdx): Configuration options - -## FAQ +## Resources - [FAQ & Troubleshooting](/faq.mdx): Common questions and solutions +- [Changelog](/guides/changelog.mdx): Release history diff --git a/logo/dark.png b/logo/dark.png index 438cc08..2e6264e 100644 Binary files a/logo/dark.png and b/logo/dark.png differ diff --git a/logo/light.png b/logo/light.png index 722e2fb..f325a13 100644 Binary files a/logo/light.png and b/logo/light.png differ diff --git a/reference/go/index.mdx b/reference/go/index.mdx new file mode 100644 index 0000000..296a8d1 --- /dev/null +++ b/reference/go/index.mdx @@ -0,0 +1,235 @@ +--- +title: "Go SDK reference" +sidebarTitle: "Go SDK" +description: "Install and use BoxLite from Go — installation, runtime, box options, and execution." +icon: "golang" +--- + +The Go SDK lives at `github.com/boxlite-ai/boxlite/sdks/go` and links the BoxLite runtime as a CGO static library. It mirrors the same primitives as the Python and Node.js SDKs, with idiomatic Go signatures (functional options, `context.Context`). + + + Requires Go 1.24+ with CGO enabled. The first install downloads a prebuilt native library (`libboxlite.a`, ~110 MB on darwin-arm64) into your Go module cache. Set `GITHUB_TOKEN` if you hit GitHub API rate limits during setup. + + +## Install + +```bash +go get github.com/boxlite-ai/boxlite/sdks/go +go run github.com/boxlite-ai/boxlite/sdks/go/cmd/setup +``` + +The `cmd/setup` step is one-time per module cache: it downloads the prebuilt native library and header for your platform and places them next to the Go sources. + +## Quick start + +```go main.go +package main + +import ( + "context" + "fmt" + "log" + + boxlite "github.com/boxlite-ai/boxlite/sdks/go" +) + +func main() { + rt, err := boxlite.NewRuntime() + if err != nil { + log.Fatal(err) + } + defer rt.Close() + + ctx := context.Background() + box, err := rt.Create(ctx, "python:slim", + boxlite.WithNetwork(boxlite.NetworkSpec{Mode: boxlite.NetworkModeEnabled}), + ) + if err != nil { + log.Fatal(err) + } + defer rt.Remove(ctx, box.ID()) + + if err := box.Start(ctx); err != nil { + log.Fatal(err) + } + defer box.Stop(ctx) + + result, err := box.Exec(ctx, "python", "-c", "print('Hello from BoxLite Go!')") + if err != nil { + log.Fatal(err) + } + fmt.Println("STDOUT:", result.Stdout) + fmt.Println("EXIT:", result.ExitCode) +} +``` + +Run it: + +```bash +go run main.go +# STDOUT: Hello from BoxLite Go! +# +# EXIT: 0 +``` + +The output above was captured live on macOS Apple Silicon against the `main` branch of the BoxLite repository. See [Known issue](#known-issue-boxlite-v0-8-2-release) below for what to do on the released `v0.8.2`. + +## Runtime + +```go +rt, err := boxlite.NewRuntime(opts ...RuntimeOption) (*Runtime, error) +``` + +| Method | Purpose | +|---|---| +| `rt.Create(ctx, image, opts...)` | Create a new box; returns `*Box` | +| `rt.Get(ctx, idOrName)` | Reattach to an existing box by id or name | +| `rt.ListInfo(ctx)` | Snapshot of all boxes' metadata | +| `rt.Remove(ctx, idOrName)` | Remove a stopped box | +| `rt.ForceRemove(ctx, idOrName)` | Remove regardless of state | +| `rt.Metrics(ctx)` | Aggregate runtime metrics | +| `rt.Images()` | Image manager (`Pull`, `List`) | +| `rt.Close()` | Release the runtime lock; required for clean shutdown | +| `rt.Shutdown(ctx, timeout)` | Graceful shutdown of all running boxes | + +### Runtime options + +| Option | Effect | +|---|---| +| `WithHomeDir(dir)` | Override `$BOXLITE_HOME` (default `~/.boxlite`) | +| `WithRegistries(refs...)` | Add custom OCI registry hosts | + +## Box + +Returned by `rt.Create` and `rt.Get`. Methods: + +| Method | Purpose | +|---|---| +| `box.ID() string` | Box id (ULID) | +| `box.Name() string` | Box name (empty if unset) | +| `box.Start(ctx) error` | Start a `configured` or `stopped` box | +| `box.Stop(ctx) error` | Stop a `running` box | +| `box.Close() error` | Release the box handle (does not stop the box) | +| `box.Info(ctx) (*BoxInfo, error)` | Current state, PID, image, resources | +| `box.Metrics(ctx) (*BoxMetrics, error)` | Per-box metrics (CPU, memory, network, exec counters) | +| `box.Exec(ctx, name string, args ...string) (*ExecResult, error)` | Run one command and collect stdout/stderr/exit | +| `box.Command(name string, args ...string) *Cmd` | Build a configurable command (see `Cmd` below) | + +`Exec` is the simple, buffered form — it returns once the guest process exits and gives you the full output. Use `Command` when you need to set environment variables, capture streams differently, or kill mid-run. + +### Box options + +The full list: + +| Option | Effect | +|---|---| +| `WithName(string)` | Set a unique box name | +| `WithCPUs(n int)` | Number of CPU cores | +| `WithMemory(mib int)` | Memory in MiB | +| `WithRootfsPath(string)` | Use a pre-prepared rootfs directory instead of an image | +| `WithEnv(key, value string)` | Append an environment variable (call repeatedly) | +| `WithVolume(host, guest string)` | Read-write bind mount | +| `WithVolumeReadOnly(host, guest string)` | Read-only bind mount | +| `WithWorkDir(string)` | Working directory inside the guest | +| `WithEntrypoint(args ...string)` | Override the image entrypoint | +| `WithCmd(args ...string)` | Override the image cmd | +| `WithNetwork(NetworkSpec)` | Outbound mode + allowlist (see below) | +| `WithSecret(Secret)` | Host-side HTTPS secret substitution | +| `WithAutoRemove(bool)` | Auto-remove on stop (default `true`) | +| `WithDetach(bool)` | Survive parent exit (default `false`); incompatible with `WithAutoRemove(true)` | + +## Cmd — fine-grained execution + +`box.Command(name, args...)` returns a `Cmd` that mirrors the shape of `os/exec.Cmd`: + +```go +cmd := box.Command("python", "-c", "print('hi')") +cmd.Env = []string{"FOO=bar"} +cmd.Stdout = os.Stdout // any io.Writer +cmd.Stderr = os.Stderr +if err := cmd.Run(ctx); err != nil { ... } +``` + +Use it when streaming directly into Go writers, when you need to attach stdin (`cmd.Stdin = ...`), or when you want fine-grained signal control. + +## Networking + +```go +type NetworkSpec struct { + Mode NetworkMode + AllowNet []string +} + +const ( + NetworkModeEnabled NetworkMode = "Enabled" + NetworkModeDisabled NetworkMode = "Disabled" +) +``` + +`Disabled` removes the guest network interface entirely. `Enabled` is the default and is the only mode that respects `AllowNet`. See [Network](/concepts/network) for the broader picture. + +## Secrets + +```go +type Secret struct { + Name string + Value string + Hosts []string + Placeholder string // default "" +} +``` + +`WithSecret(...)` enables host-side substitution: the placeholder string in your guest's outbound HTTPS traffic is rewritten to `Value` only for matching `Hosts`. The real secret never enters the VM. Same model as the Python and Node SDKs — see [Network → Secrets](/concepts/network#secrets-host-side-substitution). + +## Result types + +```go +type ExecResult struct { + ExitCode int + Stdout string + Stderr string +} + +type BoxInfo struct { + ID string + Name string + Image string + State State // configured / running / stopping / stopped / paused / unknown + Running bool + PID int + CPUs int + MemoryMiB int + CreatedAt int64 +} +``` + +Aggregate `RuntimeMetrics` and per-box `BoxMetrics` follow the same shape as the metrics surfaced over REST. + +## Parity with other SDKs + +The Go SDK currently does **not** expose the high-level box-type wrappers (`SimpleBox`, `CodeBox`, `BrowserBox`, `ComputerBox`, `InteractiveBox`) that the Python and Node.js SDKs offer. Those wrappers are a thin convenience layer; their behaviour is reproducible in Go by combining `rt.Create` + `WithCmd` / `WithEntrypoint` and the matching image. The runtime is the same. + +For runtime-level parity (lifecycle, exec, file transfer, snapshot, clone, export, networking, secrets), the Go SDK is on par with Python and Node. + +## Known issue: BoxLite v0.8.2 release + +The released `v0.8.2` of the Go SDK and its bundled native library disagree on the network-mode wire format, so box creation against `v0.8.2` fails immediately with: + +``` +boxlite: internal error: Invalid JSON options: unknown variant `Isolated`, +expected `Enabled` or `Disabled` (code=1) +``` + +The fix is already on the `main` branch (the API on this page — `WithNetwork(NetworkSpec)`, `WithSecret(Secret)`, etc. — is what `main` exports) and will ship in the next tagged release. To use the SDK today, clone the repository and point a Go module replace at the local checkout: + +```text +replace github.com/boxlite-ai/boxlite/sdks/go => /path/to/boxlite/sdks/go +``` + +The same checkout produces a matching native library via the repository's `make dev:go` target. + +## See also + +- [SDK reference overview](/reference/index) — feature comparison across all SDKs +- [Python SDK](/reference/python/index) and [Node.js SDK](/reference/nodejs/index) — the reference shapes the Go SDK mirrors +- [Network](/concepts/network) and [Lifecycle](/concepts/lifecycle) — primitives that map 1:1 across SDKs diff --git a/reference/python/index.mdx b/reference/python/index.mdx index 738298e..31afa75 100644 --- a/reference/python/index.mdx +++ b/reference/python/index.mdx @@ -257,5 +257,5 @@ Default values used by BoxLite. - [Box Types](/reference/python/box-types) - SimpleBox, CodeBox, BrowserBox, ComputerBox, InteractiveBox - [Execution](/reference/python/execution) - Command execution, streaming I/O, stdin/stdout/stderr - [Errors & Metrics](/reference/python/errors-metrics) - Exception hierarchy, metrics classes -- [Getting Started](/getting-started/index) - Installation and quickstart guide +- [Getting Started](/getting-started/install) - Installation and quickstart guide - [Configuration Reference](/reference/index) - BoxOptions details diff --git a/reference/rest/index.mdx b/reference/rest/index.mdx new file mode 100644 index 0000000..58c13b9 --- /dev/null +++ b/reference/rest/index.mdx @@ -0,0 +1,197 @@ +--- +title: "REST API reference" +sidebarTitle: "REST API" +description: "HTTP endpoints exposed by `boxlite serve` — language-neutral access to BoxLite from any client." +icon: "globe" +--- + +`boxlite serve` starts a long-running HTTP server that exposes the full BoxLite runtime over REST. Use it when you want to drive boxes from a language without a first-party SDK, or when you want a single process to mediate many short-lived clients. + +The full request and response schemas live in the OpenAPI specification at [`openapi/rest-sandbox-open-api.yaml`](https://github.com/boxlite-ai/boxlite/blob/main/openapi/rest-sandbox-open-api.yaml). This page summarises the surface and walks through the create → start → exec → cleanup flow. + +## Start the server + +```bash +boxlite serve --port 8100 +# BoxLite REST API server listening on http://0.0.0.0:8100 +``` + +The CLI binary ships separately from the SDKs. Install it with `cargo install boxlite-cli` or download the prebuilt archive for your platform from the [BoxLite releases page](https://github.com/boxlite-ai/boxlite/releases). + +## URL shape + +Every runtime endpoint is prefixed by an API version and a tenant identifier: + +``` +/v1/{prefix}/... +``` + +For a single-host installation, `prefix` is always `default`. The two unprefixed endpoints are `/v1/config` (server capabilities) and `/v1/oauth/tokens` (authentication). + + + The current single-host server assumes a single trusted user. The authentication endpoint is wired but not enforced for local installations. Treat the server as you would any local-only daemon — do not expose `0.0.0.0:8100` to a network you don't control. + + +## Endpoint groups + +| Group | Endpoints | +|---|---| +| Configuration | `GET /v1/config` | +| Authentication | `POST /v1/oauth/tokens` | +| Boxes | `POST /v1/{prefix}/boxes`, `GET /v1/{prefix}/boxes`, `GET /v1/{prefix}/boxes/{box_id}`, `HEAD /v1/{prefix}/boxes/{box_id}`, `DELETE /v1/{prefix}/boxes/{box_id}`, `POST /v1/{prefix}/boxes/{box_id}/start`, `POST /v1/{prefix}/boxes/{box_id}/stop` | +| Execution | `POST /v1/{prefix}/boxes/{box_id}/exec`, `GET /v1/{prefix}/boxes/{box_id}/executions/{exec_id}`, `GET /v1/{prefix}/boxes/{box_id}/executions/{exec_id}/output` (SSE), `POST .../input`, `POST .../signal`, `POST .../resize`, `WS /v1/{prefix}/boxes/{box_id}/exec/tty` | +| Files | `POST /v1/{prefix}/boxes/{box_id}/files` (upload), `GET /v1/{prefix}/boxes/{box_id}/files` (download) | +| Snapshots | `POST /v1/{prefix}/boxes/{box_id}/snapshots`, `GET .../snapshots`, `GET .../snapshots/{name}`, `DELETE .../snapshots/{name}`, `POST .../snapshots/{name}/restore` | +| Clone & archive | `POST /v1/{prefix}/boxes/{box_id}/clone`, `POST /v1/{prefix}/boxes/{box_id}/export`, `POST /v1/{prefix}/boxes/import` | +| Images | `POST /v1/{prefix}/images/pull`, `GET /v1/{prefix}/images`, `GET /v1/{prefix}/images/{image_id}`, `HEAD /v1/{prefix}/images/{image_id}` | +| Metrics | `GET /v1/{prefix}/metrics`, `GET /v1/{prefix}/boxes/{box_id}/metrics` | + +## Server capabilities + +Probe the server before driving any flow — capability flags tell you which optional surfaces are wired. + +```bash +curl -s http://localhost:8100/v1/config +``` + +```json +{ + "capabilities": { + "snapshots_enabled": true, + "clone_enabled": true, + "export_enabled": true, + "import_enabled": true + } +} +``` + +## End-to-end flow + +The four steps below match what every SDK does internally. + +### 1. Create a box + +```bash +curl -s -X POST http://localhost:8100/v1/default/boxes \ + -H 'Content-Type: application/json' \ + -d '{"rootfs":{"image":"alpine:latest"},"network":"Enabled"}' +``` + +```json +{ + "box_id": "k14sYHce2Rqi", + "name": null, + "status": "configured", + "created_at": "2026-04-25T15:48:04.718781+00:00", + "updated_at": "2026-04-25T15:48:04.719016+00:00", + "pid": null, + "image": "alpine:latest", + "cpus": 2, + "memory_mib": 512, + "labels": {} +} +``` + +The returned `box_id` is the only identifier you need afterwards. `status: "configured"` means the VM is provisioned but not yet booted. + +### 2. Start the box + +```bash +curl -s -X POST http://localhost:8100/v1/default/boxes/k14sYHce2Rqi/start +``` + +```json +{ + "box_id": "k14sYHce2Rqi", + "status": "running", + "pid": 39338, + "image": "alpine:latest", + "cpus": 2, + "memory_mib": 512 +} +``` + +### 3. Execute a command + +`exec` returns immediately with an `execution_id`. The execution itself runs asynchronously. + +```bash +curl -s -X POST http://localhost:8100/v1/default/boxes/k14sYHce2Rqi/exec \ + -H 'Content-Type: application/json' \ + -d '{"command":"echo","args":["Hello from REST!"]}' +``` + +```json +{ "execution_id": "61357ee0-f841-4502-8a3b-fc3cf5b24ac0" } +``` + +Poll status, or — preferred — stream output as Server-Sent Events: + +```bash +curl -N "http://localhost:8100/v1/default/boxes/k14sYHce2Rqi/executions/61357ee0-f841-4502-8a3b-fc3cf5b24ac0/output" +``` + +```text +event: stdout +data: {"data":"SGVsbG8gZnJvbSBSRVNUIQo="} + +event: stderr +data: {"data":"..."} + +event: exit +data: {"duration_ms":8482,"exit_code":0} +``` + + + `stdout` and `stderr` payloads are **base64-encoded** in the SSE `data` field — decode before displaying. The terminal `exit` event carries `duration_ms` and `exit_code`, mirroring what the SDKs surface as `ExecResult`. + + +### 4. Clean up + +```bash +curl -X POST http://localhost:8100/v1/default/boxes/k14sYHce2Rqi/stop +curl -X DELETE http://localhost:8100/v1/default/boxes/k14sYHce2Rqi +``` + +## Snapshots over REST + +```bash +curl -s -X POST http://localhost:8100/v1/default/boxes/k14sYHce2Rqi/snapshots \ + -H 'Content-Type: application/json' \ + -d '{"name":"demo-snap"}' +``` + +```json +{ + "id": "6YfnIqpz", + "box_id": "k14sYHce2Rqi", + "name": "demo-snap", + "created_at": 1777132120, + "container_disk_bytes": 268435456, + "size_bytes": 655360 +} +``` + +A snapshot has both an `id` (server-generated) and a `name` (your label). Path-style URLs use the **name** as the addressable segment: `/snapshots/{name}` and `/snapshots/{name}/restore`. See [Snapshot, clone, export](/concepts/snapshot) for what is and isn't captured (snapshot is **disk-only** — there is no in-memory checkpoint). + +## Errors + +Errors come back with a uniform envelope: + +```json +{ + "error": { + "message": "invalid state: Cannot remove snapshot: current disk depends on this snapshot. Restore a different snapshot first.", + "type": "InternalError", + "code": 500 + } +} +``` + +`code` mirrors the HTTP status. `type` enumerates the runtime error kind (`InvalidState`, `NotFound`, `InternalError`, …); see the OpenAPI components for the full list. + +## See also + +- [Python](/reference/python/index), [Node.js](/reference/nodejs/index), [Rust](/reference/rust/index), [C](/reference/c/index) SDK references — same primitives, library form +- [Snapshot, clone, export](/concepts/snapshot) — what snapshots can and cannot capture +- [`openapi/rest-sandbox-open-api.yaml`](https://github.com/boxlite-ai/boxlite/blob/main/openapi/rest-sandbox-open-api.yaml) — full request and response schemas diff --git a/reference/rust/index.mdx b/reference/rust/index.mdx index 1166659..bc6593b 100644 --- a/reference/rust/index.mdx +++ b/reference/rust/index.mdx @@ -347,7 +347,7 @@ async fn main() -> Result<(), Box> { ## See Also -- [Getting Started Guide](/getting-started/index) +- [Getting Started Guide](/getting-started/install) - [Architecture Overview](/architecture/index) - [Configuration Reference](/reference/index) - [Box Configuration](/reference/rust/box-config) diff --git a/tutorials/index.mdx b/tutorials/index.mdx index 32f7db4..6de0d11 100644 --- a/tutorials/index.mdx +++ b/tutorials/index.mdx @@ -21,6 +21,7 @@ You've installed BoxLite and run "Hello from BoxLite!" — now what? These tutor | Run an interactive shell session | [**InteractiveBox**](/getting-started/core-concepts#box-types) | [Interactive terminal](/tutorials/interactive-terminal) | | Run Node.js, Go, Rust, or any tool | [**SimpleBox**](/getting-started/core-concepts#box-types) | [Run any language](/tutorials/run-any-language) | | Run a server and access it from host | [**SimpleBox**](/getting-started/core-concepts#box-types) | [Long-running services](/tutorials/long-running-services) | +| Fork or branch sandbox state | [**SimpleBox**](/getting-started/core-concepts#box-types) or [**CodeBox**](/getting-started/core-concepts#box-types) | [Snapshot, fork, restore](/tutorials/snapshot-fork-restore) *(coming soon)* | | Handle errors and debug failures | All | [Error handling](/tutorials/error-handling) | @@ -93,6 +94,13 @@ You've installed BoxLite and run "Hello from BoxLite!" — now what? These tutor > Start HTTP servers and JSON APIs inside a sandbox, forward ports, and access them from the host. + + Take a snapshot, branch two execution paths from the same state, then roll one back — like git branch for sandbox environments. + {path}"]) + await h.wait() + + +async def main(): + runtime = boxlite.Boxlite.default() + + source = await runtime.create( + boxlite.BoxOptions(image="alpine:latest", auto_remove=False), + name="source", + ) + print(f"source.id = {source.id}") + + await write(source, "/root/marker.txt", "baseline") + print(f"source baseline: {await cat(source, '/root/marker.txt')!r}") +``` + +Two notes about this snippet: + +- `auto_remove=False` is on purpose — we want both boxes to outlive the script's `stop()` call so we can compare the timelines. +- The runtime-level handle returned by `runtime.create(...)` exposes `box.exec(...)` directly. This is the lower-level surface where you can stream stdout/stderr; the `cat()` helper above shows the pattern (consume the iterator first, then `await wait()` for the exit code). + +Running just step 1 prints: + +```text +source.id = egsJA9Qr4vZR +source baseline: 'baseline' +``` + +## Step 2 — fork by cloning the running box + +```python + clone = await source.clone_box(name="clone") + print(f"clone.id = {clone.id}") + print(f"clone sees: {await cat(clone, '/root/marker.txt')!r}") +``` + +`clone_box()` works while the source is running. Internally the runtime auto-quiesces the source box (briefly moves it through `paused`, see [Lifecycle](/concepts/lifecycle)) so the clone gets a consistent, point-in-time copy of the disk. The source returns to `running` automatically — you don't need to manage the pause yourself. + +The clone's disk is **independent**: it's a fresh copy-on-write image rooted at the source's disk state. The two boxes share nothing after this call returns. + +```text +clone.id = 2kSCsg5kLWUm +clone sees: 'baseline' +``` + +The clone correctly inherits the marker file we wrote in step 1. + +## Step 3 — diverge + +Now write something different in each box. + +```python + await write(source, "/root/marker.txt", "branch_a") + await write(clone, "/root/marker.txt", "branch_b") +``` + +## Step 4 — verify independence + +```python + print(f"source after branch A: {await cat(source, '/root/marker.txt')!r}") + print(f"clone after branch B: {await cat(clone, '/root/marker.txt')!r}") +``` + +Output: + +```text +source after branch A: 'branch_a' +clone after branch B: 'branch_b' +``` + +Two boxes, two timelines, no contamination. This is the basic shape you'd reach for whenever you want to A/B a build, fan out an evaluation, or speculatively try something risky while preserving the baseline. + +## Step 5 — clean up + +```python + for b in (clone, source): + try: + await b.stop() + except Exception: + pass + await runtime.remove(b.id, force=True) + print("cleaned up") +``` + +Because we set `auto_remove=False`, neither box was removed by its `stop()`. We tear them down explicitly via `runtime.remove(box_id, force=True)`. `force=True` removes a box regardless of state — the safest default for cleanup paths. + +## When to reach for clone vs snapshot vs export + +- **Clone** — what we just did. Two live boxes from the same point; both can run, both can be modified, neither blocks the other. +- **Snapshot** — captures the disk under the current box itself, no second box. Use it when you want to roll the *same* box back to a known state later. +- **Export / Import** — produces a portable archive on disk. Use it to ship a state to another host or to back it up. + +All three are disk-only — none of them captures memory, in-flight syscalls, or open sockets. See [Snapshot, clone, export](/concepts/snapshot) for the full breakdown. + +## Full script + +The complete file used to validate every output above: + +```python tutorials/snapshot_fork_demo.py +import asyncio +import boxlite + + +async def cat(box, path): + h = await box.exec("cat", [path]) + chunks = [] + stdout = h.stdout() + if stdout is not None: + async for line in stdout: + chunks.append(line.decode() if isinstance(line, bytes) else line) + await h.wait() + return "".join(chunks).strip() + + +async def write(box, path, value): + h = await box.exec("sh", ["-c", f"echo {value} > {path}"]) + await h.wait() + + +async def main(): + runtime = boxlite.Boxlite.default() + + source = await runtime.create( + boxlite.BoxOptions(image="alpine:latest", auto_remove=False), + name="source", + ) + print(f"source.id = {source.id}") + + await write(source, "/root/marker.txt", "baseline") + print(f"source baseline: {await cat(source, '/root/marker.txt')!r}") + + clone = await source.clone_box(name="clone") + print(f"clone.id = {clone.id}") + print(f"clone sees: {await cat(clone, '/root/marker.txt')!r}") + + await write(source, "/root/marker.txt", "branch_a") + await write(clone, "/root/marker.txt", "branch_b") + + print(f"source after branch A: {await cat(source, '/root/marker.txt')!r}") + print(f"clone after branch B: {await cat(clone, '/root/marker.txt')!r}") + + for b in (clone, source): + try: + await b.stop() + except Exception: + pass + await runtime.remove(b.id, force=True) + print("cleaned up") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Save it as `snapshot_fork_demo.py` and run with `python snapshot_fork_demo.py`. End-to-end output observed during validation: + +```text +source.id = egsJA9Qr4vZR +source baseline: 'baseline' +clone.id = 2kSCsg5kLWUm +clone sees: 'baseline' +source after branch A: 'branch_a' +clone after branch B: 'branch_b' +cleaned up +``` + +## See also + +- [Snapshot, clone, export](/concepts/snapshot) — when each primitive is the right one +- [Lifecycle](/concepts/lifecycle) — the `paused` state the runtime uses during clone +- [`examples/python/03_lifecycle/clone_export_import.py`](https://github.com/boxlite-ai/boxlite/blob/main/examples/python/03_lifecycle/clone_export_import.py) — upstream walkthrough of clone alongside export/import