Repository navigation
wasi-python 0.1.0a7
Pre-release-
Fixed: an async gate that raises
GeneratorExitrefuses the call (spec 075; the 0.1.0a6
security review's F1). An async gate (anasync def, a function returning an awaitable, or an
object with an async__call__) that raisedGeneratorExitor a subclass used to allow the
call, and the tool ran; an async tool that raised it was answeredokwithNone. Now the
gate's call isdeniedand the tool'stool_error, each with the fault in the record, as a
sync gate's and a sync tool's already were. The result of an awaitable counts only if it
returned. Also (spec 060's build review, N1): a tool list over the 16 MiB answer cap is refused
from its length on eachlistframe, without its answer being built. No public name, default
or answer on the wire changes. -
Changed: the snapshot pre-imports no package (decision 34, spec 069). It starts with the
interpreter and the stdlib's bootstrap modules (encodings,io,os,sys,json,csv,
re,_colorize), for every package now and later; the judge refuses a snapshot that holds
any other top-level module. A bare call costs 15.6M guest instructions instead of 83.5M; a call
that imports pandas pays it, 4.60G in all instead of 83.5M (a small numpy job about 0.7G more
than before), so afuel=cap set for a pandas job on the snapshot needs that much more. The
snapshot is 30.4 MB smaller (44.6 MB) and the wheel about 11 MB smaller. numpy, pandas and the
other packages now import with the call's environment. A session's shutdown costs about 43M
instructions when nothing beyond the stdlib was imported (about 306M before).snapshot=True
is unchanged as an argument; no public name, signature or default changes. MANIFEST's
x-preimportline is now read from the recipe. Known and unchanged:runon the snapshot
loses an unclosed file's buffered bytes when__main__defines a function (a gap until spec
073; close your files before a call ends). -
Bounded sessions (spec 055).
Sessionkeeps one instance of the runtime across turns:
session.run(code)andawait session.run_async(code)each run one turn in__main__, and
variables, functions, classes and imports carry over. A turn echoes its final expression,
reports an exception with<turn N>in its traceback, and reportssys.exit()as its exit
status while the session goes on, as a Jupyter kernel does. Each turn has its own fuel,
deadline, output cap, cancel, file-call and tool-call allowance; the session has one memory
cap, an idle bound (idle_s, default 15 minutes) and a lifetime (lifetime_s, default 1
hour), which caps every turn's deadline;max_open_filescounts across a session's turns and
its shutdown, since its store keeps what they open. A trap,os._exit, a bound,close()or a turn's
exit()orquit()ends it;session.endedsays which, and later turns raise
SessionEnded. A session's close and expiry, andexit()orquit()in a turn, run
Python's shutdown (atexit, flushes) withinshutdown_sseconds and the session's fuel, as a
Jupyter kernel's do; past that budget the session is stopped;sys.exit()keeps the session;
session.shutdown_outcomesays whether the shutdown finished, was stopped or did not run. Added:
Session,SessionEnded, threeDefaultsfields (session_idle_s,session_lifetime_s,
session_shutdown_s),stats()["sessions_open"], and two exports in both binaries,turnand
shutdown, whose contract the README gives for any embedder.runis unchanged. -
Deep recursion raises
RecursionError(spec 071). A__repr__that callsrepr(self),
lru_cacherecursion, a recursive sort key or a numpy, pandas or lxml callback used to end the
guest in a wasm trap; it now raises, on both binaries, at wasmtime's default 512 KiB stack.
The guest narrows CPython's C-stack guard to 128 KiB with CPython's own
PyUnstable_ThreadState_SetStackProtection, called from the reactor's recipe: no patch.
Changed: such recursion stops at about half the depth it reached before trapping (214
levels for the__repr__case, 40 to 250 by shape), so a program that recursed that deep and
finished now raises. Callbacks out of expat, recursion in C over very deep data, and freeing
very deep structures still trap, with the limits the README lists. The runner now states its
wasm stack, 512 KiB, instead of taking wasmtime's default, and the judge's guest driver makes
the runner's engine (fuel, epoch interruption, that stack).python.wasm, the snapshot and
MANIFESTchange; the zip does not. -
On Linux, a host's faulthandler can no longer turn a guest trap into the host's death
(spec 071). Iffaulthandlerwas enabled or disabled after the runner's first call, a guest
trap used to kill the host with SIGILL (and after a latesignal.signalforSIGILL, hang
it);runnow refuses such a call withRuntimeErrorbefore the guest runs, says why, and
names the remedy. Install faulthandler and crash reporters before the first call, or not at
all. -
The build runs only the pinned host Python (spec 079; the 0.1.0a6 security review's
F2). FreeType's configuration headers, HarfBuzz's generated sources and matplotlib's
matplotlibrcwere written by whateverpython3the build host had first onPATH, and
CPython's configure ran it once. Now step 10 puts a shim for the pinned CPython 3.15.0 first
on every step'sPATH. Behind it,scripts/build.shputs a stand-in for every other Python
name the host has, which fails the step if it is run. A build host needs no Python of its
own. The artifacts are the same bytes;MANIFEST's recipe lines change. -
The release judges in parallel shards (spec 063). The release runs numpy's and pandas' own
test suites (eleven groups, 63,718 passing tests) on Linux, one group of them on macOS and on
Windows, every product check and the runner's own tests on all three systems, and the rules and
the judge's own tooling on macOS and Linux, in eighteen parallel jobs after the build. A
gatherjob reads every shard's record by id and SHA-256 and publishes nothing unless each
shard ran exactly its share on the release's bytes, nothing failed, the probe passed on three
systems, and the lane's newest acceptance is on the samepython.wasm.uv run pytest --shard=<name>runs one shard. Every package now names its own suite, or why not yet, in
tests/upstream/packages.toml. -
lxml, with libxml2 and libxslt built in (spec 046). lxml 6.1.3's
lxml.etreeand
lxml.objectifyare builtins, over libxml2 2.15.4 and libxslt 1.1.45 with libexslt: XPath,
XSLT, XInclude, C14N, DTD, XML Schema, RELAX NG and Schematron validation, the HTML parser,
iterparse, and files read and written by path inside a grant. lxml's Python package
(lxml.html,lxml.builder,lxml.sax,ElementInclude,lxml.html.diff) is in the zip with
its METADATA. pandas'read_htmlwith noflavornow works (lxml is its first flavor), and so
doread_xmlandto_xmlwith their default parser. The judge's golden joblxml_xmlequals
native lxml at the same pins. What differs from native, as the README says:- Character encodings are wasi-libc's iconv, not GNU libiconv: windows-1252, ISO-8859-2
and -15, cp1251, KOI8-R, TIS-620 and UTF-16 round-trip every character; Shift_JIS,
ISO-2022-JP and GB2312 read a few characters as other characters; cp932, EUC-JP, Big5 and
cp950 documents fail to parse (in HTML andrecover=Truetheir text ends at the first
unreadable character, with no exception); most CJK writes raiseLookupError. GB18030
fails at its first non-ASCII byte, on purpose, because wasi-libc decodes 1,617 of its
characters wrongly without an error:recipes/iconv_refusals.c, linked into the reactor
with-Wl,--wrap=iconv_open,iconvandiconv_close, until a wasi-sdk carries musl's fix
(context/upstream/15-wasi-libc-gb18030-decoder/). Strict XML raises,recover=Truekeeps
no root, the HTML parser keeps only the ASCII text before it,read_htmlof a Chinese
table raises; no character is read wrongly in any mode. But a table that reaches the first
non-ASCII byte comes back cut there with no exception: its last row may be all NaN, a
column may turn float, and the rows and tables after it are missing (checkerror_log).
The README gives the working route: decode with Python'sgb18030codec and give lxml the
text. - XSLT reads and writes files by default. A stylesheet without an
access_control, or a
document it loads withdocument(), can read any file in a grant (external entities
included) and write into any writable grant; useetree.XSLTAccessControl.DENY_ALLfor
untrusted stylesheets. lxml's parser defaults stay the safe ones. - Left out:
lxml.isoschematron(it reads its stylesheets by file path, and their terms
are not approved; a stand-in raisesImportError;etree.Schematronworks), zlib in
libxml2 (XMLParser(decompress=True)raises; lxml compresses output through Python's
gzip),cssselectandlxml_html_clean. A catalog is read only from a grant at/etc/xml. - One more carried patch:
recipes/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.patchlets libxml2's file
input and output work withoutdup(), which WASI preview 1 does not have (without it
libxml2 does not compile forwasm32-wasip1). Its facts sheet for the owner:
context/upstream/patches/libxml2-2.15.4/0001-xmlio-without-dup-on-wasi.md. - The build: two new steps, 36 (libxml2 and libxslt, by their own configure, under an empty
environment, with the pinnedpkgconfand an empty search path) and 54 (lxml's two modules,
from the Cython 3.2.9 C its sdist ships); three new pins. libxml2's default catalog is
native Linux's/etc/xml/catalog, not a build folder, and a new judge check holds every
later package to the same rule: neither binary names the checkout or the builder's home. - Licences:
LICENSES/libxml2.txt(with the notices ofdict.c,list.cand
timsort.h),LICENSES/libxslt.txtandLICENSES/lxml.txt.
- Character encodings are wasi-libc's iconv, not GNU libiconv: windows-1252, ISO-8859-2
-
regex is built in (spec 047). regex 2026.9.29's
regex._regexis a builtin module and its
Python package is in the zip with itsMETADATA: fuzzy matching, full Unicode properties from
regex's own tables, graphemes, variable-length lookbehind andcaptures()give native regex's
answers (the golden jobregex_features). One carried patch,
recipes/patches/regex-2026.9.29/0001-callback-signatures.patch, gives five callbacks the
parameter CPython passes (Pattern.groupindex, pickling a pattern,Match.capturesdict(),
allcaptures()andallspans()each trapped the guest without it); its upstream facts sheet
iscontext/upstream/patches/regex-2026.9.29/0001-callback-signatures.md, for the owner to
send. The init function of each of the 71 cross-built builtins now runs inside CPython's
package context, set to its dotted name, as a native import from a shared library does, so a
single-phase module keeps its import name (regex._regex, and a compiled pattern pickles);
numpy's, pandas' and matplotlib's modules are multi-phase, or already named in full
(pandas._libs.json), and do not change. The judge checks every dotted
builtin's__name__on both binaries. Licence: Apache-2.0 and CNRI-Python, in
LICENSES/regex.txtwith a line naming the patched file.timeout=counts the guest's clock;
fuel and the deadline are the limits that hold. -
The judge's small follow-ups (spec 070). The README's Limits now says that a refused
discovery frame (max_tool_calls=0, or a request budget too small for it) makesfrom host import toolsraiseImportError, and a check holds it. The release scan reads a download's
path:as a path, so./release-assetsisrelease-assets; the progress history's volume
budget is tested at its exact byte, and a volume holding a carriage return is reported. Nothing
in the artifact set, the wheel's code or the guest changes. -
Security: a cap on files open at once (spec 068; the 0.1.0a2 security review's F11 and spec
055's pre-build review's F2; spec 049, Decision 8, exception 2).max_open_files=onrunand
run_async,Defaults.open_filesandwasi-python run --max-open-files, default 1,024: the
files and folders a guest holds open at once, in grants, the root and mounts alike. Past it an
open answersEMFILEbefore anything is opened, and the guest can close files and go on. Until
now a guest could open host descriptors until the process's own limit (122,870 on the
research's Mac), and sawEIOthere. The cap protects the embedder's process only within that
process's soft descriptor limit: raise it, or set the cap below it (the README's Safety);
wasi-python runraises its own. Descriptors wasmtime holds while it resolves a deep path are
not counted (Not protected). Calls without mounts now passpath_open,fd_closeand
fd_renumberthrough the runner, which forwards them to wasmtime; fuel does not change. -
Changed: a new, tighter default (spec 068; spec 049, Decision 8, exception 1): a program
that holds more than 1,024 files open at once passes a largermax_open_files, orNone. -
Windows judges a grant's path as Linux and macOS do (spec 076). A grant or
rootwith
..after a missing folder or a file (new\..\x) is now refused on Windows too, before any
work; before, Windows dropped the..by its text, madexand ran the guest. On Windows a
host path with a name that holds:, ends in a space or a period, or is a reserved device
name (NUL,COM1and the rest, andCOM¹toCOM³andLPT¹toLPT³; spec 077) is
refused, since Windows rewrites such names and the runner's checks could then judge other
folders than the ones granted (a spelling the embedder gives, never the guest). So is a name
that holds<,>,",|,?,*or a control character, and, out of caution,CONIN$
orCONOUT$: the refused names are exactly those Python'sntpath.isreservedrefuses (spec
077), and arootstill to be made through one is refused before any folder is made. A path rooted with no drive takes the current folder's drive. The
README states the one errno that differs by system on a grant: a write through a descriptor
opened only to read (or the reverse) isEBADFon Linux and macOS andEACCESon Windows,
wasmtime's own mapping of the host's error.
Verify a download (gh logged in; see the README's Verifying a download):
gh attestation verify <asset> --repo ClayGendron/wasi_python \
--signer-workflow ClayGendron/wasi_python/.github/workflows/release.yml@refs/tags/v0.1.0a7 \
--source-ref refs/tags/v0.1.0a7 --deny-self-hosted-runners