diff --git a/CHANGELOG.md b/CHANGELOG.md index 88382e8..eef849a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Versioning after its first public release. ### Added +- A zero-build, responsive GitHub Pages homepage under `docs/` that presents the + measured benchmark, fail-closed boundary, kubeadm quickstart, and architecture + without depending on another repository or third-party frontend assets. - Standalone local gVisor environment, Python SDK, CLI, stdio MCP, and operator Console surfaces for the first public release candidate. - `make destroy-local`, a KUBECONFIG-aware Makefile, and resource checks in `make doctor`. @@ -98,6 +101,9 @@ Versioning after its first public release. ### Fixed +- The new-profile doctor gate now reflects the default 6 GiB VM and sparse disk's + measured physical footprint (6.5 GiB available memory / 25 GiB free disk), so a + capable host is not rejected solely by the virtual disk's 60 GiB maximum size. - Findings of the 2026-09-02 pre-release review, in four groups. Control plane: request handling, admission and readiness defects found by reading the API and store paths. Lifecycle: Runtime, workspace and checkpoint state transitions diff --git a/README.md b/README.md index 815e54e..5ec12a5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[Website](https://hullwork.github.io/sandbox/) · [Documentation](docs/README.md) · [Benchmark report](docs/BENCHMARK_REPORT_2026-09-01.md) + **Run an agent's shell and file operations inside a gVisor Pod on your own Kubernetes cluster.** A Control Plane owns Workspaces, quotas and credentials. Agents reach it through a Python SDK, the `sandbox` CLI, or a stdio MCP bridge — @@ -48,7 +50,7 @@ operation fails; it never falls back to running on the host. ```bash make bootstrap # create .venv and install SDK + test dependencies -make test # 834 unit and contract tests, no network, no cluster +make test # 839 unit and contract tests, no network, no cluster make verify # complete Python, Console, manifest, Helm, wheel gate make help # every Make target with its one-line description ``` @@ -200,8 +202,8 @@ it first rather than discovering a gap halfway through the VM build. | Python | 3.11 or newer | | Host OS | macOS or Linux | | Host architecture | amd64 or arm64 (`scripts/local-cluster.yaml` pins Ubuntu images for both; gVisor is installed for `x86_64` and `aarch64`) | -| **Available memory** | **8 GiB free** for a new profile — a hard check, not a warning | -| **Free disk** | **35 GiB free** under `$LIMA_HOME` (default `~/.lima`) for a new profile — also a hard check | +| **Available memory** | **6.5 GiB free** for a new profile — the default VM reserves 6 GiB; this is a hard check, not a warning | +| **Free disk** | **25 GiB free** under `$LIMA_HOME` (default `~/.lima`) for a new profile — the 60 GiB VM disk is sparse; this is also a hard check | | Virtualization | On Linux, a readable and writable `/dev/kvm`. Without it Lima falls back to QEMU TCG software emulation, which boots kubeadm many times slower and is not usable in practice. `make doctor` warns rather than fails on this one. | | Network | Egress to pull the Ubuntu cloud image, Kubernetes apt packages, Cilium, gVisor, Metrics Server, and Rook/Ceph images | diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/.nojekyll @@ -0,0 +1 @@ + diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..db9a0b9 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,28 @@ + + + + + + + Not found · Sandbox Platform + + + + + +
+
+ +
RUNTIME_NOT_FOUND · 404
+

Outside the sandbox.

+

This route does not exist. The good news: nothing fell back to the host.

