Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,66 @@ export KUBECONFIG := $(SANDBOX_KUBECONFIG)
# ten modules that need PyYAML fail on an import rather than on anything about
# this repository.
SANDBOX_PYTHON ?= $(shell test -x .venv/bin/python && echo .venv/bin/python || echo python3)
SANDBOX_IMAGE_BUILD_JOBS ?= 4
SANDBOX_BUILD_LOG_DIR ?= $(SANDBOX_STATE_DIR)/logs/images
DOCKER_BUILD_PROGRESS ?= plain
SANDBOX_BOOTSTRAP_STAMP ?= .venv/.sandbox-bootstrap

.PHONY: help doctor images test chart-lint chart-render dev-token control-plane-forward console-forward up-local status-local down-local destroy-local
.PHONY: help bootstrap quickstart acceptance smoke-local e2e-local verify verify-manifests doctor images image-runtime image-file-service image-control-plane image-console test chart-lint chart-render dev-token control-plane-forward console-forward up-local status-local down-local destroy-local

help:
@grep -E '^[a-z-]+:.* — ' Makefile | sed 's/:.* — / — /'

doctor: ## — verify local development prerequisites
bash scripts/dev-doctor.sh

images: ## — build all local component images
docker build -t sandbox-runtime:0.5.0 -f runtime/Dockerfile .
docker build -t sandbox-file-service:0.3.0 -f file-service/Dockerfile .
docker build -t sandbox-control-plane:0.7.0 -f control_plane/Dockerfile .
docker build -t sandbox-console:0.1.0 -f console/Dockerfile console
bootstrap: $(SANDBOX_BOOTSTRAP_STAMP) ## — create .venv and install the SDK plus test dependencies

$(SANDBOX_BOOTSTRAP_STAMP): pyproject.toml
python3 -m venv .venv
PIP_DISABLE_PIP_VERSION_CHECK=1 .venv/bin/python -m pip install -e '.[test]'
@touch $(SANDBOX_BOOTSTRAP_STAMP)

quickstart: ## — go from prerequisites to a live gVisor proof and KPI summary
bash scripts/quickstart.sh

acceptance: ## — install, prove runtime value, and run every source and live E2E gate
$(MAKE) --no-print-directory quickstart
$(MAKE) --no-print-directory verify
$(MAKE) --no-print-directory e2e-local

smoke-local: ## — prove gVisor isolation, persistence, fail-closed behavior, and metrics
$(SANDBOX_PYTHON) scripts/smoke-local.py

e2e-local: ## — run all network, Runtime, storage, restart, and adversarial E2E checks
SANDBOX_KUBECONFIG=$(KUBECONFIG) \
SANDBOX_KUBE_CONTEXT=$(SANDBOX_KUBE_CONTEXT) \
PYTHON=$(SANDBOX_PYTHON) bash scripts/run-all-e2e.sh

verify: ## — run the complete source, package, Console, and manifest gate with logs
bash scripts/verify.sh

