Go and Python SDKs
Two clients for the HTTP API (rc serve), with the same methods and only the standard
library. They cover the ephemeral job and the full machine lifecycle.
| SDK | Package | Dependencies | Coverage (line / branch) |
|---|---|---|---|
| Go | runner.codes/pkg/client | none | 100% (statement; Go does not measure branches) |
| Python 3.9+ | runner_code (distribution runner-code) | none | 100% / 100% |
Node is left out. By project rule, new code is not written in JavaScript/TypeScript outside the browser. Node users
call the HTTP API directly with fetch: the table in the reference has every route
and every body.
The SDKs talk to an rc serve that is already running, on your machine or on a server:
rc serve --listen 127.0.0.1:8080
Install​
Go​
go get runner.codes/pkg/client@latest
runner.codes answers go get by pointing to the GitHub repository, so there is no clone and no replace. The
package imports only the standard library. The module asks for Go 1.27.2: go get raises the go line of your
go.mod to that version, and an older toolchain downloads the new one on its own.
Python​
pip install "git+https://github.com/andrebassi/[email protected]#subdirectory=sdk/python"
Installs the runner_code package, with no dependencies at all. To use it without installing, from a clone of the
repository, PYTHONPATH=sdk/python is enough.
First program​
An ephemeral job: the code runs in a fresh microVM, with no network, which is destroyed at the end.
package main
import (
"context"
"fmt"
"log"
"runner.codes/pkg/client"
)
func main() {
c := client.New("http://127.0.0.1:8080")
r, err := c.Execute(context.Background(), client.Job{Language: "python", Code: "print(6*7)"})
if err != nil {
log.Fatal(err) // the API did not run it: server down, 400, 500
}
fmt.Print(r.Stdout) // 42
}
from runner_code import Client, Job
c = Client("http://127.0.0.1:8080")
r = c.execute(Job(language="python", code="print(6*7)"))
print(r.stdout, end="") # 42
Recipes​
Each recipe below is a function in examples/sdk-go-recipes/main.go and in examples/sdk-python-recipes/main.py. Both
programs run all seven against a real rc serve in runbook step 10d. The output shown in each one is the one measured
in that step.
go run ./examples/sdk-go-recipes http://127.0.0.1:8080
PYTHONPATH=sdk/python python3 examples/sdk-python-recipes/main.py http://127.0.0.1:8080
1. Run outside code (generated by an LLM, for example)​
status: "success" only means the API ran the request. Whether the program succeeded is told by exception (empty
when it did) and by stderr.
code := "import sys\nprint(sum(range(10)))\nprint('aviso', file=sys.stderr)"
r, err := c.Execute(ctx, client.Job{Language: "python", Code: code, Timeout: 10})
if err != nil {
return err // transport or 4xx/5xx: the API did not run it
}
if r.Exception != "" {
return fmt.Errorf("o programa falhou: %s", r.Exception)
}
fmt.Printf("stdout=%s stderr=%s tempo=%dms\n",
strings.TrimSpace(r.Stdout), strings.TrimSpace(r.Stderr), r.ExecutionTime)
code = "import sys\nprint(sum(range(10)))\nprint('aviso', file=sys.stderr)"
r = c.execute(Job(language="python", code=code, timeout=10))
if r.exception:
raise RuntimeError(f"o programa falhou: {r.exception}")
print(f"stdout={r.stdout.strip()} stderr={r.stderr.strip()} tempo={r.execution_time}ms")
Output: stdout=45 stderr=aviso tempo=58ms (the time varies; with a full pool it stays between 26 and 69 ms).
2. Multiple files and standard input​
The first file is the entry point; the others sit in the same directory and can be imported.
r, err := c.Execute(ctx, client.Job{
Language: "python",
Files: []client.File{
{Name: "main.py", Content: "import util, sys\nprint(util.dobro(int(sys.stdin.read())))"},
{Name: "util.py", Content: "def dobro(n):\n return 2 * n"},
},
Stdin: "21",
})
r = c.execute(Job(
language="python",
files=[
{"name": "main.py", "content": "import util, sys\nprint(util.dobro(int(sys.stdin.read())))"},
{"name": "util.py", "content": "def dobro(n):\n return 2 * n"},
],
stdin="21",
))
Output: 42.
3. Deadline​
Once Timeout (seconds) is exceeded, the whole VM dies. The result arrives normally, with exception: "Timeout", and
not as an SDK error.
r, _ := c.Execute(ctx, client.Job{Language: "python", Code: "while True: pass", Timeout: 2})
fmt.Println(r.Exception) // Timeout
r = c.execute(Job(language="python", code="while True: pass", timeout=2))
print(r.exception) # Timeout
4. Compilation error​
In a compiled language (C, C++, Rust, Java, Kotlin...), a compilation failure comes with
exception: "Compilation failed" and the compiler message in stderr.
r, _ := c.Execute(ctx, client.Job{Language: "c", Code: "int main(void) { return x; }"})
fmt.Println(r.Exception, r.Stderr != "") // Compilation failed true
r = c.execute(Job(language="c", code="int main(void) { return x; }"))
print(r.exception, bool(r.stderr)) # Compilation failed True
Go is the exception: it runs through go run, which compiles and executes in a single step. The compilation error
arrives only in stderr, with an empty exception.
5. Persistent machine with filtered network​
A machine stays up between one command and the next. Here outbound internet access is allowed only to pypi.org: the
same program can talk to it and is refused at example.com.
name := "minha-maquina"
_, err := c.CreateMachine(ctx, client.MachineSpec{
Name: name,
Stack: "python",
Net: true,
Egress: &client.Egress{AllowHosts: []string{"pypi.org"}},
})
if err != nil {
return err
}
defer c.DeleteMachine(ctx, name, true) // force: deletes it even while running, if something fails midway
c.Start(ctx, name)
c.WriteFile(ctx, name, "/work/app.py", []byte(script), 0o644)
out, err := c.Exec(ctx, name, []string{"python3", "app.py"},
client.ExecOptions{Env: []string{"QUEM=sdk-go"}, Workdir: "/work", Timeout: 30})
if err == nil && out.ExitCode != 0 {
err = fmt.Errorf("exit %d: %s", out.ExitCode, out.Stderr)
}
data, _ := c.ReadFile(ctx, name, "/work/saida.txt")
name = "minha-maquina"
c.create_machine(name, stack="python", net=True, egress={"allow_hosts": ["pypi.org"]})
try:
c.start(name)
c.write_file(name, "/work/app.py", script.encode())
out = c.exec(name, ["python3", "app.py"], env=["QUEM=sdk-python"], workdir="/work", timeout=30)
if out.exit_code != 0:
raise RuntimeError(f"exit {out.exit_code}: {out.stderr}")
print(c.read_file(name, "/work/saida.txt").decode())
finally:
c.delete_machine(name, force=True)
The script tries to open https://pypi.org/simple/ and https://example.com and writes the status of each one to
saida.txt. Output: sdk-go pypi=200 example=bloqueado.
Exec returns the process exit code in ExitCode/exit_code: unlike the job, a command that fails inside the machine
does not become an SDK error.
6. Branch​
Copies a machine along with its disk. The parent is paused first, so the disk is consistent; the child starts with what the parent had written and goes on independently.
c.WriteFile(ctx, "mae", "/root/estado.txt", []byte("feito antes do branch"), 0o644)
c.Pause(ctx, "mae")
c.Branch(ctx, "mae", "filha")
c.Start(ctx, "filha")
data, _ := c.ReadFile(ctx, "filha", "/root/estado.txt")
c.write_file("mae", "/root/estado.txt", b"feito antes do branch")
c.pause("mae")
c.branch("mae", "filha")
c.start("filha")
print(c.read_file("filha", "/root/estado.txt").decode())
Output: feito antes do branch.
7. API errors​
An API refusal arrives typed, with the HTTP status and a stable code. Compare by the code, never by the message text.
_, err := c.Machine(ctx, "nao-existe")
var apiErr *client.Error
if errors.As(err, &apiErr) && apiErr.Code == "NOT_FOUND" {
fmt.Println(apiErr.Status, apiErr.Code) // 404 NOT_FOUND
}
try:
c.machine("nao-existe")
except RunnerCodeError as e:
if e.code == "NOT_FOUND":
print(e.status, e.code) # 404 NOT_FOUND
The codes of the machine routes are NOT_FOUND, INVALID_STATE, CONFLICT, CONFIG and the others in the
reference. A transport failure (server down) arrives as a regular error, not as an
API error.
Methods​
| Go | Python | Route |
|---|---|---|
Execute | execute | POST /api/v1/execute |
CreateMachine | create_machine | POST /api/v1/machines |
Machines / Machine | machines / machine | GET /api/v1/machines[/:name] |
Start / Stop | start / stop | POST .../start, .../stop?timeout=N |
Exec | exec | POST .../exec |
WriteFile / ReadFile | write_file / read_file | PUT/GET .../files?path= |
Logs | logs | GET .../logs |
UpdateEgress | update_egress | PUT .../egress |
Pause / Resume / Branch | pause / resume / branch | POST .../pause, .../resume, .../branch |
Screenshot | screenshot | GET .../screen (JPEG and size) |
ScreenActions / Click / Type / Key | screen_actions / click / type / key | POST .../screen/actions (Screen control) |
DeleteMachine | delete_machine | DELETE /api/v1/machines/:name[?force=true] |
Good to know​
- Empty fields are not sent in the body. On create, the server treats a present field as an override of
runner_code; sendingcpus: 0would wipe the value from the file. Stop: a negative timeout (Go) orNone(Python) uses the machine'sstop_timeout, and0kills immediately.- Client deadline: both wait up to 10 min for a response (Go:
http.Clientwith a 10 minTimeout; Python: 600 s), above the 300 s a job can last, so the server answers the job'sTimeoutfirst. In Go, acontextwith a shorter deadline wins; theHTTPfield ofClientaccepts anotherhttp.Client. - The machine name is escaped in the path (
a/bbecomesa%2Fb).
Tests​
task test:cov # includes pkg/client (90% floor) and the Python SDK
task sdk:py:cov # Python only: line and branch, each >= 90%, coverage.py via nix shell
Both SDKs went through 12 defects planted on purpose (name escaping, the stop query, error parsing, empty field in the
body, octal mode...). The tests caught all 12. In the runbook, step 10b runs the basic examples, 10c downloads the Go
SDK through the public go get in an outside project and 10d runs the seven recipes, all against a real rc serve,
with real VMs.