Kernels¶
run_code runs each cell in a stateful Jupyter kernel inside the MicroVM.
The kernel is chosen per cell with language; the contract is the same for
all of them (ExecuteRequest.language, CreateContextRequest.language)
and the image decides which kernels exist.
Languages and images¶
language |
Kernel | rayito-base |
rayito-base-poly |
|---|---|---|---|
python (default) |
ipykernel with the scientific stack, warmed before /ready |
yes | yes |
bash |
bash_kernel 0.10.0 (pexpect over bash), lazy start |
UNIMPLEMENTED |
yes |
javascript (alias js) |
Deno 2.9.7 Jupyter kernel, lazy start | UNIMPLEMENTED |
yes |
typescript (alias ts) |
the same Deno binary with its TypeScript kernelspec, lazy start | UNIMPLEMENTED |
yes |
Names are normalized in the SDK (Bash, JS, TypeScript, ts all
work); only python, bash, javascript and typescript travel on the
wire. Any other (r, java, tsx) is InvalidArgumentException /
InvalidArgumentError before calling the agent; in the rayito.e2b shim
it is UnimplementedError, as in E2B for what does not exist. On an image
that does not ship the kernel the agent answers UNIMPLEMENTED naming
rayito-base-poly (see below).
Lazy start and first-cell latency¶
rayito-base-poly warms neither bash nor Deno before /ready: the
snapshot still contains only the warm Python kernel, so the variant's
kernel_ready and memory size are those of rayito-base. The first cell
of each language creates its default context (default-bash,
default-javascript, default-typescript; the agent starts the kernel
under a per-language lock, so two concurrent cells start a single kernel)
and pays for that startup within its own timeout; the stream deadline
(timeout + 15 s) already covers it. The bash figures measured against
real AWS are in AWS_API_NOTES.md Q57 (the variant's kernel_ready_s and
the latency of the first and second bash cell). The Deno ones are in Q77,
measured on rayito-base-poly 5.0: the first typescript or javascript
cell of a new sandbox takes 0.5-0.75 s (including starting Deno) and
the following ones ≈ 0.1 s. Right after publishing the image, the first
VMs took as long as 10-16 s on that first cell (cold read of the Deno
binary); if that wait matters, run a trivial cell
(run_code("0", language="typescript")) when you create the sandbox. Each
Deno kernel takes ≈ 200 MiB of RSS. javascript and typescript are two
kernels (two Deno processes) if you use both; each one counts toward the
cap of 8 contexts, so choose one.
Examples¶
from rayito import Sandbox
with Sandbox.create(template="rayito-base-poly") as sbx:
out = sbx.run_code("echo hi", language="bash")
print("".join(out.logs.stdout)) # "hi\n"
ctx = sbx.create_code_context(language="bash", envs={"MODE": "dev"})
print("".join(sbx.run_code("echo $MODE", context=ctx).logs.stdout)) # "dev\n"
print([c.id for c in sbx.list_code_contexts()]) # ["default", "default-bash", "ctx-…"]
import { Sandbox } from "rayito";
const sbx = await Sandbox.create({ template: "rayito-base-poly" });
const out = await sbx.runCode("echo hi", { language: "bash" });
console.log(out.logs.stdout.join("")); // "hi\n"
const ctx = await sbx.createCodeContext({ language: "bash" });
console.log((await sbx.runCode("echo $HOME", { context: ctx })).logs.stdout.join(""));
await sbx.kill();
Rules:
languageandcontextare mutually exclusive (INVALID_ARGUMENT): a context already has its language;languageonly selects that kernel's default context.- Per-run
envsonly on Python contexts (INVALID_ARGUMENTon the rest: the silent cells that set and restore variables are Python code). Theenvsofcreate_code_contextdo work for any language: they are the environment of the kernel process. default-bashis a normal context: it counts toward the cap of 8, it can be restarted and destroyed (remove_code_context("default-bash")); the nextbashcell creates it again. Onlydefault(Python) is protected.list_code_contexts()reports each context's actual language.
JavaScript and TypeScript with Deno¶
from rayito import Sandbox
with Sandbox.create(template="rayito-base-poly") as sbx:
print(sbx.run_code("const x: number = 40 + 2; x", language="typescript").text) # "42"
print(sbx.run_code("x + 1", language="ts").text) # "43"
print(sbx.run_code("let y = [1, 2, 3].map((n) => n * 2); y", language="js").text)
out = sbx.run_code('Deno.jupyter.html`<b>hello</b>`', language="typescript")
print(out.results[0].html) # "<b>hello</b>"
ctx = sbx.create_code_context(language="typescript", envs={"MODE": "dev"})
print(sbx.run_code('Deno.env.get("MODE")', context=ctx).text) # the value of MODE
sbx.run_code('import { camelCase } from "npm:lodash-es@4"; camelCase("hello world")',
language="typescript")
import asyncio
from rayito import AsyncSandbox
async def main() -> None:
async with await AsyncSandbox.create(template="rayito-base-poly") as sbx:
out = await sbx.run_code("const x: number = 40 + 2; x", language="typescript")
print(out.text) # "42"
ctx = await sbx.create_code_context(language="javascript")
print((await sbx.run_code("[1, 2, 3].length", context=ctx)).text) # "3"
asyncio.run(main())
import { Sandbox } from "rayito";
await using sbx = await Sandbox.create({ template: "rayito-base-poly" });
console.log((await sbx.runCode("const x: number = 40 + 2; x", { language: "typescript" })).text);
console.log((await sbx.runCode("x + 1", { language: "ts" })).text);
const ctx = await sbx.createCodeContext({ language: "javascript" });
console.log((await sbx.runCode("[1, 2, 3].map((n) => n * 2)", { context: ctx })).text);
- Why Deno.
ijavascriptdoes not install onal2023-minimalARM64 without a compiler (Q57, below); Deno is a single binary linked against glibc that ships its own ZeroMQ and a Jupyter kernel (deno jupyter), and serves both languages (Q61). - The pin.
image/Dockerfiledownloadsdeno-aarch64-unknown-linux-gnu.zipfrom thev2.9.7release with a pinned sha256 checked withsha256sum -c(scripts/check_pins.pyrequires it,SECURITY.mdT10) and leaves it at/opt/rayito/deno/deno, owned by root. Bumping Deno means changingDENO_VERSIONandDENO_SHA256(the release's official.sha256sum), republishingrayito-base-polyand repeating its e2e; Dependabot does not track it. - Loopback TCP. Deno cannot open
ipcendpoints, so its kernels listen on127.0.0.1with the connection file's HMAC key (ADR-013 inARCHITECTURE.md); Python and bash stay onipc. Accepted consequence: a process in the sandbox itself can read the outputs of Deno cells (SECURITY.mdT12). - Interruption. The
timeoutinterrupts with a protocolinterrupt_requestmessage (Deno does not forwardSIGINT); if the kernel is not idle within 5 s, the agent restarts the context. An interrupted Deno cell does not emit anerrorof its own: the agent synthesizesExecutionTimeoutjust as with Python. NO_COLORand ANSI. The kernelspecs start Deno withNO_COLOR=1,DENO_NO_UPDATE_CHECK=1and aDENO_DIRunderHOME; in addition the sidecar strips the ANSI sequences from thetext/plainof Deno results (defense in depth), so.textis42and not\x1b[33m42\x1b[39m.- Permissions. Deno runs with all permissions (
allow_all), just like the Python kernel as uid 1000:Deno.env,Deno.readTextFile, network. npm:,jsr:andhttps:imports download code through the same egress as apip install(with the same outbound network policy).- Per-run
envsonly work in Python: in Deno use theenvsofcreate_code_context.
What remains Python-only¶
- The
e2b/chartande2b/dataformatters, the IPython configuration and the startup scripts: a bash kernel emitsstreamanderrorand the agent maps them tologs/errorwith the same rules. - The warm-up before
/readyand the kernel rotation on/run. - The reseed of
random/numpy.randomafter/resume: non-Python contexts are listed asskipped(their random state is that of the process itself).
Timeouts and errors in bash¶
The timeout follows the generic rule: when it expires the agent
interrupts the kernel (SIGINT, which bash_kernel forwards to its
bash) and, if it is not idle within 5 s, restarts the context; in both
cases the Execution carries error.name == "ExecutionTimeout" and the
next cell works (measured in Q57: sleep 30 with timeout=2 comes back at
5.8 s through the restart branch, because SIGINT does not leave
bash_kernel idle within 5 s). A command that exits with a non-zero code
ends with the error that bash_kernel emits: an empty name and the exit
code as value (false → ExecutionError(name='', value='1')); the
output keeps arriving through logs.stdout/logs.stderr.
UNIMPLEMENTED on rayito-base¶
On an image without the kernel, run_code("echo hi", language="bash")
fails before creating anything with UNIMPLEMENTED and a message that
names rayito-base-poly; an absent kernel is UnimplementedError in both
Python and TypeScript (0.4.0: no longer InvalidArgumentException/
InvalidArgumentError, because an absent feature is never an argument
error). The agent knows which kernels exist because the sidecar announces
them in ready (languages, according to the kernelspecs it could install
at startup).
Why not ijavascript¶
The original design planned ijavascript on Node 20 (dnf install
nodejs20). The 2026-09-16 test (Q57) ruled it out: ijavascript@5.2.1
depends on jmp@2, which only accepts zeromq@5, and zeromq@5.3.1 does
not publish prebuilt binaries for linux-arm64 or for Node 20 (ABI 115:
its prebuilds go up to node.abi108, only darwin-x64, linux-x64 and
win32); npm falls back to node-gyp rebuild, which dies with not found:
make because al2023-minimal ships no compilers. Since 0.3.0,
javascript and typescript are served by Deno, which needs none of that.
R and Java¶
They remain out (SPEC.md §4), with measured numbers: R through
conda-forge (r-base + r-irkernel) takes 1.4 GB installed, and R-core
through dnf 127 MB plus cairo, pango, harfbuzz, tk and fonts; Java would
be 262 MB of Corretto 21 headless plus IJava, unmaintained (one release
since 2023, does not compile on JDK 17/18).
Publish the poly variant¶
The same Dockerfile, a marker inside the zip
(kernel-sidecar/kernels_variant with poly) that activates a
conditional layer: it installs the pins from
kernel-sidecar/requirements-poly.txt (bash_kernel and its dependencies,
pip check), checks import bash_kernel as user, downloads and verifies
Deno 2.9.7 and checks deno --version as user. rayito-base carries no
marker and runs nothing from that layer.
# Linux / macOS
make image-publish-poly BUCKET=<bucket> # publishes rayito-base-poly
# Windows (no make; toolchain from CONTRIBUTING.md §1.3)
cargo zigbuild --release --locked --target aarch64-unknown-linux-musl -p rayd
cp target/aarch64-unknown-linux-musl/release/rayd image/rayd
python scripts/copy_sidecar.py kernel-sidecar image/kernel-sidecar
python scripts/image_zip.py image image/rayito-image-poly.zip --variant poly
uv run --project clients/python python scripts/publish_image.py \
--artifact image/rayito-image-poly.zip --variant poly \
--base-image-version 1 --bucket <bucket>
publish_image.py refuses to publish a zip whose marker does not match
--variant. The measured snapshotBuild sizes of rayito-base-poly and
of rayito-base rebuilt with the same Dockerfile (within the ±20 MB
memory / ±10 MB code band relative to 17.0) are in Q57.
Old agents¶
Agents before M7 (rayd < 0.2.0)
ExecuteRequest.language is an optional proto3 field: a rayd earlier
than 0.2.0 ignores it and runs the cell in the default context's Python
kernel without warning. Use an image 0.2.0 or later before relying on
language; the SDK/agent compatibility table is published with each
release (docs/RELEASING.md).
Agents before M9 (rayd < 0.3.0)
A rayd earlier than 0.3.0 does not know typescript: it answers
INVALID_ARGUMENT ("language must be one of python, bash, javascript")
and the SDKs deliver it as InvalidArgumentException /
InvalidArgumentError (also through the shim). javascript against
that agent is still UNIMPLEMENTED on every image. Publish
rayito-base-poly from a 0.3.0 or later release before using JS/TS.