Skip to content

Commit af22020

Browse files
authored
docs: a documentation site at openblox.sh (#14)
* docs: a documentation site at openblox.sh Everything lived in README.md and ARCHITECTURE.md, which were carrying the pitch, the quick start, the security model and the limitations at once for audiences that want different things. Five pages, built with mkdocs-material and deployed to GitHub Pages: home, quick start, security model, the image contract, contributing. The security model gets its own page rather than a README section, because it is the page that decides whether someone adopts a sandboxing library. It states the threat model, what is isolated and how, and — the part usually left out — what is NOT claimed: gVisor is not a hypervisor, side channels are not addressed, revocation is best-effort. It also names the caller as the weakest link in a default deployment and links the two issues tracking that. The image contract was folklore that surfaced as a broken preview. It is now a page: a shell, a non-root user, and nc-or-python3. CI builds on pull requests without deploying, so a broken docs build is a red check rather than a broken site. `mkdocs build --strict` turns a dead internal link into a failure instead of shipping it. Emits llms.txt: a large share of readers are agents, and this project's subject matter is running the code they write. Docs are markdown in the repository, so a behaviour change and its documentation move in the same pull request. * ci: run CI on every pull request, without path filters main now requires Lint, Test, Integration Tests and the commit-message check to pass before merge. A skipped check never reports a conclusion, so with paths-ignore in place a docs-only pull request would sit forever with four required checks pending and no action available to its author. Dropping the filter costs a couple of CI minutes on documentation changes. A category of pull request that silently cannot land costs more. Side effect worth knowing: Release fires on green CI, so it now also runs for docs-only commits. It is a no-op there — `docs:` does not bump a version under conventional commits — but it will appear in the run list where it previously did not.
1 parent d98f075 commit af22020

11 files changed

Lines changed: 657 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
name: CI
22

3+
# No paths-ignore. These jobs are required for merge, and a skipped required
4+
# check never reports — so a docs-only pull request would be unmergeable
5+
# forever, with nothing the author could do about it. The jobs are fast; that
6+
# is a better trade than a class of PR that silently cannot land.
37
on:
48
push:
59
branches: [main]
6-
paths-ignore:
7-
- '**.md'
8-
- 'docs/**'
9-
- '.gitignore'
1010
pull_request:
1111
branches: [main]
12-
paths-ignore:
13-
- '**.md'
14-
- 'docs/**'
15-
- '.gitignore'
1612

1713
concurrency:
1814
group: ${{ github.workflow }}-${{ github.ref }}

.github/workflows/docs.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Docs
2+
3+
# Builds the documentation site and deploys it to GitHub Pages at openblox.sh.
4+
#
5+
# A pull request builds but does not deploy, so a broken docs build is a red check
6+
# rather than a broken site. Only main deploys.
7+
8+
on:
9+
push:
10+
branches: [main]
11+
paths:
12+
- 'docs/**'
13+
- 'mkdocs.yml'
14+
- 'requirements-docs.txt'
15+
- '.github/workflows/docs.yml'
16+
pull_request:
17+
paths:
18+
- 'docs/**'
19+
- 'mkdocs.yml'
20+
- 'requirements-docs.txt'
21+
- '.github/workflows/docs.yml'
22+
workflow_dispatch:
23+
24+
permissions:
25+
contents: read
26+
27+
concurrency:
28+
group: docs
29+
cancel-in-progress: false
30+
31+
defaults:
32+
run:
33+
shell: bash
34+
35+
jobs:
36+
build:
37+
name: Build
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 10
40+
steps:
41+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
42+
43+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
44+
with:
45+
python-version: '3.12'
46+
cache: pip
47+
cache-dependency-path: requirements-docs.txt
48+
49+
- name: Install
50+
run: pip install -r requirements-docs.txt
51+
52+
# --strict turns warnings into errors, so a broken internal link or a page
53+
# missing from the nav fails here instead of shipping.
54+
- name: Build
55+
run: mkdocs build --strict
56+
57+
- uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
58+
with:
59+
path: site
60+
61+
deploy:
62+
name: Deploy
63+
if: github.event_name != 'pull_request'
64+
needs: build
65+
runs-on: ubuntu-latest
66+
timeout-minutes: 10
67+
permissions:
68+
pages: write
69+
id-token: write
70+
environment:
71+
name: github-pages
72+
url: ${{ steps.deployment.outputs.page_url }}
73+
steps:
74+
- id: deployment
75+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ go.work.sum
3030
# Editor/IDE
3131
# .idea/
3232
# .vscode/
33+
34+
# mkdocs build output
35+
site/

docs/CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
openblox.sh

docs/contributing.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Contributing
2+
3+
The full guide lives in
4+
[CONTRIBUTING.md](https://github.com/blox-eng/openblox/blob/main/CONTRIBUTING.md). This
5+
is the short version.
6+
7+
## Setup
8+
9+
```sh
10+
git clone https://github.com/blox-eng/openblox
11+
cd openblox
12+
make # vet + lint + test
13+
```
14+
15+
Requires Go 1.25+ and [golangci-lint](https://golangci-lint.run) v2.
16+
17+
**Install golangci-lint before you push.** `go vet` and `go test` catch less than CI
18+
does — the lint step adds revive, gosec, errorlint and bodyclose. Without it on your
19+
PATH, `make lint` fails open and you learn about a violation from a red PR instead of
20+
from your terminal.
21+
22+
## Tests
23+
24+
Unit tests run anywhere. Integration tests are behind the `integration` build tag and
25+
need a Docker host with gVisor registered as `runsc`:
26+
27+
```sh
28+
make test-integration
29+
```
30+
31+
CI currently *compiles* the integration tests but does not run them — a gap tracked in
32+
[#3](https://github.com/blox-eng/openblox/issues/3). A test that no longer builds is a
33+
test nobody runs, so keep them compiling even when you cannot execute them locally.
34+
35+
## Working on the docs
36+
37+
```sh
38+
pip install -r requirements-docs.txt
39+
mkdocs serve # http://127.0.0.1:8000
40+
```
41+
42+
The docs are markdown in `docs/`, so a change to behaviour and the change to its
43+
documentation belong in the same pull request.
44+
45+
## Commits
46+
47+
[Conventional Commits](https://www.conventionalcommits.org/) — CI enforces it, and
48+
releases are cut from it.
49+
50+
```
51+
feat(sandbox): add per-command timeout clamping
52+
fix(docker): drop CAP_NET_RAW on create
53+
docs: explain the egress default
54+
```
55+
56+
## Where to start
57+
58+
Issues labelled [`good first issue`](https://github.com/blox-eng/openblox/labels/good%20first%20issue),
59+
or anything in the open backlog that looks like your kind of problem. If you are
60+
planning something substantial, open an issue first so the design can be discussed
61+
before you spend the effort.

docs/getting-started.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Quick start
2+
3+
## Prerequisites
4+
5+
**A Docker daemon**, and **gVisor (`runsc`) registered with it**.
6+
7+
openblox will not run a sandbox without gVisor. It does not fall back to `runc`, and
8+
that refusal is deliberate: falling back would silently run untrusted code on the host
9+
kernel while the API kept reporting success.
10+
11+
Install gVisor per the [official instructions](https://gvisor.dev/docs/user_guide/install/),
12+
then register it:
13+
14+
```json title="/etc/docker/daemon.json"
15+
{
16+
"runtimes": {
17+
"runsc": {
18+
"path": "/usr/bin/runsc"
19+
}
20+
}
21+
}
22+
```
23+
24+
```sh
25+
sudo systemctl reload docker
26+
docker info --format '{{json .Runtimes}}' | grep runsc # confirm
27+
```
28+
29+
If `runsc` is absent, `Create` returns an error wrapping `sandbox.ErrRuntimeUnavailable`
30+
— branch on that if you want to degrade gracefully rather than fail.
31+
32+
## Install
33+
34+
```sh
35+
go get github.com/blox-eng/openblox
36+
```
37+
38+
## Run something
39+
40+
```go
41+
package main
42+
43+
import (
44+
"context"
45+
"fmt"
46+
"log"
47+
48+
"github.com/blox-eng/openblox/pkg/docker"
49+
"github.com/blox-eng/openblox/pkg/sandbox"
50+
)
51+
52+
func main() {
53+
ctx := context.Background()
54+
55+
backend, err := docker.New()
56+
if err != nil {
57+
log.Fatal(err)
58+
}
59+
defer backend.Close()
60+
61+
sb, err := backend.Create(ctx, "session-1",
62+
sandbox.WithImage("ghcr.io/blox-eng/openblox-sandbox:latest"))
63+
if err != nil {
64+
log.Fatal(err)
65+
}
66+
defer backend.Destroy(ctx, "session-1")
67+
68+
res, err := sb.Exec(ctx, sandbox.Command{
69+
Argv: []string{"python3", "-c", "print(6 * 7)"},
70+
})
71+
if err != nil {
72+
log.Fatal(err)
73+
}
74+
fmt.Println(string(res.Stdout)) // 42
75+
}
76+
```
77+
78+
`Create` is keyed by name: calling it again with the same name returns the existing
79+
sandbox rather than a second one. That makes it safe to call per request without
80+
tracking what already exists.
81+
82+
A non-zero `res.ExitCode` is the program failing, not an openblox error. `err` is
83+
reserved for openblox failing to run it at all.
84+
85+
!!! note "Argv, not a shell string"
86+
`Command.Argv` is passed directly to `exec`. Nothing in it is parsed as shell
87+
syntax, so a caller cannot accidentally create an injection by interpolating
88+
untrusted text into a command line.
89+
90+
## Files
91+
92+
```go
93+
err := sb.WriteFile(ctx, "/workspace/data.csv", 0o644, strings.NewReader("a,b\n1,2\n"))
94+
95+
rc, err := sb.ReadFile(ctx, "/workspace/out.json")
96+
defer rc.Close()
97+
body, err := io.ReadAll(rc)
98+
```
99+
100+
Both stream, so they are safe for large payloads. Paths are absolute inside the
101+
sandbox; `/workspace` is the working directory and is writable.
102+
103+
## Background processes
104+
105+
```go
106+
err := sb.StartProcess(ctx, "web", sandbox.Command{
107+
Argv: []string{"python3", "-m", "http.server", "8080", "--bind", "127.0.0.1"},
108+
})
109+
```
110+
111+
Idempotent: if something is already running under that name, it is left alone and no
112+
error is returned. Call it on every request rather than tracking state yourself.
113+
114+
## Preview links
115+
116+
A sandbox has **no network interface**, so a port inside it is not reachable by any
117+
ordinary route. openblox reaches it over the exec channel and fronts it with a signed,
118+
expiring URL.
119+
120+
```go
121+
backend, err := docker.New(
122+
docker.WithPreviews(signingKey, "https://example.com"), // key >= 32 random bytes
123+
)
124+
125+
// Mount the handler where the signed URLs will resolve.
126+
http.Handle(preview.RoutePrefix+"/", backend.PreviewHandler())
127+
128+
p, err := sb.Expose(ctx, 8080, 10*time.Minute)
129+
// p.URL + p.Token — send the token as an Authorization header, never a query param.
130+
```
131+
132+
!!! warning "Revocation is best-effort; expiry is the guarantee"
133+
Verification is a local HMAC check that consults no shared state, so `Revoke` only
134+
holds in the process that recorded it. If you run several replicas, treat the TTL
135+
as the real bound and keep it short.
136+
137+
## Cleaning up
138+
139+
```go
140+
removed, err := backend.Reap(ctx) // destroys sandboxes past idle timeout or max age
141+
```
142+
143+
Call it from a ticker. It is safe to run concurrently with everything else, and safe to
144+
run from several processes at once.
145+
146+
## Defaults
147+
148+
A sandbox created with no options gets:
149+
150+
| | |
151+
|---|---|
152+
| Runtime | `runsc` (gVisor) |
153+
| Network | none |
154+
| User | `1000:1000` (non-root) |
155+
| Root filesystem | read-only |
156+
| CPUs | 2 |
157+
| Memory | 2 GiB |
158+
| Scratch disk | 1 GiB (tmpfs, drawn **from** the memory budget) |
159+
| Max processes | 256 |
160+
| Idle timeout | 15 minutes |
161+
| Max age | 2 hours |
162+
| Command timeout | 60s default, 10m ceiling |
163+
164+
Scratch space is tmpfs, so it comes out of memory — keep disk at or below memory, and
165+
size both deliberately if your workload is heavy.

0 commit comments

Comments
 (0)