Skip to main content

kern-sandbox

Run AI-generated code in a real sandbox, one fresh box per call, in about 4 ms.

kern-sandbox is the Python binding for kern: a rootless, kernel-enforced sandbox out of one static binary, with no daemon, no VM and no cloud. An agent's tool-call, a model's generated snippet, a notebook cell, a CI step: code that runs before anyone reads it gets its own box, and the box is thrown away after.

pip install kern-sandbox
import kern_sandbox as kern

r = kern.run_code("import sys; print(sys.version)")
print(r.stdout, r.success)

Network off, memory and PID caps the kernel enforces, capabilities dropped, a deny-by-default seccomp allowlist, and a wall-clock deadline applied from outside the box, so code that hangs cannot outlive it. Node and TypeScript get the same package on npm: kern-sandbox.

The failure comes back as data, not as an exception

This is the part that matters in an agent loop. A timeout, an OOM-kill, a blocked syscall or a missing interpreter is a typed field on the result, beside stdout and the exit code. Your loop reads a field and decides; it does not parse a traceback to find out that the sandbox, not the code, ended the run.

r = kern.run_code("while True: pass", timeout_s=5)
r.fault.type      # 'timeout'      the sandbox stopped it
r.success         # False

A Python exception in the code is NOT a fault. That is exit_code != 0, a traceback in stderr, and fault is None, because the code ran and the sandbox did nothing. fault is set only when the sandbox acted:

