# joulebox > A sandboxed Linux shell over HTTP, for coding agents. Each session is a home of its own; go, python3, git and the GNU text tools answer as on a Linux box, and what the box does not run is refused by name. A line is billed by its CPU time. ## API - POST https://joulebox.dev/v1/session: a new session. The answer has id, allow (the hosts it may reach) and ttl_s. - POST https://joulebox.dev/v1/exec with {"session": ID, "line": LINE}: runs one shell line. The answer has stdout, stderr, status, ms, cpu_ms, usd, total_usd and cwd. - POST https://joulebox.dev/v1/fs with {"session": ID, "op": "W", "path": PATH, "data": TEXT}: writes a file; op "R" reads one. - GET https://joulebox.dev/v1/session/ID/ledger: every line the session ran, and what each cost. - DELETE https://joulebox.dev/v1/session/ID: ends the session. ## Network A program reaches the network only through the box's proxy: https only, host names, not IP literals. An anonymous session may reach: example.com. ## Examples Each block runs as it is in bash or zsh, after the setup block, against this box. A session is one sandboxed home. run sends one shell line to POST /v1/exec, which answers JSON with stdout, stderr, status and what the line cost; put writes a file through POST /v1/fs. The working directory carries over from one line to the next, as in a shell, so a block that changes it goes home at its end. GET /v1/session/ID/ledger lists every line the session ran and its cost. go run, go test, python3, git (clone, status, diff, log, show), jq, diff, curl and the GNU text tools answer as they do on a Linux box; what this box does not run is refused by name on stderr. ### A fix loop, in curl ```sh O=https://joulebox.dev SID=$(curl -sS -X POST $O/v1/session | jq -r .id) put() { jq -Rs --arg sid "$SID" --arg p "$1" '{session:$sid,op:"W",path:$p,data:.}' | curl -sS --fail-with-body $O/v1/fs --json @- > /dev/null; } run() { jq -n --arg sid "$SID" --arg l "$1" '{session:$sid,line:$l}' | curl -sS --fail-with-body $O/v1/exec --json @-; } echo '1. The first try has a bug.' put fib.go <<'EOF' package main import "fmt" func fib(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) } func main() { fmt.Println(fib("30")) } EOF run 'go run fib.go' | jq -r .stderr echo '2. The error names line 13. Fix it in the session, and run again.' run "sed -i 's/fib(\"30\")/fib(30)/' fib.go" > /dev/null run 'go run fib.go' | jq -c '{stdout, status}' echo '3. The receipt: what each command took, and what it cost.' curl -sS $O/v1/session/$SID/ledger | jq -c '(.lines[] | {line, ms, usd}), {total_usd}' ``` ### A fix loop, in Python ```python import json, urllib.request base = "https://joulebox.dev" def call(method, path, obj=None): data = None if obj is None else json.dumps(obj).encode() req = urllib.request.Request(base + path, data=data, method=method, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req) as r: body = r.read() return json.loads(body) if body else None sid = call("POST", "/v1/session", {})["id"] def write(path, src): call("POST", "/v1/fs", {"session": sid, "op": "W", "path": path, "data": src}) def run(line): return call("POST", "/v1/exec", {"session": sid, "line": line}) # 1. the first try has a bug src = '''package main import "fmt" func fib(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) } func main() { fmt.Println(fib("30")) } ''' write("fib.go", src) r = run("go run fib.go") print(r["status"], r["stderr"]) # 1 # command-line-arguments # ./fib.go:13: cannot use "30" (untyped string constant) as int value in argument to fib # 2. the error names the line: fix it, run again write("fib.go", src.replace('fib("30")', 'fib(30)')) r = run("go run fib.go") print(r["status"], r["stdout"]) # 0 832040 # 3. the receipt: what each command took and what it cost ledger = call("GET", "/v1/session/" + sid + "/ledger") for l in ledger["lines"]: print(f'{l["ms"]:7.2f} ms ${l["usd"]:.2e} {l["line"]}') print("total", ledger["total_usd"], "USD") # optional: drop the files now. Otherwise they go when the session ends. # call("DELETE", "/v1/session/" + sid) ``` ### A fix loop, in TypeScript ```typescript const base = "https://joulebox.dev"; const call = async (method, path, obj) => { const r = await fetch(base + path, {method, headers: {"Content-Type": "application/json"}, body: obj === undefined ? undefined : JSON.stringify(obj)}); const text = await r.text(); return text ? JSON.parse(text) : null; }; const {id} = await call("POST", "/v1/session", {}); const write = (path, data) => call("POST", "/v1/fs", {session: id, op: "W", path, data}); const run = (line) => call("POST", "/v1/exec", {session: id, line}); // 1. the first try has a bug const src = [ "package main", "", "import \"fmt\"", "", "func fib(n int) int {", "\tif n < 2 {", "\t\treturn n", "\t}", "\treturn fib(n-1) + fib(n-2)", "}", "", "func main() {", "\tfmt.Println(fib(\"30\"))", "}", ].join("\n") + "\n"; await write("fib.go", src); let r = await run("go run fib.go"); console.log(r.status, r.stderr); // 1 # command-line-arguments // ./fib.go:13: cannot use "30" (untyped string constant) as int value in argument to fib // 2. the error names the line: fix it, run again await write("fib.go", src.replace('fib("30")', "fib(30)")); r = await run("go run fib.go"); console.log(r.status, r.stdout); // 0 832040 // 3. the receipt: what each command took and what it cost const ledger = await call("GET", "/v1/session/" + id + "/ledger"); for (const l of ledger.lines) console.log(l.ms.toFixed(2).padStart(7), "ms $" + l.usd.toExponential(2), "", l.line); console.log("total", ledger.total_usd, "USD"); // optional: drop the files now. Otherwise they go when the session ends. // await call("DELETE", "/v1/session/" + id); ``` ### Setup: a session, and two helpers ```sh O=https://joulebox.dev SID=$(curl -sS -X POST $O/v1/session | jq -r .id) put() { jq -Rs --arg sid "$SID" --arg p "$1" '{session:$sid,op:"W",path:$p,data:.}' | curl -sS --fail-with-body $O/v1/fs --json @- > /dev/null; } run() { jq -n --arg sid "$SID" --arg l "$1" '{session:$sid,line:$l}' | curl -sS --fail-with-body $O/v1/exec --json @-; } ``` ### 1. Look around a codebase ```sh run 'ls -la' | jq -r .stdout run 'grep -rn "func main" examples | head -3' | jq -r .stdout run 'sed -n "1,8p" hello.go' | jq -r .stdout run 'find . -name "*.go" | wc -l' | jq -r .stdout ``` ### 2. A failing test, the fix, the test again ```sh run 'mkdir -p calc' > /dev/null put calc/go.mod <<'EOF' module example.com/calc go 1.25 EOF put calc/calc.go <<'EOF' package calc func Add(a, b int) int { return a - b } EOF put calc/calc_test.go <<'EOF' package calc import "testing" func TestAdd(t *testing.T) { if got := Add(2, 3); got != 5 { t.Fatalf("Add(2, 3) = %d, want 5", got) } } EOF run 'cd ~/calc && go test ./... 2>&1 | tail -4' | jq -r .stdout run "sed -i 's/a - b/a + b/' ~/calc/calc.go" > /dev/null run 'cd ~/calc && go test -v ./...' | jq -r .stdout run 'cd' > /dev/null ``` ### 3. JSON with jq, counted with sort | uniq -c ```sh put users.json <<'EOF' {"users":[{"name":"ada","lang":"go"},{"name":"bob","lang":"python"},{"name":"cy","lang":"go"}]} EOF run "jq -r '.users[].lang' users.json | sort | uniq -c | sort -rn" | jq -r .stdout run "jq -c '[.users[] | select(.lang == \"go\") | .name]' users.json" | jq -r .stdout ``` ### 4. Shell: substitution, loops, arithmetic ```sh run 'n=$(ls examples | wc -l); echo "examples: $n"' | jq -r .stdout run 'for f in examples/0*.go; do wc -l $f; done | sort -rn | head -3' | jq -r .stdout run 'x=7; if [ $((x * 6)) -eq 42 ]; then echo yes; else echo no; fi' | jq -r .stdout ``` ### 5. Python with arguments ```sh put words.py <<'EOF' import sys words = sys.argv[1:] lengths = {w: len(w) for w in words} for w in sorted(words, key=len): print(f"{w:>8} {lengths[w]}") print("total", sum([lengths[w] for w in words])) EOF run 'python3 words.py go python shell' | jq -r .stdout ``` ### 6. git: clone, status, diff ```sh run 'git clone https://github.com/octocat/Hello-World.git' | jq -r .stderr run 'cd ~/Hello-World && git status' | jq -r .stdout run "cd ~/Hello-World && sed -i 's/World/agents/' README && git diff" | jq -r .stdout run 'cd' > /dev/null ``` ### 7. diff -u and sha256sum ```sh run 'cp hello.go hello2.go && sed -i "s/edit me/edit you/" hello2.go' > /dev/null run 'diff -u hello.go hello2.go | tail -4; sha256sum hello.go hello2.go' | jq -r .stdout ``` ### 8. curl to a host the session may reach ```sh run 'curl -s -o /dev/null -w "%{http_code}\n" https://example.com/' | jq -r .stdout ``` ### 9. The receipt, and closing the session ```sh curl -sS $O/v1/session/$SID/ledger | jq -c '{commands: (.lines | length), total_ms: ([.lines[].ms] | add | floor), total_usd}' curl -sS -X DELETE $O/v1/session/$SID ```