Skip to content
GitHub Get Started
Operating System

Python Runtime

The Python runtime is powered by the Rivet Secure Exec project, which embeds PyodideCPython 3.13 compiled to WebAssembly — inside the same isolate that runs guest JavaScript. Guest Python is fully sandboxed from the host: every file read, network connection, and subprocess goes through the kernel, never a real host API.

Most agentOS software (coreutils, ripgrep, jq, …) ships as unmodified upstream binaries compiled to wasm32-wasip1 against a custom libc and a patched Rust std/sysroot. The portability work lives in that shared libc/std layer, so the programs themselves are not forked or rewritten per‑WASM.

Python is the deliberate exception. CPython is a large C runtime that dynamically loads C extension modules (numpy, pandas, and most of the scientific stack are native code) and leans on OS facilities like threads and signals — none of which map cleanly onto the wasip1 + custom‑libc model that suits self‑contained Rust/C CLIs. Porting CPython and its extension ecosystem to that toolchain would be a huge, duplicative maintenance burden.

Pyodide already solves exactly this: it is the upstream CPython‑on‑WebAssembly project, shipping a full interpreter plus a precompiled package ecosystem on the emscripten target, and it runs in the same V8 isolate as guest JavaScript. agentOS embeds it rather than maintaining its own CPython port — which is also why the runtime targets emscripten (not wasip1) and carries the behavioral differences listed below.

python (and the python3 alias) is a first-class command, resolved by the runtime exactly like node — no install step, no node_modules/venv. Run it through the normal command API:

await vm.execArgv("python", ["-c", "print(1 + 1)"]); // inline code
await vm.exec('python /workspace/main.py alpha beta'); // script + sys.argv
await vm.execArgv("python", ["-m", "json.tool"]); // module (-m)
await vm.execArgv("python", ["-"], { stdin: "print('hi')\n" }); // program on stdin

Supported invocation forms: -c CODE, a script path, -m MODULE (via runpy), - / piped stdin programs, and a bare interactive REPL (PS1/PS2, EOF to exit). Each form sets sys.argv the way CPython does.

  • Bundled, offline: numpy and pandas ship with the runtime and import with no network access. They are real native (C/Fortran) libraries — compiled ahead of time to WebAssembly by Pyodide.
  • pip / micropip: pip install <pkg> (and python -m pip ...) installs wheels through Pyodide’s micropip. This covers pure-Python wheels and packages that already have a Pyodide/emscripten WASM wheel. Wheel downloads go through the kernel network adapter, so egress obeys the VM’s network policy (default-deny + allowlist) — never an ambient host fetch.
  • requests is shimmed to route HTTP through the kernel bridge, so common HTTP code works under the same network policy.

The runtime targets practical parity for agent workloads, but it is Pyodide/WASM, not a host CPython. Know these gaps:

  • Ephemeral per-process state. Every python (or pip) invocation is a fresh interpreter. A pip install in one command does not persist to a later, separate command — there is no shared site-packages on disk. Install a package and use it within the same program.
  • Filesystem scope. Only /workspace is bridged into the Python filesystem. Scripts you run and files you open with Python must live under /workspace; paths like /tmp are not visible to the interpreter (even though guest shell commands can see them).
  • No /bin/python on PATH. python is a runtime command intercepted at exec time (the same model as node, which also has no /bin/node). vm.exec/execArgv/spawn route it directly; a literal which python or sh -c "python …" PATH-file lookup inside the guest shell will not find it.
  • Native extensions need a WASM wheel. Arbitrary C/Rust extension modules cannot be compiled at install time. A package only works if it is pure Python or ships a Pyodide/emscripten wheel; the bundled set is numpy + pandas.
  • Single-threaded, no real processes. Pyodide is single-threaded WASM: no OS threads, no os.fork, no multiprocessing. subprocess.run is shimmed to dispatch through the kernel (so you can launch other runtime commands like node), but it does not accept input= and is not a full POSIX fork/exec.
  • Networking is brokered. Sockets and HTTP go through the kernel network adapter under the VM’s policy; requests, pyodide.http, and micropip are wired to it. Low-level/raw socket use is limited to what the bridge supports.
  • Bounded by default. Python runs in the shared isolate model — heap, CPU time, and captured output are bounded, and a runaway execution terminates its isolate rather than the host (see Limits & Observability).

For the underlying runtime internals, see the Secure Exec project at secureexec.dev.