fault.type what happened
timeout the call exceeded timeout_s; the binding owns that deadline
oom SIGKILL and a memory cap that actually bound (kern reports enforcement on an unforgeable per-box channel, not on the workload's stderr)
killed SIGKILL that is not attributable to the box's own ceiling: host pressure, or a cap that did not bind here
escape_blocked a syscall the seccomp filter refused (SIGSYS)
exec_failed the box started, the command did not exist in the image; the message names both the binary and the image

An enforced pids cap produces no fault, deliberately. A refused fork returns EAGAIN, which a program is allowed to catch and exit 0 on, so a contained fork bomb reads as a successful run. Labelling that a sandbox fault would misreport a process that exited cleanly. The cap is still enforced: on WSL2, pids=32 blocked at 29 forks while pids=256 let 120 through.

A box that fails to start raises SandboxError instead, because the code never ran.

Use it from Claude Desktop or Cursor (MCP)

The package ships kern-mcp, a dependency-free Model Context Protocol stdio server that gives the model a local code interpreter: it writes code, kern runs it on your machine, and charts come back as images the model can see.

{
  "mcpServers": {
    "kern": {
      "command": "kern-mcp",
      "env": { "KERN_MCP_SETUP": "pip install numpy pandas matplotlib" }
    }
  }
}

Tools: run_code (python/bash, and node on an image that has it), write_file, read_file, list_files. File state persists across calls; each call is a fresh, network-off box. The tool schema names the configured image and says which interpreters it provides, so the model is not left to infer that from the enum.

Env var Default What it does
KERN_MCP_IMAGE python:3.12-slim OCI image the boxes run in
KERN_MCP_SETUP (none) one-time pip install ..., the ONLY network-on moment
KERN_MCP_MEMORY_MB 1024 hard RAM cap per box; 0 sends no flag, so a vcpu: profile's own memory= applies
KERN_MCP_TIMEOUT 60 per-call wall-clock deadline
KERN_MCP_WORKSPACE temp dir persist file state at this path
KERN_MCP_PROFILES (none) attach kern.toml profiles, e.g. vcpu:heavy,vgpio:sensors: the only way to grant an edge agent a hardware device
KERN_MCP_KERNEL off 1 routes Python through one warm interpreter: state persists, each call is sub-millisecond. The one case where "a fresh box per call" stops being true, and the tool description says so to the model
KERN_MCP_QUIET on 0 restores kern's non-fatal notes
KERN_MCP_TMPFS_MB 64 scratch at /tmp, charged to the box's own memory cap; 0 removes it and puts /tmp back inside the read-only root

Why local rather than hosted. E2B, Modal and Daytona need an account, an API key and a network round-trip, and the model's code runs on someone else's machine. This runs on yours: no account, no egress, works air-gapped, same shape.

The model: file state persists, processes do not

  • File state persists through a /workspace directory shared into every box. Write a file in one call, read it in the next.
  • Processes are ephemeral. Each call is a fresh box, so in-memory state does not survive: x = 40 in one call is gone in the next. Write to disk if you need continuity, which agents should do anyway, since it survives a crash and can be inspected.

That is deliberate: it keeps the density (hundreds of ephemeral boxes, not hundreds of resident interpreters holding RAM). When you do want in-memory state, open a kernel(): one warm interpreter in a long-lived box, per-cell cost sub-millisecond instead of a ~12 ms CPython boot, with the explicit trade that cells share one process and one box.

with kern.Sandbox() as sbx, sbx.kernel() as k:
    k.run_code("import numpy as np; a = np.arange(1_000_000)")
    r = k.run_code("a.sum()")          # 'a' is still here
    print(r.results[0].text)           # 499999500000

Charts and rich results, without a Jupyter kernel

run_code captures mime-typed values into result.results the way a notebook cell does, with no Jupyter kernel: the last bare expression, every display(obj), and every open matplotlib figure automatically, with no savefig. Accessors: .png, .jpeg, .html, .svg, .markdown, .json, .text.

with kern.Sandbox(setup="pip install pandas matplotlib") as sbx:
    sbx.write_file("data.csv", "a,b\n1,2\n3,4\n")
    r = sbx.run_code("import pandas as pd; pd.read_csv('data.csv').describe()")
    r.results[0].html          # the DataFrame as an HTML table

    r = sbx.run_code("import matplotlib; matplotlib.use('Agg')\n"
                     "import matplotlib.pyplot as p; p.plot([1, 4, 9])")
    png = next((x.png for x in r.results if x.png), None)   # PNG bytes, send it to the model

Capture never touches stdout, stderr or exit_code. Pass on_stdout / on_stderr to stream output as it arrives (best-effort: a slow callback drops chunks rather than stalling the box).

Safe by default

A bare Sandbox() has no network, no host mounts, seccomp on, dangerous capabilities dropped and a mandatory finite timeout. Every relaxation is a named argument:

Sandbox(
    image="python:3.12-slim",   # OCI image
    setup="pip install pandas", # the ONLY network window: a separate net-on box; run_code is net-off
    workspace=None,             # None -> temp dir, deleted on exit; a path -> persists
    memory_mb=512,
    cpus=None,                  # CPU cap in cores (e.g. 1.5); None = uncapped
    pids=256,                   # fork-bomb ceiling
    timeout_s=30,               # MANDATORY per-call wall-clock limit
    network=False,              # RELAXES ISOLATION: True shares the host network for every run
    mounts=None,                # {host_src: box_target}; sensitive sources refused even if asked
    tmpfs=None,                 # None -> 64 MiB of scratch at /tmp; {} -> none; {"/tmp": "512m"}
    profiles=None,              # kern.toml profiles: ["vcpu:heavy", "vgpio:leds", "vdisk:scratch"]
    max_output_bytes=64 << 20,  # cap on captured stdout/stderr EACH; result.truncated on overflow
    deps_readonly=True,         # run_code cannot modify setup= deps; False re-opens it
    security_profile=None,      # "untrusted" = seccomp allowlist + cap-drop ALL + read-only root
    apparmor=None,              # a pre-loaded AppArmor profile; kern fails CLOSED if it is not loaded
    require_limits=False,       # True = refuse to start unless memory/pids caps are enforced
    cap_drop=("ALL",),          # default drops ALL; pass () only if the box must bind a port < 1024
)

Mounts over sensitive sources (/, /etc, $HOME, the docker socket) are refused even if you ask for them, and so is a tmpfs that would COVER a mounts bind: mounts stack, kern puts the tmpfs on top whatever the argument order, and the bind's files would be present on the host and invisible in the box. "Cover" is the mountpoint relation, not a string compare, so a tmpfs at /tmp is refused against a bind at /tmp and at /tmp/sub. The other direction is legal and is not refused: mounts={host: "/tmp"} with tmpfs={"/tmp/scratch": "8m"} gives a persistent /tmp with a bounded ephemeral subtree, and both halves work. Captured output is bounded, so a flooding box cannot OOM the host.

setup= output is read-only to your code, by default. .deps is what setup= installed, so run_code mounts it read-only and a cell cannot change what the next cell imports. This closes a cross-call vector that is not obvious: .pyc files are validated on the source's timestamp and size, so a cell could rewrite a dependency's BYTECODE, re-paste the legitimate 16-byte header, leave the .py untouched, and the next import would run it, invisibly to both result.files and list_files(). Not a sandbox escape, since both cells are your untrusted workload; what it protects is the assumption that import x in call N+1 runs the x call N could see.

The setup box compiles the bytecode before the mount closes, so the default costs nothing: without that step a session whose setup skipped compilation paid +40 ms on every call, forever (250 ms against 290, measured on requests). Pass deps_readonly=False if a workload legitimately writes into .deps at run time, and note that it will get EROFS rather than a silent failure.

Network policy: the network is on only during setup=, in a separate box that dies when setup ends. There is no per-call override; network=True is a session-level, explicit choice.

Resource profiles attach slices defined once in ~/.config/kern/kern.toml: vcpu: (CPU and memory), vdisk: (a size-capped scratch disk), vgpio: (a specific device set, the only way to give a box hardware). A vcpu: profile can carry memory=, but memory_mb defaults to 512 and an explicit flag beats a profile, so pass memory_mb=None to let the profile's own value apply.

Not capped: the workspace on disk. It is a host directory, and file state persisting is the point. A cell writing in chunks put 400 MB on the host under memory_mb=128, because a memory cap only stops the version that builds the payload in RAM first. Where that matters, point workspace= at a filesystem you have already bounded.

Writable paths: /workspace, /tmp and /dev/shm. The box root is read-only, so /tmp is a 64 MiB tmpfs the binding mounts for you. Two things break without it, and both are quiet: a write naming /tmp fails with EROFS, and tempfile falls back to the current directory, putting scratch into your persistent workspace where list_files then reports it. The bytes are charged to the box's own memory cgroup, so filling /tmp is an OOM of the box and never the host disk. Resize it with tmpfs={"/tmp": "512m"}, remove it with tmpfs={}, or bind your own directory at /tmp through mounts and the default steps aside (including a :ro bind, which leaves /tmp read-only: that is your call, not an accident). The unit is required and the target may not contain a :. kern's CLI takes both spellings and means the opposite of what you do: a bare "64" is 64 BYTES, "0" is UNLIMITED, and ["/scratch:9g"] mounts /scratch at 9 GiB rather than a directory by that name. All three measured, all three refused here with the reason. A size larger than memory_mb is refused for the same family of reason: df would report it to a program that preflights, which then plans against a number that OOM-kills it. The binding's own default is clamped to half the cap instead.

memory_mb bounds the cgroup, not the workload's usable memory. The cap is shared with memory-backed filesystems in the same box, and one of them is not bounded at all, so a box sized for a job can still be killed by a path the caller never mentioned. Measured: 200 MiB written to /dev/shm under memory_mb=128 OOM-kills the box no matter what /tmp is clamped to, while the same 200 MiB to /tmp returns ENOSPC and the box lives.

/dev/shm is the one this SDK does not control: it is present in every box, it is a tmpfs with no size at all (measured at 15.6 GB, half of host RAM), and tmpfs={"/dev/shm": ...} is refused by kern because it would shadow the hardened /dev. It is charged to memory_mb like any tmpfs, so the memory cap is the only thing bounding it. Docker sizes this with --shm-size; kern has no equivalent yet. Its apparent size is a fact about the HOST rather than about your box: no size= means the kernel's tmpfs default, half of host RAM, so the same code sees 2 GB on a 4 GB board and 64 GB on a 128 GB server while memory_mb says 128. mounts={host_dir: "/dev/shm"} IS accepted and STACKS on top of kern's own mount rather than replacing it (the last mount is the one that resolves), and it is a real workaround rather than only an access fact: measured through the bind, multiprocessing.shared_memory and a multiprocessing.Queue (POSIX semaphores) both still work, because shm_open is a path-based open and neither asserts on the filesystem type. Two costs come with it. A plain directory swaps an unbounded RAM path for an unbounded DISK one, so to bound it you bind a host directory that is itself a sized tmpfs. And it has no tmpfs lifetime: a file written to /dev/shm in the box is still on the host after the box dies, which is a residue class the real mount does not have. And Python's multiprocessing uses /dev/shm by default, so this is not a corner. The first sentence of this paragraph said "and nothing else" until a test that pins the writable set per security profile said otherwise.

Scratch does not survive a call, except in a kernel(). Each run_code is a fresh box, so /tmp is fresh too while the workspace persists. A kernel() is one long-lived box and the opposite holds: its /tmp accumulates. Measured at 10 MiB per step under the 64 MiB default, ten run_code calls all pass and ten kernel cells fail from the seventh with OSError: [Errno 28]. A read-only /tmp failed loudly at the moment of the mistake; now a tool that writes state to the workspace and a lock to /tmp writes both, and the next call finds the state pointing at a path that is gone. Put anything a later call must find in the workspace. The setup= box is the exception: an install needs unbounded scratch, so the default is not applied there (an explicit tmpfs= still is).

Toolchains in the box. npm, Go, Rust and .NET cache under $HOME, and $HOME is inside the read-only root. The scratch at /tmp is half the answer; HOME is the other half, and no error says so. Go reports failed to initialize build cache at /root/.cache, which is true and does not mention HOME. npm is worse: a failed mkdir /root/.npm reaches the user as Invalid response body while trying to fetch https://registry.npmjs.org/express, which reads as a network fault and is not one. Measured on node:22: neither -> exit 2, HOME alone with a read-only /tmp -> still exit 2, both -> exit 0.

Sandbox(
    image="golang:1.23-alpine",
    env={"HOME": "/workspace"},   # npm's ~/.npm, Go's ~/.cache, Rust's CARGO_HOME, .NET's NuGet
    tmpfs={"/tmp": "512m"},       # scratch; 64 MiB fits a small install, a real one needs more
)

That message is verbatim from a box, and the recipe above is what makes the same build print its output. Point HOME at the workspace, not at the scratch: npm install webpack webpack-cli typescript eslint needs 81 MiB of cache, so HOME=/tmp fails with ENOSPC against the 64 MiB default while HOME=/workspace succeeds. One small package fits either way, which is why testing with express proves nothing.

Two numbers inside a box describe the host, not your box, and a program will act on them. df reports a tmpfs's own size, and nproc reports the host's CPU count: measured under cpus=0.5, nproc says 28 while cpu.max says 50000 100000, so make -j$(nproc) starts 28 jobs against half a core and a pids ceiling. The same shape reaches SQLite, which spills CREATE INDEX into /tmp: a 309 MB database on the workspace fails with database or disk is full while df /workspace shows 202 GB free, and by the time you look, /tmp is empty again because SQLite cleaned up. Point TMPDIR at the workspace, or raise the scratch, when the job sorts more than it can hold.

setup= installs Python packages into the workspace, not system packages into the image. The root is read-only, so a package manager cannot run at all: apk add git answers ERROR: Unable to lock database: Read-only file system, and apt-get install fails the same way. If the job needs git, make or a compiler, that is a choice of image=, not something setup= can add.

max_output_bytes limits what you RECEIVE, not what the job costs. Measured: past the cap the output is discarded and the process keeps running to the end, so a marker file written after the noisy part is there and exit_code is 0 with truncated=True. A runaway producer therefore runs until timeout_s, and the two caps are per-stream, so a failure on stderr survives a flood on stdout.

A JVM's heap and this scratch add up to less than the cap by luck, not by design. The JVM takes 1/4 of the cgroup (measured: MaxHeapSize 134217728 under memory_mb=512) and the scratch clamp takes at most 1/2, and 3/4 fits. Write -Xmx at 3/4 of memory_mb, which people do, and the composition breaks: neither side knows about the other, and /dev/shm is in the same budget with no bound at all.

track_files reports the workspace, and only the workspace. A job whose product lands in /tmp reports nothing changed while having produced output. Measured: writing /workspace/a and /tmp/b in one call reports ['a'].

Nothing in /tmp survives a snapshot. A tmpfs is on no layer, so a marker written to the scratch is gone after restore while the workspace marker is there. A setup= that stages files in /tmp loses them.

matplotlib works and complains. It falls back to a temporary MPLCONFIGDIR because $HOME is not writable, so the figure is produced AND stderr carries mkdir -p failed for path /root/.config/matplotlib: [Errno 30] Read-only file system. exit_code == 0 is green for a run the user will report as broken. Pass env={"MPLCONFIGDIR": "/tmp"}, which is what the MCP server already does.

Server images need three things, and each announces itself separately. Measured on nginx:alpine: open("/run/nginx.pid") failed (30: Read-only file system), then chown(...) failed (1: Operation not permitted), then it serves.

Sandbox(image="nginx:alpine",
        tmpfs={"/run": "1m", "/var/cache/nginx": "16m", "/var/log/nginx": "4m"},
        cap_drop=())   # CAP_CHOWN is in the default drop, and nginx chowns its cache

cap_drop=() widens the default posture, and it is the only recipe here that does: measured, CapEff goes from 0000000000000000 to 00000110bd84efff. Under security_profile="untrusted" it does not, because the bundle wins over the option (CapEff stays zero even with cap_drop=()), so a server image and that bundle are mutually exclusive today. Both facts are pinned by the posture test.

Name the REAL mountpoint: /var/run is a symlink to /run on Alpine, and a tmpfs at the alias leaves the path the program opens untouched. And a server that refuses to run as root (postgres: initdb: error: cannot be run as root) has no answer here yet, because this binding does not expose kern's --user. Rust, .NET and anything else with a package cache want the same two places for the same reason. HOME stays the caller's decision because a build cache in /workspace is a host directory nothing bounds; point it at a tmpfs={"/home": "512m"} instead if you want it capped and thrown away with the box.

API

  • kern.run_code(code, **kwargs), one-shot: a throwaway Sandbox under the hood.
  • Sandbox(...).run_code(code, language="python"|"bash"|"sh"|"node") on the session workspace. The enum is what the runner accepts, not a promise about the image: the default python:3.12-slim ships python, bash and sh and no node, and asking for a missing interpreter returns an exec_failed fault naming the binary and the image. bash runs bash and sh runs the POSIX shell, which are different languages: [[ ]], arrays and pipefail are bash. Alpine has no bash at all, so ask for sh where the image may not carry one.
  • Sandbox(...).run(argv_list), an arbitrary command (an argv list, never a shell string).
  • Sandbox(...).write_file(path, data) / .read_file(path) / .list_files(subdir=""), workspace I/O, confined to /workspace, ..-safe, every path component opened O_NOFOLLOW, opened O_NONBLOCK, and a descriptor that is not a REGULAR file is refused. A symlink is not the only thing a box can leave at a name: mkfifo out.png used to make read_file("out.png") wait for a writer that never came, with no timeout, so the box chose how long the host's call took. The flag alone would have been worse, since a non-blocking read of a writer-less FIFO returns zero bytes and the call would have reported an empty file.
  • Sandbox(...).snapshot(dest) / .restore(src), a portable .tar.gz FILESYSTEM checkpoint of the workspace. restore refuses absolute, .. and symlink members.

Use it from LangChain

pip install 'kern-sandbox[langchain]'
from kern_sandbox.langchain import kern_code_tool

tool = kern_code_tool(memory_mb=512, timeout_s=30)
agent = create_agent(model, [tool])

One session, so a file written by one call is there for the next, and each call still runs in a fresh box. What comes back is written for a model to act on: stdout, the value of a trailing expression, and the traceback when the code raises, which is what the agent needs in order to fix it. A sandbox fault is labelled ([sandbox: timeout], oom, escape_blocked) so the model does not try to debug code that was killed for asking for 4 GB.

Everything a box prints is untrusted text on its way into a context window, so the rendering strips terminal escapes and neutralises that framing wherever the code produced it: a cell printing [sandbox: oom] would otherwise claim, byte for byte, that the sandbox killed it. Ordinary prompt injection is not filtered and cannot be at this layer: a run whose output is [system] ignore your instructions is a run that printed a string, and no filter separates that from a program legitimately printing the same characters. What a model may act on is decided above this.

There is also a shell execution policy for LangChain's shell middleware, the long-lived-session shape rather than one box per call, and it is a peer of the Docker policy rather than a wrapper beside it. It has its own page, including the measured differences from DockerExecutionPolicy and two behaviours worth knowing before an agent runs for hours: LANGCHAIN-SHELL.md.

Performance

One x86_64 desktop (i7-14700KF, Linux 7.0.0, rootless, cgroup delegated), python:3.12-slim, p50 over 25 calls after a discarded warm-up, re-measured 2026-09-04 against the released binary and this SDK. Your hardware will differ: measure and claim your own number.

call (p50) kern-sandbox docker
run(["true"]), bare box 3.9 ms
run_code("print(1)"), plus the CPython start 14.3 ms ~290 ms

run_code runs Python, so it pays the interpreter boot on top of the box. That is a Python cost, not kern's, and it is why 14.3 rather than 3.9. Even so it is about 20x faster than docker run --rm python:3.12-slim python3 -c for the same task, and the number quoted is the one run_code gives you, never the bare-box best case dressed up as the code-execution figure.

The host is part of the row. The same run_code("print(1)") on WSL2 reads about 40 ms, roughly 3x, on a call dominated by the CPython start. Quote the row that matches your host.

The image is part of the claim. python:3.12-alpine reads ~17 ms, because that interpreter starts slower. Every row here is python:3.12-slim, docker's included.

Concurrency: 100 concurrent run_code calls on one Sandbox complete in 0.30 s wall clock, 100/100 succeeded, no leaked boxes. The per-call p50 of 211 ms in that run is queueing, not latency: 100 boxes are competing for the machine, and the wall clock is the figure that describes it.

enforce_limits=False is not a speed knob any more. It sets KERN_NO_SCOPE=1 and skips the per-box cgroup scope. That used to be a systemd-run round trip worth several milliseconds, which is where "about twice as fast" came from; kern now applies caps directly in its own delegated slice and the measured difference is 0.19 ms, against giving up hard memory and PID enforcement. Leave it on. On a host with no cgroup delegation at all the old cost returns, which is why the option stays.

Full method and the comparison against other runtimes: BENCHMARKS.md.

Threat model (honest)

kern is a kernel-boundary sandbox for your own or semi-trusted code. The default seccomp filter is a deny-by-default allowlist (moby's own default minus kern's 35 escape syscalls): suitable for agent-generated code, not a hard boundary against deliberately hostile multi-tenant code. For that, use a microVM (Firecracker, Kata) or gVisor. security_profile="untrusted" bundles the allowlist with --cap-drop ALL and --read-only. The full statement is in SECURITY.md.

Requirements

The kern binary on PATH (or $KERN_BIN). A Linux kernel with unprivileged user namespaces and cgroup v2; on Windows it runs under WSL2. Python 3.9+.

On a Mac this package installs but cannot run, and it says so rather than looking for a download that does not exist: kern is Linux-only, because macOS has no namespaces and no cgroups. Run it inside a Linux VM (colima, Lima, OrbStack, UTM). Verified on Apple Silicon with an Ubuntu 24.04 guest. Install notes.

License

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kern_sandbox-0.1.36.tar.gz (178.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kern_sandbox-0.1.36-py3-none-any.whl (99.3 kB view details)

Uploaded Python 3

File details

Details for the file kern_sandbox-0.1.36.tar.gz.

File metadata

  • Download URL: kern_sandbox-0.1.36.tar.gz
  • Upload date:
  • Size: 178.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for kern_sandbox-0.1.36.tar.gz
Algorithm Hash digest
SHA256 ac37a12cd37bbab0abbecd95cc0d217c03feff380ea2f770d3e473e8ef0ba9a7
MD5 e91b6914110665c9147d844035b8c230
BLAKE2b-256 829bd62e31b75b67fae0faea12368d3dac54f96e8b8c58f610402dad0aaa6d55

See more details on using hashes here.

File details

Details for the file kern_sandbox-0.1.36-py3-none-any.whl.

File metadata

  • Download URL: kern_sandbox-0.1.36-py3-none-any.whl
  • Upload date:
  • Size: 99.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for kern_sandbox-0.1.36-py3-none-any.whl
Algorithm Hash digest
SHA256 321a1b6faaa583b0e3f0c96203729d1063acc468c02981213eba97fe69b37863
MD5 74d90696710eb77de20b5d7064545263
BLAKE2b-256 609ff35a467b026fa8478383e3d36bceb900351966e401fd356b34505b8b0715

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.42

2 files

0.1.41

2 files

0.1.40

2 files

0.1.39

2 files

0.1.38

2 files

0.1.37

2 files

This release

0.1.36 This release

2 files

0.1.35

2 files

0.1.34

2 files

0.1.33

2 files

0.1.32

2 files

0.1.31

2 files

0.1.30

2 files

0.1.29

2 files

0.1.28

2 files

0.1.27

2 files

0.1.26

2 files

0.1.25

2 files

0.1.24

2 files

0.1.23

2 files

0.1.22

2 files

0.1.21

2 files

0.1.20

2 files

0.1.19

2 files

0.1.17

2 files

0.1.16

2 files

0.1.15

2 files

0.1.14

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page