joulebox

Agentic Go and Python (experimental)

Pay for the work, not for the lease.

For humans

last — ms — Jsession — J · $0.20/kJ
:w write · :wq / ZZ write+close · :q close · :fmt gofmt · Ctrl-S write

Not asciinema. Live.

For agents

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.

More examples for agents

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.

Setup: a session, and two helpers

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

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

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

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

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

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

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

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

run 'curl -s -o /dev/null -w "%{http_code}\n" https://example.com/' | jq -r .stdout

9. The receipt, and closing the session

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

Reaching the network

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.

What a run costs

A line's energy is CPU time × 2 W, at $0.20/kJ. The Graviton has no energy counter.