Quick start · API · Deployment · Security · Comparison · SDKs · Website · 简体中文
1 server · 1 binary · 1 container · 1 volume · SQLite state · Go / Python / Node SDKs
Runbox is a lightweight agent sandbox built for a single server. It gives each caller a persistent Linux workspace: upload inputs, run Python, Node or shell commands, and collect the results. Identities share one long-running container, but keep separate UIDs, private workspaces, quotas and path-safe file access.
It is small on purpose: one Go binary, one container, one volume, and a single HTTP API. No scheduler, no control plane, no external database. The name is literal: run in a box.
| Path | Contents |
|---|---|
cmd/runboxd/ |
Runner entrypoint: configuration, signals, graceful shutdown. |
internal/server/ |
Gin HTTP layer: auth, identity, streaming uploads, Range/HEAD downloads. |
internal/runner/ |
Workspace isolation, UID credentials, command execution, quotas, cleanup. |
internal/store/ |
SQLite (GORM) identities, UID assignments and metadata. |
internal/config/ |
RUNBOX_* configuration and advertised limits. |
sdk/go/ |
Go SDK (package sandbox), stdlib-only. |
sdk/python/ |
Python SDK, stdlib-only, pip install runbox-sdk. |
sdk/node/ |
TypeScript/Node SDK, npm install runbox-sdk. |
website/ |
Product site for runbox.xyun.dev. |
docs/ |
API, architecture, security, deployment, production and migration notes. |
examples/ |
Runnable Go, Python and curl walkthroughs. |
validation/ |
Adversarial and performance harnesses behind the validation report. |
.github/workflows/ |
CI, container publishing, SDK releases and GitHub Pages. |
Container-per-sandbox platforms isolate every sandbox in its own container. MicroVM platforms go further with a VM per sandbox. Both are the right trade-off for mutually hostile tenants — and both are heavy on a single machine: one more container or VM per user, often another copy of Python/LibreOffice, and another thing to schedule.
Runbox takes the dify-sandbox shape: one long-lived container, many identities. Isolation comes from Linux credentials, file modes, path resolution and quotas — not from a new container per user. On top of that shape, Runbox adds a file isolation layer that a plain per-task UID sandbox does not have.
| Container per sandbox | MicroVM per sandbox | Runbox | |
|---|---|---|---|
| Isolation unit | one container per sandbox | one VM per sandbox | UID + private workspace in one container |
| New runtime per identity | yes | yes | no |
| Single-machine footprint | control plane + many containers | KVM + runtime + VM images | one binary, one container, one volume |
| Cold start per identity | container start or pool checkout | VM boot or snapshot restore | none |
| File isolation | volume or API per sandbox | strong | built-in per identity |
| Best fit | multi-node, stronger per-sandbox isolation | hostile tenants | internal or semi-trusted, single machine |
Choose Runbox when you want one container next to your application, semi-trusted commands, Python/Node/shell workloads, and per-identity file isolation without operating a sandbox platform. If tenants are mutually hostile, run Runbox under gVisor or a microVM instead of treating the shared kernel as a VM boundary.
| What you get | |
|---|---|
| Single-machine first | Designed for one server: no scheduler, no control plane, no external database. SQLite and the workspaces live on the same volume. |
| One container, many identities | The HTTP runner and the execution environment share one long-lived container. Each identity runs under its own UID; calls for one identity are serialized, different identities run concurrently. |
| Per-identity file isolation | A private workspace, a workspace quota and symlink-safe path resolution. Files are downloaded by workspace-relative path; persistence is the caller's job. |
| Persistent workspaces | Installed packages and intermediate files survive across calls. HOME, XDG directories and pip/npm prefixes all point inside the workspace and count toward the quota. |
| Caller-managed persistence | Files produced under out/ are returned with their paths and downloadable directly. Runbox does not keep a second long-term copy; download what you need and store it yourself. |
| Bounded everything | Upload size, command length, input count, captured stdout (head + tail), output count and size, workspace quota, timeout, concurrency and retention. |
| Files are first-class | Upload, execute, list, download by workspace path, delete. Attachments stream as multipart; commands reference files by relative path. |
| Large files | Uploads stream to disk without buffering the whole body in memory; downloads support Range and HEAD. |
| Batch and paths | Upload several files at once; download any workspace file by relative path, or stream a directory as a tar archive. Intermediate files need no extra identifier. |
| Fail-closed hardening | Read-only root filesystem, dropped capabilities, no-new-privileges, tini as PID 1. Startup refuses to run as root without the capabilities it needs. |
| Observable by default | /ready readiness, Prometheus metrics at /metrics, structured text or JSON request logs. |
| Agent-friendly toolchain | git, curl/wget, jq, ripgrep, fd, bat, tree, file, sqlite3, less, nano, lsof and standard GNU tools are preinstalled. |
| Small enough to audit | Gin on the HTTP edge, a stdlib-only SDK, and a small operational footprint. |
Three variants share the same API and run as root inside the container so they can assign per-identity UIDs:
| Variant | Pull | Download | Use |
|---|---|---|---|
full |
docker pull ghcr.io/xy200303/runbox:latest |
~600 MB | Office documents, spreadsheets, pandas/numpy, native builds |
minimal |
docker pull ghcr.io/xy200303/runbox:latest-minimal |
~170 MB | Code, text, PDF extraction |
toolbox |
docker pull ghcr.io/xy200303/runbox:latest-toolbox |
~285 MB | Compiling without the Office layer |
Versioned tags follow the release, for example v0.2.3, v0.2.3-minimal and
v0.2.3-toolbox.
Run any variant with the same command; only the image tag changes:
export RUNBOX_API_KEY="$(openssl rand -hex 32)"
docker run -d --name runbox \
--read-only \
--tmpfs /tmp:rw,exec,nosuid,size=512m \
--cap-drop ALL \
--cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID \
--cap-add DAC_OVERRIDE --cap-add KILL \
--security-opt no-new-privileges:true \
-p 8099:8099 \
-e RUNBOX_API_KEY="$RUNBOX_API_KEY" \
-v runbox-data:/workspace \
ghcr.io/xy200303/runbox:latestUse :latest-minimal for code/text workloads and :latest-toolbox when pip or
npm needs to build native extensions.
git clone https://github.com/xy200303/runbox.git
cd runbox
cp .env.example .env
# set RUNBOX_API_KEY to a long random value: openssl rand -hex 32
docker compose up -d --build
curl -fsS http://localhost:8099/health{"status":"healthy","version":"dev","uptime_seconds":2}The default image includes LibreOffice, Python and Node. For text/code workloads that do not need Office conversion, build the smaller image:
docker build --target minimal -t runbox:minimal .export RUNBOX_API_KEY="$(grep '^RUNBOX_API_KEY=' .env | cut -d= -f2)"
curl -fsS http://localhost:8099/v1/exec \
-H "Authorization: Bearer $RUNBOX_API_KEY" \
-H "X-Runbox-Namespace: team-a" \
-H "X-Runbox-Principal: user-42" \
-H "Content-Type: application/json" \
-d '{"command":"echo hello from runbox"}'{"exit_code":0,"stdout":"hello from runbox\n","files":[],"duration_ms":3}curl -fsS http://localhost:8099/v1/exec \
-H "Authorization: Bearer $RUNBOX_API_KEY" \
-H "X-Runbox-Namespace: team-a" \
-H "X-Runbox-Principal: user-42" \
-H "Content-Type: application/json" \
-d '{"command":"mkdir -p out && echo hello > out/report.txt"}'The response lists out/report.txt with its workspace-relative path. Download it with GET /v1/path?path=out/report.txt.
Windows, macOS and Linux are supported for development. UID isolation is Linux-only.
make run
# or
RUNBOX_ALLOW_ANONYMOUS=1 RUNBOX_ROOT=./.runbox go run ./cmd/runboxdimport (
"context"
"os"
"github.com/xy200303/runbox/sdk/go"
)
client, err := sandbox.NewClient(sandbox.ClientOptions{
BaseURL: "http://localhost:8099",
APIKey: os.Getenv("RUNBOX_API_KEY"),
Identity: sandbox.ScopedIdentity("team-a", "user-42"),
})
if err != nil {
return err
}
result, err := client.Run(context.Background(), sandbox.ExecRequest{
Command: `python3 -c "print(6 * 7)"`,
Timeout: 30 * time.Second,
})
if err != nil {
return err
}
fmt.Println(result.Stdout) // a non-zero ExitCode is a result, not an errorAttachments stream straight from an io.Reader; nothing is buffered first:
result, err := client.Run(ctx, sandbox.ExecRequest{
Command: "python3 analyze.py files/input.csv",
Files: []sandbox.FileUpload{{Name: "input.csv", Reader: file}},
})Downloads have two shapes. OpenPath hands you the body so an HTTP handler can
forward it, and DownloadPathTo writes into any io.Writer:
reader, name, err := client.OpenPath(ctx, sandbox.Identity{}, "out/report.csv")
if err != nil {
return err
}
defer reader.Close()
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, name))
c.DataFromReader(http.StatusOK, -1, "text/csv", reader, nil)
archive, err := client.OpenArchive(ctx, sandbox.Identity{}, []string{"out", "tmp"})Client also implements sandbox.Backend: use Exec(ctx, identity, …) when the
identity is passed per call, or Run with a client bound to one identity.
Python is stdlib-only:
pip install runbox-sdkfrom runbox import Client, FileUpload, Identity
client = Client(
"http://localhost:8099",
api_key=os.environ["RUNBOX_API_KEY"],
identity=Identity(namespace="team-a", principal="user-42"),
)
result = client.exec('python3 -c "print(6 * 7)"')
print(result["stdout"])
client.download_path("out/report.csv", "report.csv")
client.download_archive(["out", "tmp"], "artifacts.tar")
with client.open_path("out/report.csv") as body:
for chunk in body:
handle.write(chunk)Node ships typed ESM:
npm install runbox-sdkimport { Client } from 'runbox-sdk'
const client = new Client({
baseUrl: 'http://localhost:8099',
apiKey: process.env.RUNBOX_API_KEY,
identity: { namespace: 'team-a', principal: 'user-42' },
})
const result = await client.exec('echo hello')
await client.downloadPath('out/report.csv', 'report.csv')
await client.downloadArchive(['out', 'tmp'], 'artifacts.tar')
const body = await client.openPath('out/report.csv')
body.pipe(response)Source: sdk/go, sdk/python and sdk/node.
A full curl walkthrough — upload, exec, list, download — is in
examples/curl.sh.
An agent backend usually receives an upload and immediately hands it to the sandbox. All three SDKs accept a stream, so the file never has to be written to local disk and re-read:
| SDK | Upload from a stream | Download into a stream |
|---|---|---|
| Go | FileUpload{Reader: r} (also Upload(ctx, name, r, size)) |
OpenPath / OpenArchive / DownloadPathTo |
| Python | upload_stream(name, f), FileUpload(name, stream=f), exec(..., files=[...]) |
open_path / open_archive / download_path_to |
| Node | uploadStream(name, s), FileUpload{stream: s}, exec(..., {files: [...]}) |
openPath / openArchive / downloadPathTo |
When the length is unknown the request is framed with
Transfer-Encoding: chunked; when it is known (a seekable file, or size= given
explicitly) the SDK sends Content-Length instead. Streams are read once and
never closed by the SDK — the caller still owns them.
func analyze(c *gin.Context) {
header, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
upload, err := header.Open() // multipart.File: an io.Reader over the request body
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
defer upload.Close()
name := filepath.Base(header.Filename)
result, err := client.Run(c.Request.Context(), sandbox.ExecRequest{
Command: "python3 analyze.py files/" + name,
Files: []sandbox.FileUpload{{Name: name, Reader: upload}},
})
if err != nil {
c.String(http.StatusBadGateway, err.Error())
return
}
c.JSON(http.StatusOK, result)
}The upload is copied from the client to runboxd as it arrives: no temp file, no
second read, one round trip, and the command timeout is bounded by the request
context.
# FastAPI: UploadFile.file is a SpooledTemporaryFile
@app.post("/analyze")
async def analyze(file: UploadFile) -> dict:
result = await run_in_threadpool(
client.exec,
f"python3 analyze.py files/{file.filename}",
files=[FileUpload(file.filename, stream=file.file)],
)
return {"stdout": result["stdout"]}
# Flask: request.files[...].stream is the raw upload stream
@app.post("/analyze")
def analyze():
upload = request.files["file"]
client.upload_stream(upload.filename, upload.stream)
return {"path": f"files/{upload.filename}"}Returning a file without saving it:
from flask import Response
@app.get("/report")
def report():
body = client.open_path("out/report.csv")
return Response(
(chunk for chunk in iter(lambda: body.read(65536), b"")),
mimetype="text/csv",
)app.post('/analyze', async (req, res) => {
const uploaded = await client.uploadStream('input.csv', req, {
contentType: 'text/csv',
})
const result = await client.exec(`python3 analyze.py ${uploaded.path}`)
const file = await client.openPath('out/report.csv')
res.setHeader('Content-Type', 'text/csv')
file.pipe(res)
})req is a Node Readable, so it is forwarded as-is. If you parse multipart with
busboy, pass the per-file stream instead of req.
Every SDK follows the same contract:
- Base URL points at
runboxd, for examplehttp://localhost:8099. - Authentication is
Authorization: Bearer <RUNBOX_API_KEY>. - Identity is exactly one form:
X-Runbox-Identity, orX-Runbox-Namespace+X-Runbox-Principal. - Files are paths. Every path is relative to that identity's workspace; there are no file IDs to translate.
- Uploads return paths:
POST /v1/fileswith fieldfile, orPOST /v1/files/batchwith repeatedfiles. - Exec attachments use repeated
filesfields and land infiles/; the command refers to them asfiles/<name>. - Downloads use
GET /v1/path?path=...for one file andGET /v1/archive?path=...&path=...for a batch tar. - Streams in, streams out. Uploads accept any reader; downloads expose an
open body (
OpenPath,open_path,openPath,OpenArchive,open_archive,openArchive) and a writer sink (DownloadPathTo,download_path_to,downloadPathTo). - Operations are available in every SDK: health, readiness
(
WaitReady/wait_ready/waitReady), capabilities, metrics, list and workspace deletion.
Every request carries exactly one identity form:
| Mode | Headers | Use when |
|---|---|---|
| Single token | X-Runbox-Identity: <token> |
Your application already mints an opaque subject ID. |
| Scoped | X-Runbox-Namespace: <namespace>X-Runbox-Principal: <principal> |
You need tenant + user, and want metadata for tenant-level operations later. |
Rules:
- Supplying both forms, or only half of the scoped form, returns
400. - Tokens are 1–256 visible ASCII characters with no spaces.
- Namespace and principal are 1–128 characters from
[A-Za-z0-9._-]. - Both forms normalize to one canonical SHA-256 key; storage paths use that key.
- Raw tokens are never written to disk. Scoped namespace/principal and the UID assignment are stored in the
identitiestable ofstate/runbox.db. - Paths are resolved relative to the identity workspace. Another identity sees its own workspace, or
404.
Authentication is Authorization: Bearer <RUNBOX_API_KEY>. /health and /ready are unauthenticated; everything else requires the bearer token.
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Liveness, version, uptime (no auth) |
| GET | /ready |
Readiness: storage directories exist and are writable (no auth) |
| GET | /metrics |
Prometheus metrics (auth required) |
| GET | /v1/capabilities |
Limits and detected tooling |
| POST | /v1/exec |
Run a command (JSON or multipart) |
| POST | /v1/files |
Upload one file; returns its path |
| POST | /v1/files/batch |
Upload several files; returns their paths |
| GET | /v1/path?path=out/x |
Download by workspace path |
| GET | /v1/list?path=out |
List a workspace directory |
| GET | /v1/archive?path=out&path=tmp |
Download files or directories as a tar archive |
| DELETE | /v1/workspace |
Remove the workspace |
Full request and response reference: docs/api.md.
Download a directory or several paths as one tar stream:
curl -fsS \
-H "Authorization: Bearer $RUNBOX_API_KEY" \
-H "X-Runbox-Identity: alice" \
"http://localhost:8099/v1/archive?path=out&path=tmp/step.txt" \
-o artifacts.tarSDKs expose the same entry as DownloadArchive (Go), download_archive
(Python) and downloadArchive (Node). No file IDs are involved; paths are
relative to the caller's workspace.
flowchart LR
A["Agent or application"] -->|"HTTP · Bearer · X-Runbox-*"| B["runboxd"]
B -->|"fork/exec"| C["/bin/sh -c command · UID 20000+n"]
B --> D[("workspaces/identity-key · mutable")]
RUNBOX_ROOT/
├── workspaces/<identity-key>/ 0750, owned by the identity's UID
│ ├── files/ uploaded inputs
│ ├── out/ files returned from the last command
│ ├── .local/ .npm-global/ persistent pip / npm installs
│ ├── .cache/ .config/ .tmp/ persistent XDG state and temp files
│ └── ...
└── state/
├── runbox.db identities, UIDs and metadata (SQLite)
├── runbox.db-wal WAL journal while the runner is active
└── runbox.db-shm shared-memory index while the runner is active
- The command runs in its own process group as the identity's UID. A timeout kills the group, not the container.
- Output is captured with a bounded head + tail buffer, so a command printing gigabytes cannot exhaust RAM.
- Files created under
out/during the call are returned with their workspace-relative paths. Download them by path; Runbox keeps no second copy. - A janitor removes workspaces idle longer than the retention window.
- The API key is compared in constant time; identity values are validated, not sanitized — sanitizing would merge distinct identities onto one workspace.
More detail: docs/architecture.md.
- Path resolution rejects
.., absolute paths, Windows drive letters and symlink escapes. Serving a symlink planted in the workspace would otherwise read files as root. - Commands never inherit the runner's environment, including the API key.
- The compose file runs read-only with
cap_drop: ALL, adds onlyCHOWN,FOWNER,SETUID,SETGID,DAC_OVERRIDEandKILL, setsno-new-privileges, and caps memory, CPU and pids. - Images run tini as PID 1 so killed command trees are reaped.
- Startup fails closed when running as root without the required capabilities.
RUNBOX_ALLOW_MISSING_CAPS=1is a development-only override.
Runbox is not a strong multi-tenant boundary on its own. It relies on the container's namespaces/cgroups plus per-UID file permissions. For hostile tenants, run it under gVisor or a microVM. See docs/security.md.
140 adversarial checks, no bypasses. The harnesses ship in
validation/ and can be re-run against any instance.
| Suite | Checks | Covers |
|---|---|---|
| Security | 78 | auth, identity headers, cross-tenant reads, traversal and encoding, container escapes, limits |
| Deep attacks | 36 | hardlinks, FIFOs, symlink races, /proc, kernel escapes, request smuggling, UID recycling |
| Obfuscation and nesting | 26 | base64/hex/eval, three-level scripts, path splicing, dynamic unshare/chroot |
Isolation is decided at fork/exec, not by inspecting command text, so encoding
or nesting a payload does not change the outcome.
| Metric | Result |
|---|---|
exec true |
p50 8.5 ms, p95 30.9 ms |
| 16 identities in parallel | 667 exec/s |
| Download 32 MB | 66 MiB/s |
Full tables, the known boundaries and the reproduction steps: docs/validation.md and runbox.xyun.dev/validation.
/readyverifies that the data directories exist and the state directory is writable./metricsexposes bounded Prometheus counters: requests, execs, timeouts, busy, upload/download bytes, workspace deletes, active commands and uptime.- Graceful shutdown cancels in-flight commands after the configured budget and kills their process groups.
- Logs are structured text or JSON (
RUNBOX_LOG_FORMAT).
See docs/production.md for the production checklist: SLOs, capacity, quotas, key handling, gVisor, multi-replica and runbooks.
All configuration
Durations accept 30s, 15m, 7d or bare seconds; sizes accept 20MiB, 20MB, 1GiB or bytes.
| Variable | Default | Meaning |
|---|---|---|
RUNBOX_API_KEY |
— (required) | Shared API key. Startup fails without it unless anonymous mode is on. |
RUNBOX_ALLOW_ANONYMOUS |
false |
Disable authentication. Local development only. |
RUNBOX_ALLOW_MISSING_CAPS |
false |
Allow root startup without the Linux capabilities Runbox needs. Development only; production fails closed. |
RUNBOX_ADDR |
:8099 |
Listen address. |
RUNBOX_ROOT |
.runbox |
Data root. The image sets /workspace. |
RUNBOX_MAX_CONCURRENCY |
2 |
Concurrent commands per runner. Excess calls get 409 busy. |
RUNBOX_MAX_REQUEST_BYTES |
200MiB |
HTTP body limit. |
RUNBOX_MAX_UPLOAD_BYTES |
20MiB |
Per-file limit. |
RUNBOX_MAX_COMMAND_BYTES |
64KiB |
Maximum command length. |
RUNBOX_MAX_ATTACHMENTS |
64 |
Maximum multipart attachments per execution. |
RUNBOX_MAX_STDOUT_BYTES |
200KiB |
Captured stdout+stderr limit; keeps the head and the tail. |
RUNBOX_MAX_OUTPUT_FILES |
20 |
Files returned from one execution. |
RUNBOX_MAX_OUTPUT_BYTES |
200MiB |
Total output size returned from one execution. |
RUNBOX_MAX_BATCH_FILES |
1000 |
Files per batch upload or tar archive. |
RUNBOX_MAX_BATCH_BYTES |
1GiB |
Total size of one tar archive download. |
RUNBOX_QUOTA_BYTES |
1GiB |
Per-workspace quota; execution is refused above it. 0 = unlimited. |
RUNBOX_RETENTION |
7d |
Idle workspace retention. |
RUNBOX_SWEEP_INTERVAL |
1h |
Janitor interval. |
RUNBOX_UID_MIN / RUNBOX_UID_MAX |
20000 / 62000 |
UID pool used for Linux isolation; a UID returns to the pool when its workspace is deleted or expires. |
RUNBOX_DEFAULT_TIMEOUT |
60s |
Timeout when the request omits one. |
RUNBOX_MAX_TIMEOUT |
300s |
Upper bound; longer requests are clamped. |
RUNBOX_SHUTDOWN_TIMEOUT |
30s |
Graceful shutdown budget. |
RUNBOX_TLS_CERT / RUNBOX_TLS_KEY |
— | Terminate TLS in-process. A reverse proxy is usually better. |
RUNBOX_LOG_FORMAT |
text |
text or json. |
| Capability | Full | Minimal | Toolbox |
|---|---|---|---|
| Agent CLI (git, curl/wget, jq, ripgrep, fd, bat, tree, file, sqlite3, less, nano, lsof) | yes | yes | yes |
| Python 3, Node, npm, unzip/zip | yes | yes | yes |
pdftotext |
yes | yes | yes |
| LibreOffice (Writer/Calc/Impress) | yes | no | no |
| CJK fonts | yes | no | no |
| Python document/data libraries | yes | no | no |
Node xlsx, csv-parse, dayjs |
yes | no | no |
build-essential and development headers |
yes | no | yes |
Sizes above are the amd64 download size; unpacking takes roughly three to four
times that on disk (docker images reports about 700 MB for minimal). full
now includes everything in toolbox, so pick minimal for code/text workloads
and toolbox only when you want the compiler without the Office layer.
Build arguments for restricted networks: GOPROXY, APT_MIRROR, PIP_INDEX, NPM_REGISTRY. See docs/deployment.md.
| Runbox | Container-per-sandbox (OpenSandbox / CubeSandbox) | MicroVM (E2B / microsandbox) | |
|---|---|---|---|
| Isolation unit | UID + private workspace in one container | one container (or pool slot) per sandbox | one VM per sandbox |
| New container/VM per user | no | yes | yes |
| Single-machine footprint | one binary, one container, one volume | control plane + many containers | KVM + runtime + VM images |
| Startup per identity | no container/VM boot | container start or pool checkout | VM boot or snapshot restore |
| File isolation | built-in: workspace, quota, symlink-safe paths | volume or API per sandbox | strong |
| Best fit | internal/semi-trusted, single machine | multi-node, stronger per-sandbox isolation | hostile tenants |
OpenSandbox and CubeSandbox are control-plane projects and support different deployment modes; the column describes the common container-per-sandbox model, not a claim about every mode. See docs/comparison.md for gVisor, Dify and the full trade-off list.
The closest prior art is dify-sandbox: one container with a per-task UID. Runbox keeps that shape and adds the per-identity file isolation described above.
Does every user get a container?
No. Every identity shares one long-lived container and gets a UID plus a private workspace inside it. That is what makes single-machine deployment practical.
Can I run untrusted code?
Not as a hard multi-tenant boundary. Runbox uses the container's namespaces and cgroups plus per-UID file permissions, so the kernel is shared. For hostile tenants, run it under gVisor or a microVM.
How is it different from Dify's sandbox?
Dify's sandbox is part of the Dify stack and scopes files per task. Runbox is standalone, exposes a stable HTTP contract, and adds per-identity workspaces, quotas and symlink-safe paths.
Does it work on Windows or macOS?
For development, yes. Production isolation is Linux-only, because per-identity UID isolation depends on Linux credentials and file ownership.
- Read-only usage endpoint: per-namespace identity count, workspace bytes and in-flight commands
- SBOM and provenance attestations for published images
- Signed container images with cosign/Sigstore
Egress filtering, filesystem-level disk quotas, seccomp profiles and multi-replica routing are deployment decisions, not runner features. Runbox documents how to handle each one in docs/deployment.md and docs/production.md rather than building it into the daemon.
Issues and pull requests are welcome. Before opening a PR, run:
make check # gofmt, go vet, go test -raceSee CONTRIBUTING.md for the development workflow and SECURITY.md for reporting security issues.
MIT. See LICENSE.
Run in a box. If Runbox is useful to you, a star helps others find it.