+ Return to safety +
+
+ + diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 48a8aed..9d3292a 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -52,7 +52,7 @@ proof. It records phase timing and outcome in `.sandbox/quickstart-summary.json`. Use `make up-local` directly when the Python environment is already prepared and only the deployment needs updating. -For a new profile, `make doctor` fails when less than 8 GiB of memory or 35 GiB +For a new profile, `make doctor` fails when less than 6.5 GiB of memory or 25 GiB of disk is free. When the dedicated `sandbox-local` VM already exists, it uses a 2 GiB memory / 5 GiB disk reuse gate instead. It warns when `/dev/kvm` is absent on Linux (Lima then falls back to QEMU software diff --git a/docs/README.md b/docs/README.md index a6a4d58..fe4c0c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,10 @@ Sandbox Platform provides an execution-as-a-service boundary for agents: a Control Plane, gVisor Runtime, workspace volume services, official Python SDK, and MCP bridge. Start with the [README](../README.md), then use the documents below. +The project homepage is the zero-build static site in [index.html](index.html). +GitHub Pages serves this directory from `main:/docs`; assets stay repository-local +so the public site has no runtime dependency on a separate project or frontend build. + | Document | Audience | Use it for | | --- | --- | --- | | [README](../README.md) | All users | Product scope, quick start, architecture summary, and current status | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index bc268a0..f783149 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -12,8 +12,8 @@ export KUBECONFIG="$PWD/.sandbox/kubeconfig" ## `make doctor` cannot reach Docker or reports insufficient capacity **Symptom.** The first quickstart phase stops before creating a VM because the -Docker daemon is unreachable, available memory is below 8 GiB, or free space under -`$LIMA_HOME` is below 35 GiB for a new profile. Reusing an existing `sandbox-local` +Docker daemon is unreachable, available memory is below 6.5 GiB, or free space under +`$LIMA_HOME` is below 25 GiB for a new profile. Reusing an existing `sandbox-local` profile lowers the capacity gate to 2 GiB memory and 5 GiB disk. **Fix.** On macOS, start Docker Desktop (`open -a Docker`) and wait until it is diff --git a/docs/assets/sandbox-mark.svg b/docs/assets/sandbox-mark.svg new file mode 100644 index 0000000..3d261af --- /dev/null +++ b/docs/assets/sandbox-mark.svg @@ -0,0 +1,13 @@ + + Sandbox Platform + + + + + + + + + + + diff --git a/docs/assets/site.css b/docs/assets/site.css new file mode 100644 index 0000000..64a1981 --- /dev/null +++ b/docs/assets/site.css @@ -0,0 +1,429 @@ +:root { + color-scheme: dark; + --bg: #07090d; + --panel: #0d1117; + --panel-2: #11181a; + --text: #f4f7f4; + --muted: #9aa7a3; + --line: rgba(183, 219, 205, 0.14); + --green: #a7ff83; + --mint: #35e6b0; + --blue: #67a9ff; + --max: 1180px; +} + +* { box-sizing: border-box; } + +html { + scroll-behavior: smooth; + background: var(--bg); +} + +body { + margin: 0; + overflow-x: hidden; + color: var(--text); + background: + radial-gradient(circle at 78% 5%, rgba(53, 230, 176, 0.12), transparent 27rem), + radial-gradient(circle at 8% 34%, rgba(103, 169, 255, 0.06), transparent 24rem), + var(--bg); + font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + -webkit-font-smoothing: antialiased; +} + +body::before { + position: fixed; + inset: 0; + z-index: -1; + content: ""; + opacity: 0.14; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.04) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: linear-gradient(to bottom, black, transparent 70%); +} + +a { color: inherit; text-decoration: none; } + +.container { + width: min(calc(100% - 48px), var(--max)); + margin-inline: auto; +} + +.skip-link { + position: fixed; + top: 8px; + left: 8px; + z-index: 100; + padding: 10px 14px; + color: #07100c; + background: var(--green); + border-radius: 8px; + transform: translateY(-150%); +} + +.skip-link:focus { transform: translateY(0); } + +.nav-shell { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid var(--line); + background: rgba(7, 9, 13, 0.76); + backdrop-filter: blur(20px); +} + +.nav { + min-height: 72px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.brand { + display: flex; + align-items: center; + gap: 11px; + font-weight: 760; + letter-spacing: -0.03em; + font-size: 20px; +} + +.brand img { filter: drop-shadow(0 0 13px rgba(167, 255, 131, 0.16)); } + +.nav-links { + display: flex; + align-items: center; + gap: 34px; + color: #b7c2be; + font-size: 14px; +} + +.nav-links a, .github-link { transition: color 160ms ease; } +.nav-links a:hover, .github-link:hover { color: var(--green); } + +.github-link { + padding: 9px 14px; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 14px; + font-weight: 680; +} + +.hero { + min-height: 760px; + display: grid; + grid-template-columns: 1.08fr 0.92fr; + align-items: center; + gap: 7%; + padding-block: 104px 96px; +} + +.eyebrow, .kicker { + color: var(--green); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + font-weight: 720; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.eyebrow { display: flex; align-items: center; gap: 9px; } + +.pulse { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 0 0 rgba(167, 255, 131, 0.55); + animation: pulse 2.2s infinite; +} + +@keyframes pulse { + 70% { box-shadow: 0 0 0 8px rgba(167, 255, 131, 0); } + 100% { box-shadow: 0 0 0 0 rgba(167, 255, 131, 0); } +} + +h1 { + max-width: 760px; + margin: 25px 0 25px; + font-size: clamp(54px, 6vw, 88px); + line-height: 0.98; + letter-spacing: -0.07em; + font-weight: 760; +} + +h1 em, .closing em { + color: transparent; + background: linear-gradient(100deg, var(--green), var(--mint)); + background-clip: text; + -webkit-background-clip: text; + font-style: normal; +} + +.hero-lede { + max-width: 630px; + margin: 0; + color: #b7c2be; + font-size: clamp(18px, 1.7vw, 21px); + line-height: 1.65; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 36px; +} + +.button { + min-height: 50px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 22px; + border: 1px solid transparent; + border-radius: 10px; + font-size: 15px; + font-weight: 740; + transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; +} + +.button:hover { transform: translateY(-2px); } +.button.primary { color: #07100c; background: var(--green); box-shadow: 0 12px 32px rgba(112, 235, 145, 0.12); } +.button.primary:hover { background: #bdff9f; } +.button.secondary { color: var(--text); border-color: var(--line); background: rgba(255,255,255,0.025); } +.button.secondary:hover { border-color: rgba(167,255,131,0.45); } + +.trust-row { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-top: 32px; + color: #71807a; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.trust-row span::before { content: "◆"; margin-right: 7px; color: #43544d; font-size: 7px; vertical-align: 1px; } + +.hero-visual { position: relative; min-height: 520px; display: grid; place-items: center; } + +.terminal-card { + position: relative; + z-index: 2; + width: min(100%, 520px); + overflow: hidden; + border: 1px solid rgba(180, 234, 212, 0.18); + border-radius: 18px; + background: rgba(10, 14, 18, 0.9); + box-shadow: 0 30px 100px rgba(0, 0, 0, 0.52), 0 0 0 1px rgba(255,255,255,0.02) inset; + transform: perspective(1100px) rotateY(-4deg) rotateX(2deg); +} + +.terminal-bar { + height: 49px; + display: flex; + align-items: center; + gap: 7px; + padding: 0 17px; + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.018); +} + +.dot { width: 9px; height: 9px; border-radius: 50%; } +.dot.red { background: #ff6b64; } +.dot.amber { background: #e7ba55; } +.dot.green { background: #67d78a; } +.terminal-title { margin-left: auto; color: #65716d; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } + +.terminal-body { + min-height: 330px; + padding: 27px 28px; + font: 14px/1.85 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.terminal-body p { margin: 0; } +.prompt, .ok { color: var(--green); } +.dim { color: #58645f; } +.result { margin-top: 16px !important; color: #effff4; font-weight: 650; } +.cursor { display: inline-block; width: 8px; height: 16px; margin-left: 5px; vertical-align: -3px; background: var(--green); animation: blink 1.1s steps(1) infinite; } +@keyframes blink { 50% { opacity: 0; } } + +.terminal-foot { + display: flex; + justify-content: space-between; + padding: 14px 18px; + color: #6f7c77; + border-top: 1px solid var(--line); + font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.healthy { color: #9bd7ae; } +.healthy i { display: inline-block; width: 6px; height: 6px; margin-right: 5px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); } + +.orbital { position: absolute; border: 1px solid rgba(167, 255, 131, 0.12); border-radius: 50%; } +.orbital::after { position: absolute; width: 8px; height: 8px; content: ""; border-radius: 2px; background: var(--green); box-shadow: 0 0 18px var(--green); } +.orbital-one { width: 480px; height: 480px; animation: spin 22s linear infinite; } +.orbital-one::after { left: 60px; top: 55px; } +.orbital-two { width: 390px; height: 390px; border-style: dashed; animation: spin 17s linear infinite reverse; } +.orbital-two::after { right: 26px; bottom: 92px; background: var(--blue); box-shadow: 0 0 18px var(--blue); } +@keyframes spin { to { transform: rotate(360deg); } } + +.metrics { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-block: 1px solid var(--line); +} + +.metrics article { padding: 32px 25px; border-right: 1px solid var(--line); } +.metrics article:last-child { border-right: 0; } +.metrics strong { display: block; font-size: clamp(30px, 3.2vw, 44px); letter-spacing: -0.045em; } +.metrics strong span { margin-left: 3px; color: var(--green); font-size: 0.48em; font-weight: 650; } +.metrics p { margin: 7px 0 0; color: var(--muted); font-size: 13px; } + +.section { padding-block: 132px; } +.section-intro { max-width: 750px; } +.section-intro h2, .quickstart-card h2, .closing h2 { + margin: 17px 0 20px; + font-size: clamp(39px, 5vw, 64px); + line-height: 1.05; + letter-spacing: -0.055em; +} +.section-intro p, .quickstart-card p { color: var(--muted); font-size: 17px; line-height: 1.75; } + +.architecture { margin-top: 72px; } +.arch-layer { position: relative; border: 1px solid var(--line); border-radius: 18px; background: rgba(13,17,23,0.82); } +.layer-label { position: absolute; top: -9px; left: 18px; padding: 2px 9px; color: #708078; background: var(--bg); font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.1em; } +.arch-nodes { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; } +.arch-nodes > div { min-height: 120px; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(255,255,255,0.012); } +.arch-nodes > div + div { border-left: 1px solid var(--line); } +.arch-layer b { font-size: 15px; } +.arch-layer small { display: block; margin-top: 7px; color: #75817d; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } +.arch-link { height: 84px; display: grid; place-items: center; color: #72807a; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } +.arch-link::before, .arch-link::after { position: absolute; content: ""; } +.arch-link::before { height: 84px; border-left: 1px dashed #3a5148; } +.arch-link span { position: relative; z-index: 1; padding: 6px 11px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); } +.control-layer { padding: 40px 28px 27px; box-shadow: 0 0 70px rgba(53,230,176,0.045) inset; } +.arch-core { display: grid; grid-template-columns: auto 1fr auto; gap: 18px; align-items: center; } +.mark-small { width: 46px; height: 46px; } +.mark-small img { width: 100%; } +.arch-store { display: flex; gap: 8px; } +.arch-store span, .runtime-details span, .runtime-badge { padding: 7px 10px; color: #93a59d; border: 1px solid var(--line); border-radius: 7px; background: rgba(255,255,255,0.018); font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; } +.boundary { position: relative; height: 86px; display: grid; place-items: center; } +.boundary::before { position: absolute; inset: 42px 0 auto; content: ""; border-top: 1px dashed rgba(255, 107, 100, 0.35); } +.boundary span { position: relative; z-index: 1; padding: 7px 12px; color: #da8d86; border: 1px solid rgba(255,107,100,0.22); border-radius: 999px; background: var(--bg); font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.05em; } +.runtime-layer { display: grid; grid-template-columns: 1.4fr 0.6fr; gap: 12px; padding: 36px 20px 20px; border-color: rgba(255,107,100,0.18); } +.runtime-pod, .workspace-node { min-height: 132px; display: flex; flex-direction: column; justify-content: center; padding: 22px; border: 1px solid var(--line); border-radius: 12px; background: rgba(255,255,255,0.015); } +.runtime-badge { width: max-content; margin-bottom: 15px; color: var(--green); border-color: rgba(167,255,131,0.22); } +.runtime-details { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 15px; } + +.proof-section { border-block: 1px solid var(--line); background: rgba(255,255,255,0.018); } +.proof-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 62px; } +.proof-card { position: relative; min-height: 290px; padding: 34px; overflow: hidden; border: 1px solid var(--line); border-radius: 16px; background: var(--panel); } +.proof-card::after { position: absolute; right: -70px; bottom: -80px; width: 180px; height: 180px; content: ""; border: 1px solid rgba(167,255,131,0.08); border-radius: 50%; } +.proof-number { color: var(--green); font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } +.proof-card h3 { margin: 38px 0 12px; font-size: 24px; letter-spacing: -0.035em; } +.proof-card p { max-width: 480px; margin: 0; color: var(--muted); line-height: 1.65; } +.proof-card code { position: absolute; bottom: 29px; left: 34px; color: #6d7b75; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; } + +.benchmark-layout { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 9%; align-items: center; } +.text-link { display: inline-flex; gap: 9px; margin-top: 20px; color: var(--green); font-size: 14px; font-weight: 680; } +.benchmark-panel { padding: 12px 30px; border: 1px solid var(--line); border-radius: 18px; background: linear-gradient(145deg, rgba(17,24,26,0.9), rgba(10,14,18,0.95)); } +.benchmark-row { display: grid; grid-template-columns: 1.15fr 0.85fr auto; gap: 20px; align-items: center; padding: 26px 0; border-bottom: 1px solid var(--line); } +.benchmark-row b { font-size: 14px; } +.benchmark-row small { display: block; margin-top: 6px; color: #6e7a75; font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; } +.benchmark-row > strong { color: var(--green); font: 14px ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; } +.benchmark-row > strong small { display: inline; } +.bar-track { height: 3px; overflow: hidden; background: #1d2924; border-radius: 3px; } +.bar-track i { display: block; width: var(--bar); height: 100%; background: linear-gradient(90deg, var(--mint), var(--green)); box-shadow: 0 0 8px var(--mint); } +.benchmark-note { margin: 20px 0 10px; color: #66736d; font-size: 11px; line-height: 1.6; } + +.quickstart-card { + display: grid; + grid-template-columns: 0.9fr 1.1fr; + gap: 8%; + padding: clamp(34px, 6vw, 72px); + border: 1px solid rgba(167,255,131,0.18); + border-radius: 24px; + background: radial-gradient(circle at 85% 0, rgba(167,255,131,0.11), transparent 48%), var(--panel-2); + box-shadow: 0 40px 100px rgba(0,0,0,0.25); +} +.quickstart-card h2 { font-size: clamp(36px, 4vw, 52px); } +.command-block { align-self: center; display: flex; align-items: center; gap: 10px; padding: 11px 11px 11px 21px; border: 1px solid var(--line); border-radius: 12px; background: #080c0e; } +.command-block code { flex: 1; color: #eaf5ee; font: 14px ui-monospace, SFMono-Regular, Menlo, monospace; } +.command-block code span { margin-right: 8px; color: var(--green); } +.command-block button { padding: 10px 14px; color: #a9b8b1; border: 1px solid var(--line); border-radius: 7px; background: #151d1c; cursor: pointer; } +.command-block button:hover { color: var(--green); border-color: rgba(167,255,131,0.35); } +.quick-links { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; margin-top: 15px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: var(--line); } +.quick-links a { display: flex; justify-content: space-between; padding: 18px; background: rgba(8,12,14,0.96); font-size: 13px; transition: color 160ms ease, background 160ms ease; } +.quick-links a:hover { color: var(--green); background: #111b18; } + +.closing { padding: 150px 0; text-align: center; border-top: 1px solid var(--line); background: radial-gradient(circle at 50% 45%, rgba(53,230,176,0.12), transparent 24rem); } +.closing img { filter: drop-shadow(0 0 24px rgba(167,255,131,0.18)); } +.closing h2 { margin-inline: auto; max-width: 850px; } +.closing .button { margin-top: 17px; } + +footer { border-top: 1px solid var(--line); color: #65716c; font: 10px ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.06em; text-transform: uppercase; } +.footer-inner { min-height: 84px; display: flex; justify-content: space-between; align-items: center; gap: 20px; } + +.reveal { opacity: 0; transform: translateY(18px); transition: opacity 700ms ease, transform 700ms ease; } +.reveal.visible { opacity: 1; transform: translateY(0); } + +@media (max-width: 900px) { + .hero { grid-template-columns: 1fr; padding-top: 80px; } + .hero-visual { min-height: 490px; } + .terminal-card { transform: none; } + .metrics { grid-template-columns: repeat(2, 1fr); } + .metrics article:nth-child(2) { border-right: 0; } + .metrics article:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .benchmark-layout, .quickstart-card { grid-template-columns: 1fr; } + .benchmark-panel { margin-top: 42px; } + .quick-links { grid-template-columns: 1fr; } +} + +@media (max-width: 640px) { + .container { width: min(calc(100% - 30px), var(--max)); } + .nav { min-height: 64px; } + .nav-links { display: none; } + .github-link { padding: 8px 12px; } + .hero { min-height: auto; padding-block: 72px 55px; } + h1 { font-size: clamp(48px, 14vw, 68px); } + .hero-lede { font-size: 17px; } + .hero-actions .button { width: 100%; } + .trust-row { gap: 11px 16px; } + .hero-visual { min-height: 420px; } + .terminal-body { min-height: 285px; padding: 22px 18px; font-size: 12px; } + .orbital-one { width: 380px; height: 380px; } + .orbital-two { width: 300px; height: 300px; } + .metrics article { padding: 24px 16px; } + .section { padding-block: 92px; } + .proof-grid { grid-template-columns: 1fr; } + .proof-card { min-height: 280px; padding: 28px; } + .proof-card code { left: 28px; } + .arch-nodes { grid-template-columns: 1fr; } + .arch-nodes > div { min-height: 86px; } + .arch-nodes > div + div { border-left: 0; border-top: 1px solid var(--line); } + .arch-core { grid-template-columns: auto 1fr; } + .arch-store { grid-column: 1 / -1; } + .runtime-layer { grid-template-columns: 1fr; } + .benchmark-panel { padding-inline: 20px; } + .benchmark-row { grid-template-columns: 1fr auto; gap: 10px; } + .bar-track { grid-column: 1 / -1; grid-row: 2; } + .quickstart-card { padding: 30px 22px; } + .command-block { padding-left: 14px; } + .command-block code { font-size: 12px; } + .footer-inner { align-items: flex-start; flex-direction: column; justify-content: center; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } +} diff --git a/docs/assets/site.js b/docs/assets/site.js new file mode 100644 index 0000000..f6a3b8a --- /dev/null +++ b/docs/assets/site.js @@ -0,0 +1,38 @@ +const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +const reveal = () => { + const nodes = document.querySelectorAll('.reveal'); + if (reduceMotion || !('IntersectionObserver' in window)) { + nodes.forEach((node) => node.classList.add('visible')); + return; + } + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.12 } + ); + nodes.forEach((node) => observer.observe(node)); +}; + +document.querySelectorAll('[data-copy]').forEach((button) => { + button.addEventListener('click', async () => { + const label = button.textContent; + try { + await navigator.clipboard.writeText(button.dataset.copy); + button.textContent = 'Copied'; + } catch { + button.textContent = 'Select command'; + } + window.setTimeout(() => { + button.textContent = label; + }, 1800); + }); +}); + +reveal(); diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..9cdc385 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,239 @@ + + + + + + + + + + + + Sandbox Platform — Secure execution for AI agents + + + + + + + + + +
+
+
+
Open source · Alpha 0.1.0
+

Agent execution
without the leap of faith.

+

+ Run untrusted shell and file operations on your own Kubernetes cluster. + Every runtime is a gVisor Pod. Every workspace survives it. Nothing falls back to your host. +

+ +
+ MIT licensed + Self-hosted + kubeadm + No vendor runtime +
+
+ +
+
+
+
+
+ + sandbox — quickstart +
+
+

$ make quickstart

+

[1/5] checking host prerequisites

+

✓ kubeadm cluster ready

+

✓ gVisor kernel verified

+

✓ workspace survived restart

+

✓ host fallback blocked

+

sandbox-ready

+
+
+ runtime/gvisor healthy +
+
+
+
+ +
+
2.497s

cold start p50

+
35.72ms

warm execution p50

+
839

contract tests

+
0

host fallbacks

+
+ +
+
+
Designed for a hostile workload
+

A narrow waist between your agent and everything else.

+

+ One authenticated API crosses the boundary. The control plane owns policy; + the runtime gets only the capability it needs. +

+
+ +
+
+ YOUR PROCESS +
+
Python SDKtyped client
+
sandbox CLIshell native
+
MCP bridge9 agent tools
+
+
+ +
+ TRUSTED · SANDBOX-SYSTEM +
+
+
Control Planeadmission · quotas · credentials · lifecycle
+
SQL stateS3 checkpoints
+
+
+
default-deny NetworkPolicy
+
+ UNTRUSTED · SANDBOX-WORKLOADS +
+ gVisor RuntimeClass + Ephemeral Runtime Pod +
non-rootread-only rootno SA token
+
+
Durable WorkspacePVC · independent lifecycle
+
+
+
+ +
+
+
+
The security model is executable
+

Claims you can grep. Boundaries you can break-test.

+
+
+
+ 01 +

Fail closed, always

+

If the control plane or runtime disappears, execution stops. There is no local subprocess escape hatch.

+ fallbacks_to_host = false +
+
+ 02 +

Credential-bound tenancy

+

Tenant identity comes from the API key—not a request field. Cross-tenant access is tested before Kubernetes is called.

+ wrong_tenant → 404 +
+
+ 03 +

Disposable compute, durable files

+

Stop and replace a Runtime without discarding its Workspace. Checkpoints are explicit recovery, not hidden magic.

+ runtime ≠ workspace +
+
+ 04 +

No privileged shortcuts

+

No Docker socket, no service-account token, non-root processes, and a read-only root filesystem.

+ kernel = gVisor +
+
+
+
+ +
+
+
+
Measured, not hand-waved
+

Fast enough to stay in the agent loop.

+

+ Apple Silicon reference profile, dedicated Lima VM, five runs and + one hundred measured iterations. No warm runtime pool. +

+ Read the benchmark method → +
+
+
+
gVisor Runtime cold startnew Pod · schedule to ready
+
+ 2.497 s p50 +
+
+
Warm executionexisting Runtime
+
+ 35.72 ms p50 +
+
+
Workspace createdurable PVC-backed state
+
+ 29.21 ms p50 +
+

Transparent scope: local, single-node, warm images. Not presented as a cloud or node-cold result.

+
+
+
+ +
+
+
+
From clone to proof
+

One command. A real kubeadm cluster.

+

+ The quickstart creates an isolated Lima VM, installs Kubernetes and gVisor, + deploys the platform, and exercises the security and persistence contract. +

+
+
+ $ make quickstart + +
+ +
+
+ +
+
+ +

Give agents a place to run.
Not a reason to trust them.

+ Explore Sandbox on GitHub +
+
+
+ + + + diff --git a/pyproject.toml b/pyproject.toml index 47be1fe..cdf60f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/hullwork/sandbox" +Homepage = "https://hullwork.github.io/sandbox/" Repository = "https://github.com/hullwork/sandbox" Issues = "https://github.com/hullwork/sandbox/issues" diff --git a/scripts/dev-doctor.sh b/scripts/dev-doctor.sh index 018e43a..db8f273 100755 --- a/scripts/dev-doctor.sh +++ b/scripts/dev-doctor.sh @@ -57,7 +57,9 @@ if [ "$(uname -s)" = Linux ]; then fi # A new default VM reserves 6 GiB of memory and a 60 GiB disk (sparse) -# (scripts/local-cluster.sh), so a clean install needs substantial headroom. +# (scripts/local-cluster.sh). The reference clean install consumes under 15 GiB +# of physical host storage; 6.5/25 keeps a real safety margin without rejecting a +# host that can run the profile merely because the sparse virtual size is 60 GiB. # Reusing an existing VM only needs enough headroom to build images and roll # Pods; applying the new-VM threshold after a successful install made retries # fail precisely because the VM was already consuming those resources. @@ -67,8 +69,8 @@ if [ "${SANDBOX_DOCTOR_SKIP_RESOURCES:-0}" = 1 ]; then else LIMA_DISK_DIR="${LIMA_HOME:-$HOME/.lima}" [ -d "$LIMA_DISK_DIR" ] || LIMA_DISK_DIR="$HOME" - DEFAULT_MIN_MEMORY_GIB=8 - DEFAULT_MIN_DISK_GIB=35 + DEFAULT_MIN_MEMORY_GIB=6.5 + DEFAULT_MIN_DISK_GIB=25 RESOURCE_MODE=new-profile if limactl list --format '{{.Name}}' 2>/dev/null \ | grep -Fx "${SANDBOX_LOCAL_VM:-sandbox-local}" >/dev/null; then diff --git a/tests/test_documentation.py b/tests/test_documentation.py index 803ad47..7782205 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -127,6 +127,10 @@ def test_documented_vm_disk_matches_installer_default(self) -> None: self.assertIn(f"{size} GiB disk by default", readme) self.assertIn(f"its {size} GiB disk", readme) self.assertIn(f"a {size} GiB disk", doctor) + self.assertIn("DEFAULT_MIN_MEMORY_GIB=6.5", doctor) + self.assertIn("DEFAULT_MIN_DISK_GIB=25", doctor) + self.assertIn("**6.5 GiB free**", readme) + self.assertIn("**25 GiB free**", readme) def test_mysql_driver_documentation_matches_the_image(self) -> None: dockerfile = (ROOT / "control_plane/Dockerfile").read_text(encoding="utf-8") diff --git a/tests/test_homepage.py b/tests/test_homepage.py new file mode 100644 index 0000000..1a5812e --- /dev/null +++ b/tests/test_homepage.py @@ -0,0 +1,94 @@ +"""Contract checks for the zero-build GitHub Pages homepage.""" + +from __future__ import annotations + +from html.parser import HTMLParser +import pathlib +import unittest +from urllib.parse import urlparse + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SITE = ROOT / "docs" + + +class _PageParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.ids: set[str] = set() + self.links: list[str] = [] + self.assets: list[str] = [] + self.landmarks: set[str] = set() + self.images_without_alt: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + values = dict(attrs) + if values.get("id"): + self.ids.add(values["id"] or "") + if tag in {"header", "nav", "main", "footer"}: + self.landmarks.add(tag) + if tag == "a" and values.get("href"): + self.links.append(values["href"] or "") + if tag in {"link", "script", "img"}: + target = values.get("href") or values.get("src") + if target: + self.assets.append(target) + if tag == "img" and "alt" not in values: + self.images_without_alt.append(values.get("src") or "") + + +def parse(name: str = "index.html") -> tuple[str, _PageParser]: + text = (SITE / name).read_text(encoding="utf-8") + parser = _PageParser() + parser.feed(text) + return text, parser + + +class HomepageTests(unittest.TestCase): + def test_homepage_has_navigation_landmarks_and_skip_link(self) -> None: + _, page = parse() + self.assertEqual({"header", "nav", "main", "footer"}, page.landmarks) + self.assertIn("#main", page.links) + self.assertEqual([], page.images_without_alt) + + def test_local_assets_exist_and_are_project_path_safe(self) -> None: + for document in ("index.html", "404.html"): + _, page = parse(document) + for target in page.assets: + parsed = urlparse(target) + self.assertFalse(parsed.scheme, target) + self.assertFalse(target.startswith("/"), target) + self.assertTrue((SITE / target).resolve().is_file(), target) + + def test_homepage_has_no_third_party_runtime_dependency(self) -> None: + text, page = parse() + self.assertNotIn("http://", text) + for target in page.assets: + self.assertFalse(target.startswith("https://"), target) + + def test_project_claims_match_the_documented_benchmark(self) -> None: + homepage, _ = parse() + report = (SITE / "BENCHMARK_REPORT_2026-09-01.md").read_text(encoding="utf-8") + for value in ("2.497", "35.72", "29.21"): + self.assertIn(value, homepage) + self.assertIn(value, report) + + def test_homepage_is_sandbox_only(self) -> None: + homepage, _ = parse() + metadata = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + # Keep retired product names out of tracked text while still making the + # homepage test fail if one is assembled into the generated page. + forbidden = ( + "platform-" + "composition", + "mi" + "ni-" + "agent", + "mi" + "ni-" + "sites", + "hullwork/" + "agent", + ) + for name in forbidden: + self.assertNotIn(name, homepage) + self.assertIn("https://github.com/hullwork/sandbox", homepage) + self.assertIn('Homepage = "https://hullwork.github.io/sandbox/"', metadata) + + +if __name__ == "__main__": + unittest.main()