Pay for the work, not for the lease.
Not asciinema. Live.
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}'
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)
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);
The curl tab needs jq, and curl 7.82 or later for --json. It pastes into bash or zsh as it is.
For agents. 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. The same, as text: /llms.txt.
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 @-; }
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
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
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
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
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
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
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
run 'curl -s -o /dev/null -w "%{http_code}\n" https://example.com/' | jq -r .stdout
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
SID=$(curl -sS -X POST "$ORIGIN/v1/session" | jq -r .id)
jq -Rs --arg sid "$SID" '{session:$sid,op:"W",path:"get.go",data:.}' <<'EOF' \
| curl -sS --fail-with-body "$ORIGIN/v1/fs" --json @-
package main
import "fmt"
import "io"
import "net/http"
func main() {
resp, err := http.Get("https://example.com/")
if err != nil { fmt.Println(err); return }
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(resp.Status)
fmt.Println(string(b))
}
EOF
curl -sS --fail-with-body "$ORIGIN/v1/exec" \
--json '{"session":"'"$SID"'","line":"go run get.go"}' | jq -r .stdout
A program reaches the network only through the proxy: https only, port 443, host names, not IP literals. A response is at most 1 MiB, the exchange at most 2 seconds, and at most 3 redirects.
An anonymous session's list is the box's, today example.com, and allow in the request is ignored. POST /v1/session answers with "allow", the list that session got. A session of your own hosts needs a key: Authorization: Bearer <secret> and {"allow":["api.example.org"]}, at most 16 hosts. A wrong key is 401. More than 16 hosts is 400. Keys are issued by hand for now.
A refusal names its rule: proxy: host not allowed (not on the list, before DNS), proxy: address refused (the name is metadata, or a DNS answer is not a public address; nothing is dialed), proxy: scheme not allowed (not https), proxy: bad request (an IP literal, or a malformed URL), proxy: too many redirects, proxy: timeout, proxy: too large, proxy: budget exhausted, proxy: unavailable, proxy: no egress (this session has no list).
git clone works for public repositories on github.com and gitlab.com, over https: one branch at depth 1, no submodules, at most 30 seconds, and the clone has to fit in the session's 4 MiB. It does not go through the proxy, so those two hosts need not be on the list. It is the one git command.
A line's energy is CPU time × 2 W, at $0.20/kJ. The Graviton has no energy counter.