verify-manifests:
bash -n scripts/*.sh
@for overlay in k8s overlays/rwo-single-node overlays/local overlays/eks overlays/external-deps; do \
kubectl kustomize "$$overlay" >/dev/null || exit; \
done
$(MAKE) --no-print-directory chart-lint
$(MAKE) --no-print-directory chart-render

images: image-runtime image-file-service image-control-plane image-console ## — build all local component images

image-runtime:
@bash scripts/build-image.sh sandbox-runtime:0.5.0 -f runtime/Dockerfile .

image-file-service:
@bash scripts/build-image.sh sandbox-file-service:0.3.0 -f file-service/Dockerfile .

image-control-plane:
@bash scripts/build-image.sh sandbox-control-plane:0.7.0 -f control_plane/Dockerfile .

image-console:
@bash scripts/build-image.sh sandbox-console:0.1.0 -f console/Dockerfile console

test: ## — run standalone unit tests
$(SANDBOX_PYTHON) -m unittest discover -s tests -p 'test_*.py'
Expand Down
67 changes: 53 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,18 @@ operation fails; it never falls back to running on the host.
## Try it

```bash
python3 -m venv .venv # a system-wide install is an error on Debian,
.venv/bin/pip install -e '.[test]' # Ubuntu and Fedora (PEP 668)
make test # 831 unit and contract tests, no network, no cluster
make bootstrap # create .venv and install SDK + test dependencies
make test # 833 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
```

That is the contract suite: no cluster, no credentials, nothing to clean up.
The virtual environment is not optional on Debian, Ubuntu or Fedora — a
system-wide `pip install` is refused there (PEP 668). `make test` uses
`.venv/bin/python` when it exists and `python3` otherwise, so there is no
activation step.
`.venv/bin/python` when it exists and `python3` otherwise, so `make bootstrap`
and `make test` need no activation step. CLI examples below use the explicit
`.venv/bin/` path for the same reason.
[Run the full local cluster](#run-the-full-local-cluster) when you want a real
gVisor Runtime.

Expand Down Expand Up @@ -122,12 +123,47 @@ Full method, raw evidence layout, and the explicit statement of what this number
No Control Plane needed — each of these prints what it accepts:

```bash
sandbox --help # create / run / exec / stop / list
sandboxctl --help # operator surface: workspaces, templates, admin keys, audit
sandbox-mcp --help # the nine agent-scoped MCP tools and their required env vars
.venv/bin/sandbox --help # create / run / exec / stop / list
.venv/bin/sandboxctl --help # workspaces, templates, admin keys, audit
.venv/bin/sandbox-mcp --help # nine agent-scoped MCP tools and required env vars
make help # every Make target with its one-line description
```

## One command to see the point

From a fresh clone, this is the shortest path from prerequisites to a real
gVisor command and durable Workspace proof:

```bash
make quickstart
```

It checks the host, creates `.venv`, creates or reuses the repository's isolated
Lima cluster, and proves all of the following against the live deployment:

- the command reports a gVisor kernel and runs non-root on a read-only root;
- the Runtime has no Kubernetes service-account token;
- a Workspace file survives stopping and replacing its Runtime;
- an unavailable Control Plane fails closed without executing on the host;
- `/healthz`, `X-Request-Id`, and Prometheus metrics are visible.

The command ends with the first Runtime-call latency and total elapsed time. It
also writes `.sandbox/quickstart-summary.json` and
`.sandbox/showcase-result.json`, so install success, phase duration, manual
interventions, and value-proof results can be compared between machines or CI
runs. `make smoke-local` reruns only the live proof against an existing cluster.
The local profile exposes raw Prometheus metrics but intentionally does not install
Prometheus or Grafana; the Console's Grafana-backed Observability tab appears only
when an operator configures that external dependency.

For contributors, `make verify` is the one-command pre-PR gate corresponding to
the executable parts of CI. It keeps successful output compact, stores one log per
phase under `.sandbox/logs/verify/`, and writes durations and outcome to
`.sandbox/verify-summary.json`; a failing phase prints its last 100 log lines.
`make e2e-local` runs all five live cluster scenarios. Release candidates can run
`make acceptance` to execute quickstart, the source gate, and the full live E2E in
that order with one command.

## Run the full local cluster

`make up-local` builds a single-node kubeadm Kubernetes cluster inside a Lima VM with
Expand All @@ -145,20 +181,23 @@ 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** — a hard check, not a warning |
| **Free disk** | **40 GiB free** under `$LIMA_HOME` (default `~/.lima`) — also a hard check |
| **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 |
| 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 |

Set `SANDBOX_DOCTOR_SKIP_RESOURCES=1` to bypass only the memory and disk checks. The
VM itself uses 4 CPUs, 6 GiB memory, and a 60 GiB disk by default, adjustable through
`SANDBOX_LOCAL_CPUS`, `SANDBOX_LOCAL_MEMORY_GIB`, and `SANDBOX_LOCAL_DISK_GIB`.
When `sandbox-local` already exists, doctor switches to a 2 GiB memory / 5 GiB disk
reuse gate because it is not allocating a second VM; explicit
`SANDBOX_DOCTOR_MIN_*` overrides still win.

### Bring it up

```bash
make doctor
python3 -m venv .venv && .venv/bin/pip install -e '.[test]'
make bootstrap
make up-local
```

Expand All @@ -181,7 +220,7 @@ The Makefile exports `KUBECONFIG` for its own targets, so `make dev-token`,
```bash
export SANDBOX_CONTROL_PLANE_URL=http://127.0.0.1:18080
export SANDBOX_TOKEN="$(make --no-print-directory dev-token)"
sandbox run --name demo --stop -- python -c 'print("sandbox-ready")'
.venv/bin/sandbox run --name demo --stop -- python -c 'print("sandbox-ready")'
```

```text
Expand All @@ -202,8 +241,8 @@ make destroy-local # delete the VM with its disk and the generated .sandbox/ st

`down-local` is the right choice between sessions — `up-local` reuses the stopped VM.
`destroy-local` is irreversible: Workspace files, checkpoints, and the SQLite state
inside the VM are gone. Images that `make images` built on the host stay in the local
Docker daemon until removed with `docker rmi`.
inside the VM are gone. It also removes the four fixed-tag project images from the
local Docker daemon; shared base layers remain available to Docker's cache.

---

Expand Down
7 changes: 5 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Security fixes target the latest published release and current `main`. Historica

Use the repository's GitHub private vulnerability reporting form. Do not open a public issue for vulnerabilities.

If the form is unavailable, email the maintainers at `<MAINTAINER_SECURITY_EMAIL>` with the same information; encrypt anything sensitive and expect a reply from the same address.
If the form is unavailable, open a public issue containing no vulnerability
details and ask the maintainers to establish a private reporting channel.

Include the commit, affected component, impact on workspace isolation or credential scope, reproduction steps, and logs with secrets removed. Maintainers aim to acknowledge reports within five business days.

Expand All @@ -19,7 +20,9 @@ Include the commit, affected component, impact on workspace isolation or credent
- Runtime Pods must remain non-root, read-only at the root, without Kubernetes service-account credentials, and subject to the documented egress policy.
- Workspace ownership and scoped tokens prevent one workspace from reading or mutating another.
- Control Plane admin credentials are separate from tenant and runtime credentials and must never be exposed as agent identity.
- Object access uses separately scoped credentials and owner-partitioned keys; Control Plane service accounts must not hold bucket administration privileges.
- Object access uses separately scoped credentials and owner-partitioned keys;
Control Plane service accounts must not hold Ceph administrative API
capabilities or create buckets beyond the configured quota.
- Checkpoint restore rejects path traversal, links, devices, oversized archives, and unexpected archive structure.
- The local topology exercises gVisor but does not provide durable PostgreSQL,
multi-node storage, or production high availability.
Expand Down
15 changes: 11 additions & 4 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ images plus the pinned Metrics Server image into containerd, and applies a
self-contained development profile:

```bash
make doctor
make up-local
make quickstart
make status-local
make dev-token
make down-local
```

`make doctor` fails when less than 8 GiB of memory or 40 GiB of disk is free and
`make quickstart` runs `make doctor`, creates the repository-local `.venv`,
deploys this kubeadm profile, and executes a live gVisor/persistence/fail-closed
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
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
emulation, which is far slower). Set `SANDBOX_DOCTOR_SKIP_RESOURCES=1` to skip the
resource gate.
Expand Down Expand Up @@ -216,7 +223,7 @@ current context by accident:
KUBECONFIG=/path/to/sandbox.kubeconfig \
SANDBOX_KUBE_CONTEXT=sandbox-local \
SANDBOX_CONTROL_PLANE_URL=http://127.0.0.1:18080 \
bash scripts/run-all-e2e.sh
make e2e-local
```

The runner verifies network policy, core behavior, object storage, restart recovery, and adversarial paths. It requires both documented namespaces and a reachable Control Plane health endpoint.
Expand Down
15 changes: 15 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ other clusters. The local kubeconfig is `.sandbox/kubeconfig` with context
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`
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
ready. On Linux, start the Docker service and verify `docker context show`. Free
unused Docker build cache or move `LIMA_HOME` to a larger volume before retrying.
`SANDBOX_DOCTOR_SKIP_RESOURCES=1` bypasses only the capacity gate and should be used
only after checking the host yourself. `make quickstart` is resumable: it reuses the
partially created Lima VM and writes the failed phase to
`.sandbox/quickstart-summary.json`.

## `context "sandbox-local" does not exist`

**Symptom.** `make dev-token`, `make control-plane-forward`, or a manual `kubectl --context sandbox-local` fails with this message.
Expand Down
15 changes: 9 additions & 6 deletions overlays/local-dev/object-store.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ spec:
deadline = time.monotonic() + 120
while True:
try:
store.list_buckets()
existing_buckets = {
item["Name"] for item in store.list_buckets().get("Buckets", [])
}
break
except (ClientError, BotoCoreError) as error:
if time.monotonic() >= deadline:
Expand All @@ -70,12 +72,13 @@ spec:
"OBJECT_STORE_WORKSPACE_BUCKET",
):
bucket = os.environ[variable]
try:
# RGW checks the user's max-bucket quota before checking
# whether CreateBucket names an existing bucket. Once all
# three local buckets exist, blindly creating them again can
# return TooManyBuckets instead of AlreadyOwnedByYou.
if bucket not in existing_buckets:
store.create_bucket(Bucket=bucket)
except ClientError as error:
code = error.response.get("Error", {}).get("Code", "")
if code not in ("BucketAlreadyExists", "BucketAlreadyOwnedByYou"):
raise
existing_buckets.add(bucket)
store.put_bucket_versioning(
Bucket=bucket,
VersioningConfiguration={"Status": "Enabled"},
Expand Down
29 changes: 29 additions & 0 deletions scripts/build-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -ne 4 ] || [ "$2" != -f ]; then
echo "usage: scripts/build-image.sh <tag> -f <dockerfile> <context>" >&2
exit 2
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE="$1"
DOCKERFILE="$3"
CONTEXT="$4"
LOG_DIR="${SANDBOX_BUILD_LOG_DIR:-$REPO_ROOT/.sandbox/logs/images}"
PROGRESS="${DOCKER_BUILD_PROGRESS:-plain}"
LOG_FILE="$LOG_DIR/${IMAGE//[:\/]/-}.log"
STARTED_AT="$SECONDS"

mkdir -p "$LOG_DIR"
printf ' building %-32s (log: %s)\n' "$IMAGE" "$LOG_FILE"
if docker build --progress="$PROGRESS" -t "$IMAGE" -f "$DOCKERFILE" "$CONTEXT" >"$LOG_FILE" 2>&1; then
printf ' built %-32s %ss\n' "$IMAGE" "$((SECONDS - STARTED_AT))"
else
status=$?
printf ' failed %-32s %ss; last 80 log lines follow\n' \
"$IMAGE" "$((SECONDS - STARTED_AT))" >&2
tail -n 80 "$LOG_FILE" >&2
exit "$status"
fi
Loading
Loading