|
| 1 | +# kedge-sandbox-runner |
| 2 | + |
| 3 | +The container image that runs inside every **SandboxRunner** pod — an App Studio |
| 4 | +*live development environment*. It is owned by the **infrastructure provider**, |
| 5 | +alongside the [`sandbox-runner` template](../install/templates/sandbox-runner.yaml) |
| 6 | +whose `spec.runnerImage` defaults to it (`ghcr.io/faroshq/kedge-sandbox-runner:latest`). |
| 7 | + |
| 8 | +## What it is |
| 9 | + |
| 10 | +A single, dependency-free Go program (`main.go`, standard library only) that acts |
| 11 | +as the pod's entrypoint and **control plane for one workspace**. It lets the |
| 12 | +platform sync code, restart the app, and read logs **without** shelling into the |
| 13 | +pod or handing anyone a runtime-cluster credential. |
| 14 | + |
| 15 | +The image is built `FROM node:22-bookworm` (plus `git` + `ca-certificates`) |
| 16 | +because the supervised dev process typically runs `npm` / `vite`. The Go binary |
| 17 | +is the `ENTRYPOINT`; the user's app is a child process it manages. |
| 18 | + |
| 19 | +``` |
| 20 | +┌─ SandboxRunner pod ────────────────────────────────────────┐ |
| 21 | +│ sandbox-runner (Go) user dev process │ |
| 22 | +│ ── control API :7070 ──┐ ── npm run dev :3000 ──┐ │ |
| 23 | +│ /healthz /sync │ start/stop/restart │ │ |
| 24 | +│ /restart /logs └──────────► (child) ◄── logs ┘ │ |
| 25 | +└──────────▲─────────────────────────────────────────────────┘ |
| 26 | + │ reverse-proxied by the infra provider data-plane |
| 27 | + │ (caller's logs/sync/restart, control token injected) |
| 28 | +``` |
| 29 | + |
| 30 | +## What the Go app does |
| 31 | + |
| 32 | +1. **Supervises the dev process.** Runs `SANDBOX_START_COMMAND` (e.g. |
| 33 | + `npm install && npm run dev`) in the workspace, streams its stdout/stderr into |
| 34 | + a 500-line in-memory ring buffer, and can stop/start it on demand. |
| 35 | + |
| 36 | +2. **Serves an HTTP control API on `:7070`:** |
| 37 | + |
| 38 | + | Method & path | Auth | Purpose | |
| 39 | + |---|---|---| |
| 40 | + | `GET /healthz` | none | Liveness probe. | |
| 41 | + | `POST /sync` | token | Write/delete workspace files, optionally restart. Body: `{files:[{path,content}], deletePaths:[], restart:"auto"\|"always"\|""}`. | |
| 42 | + | `POST /restart` | token | Stop + start the dev process. | |
| 43 | + | `GET /logs` | token | Buffered dev-process output (`text/plain`). | |
| 44 | + |
| 45 | + `restart:"auto"` restarts **only** when a startup-affecting file actually |
| 46 | + changed (`package.json`, a lockfile, `vite.config.*`, `server.js`) or the |
| 47 | + process isn't currently running — so editing a component doesn't bounce the |
| 48 | + server, but changing dependencies does. |
| 49 | + |
| 50 | +## How it's driven |
| 51 | + |
| 52 | +App Studio never talks to this pod directly. The infrastructure provider's |
| 53 | +data-plane handler authorizes the caller against their workload instance, then |
| 54 | +**reverse-proxies** the `logs` / `sync` / `restart` verb to this runner's `:7070` |
| 55 | +through the runtime cluster's Service, injecting the per-instance control token. |
| 56 | +Consumers therefore hold no runtime credential. |
| 57 | + |
| 58 | +## Configuration (environment) |
| 59 | + |
| 60 | +Set by the template's ResourceGraphDefinition on the pod: |
| 61 | + |
| 62 | +| Variable | Default | Meaning | |
| 63 | +|---|---|---| |
| 64 | +| `SANDBOX_WORKDIR` | `/workspace` | Workspace root; all file ops are confined here. | |
| 65 | +| `SANDBOX_START_COMMAND` | — | Command to launch the dev process (run via `sh -c`). | |
| 66 | +| `SANDBOX_PORT` | — | Port the dev app binds (surfaced for the start command). | |
| 67 | +| `SANDBOX_CONTROL_TOKEN` | — | Bearer for the control API. Read once, then **unset** from the environment. | |
| 68 | +| `SANDBOX_ALLOW_INSECURE_CONTROL` | `false` | Dev-only escape hatch: serve the control API with no token. | |
| 69 | + |
| 70 | +## Security posture |
| 71 | + |
| 72 | +- **Workspace confinement.** All `/sync` writes/deletes go through `os.Root` rooted |
| 73 | + at `SANDBOX_WORKDIR`, plus path cleaning that rejects absolute paths and `../` |
| 74 | + escapes — a forged path can't write outside the workspace. |
| 75 | +- **Authenticated control.** Every endpoint except `/healthz` requires |
| 76 | + `X-Sandbox-Control-Token`, constant-time compared against `SANDBOX_CONTROL_TOKEN`. |
| 77 | +- **Non-root.** The template runs the pod as `runAsNonRoot` (uid 1000, seccomp |
| 78 | + `RuntimeDefault`). |
| 79 | + |
| 80 | +This runs user-provided code; it is a **development** runtime, not a hardened |
| 81 | +multi-tenant sandbox (no per-pod network policy or CPU/mem quotas are enforced |
| 82 | +by the runner itself). |
| 83 | + |
| 84 | +## Build & publish |
| 85 | + |
| 86 | +Self-contained module (its own `go.mod`, no external requires), so the build |
| 87 | +context is just this directory. |
| 88 | + |
| 89 | +```bash |
| 90 | +# build the image locally and load it into the local kind runtime cluster |
| 91 | +make load-sandbox-runner-image # → docker-build-sandbox-runner + kind load |
| 92 | + |
| 93 | +# run the tests |
| 94 | +go test ./... |
| 95 | +``` |
| 96 | + |
| 97 | +CI: [`.github/workflows/infrastructure-sandbox-runner.yaml`](../../../.github/workflows/infrastructure-sandbox-runner.yaml) |
| 98 | +tests + builds on every change under this directory, and on push to `main` |
| 99 | +publishes `ghcr.io/<owner>/kedge-sandbox-runner` as `:latest` plus an immutable |
| 100 | +`:sha-<short>` tag (PRs build single-arch without pushing). This mirrors how the |
| 101 | +`application` template's example app images are published. |
0 commit comments