Upgrading
pydeno is pre-1.0: a minor release can change behaviour. This page lists, per minor, what changed in
a way you could notice, who it touches (Runtime is the in-process runtime, IsolatedRuntime the
sandboxed worker, which also covers AgentSandbox and pydeno.configure_default_runtime(isolated=True)),
and what to change. The full history is in CHANGELOG.md; facts here come from it and from
git diff v0.4.5 v0.5.0 and git diff v0.5.0 v0.6.0.
0.4.x to 0.5.0
The changelog says "no breaking changes: every new limit is opt-in and Runtime is unchanged".
IsolatedRuntime is new in 0.5.0, so nothing in it can break an existing caller.
| Change | Affects | Who notices | What to change |
|---|---|---|---|
import pydeno is lazy: IsolatedRuntime, WorkerCrashed, ToolBridge, ToolError and friends, WEB_POLYFILLS, snapshot signing load on first use |
Runtime |
Code that reaches into private modules after a bare import pydeno, or that measured import time (about 51 ms down to 19 ms) |
Import what you use (from pydeno import ToolBridge works unchanged); import pydeno._tools explicitly if you really need it |
Default-runtime helpers can return an IsolatedRuntime (new configure_default_runtime) |
Runtime |
Type checkers: get_default_runtime() is now Runtime \| IsolatedRuntime |
Nothing at run time unless you opt in; narrow the type or call configure_default_runtime() once |
deno_core 0.409 to 0.412 (V8 stays 150.4) |
Runtime |
Anyone who pinned behaviour of a particular deno_core |
Re-run your test suite; no API change |
| Release extension is stripped and LTO'd (macOS 60 MB to 43 MB) | both | Native crash reports have fewer symbols | Keep a debug build if you symbolicate native crashes |
Dropping a SnapshotBuilder without build() no longer leaks its isolate |
Runtime |
Nobody (a fix) | Nothing |
New opt-in RuntimeConfig(max_buffer_bytes=...) |
Runtime |
Nobody unless set | Set it to bound ArrayBuffer bytes (max_heap_size does not) |
New IsolatedRuntime, sign_snapshot / verify_snapshot, clock=, random_seed=, WEB_POLYFILLS |
new | Adopters | See the isolation guide |
Isolation wire uses a native JSON codec; the native parser is stricter than json.loads about encodings (no BOM, no UTF-16/32, no raw surrogates) |
IsolatedRuntime |
Only code that feeds odd encodings through the boundary | Send well-formed UTF-8 |
0.5.x to 0.6.0
0.6.0 is the first release whose defaults change. Most items touch IsolatedRuntime only.
| Change | Affects | Who notices | What to change |
|---|---|---|---|
redact_host_errors now defaults to True (was False) |
IsolatedRuntime |
Guests, tests or logs that read a host function's exception text: they now see a generic message with the class name | Pass redact_host_errors=False if the text is safe to show the guest; otherwise branch on the class name |
max_buffer_bytes defaults to max_memory // 4 |
IsolatedRuntime |
Guests that allocate big typed arrays: an over-budget allocation is now a catchable RangeError, not a worker killed for memory |
Set max_buffer_bytes= (or raise max_memory) if you legitimately need more |
Signed snapshots are bound to the pydeno release (format pydeno-snap2); a snapshot signed by another release is refused |
Runtime (and anyone using sign_snapshot) |
Anyone who stores signed snapshots across upgrades: verify_snapshot raises SnapshotAuthenticationError |
Rebuild and sign snapshots with the release you run |
RuntimeConfig(snapshot=...) is refused by IsolatedRuntime (it used to be silently dropped, with its bootstrap) |
IsolatedRuntime |
ValueError at construction |
Use bootstrap= source instead of a snapshot |
Two more V8 flags in the worker (linear-time regexp fallback, --freeze-flags-after-init) |
IsolatedRuntime |
Anything that passed v8_flags that must change after V8 starts |
Pass flags at construction only |
sandbox="auto" emits a RuntimeWarning when the platform's full set of layers did not apply |
IsolatedRuntime |
Test suites with -W error; log noise on kernels without Landlock |
Use sandbox="require" in production, or filter the warning where degraded is expected. pydeno.sandbox_status() answers it up front |
Bind names must be plain identifiers (IsolatedRuntime now checks, as ToolBridge already did) |
IsolatedRuntime |
ValueError for names such as "my-tool" or "a.b" |
Rename to [A-Za-z_][A-Za-z0-9_]* |
| Startup self-test: the worker tries forbidden operations before guest code and refuses to start if one works | IsolatedRuntime |
WorkerCrashed("worker failed to start: sandbox self-test failed ...") on a host with a gap |
Treat as a real finding, not a flake (it is sandbox_unavailable in classify_error) |
sandbox="require" refuses when the worker's memory, CPU or thread usage cannot be read (hardened /proc, missing libproc) |
IsolatedRuntime |
WorkerCrashed("... cannot be enforced on this system ..."); other modes only warn |
Make /proc readable, or accept sandbox="auto" knowing the limits will not fire |
| Host-call arguments from the guest are capped (1M values, depth 128) and the bridge captures its intrinsics, runs in strict mode | Runtime and IsolatedRuntime |
A guest passing a huge or deeply nested argument to a host function now gets a RangeError |
Pass less data, or chunk it |
| A worker with more than 64 threads is killed; memory and threads are watched while a host function runs; the in-flight host-call cap is runtime-wide | IsolatedRuntime |
WorkerCrashed for thread floods; max_inflight_host_calls counts calls across commands |
Lower concurrency in the guest, or raise max_inflight_host_calls |
macOS profile no longer allows sysctl-read and denies process-info, IOKit, hardware-ID and host-statistics routes |
IsolatedRuntime (macOS) |
A library that read host facts (CPU count, memory size) inside the guest | Pass such facts in as arguments |
Linux seccomp: stream-only socketpair, prctl allow-list, no sysinfo, no executable mappings when jitless, and more |
IsolatedRuntime (Linux) |
A bundle that relied on one of those syscalls | Run it on the 0.6 matrix (scripts/linux_matrix.sh); report real regressions |
Forked children forget the parent's workers; eval_async uses a thread of its own |
IsolatedRuntime |
Pre-fork servers that shared a runtime with children | Create the runtime after the fork |
New AgentSandbox, pydeno.integrations.pydantic_ai, optional extra pydantic-ai |
new | Adopters | See Agent sessions |
0.6.x to 0.7.0
Mostly additions (async classes, results, schema tools, diagnostics). Four things can change behaviour you have today.
| Change | Affects | Who notices | What to change |
|---|---|---|---|
A JS Map, WeakMap, WeakSet or Error crossing the boundary raises (Cannot serialize ...); it used to become an empty dict silently |
both | Code that returned one of these and got {} |
Convert first: Object.fromEntries(map), [...map], {name: e.name, message: e.message} |
AgentSandbox.dump() after a crash, timeout or kill returns the last good journal plus a lost record; it used to raise |
AgentSandbox |
Code that relied on the error to detect a dead session | Check is_closed(); load() charges the lost run's tool calls (no budget refund) |
AgentSandbox always routes console.* through the parent, and Done/Failed carry the console output (left out of equality) |
AgentSandbox |
A host that set max_host_calls: console calls now count against it |
Raise the cap, or stop logging in a loop |
Messages pydeno writes itself (catalog guidance, wrong-argument TypeError) are not hidden by redact_host_errors; errors from host tools are still redacted |
AgentSandbox |
Code that matched on the generic "host function failed" text for those | Match on the error class |
The isolation worker starts with -I -S and imports pydeno from the parent's own package directory (no site, so no .pth or sitecustomize runs in the worker); a custom python= is started as before |
IsolatedRuntime |
Code that relied on a .pth file or sitecustomize taking effect inside the worker |
Do that work in the parent, or pass what the worker needs through bootstrap/bound functions |
The worker's seccomp filter denies memfd_create |
IsolatedRuntime (Linux) |
Nobody running normal JavaScript | Nothing |
New: AsyncIsolatedRuntime, AsyncAgentSandbox, SessionPool, ExecutionResult, SchemaTool and the lazy catalog, sandbox_status(), classify_error(), check_source() |
new | Adopters | See the async guide, async agent sessions and the reference |
0.6.1 fixed a macOS-only bug: the sandboxed worker aborted at start on Python 3.10 to 3.12. If you are on 0.6.0 there, upgrade.
0.7.x to 0.8.0: the Pydeno front door
Three things change behaviour: python -m pydeno (the CLI rows below), the limit fixes
(the last rows of the first table) and the restrictions from the 0.8 red team (the second table;
three rows are marked BREAKING). Pydeno / AsyncPydeno and their sessions, snapshots, limits
and errors are new names; otherwise every existing class keeps its behaviour, and the docs now lead
with Pydeno and file the building blocks under "Advanced".
| Change | Affects | Who notices | What to change |
|---|---|---|---|
| A supervisor without worker termination authority is refused in every sandbox mode | IsolatedRuntime, async and front doors |
Startup raises non-retryable sandbox_unavailable, including auto and off |
Restore signal permission to the hardened worker; changing sandbox mode cannot restore enforcement |
New: Pydeno, AsyncPydeno, PydenoSession, AsyncPydenoSession, PydenoSnapshot, AsyncPydenoSnapshot, PydenoComplete, PydenoLimits and the PydenoError family |
new | Adopters | See the front-door guide; a Monty user can keep their code's shape |
AgentSandbox / AsyncAgentSandbox accept runtime= (an already-built, fresh runtime, such as a pool checkout) |
new, opt-in | Nobody unless passed | Nothing |
classify_error() sees through a PydenoError to the pydeno exception it wraps |
new | Nobody | Nothing |
New: Pydeno(max_tool_threads=...) / AsyncPydeno(max_tool_threads=...) (default 128, clamped to the process ceiling of 512) and ToolThreadLimitError: per-pool cap on session tool threads (an upper bound, not a reservation) |
new | Pools whose sessions hold many tool threads at once (many concurrent sessions calling externals, or externals that never return); processes with more than four default pools open (a RuntimeWarning) |
Raise or lower the caps so they fit the ceiling, or give externals their own timeouts |
AgentSandbox.run() / execute() drive the worker from the calling thread, which keeps enforcing every limit; tool calls are answered on the session's own threads (a tool thread for plain functions, its loop thread for coroutine functions; started at its first tool call, never shared with another session), in the order the guest made them, as before, each call in a fresh copy of the caller's context, instead of on the calling thread |
AgentSandbox |
Tools that read the caller's thread-local state (threading.local()): they no longer see it. Tools see the caller's contextvars as before (a copy per call) |
Keep per-call state in contextvars or closures, not thread-locals |
AsyncAgentSandbox.run() / execute() stop waiting for a tool once its run has ended (the supervisor killed the worker for max_pause, the CPU cap or memory): the call raises at once instead of when the tool returns; the tool is cancelled |
AsyncAgentSandbox |
Nobody, unless they waited for a slow tool to finish after its run was killed | Nothing |
SandboxPool builds its runtimes through an overridable core (_core_type, private) |
internal | Nobody | Nothing |
python -m pydeno runs code in IsolatedRuntime(sandbox="require"); it used the in-process Runtime |
the CLI | Scripts that ran python -m pydeno on a machine without the complete OS sandbox: they now exit with code 4 |
Pass --sandbox auto, or --no-sandbox for code you trust; pydeno.sandbox_status() shows what is missing |
| A positional argument is JavaScript to evaluate; it used to be a file name | the CLI | python -m pydeno script.js now evaluates the text script.js (a ReferenceError, exit code 1) |
python -m pydeno -f script.js |
The result prints as JSON ("text" with quotes, {"a": 1}); it used to print Python's str() of it |
the CLI | Scripts that parse the output | Parse it as JSON, or pass --raw for a plain string |
A JavaScript error exits with code 1 and pydeno: js_error: ... on stderr; a timeout exits 3, a missing sandbox 4, another failure 5, a result with no JSON form 6 (all used to exit 1) |
the CLI | Scripts that match on the old JavaScript Error: text or treat every failure the same |
See the exit codes in Command line |
The code runs in a separate worker with --jitless V8 (no WebAssembly), a 30 s default deadline and a 1 GiB memory cap; the old CLI had no deadline |
the CLI | Code that ran long, used a lot of memory or used WebAssembly from the CLI | --timeout, --max-memory; for WebAssembly use the API (IsolatedRuntime(jitless=False)) |
Limit values are validated: NaN, infinity, durations of zero or below (where 0 is not meaningful) or above about 70 years, and counts outside their range raise ValueError; a bool, a string, None where a value is required, or a float for a count raises TypeError (max_memory must be an int between 1 and 2**53 - 1) |
IsolatedRuntime, AsyncIsolatedRuntime, pool constructors and checkout(), AgentSandbox/AsyncAgentSandbox, SessionPool (and get(timeout=)), PydenoLimits, per-call timeout= |
Code passing such values (NaN and infinity silently disabled the limit before); code that caught ValueError for a wrong type (OutputCapture(1.5), AgentSandbox(max_tool_calls=True), ...) now gets TypeError; a Decimal used as a count (it was accepted by comparison) |
Pass None to remove a limit; use an int (or numpy int) for byte and call counts; real numbers (Fraction, Decimal, numpy floats) still work as seconds |
Console output pauses the hard deadline only within an allowance of one hard deadline per command (it used to pause it like a tool call, up to max_host_wait); console time during an in-flight tool call is covered by the tool's pause |
runtimes with on_console / capture_console, agent sessions, Pydeno feeds |
A host with a slow on_console or print_callback and a very chatty guest: the run now ends by about twice its deadline instead of running up to max_host_wait |
Make the console handler fast (buffer it), or raise the deadline |
Console calls are no longer refused by max_inflight_host_calls (they still count toward max_host_calls) |
runtimes with a small in-flight cap and console routing | Nobody, unless they relied on console output being dropped while tools were in flight | Nothing |
Pydeno / AsyncPydeno without a print_callback write at most 1 MiB of console output per feed to stdout/stderr, then one [truncated] line |
the front door's default printer | Feeds that print more than 1 MiB and read it from the host's stdout | Pass a print_callback (it is not capped) |
Restrictions and fixes from the 0.8 red team (host boundary and state). The first three can change behaviour you have today:
| Change | Affects | Who notices | What to change |
|---|---|---|---|
BREAKING: a tool named like a guest global (JSON, Promise, console, globalThis, eval, Math, ...) installed as a bare global, a namespace with such a name, or any tool name starting with __pydeno / __host_op, is refused with ValueError |
AgentSandbox, AsyncAgentSandbox, SessionPool |
Constructing the session raises (such a tool replaced the global and broke the session or was unreachable) | Rename the tool, or pass namespace="tools" (tools.JSON(...) is fine) |
BREAKING: SessionPool keeps a session's spent tool budget when its journal outgrows max_journal_bytes: the next get restores a stateless session with the budget spent (lost_runs == 1) instead of a fresh one |
SessionPool |
Code that relied on JournalTooLarge handing out a fresh budget |
drop() the session if you want to start over with a fresh budget |
BREAKING: ExecutionResult.stdout/stderr (and Done/Failed's), error messages and the front door's default printer replace C0/C1 controls (except tab and newline), bidirectional overrides and isolates, line and paragraph separators, zero-width space, word joiner, invisible operators, soft hyphen, BOM and Unicode tag characters with ? (the CLI already does) |
IsolatedRuntime.execute, agent sessions, Pydeno |
Guests that print ANSI colours, explicit bidi controls or emoji sequences (joiners and variation selectors become ?, as in the CLI); tests comparing such output |
Nothing for ordinary text (letters of every script are kept). A print_callback of your own still gets the raw text |
A tool that raises a BaseException (SystemExit, KeyboardInterrupt, a cancellation) during AgentSandbox.run() / execute() / feed_run ends the run like a crash (WorkerCrashed, run recorded as lost); it used to reach the guest as an error that the journal did not record |
AgentSandbox, Pydeno |
Tools that raise SystemExit & co. on purpose |
Raise an Exception subclass for answers the guest should see |
Concurrent tool calls past max_inflight_host_calls - 1 wait their turn instead of failing with "more than 64 host calls in flight" |
agent sessions, Pydeno |
Guests that counted on those failures | Nothing |
SessionPool.drop() wins against an overlapping get or release; pool.session() releases only its own lease |
SessionPool |
Nobody (fixes) | Prefer async with pool.session(...) over bare release |
SessionPool.get of a live session reads the stored counter (one store read) and restores the stored journal if another pool stored a newer one; release raises StaleJournal instead of storing over a newer journal |
SessionPool |
Deployments where two pools serve one session (they now see each other's journals, or get StaleJournal) |
Route each session to one pool |
A session whose release failed stays live (not evicted) until a release stores it; ids must be valid UTF-8 |
SessionPool |
Code that let a failed release be cleaned up by eviction; ids with lone surrogates (ValueError) |
Release again after fixing the cause; pass valid strings |
PydenoSession.dump / load_session / load_snapshot (and the async ones, and snapshot.dump) take associated_data= |
new, opt-in | Nobody unless passed | Bind dumps to a tenant and a counter you keep if they leave your control |
A front-door snapshot is no longer used up by an answer the session refuses (resume(error="...")) |
Pydeno |
Nobody (fix) | Nothing |
feed_start surfaces snapshots only for functions in that feed's external_lookup; a call to another name throws a ReferenceError in the guest |
Pydeno |
Drivers that expected snapshots for names they did not declare | Declare every function the feed may call |
| Journal associated data may be up to 4096 bytes (was 1024) | agent sessions, SessionPool |
Nobody (a relaxation: 256-character non-ASCII pool ids now persist) | Nothing |
Safe to bump?
From 0.4.x to 0.5.0 (Runtime users: nothing to change)
- [ ]
import pydenofollowed bypydeno.Xstill works for every public name (it is lazy, not removed). - [ ] Your type checker accepts
get_default_runtime()returningRuntime | IsolatedRuntime. - [ ] Your test suite passes on
deno_core0.412. - [ ] If you adopt
IsolatedRuntime: setsandbox="require"and read.sandbox.
From 0.5.x to 0.6.0
- [ ] You use
Runtimeonly, and store no signed snapshots: bump; the bridge argument caps are the one thing to test. - [ ] You use
IsolatedRuntime: decide onredact_host_errorsexplicitly (the default flipped), and check anything that reads host exception text. - [ ] No
-W errorjob fails on the new degraded-sandboxRuntimeWarning(or you usesandbox="require"). - [ ] Every bound name is a plain identifier.
- [ ] You pass no
snapshot=toIsolatedRuntime. - [ ] Signed snapshots were rebuilt and re-signed with 0.6.
- [ ] Large typed arrays in guests fit
max_buffer_bytes(max_memory // 4by default). - [ ] On your deployment hosts
pydeno.sandbox_status().completeisTrue. - [ ] You handle
WorkerCrashedby cause: see Error kinds.