Repository navigation
Releases: ClayGendron/wasi-python
Release list
wasi-python 0.1.1
Changed
- The platform module is split in three (spec 131).
_platform.pyheld every platform difference at 1,464 of its 1,500 lines. The Windows class, its handle structure, its constants and its three helpers are now_platform_windows.py; what the Windows and POSIX classes both run (the walk's result and its refusal, the steps taken by path, the remover's budget and its walk by path, the cache's kind checks) is_platform_common.py, below both, so the import graph stays acyclic;_platform.pykeeps the interface, the POSIX hosts and the one choice, at 828 lines. Nothing an embedder sees changes. - The module cache is read through the descriptor it was checked by (spec 135). A compiled module is loaded through the descriptor it was checked by, so the file read is the file checked; a cache whose path passes through a folder another user can write is not used, with a warning; a set given through
artifacts=is hashed and compiled from one read; a cache folder or compiled module with an access list (macOS, Linux) is not used; on Windows the file is held open from its check to its load;cache_dir=''is refused, and a relativecache_diris made absolute at the call. Running the runner with another user's home is not supported. - The pin moves to wasmtime 50 (spec 127). The wheel requires
wasmtime>=50,<51; wasmtime-py 50.0.1 bundles wasmtime 50.0.0, whosepath_renameandfd_readdircarry the fixes of the two advisories the runner had applied itself since 0.1.0a4 (a rename of a symbolic link named with a trailing slash could move a host folder into the sandbox on macOS; directory listings carried up to three bytes of host memory per entry), and whose fuel counts the work of a C++ call that ends in a caught exception, so the judge's row for it now passes. The runner's two shadows are gone, and a call without mounts passes only the three calls the cap on open files counts through the runner; the two sandbox checks now run against wasmtime's own calls. A slash-named rename on a bad descriptor or a read-only pair answers in wasmtime's own order (EBADForEPERMbeforeENOTDIR). Fuel is equal to the instruction on the snapshot and in the judge's plain-reactor checks; a guest that catches a C++ exception is now charged for the work before the catch, a few thousand instructions in the kiwisolver check. On macOS, ending a process that compiled or loaded the guest no longer costs about 5 s:wasi-python runof a one-line program takes about 0.4 s where it took 5.4 s; wasmtime 50's release notes attribute this to registering unwind information with the system in bulk. A guest that passes a file call an argument out of its type's range ends with wasmtime 50's words, which name the direction. - The build reads its pins by exact name, and the release checks every inner file (spec 136). The build selects pins by exact name, meson builds never download, every licence input is read through its pin, the release checks every file inside its tarball, and the verify command in the release notes follows the repository's name.
scripts/steps/common.sh'spinned()and its newpin_of()find a pin by its name field compared as text, once, where agreplet.match any character; every meson configure passes--wrap-mode=nodownload(HarfBuzz keeps its ownnofallback; contourpy, which has no wrap file to fetch and records its configure options in the shipped zip, is the one exemption); the download usescurl --proto '=https' --proto-redir '=https' --tlsv1.2and names a present file that does not match with the fix;scripts/assemble_artifacts.pyreads the wheels, the sympy wheel, the wasi-libc and LLVM notices and the Unicode terms throughpinned_bytes(); the release's same-bytes job checks every file of the unpacked.tar.xzagainst itsSHA256SUMS(no other file, no symlink, every published file listed), and the build job verifies the set again just before it packs it;release/version.py notestakes--repository, which the publish job sets to$GITHUB_REPOSITORY, and a unit check holds the documented command equal to the one the attest job runs. The zero-patch check now names asubprojects/folder its tarball did not ship, and.gitignorenames.env,.env.*,*.pem,*.keyand.claude/settings.local.json. The three artifacts andLICENSES/do not change;MANIFEST'srecipelines for the edited scripts do, and so doesSHA256SUMS's line forMANIFEST. - Pull requests into
devrun what their paths need (spec 138, step 2): the release workflow runs on every pull request intodevandmain, on every merge intodevand weekly, besides the tag and a dispatch. One decision job,release/ci_plan.py, reads the changed paths with git and gates every later job withif::quick(ruff, mypy, the unit tests, the rules) runs always; the judge's shards run from the cached set as the paths need (Linux and Windows for any code, macOS when the runner, the set or the harness changed, the upstream groups on Linux when the set, the suites or the harness changed); the gather is narrowed to the pairs that ran, lists a growth instead of failing on it, and leaves G7 to the release;ci-statusis the one required check. A merge intodevruns every normal-tier shard on three systems, the build shard andverifyon three. The judge jobs' matrices are generated fromtests/release_shards.toml, every reader of the build's files runs one local action, the build shard and the arm64 rebuild share one compile, andrelease.ymlis 1,316 lines, from 2,602. - The per-call comparison of matplotlib's data takes half the interpreter's time (spec 128). Every call compares the whole
mpl-datatree, 213 entries, with its record before granting it; the comparison walked the tree withos.walkand built a path object per entry, 2.3 to 2.9 ms of interpreter time per call under the zip folder's lock. It now lists withos.scandir, stamps each entry with oneos.lstatas the record does, and keeps plain strings: the same comparison, the same one stat per entry on every system, about 1 ms. On the research machine the eight-thread wall of eight hellos shortened by 14 to 33 % in three windows with a result file (their ratio to the sequence 1.5 to 1.9 before, 1.9 to 2.0 after; on a shared machine the wall is the steadier number), and a hello in sequence is one to two milliseconds shorter (benchmark facts, not checks). The memo behind it found no lock of the runner's held across a guest and none waited on: concurrent runs wait on the interpreter's lock over the runner's per-call checks, and that ceiling is named, not hidden. A check now pins that the home thread holds no process lock while its guest runs. - A guest's output is read from a pipe on macOS and Linux (spec 129). wasmtime hands a custom output its bytes 4 KiB at a time on a thread of its own, and the runner's callback there cost about 14 µs a piece, most of it the hand-off and a Python thread state made and destroyed per piece: 32 MiB of output took 160 to 240 ms, ten thousand printed lines 300 ms. wasmtime now writes each stream into a pipe the runner reads on one thread of its own per call: about 40 ms and 35 ms. The output cap holds to the byte,
truncatednames the same streams, stdout and stderr stay apart, and nothing in the API or its defaults changes. Windows keeps the callback for now. - Every merge into
devis accepted by CI (spec 138, step 3): the run after a merge judges the normal tier on three systems, and two jobs follow a green gather.accept-recordrunsuv run pytest --accept-from <record.json> --claims context/changes/with no secret, andaccept-push, in theacceptenvironment, commits the two progress files alone and pushes them ontodevwith a deploy key, only whiledevis still the commit judged. A change's CHANGELOG line, package rows and claims are its news file undercontext/changes/, whichrelease/version.py bumpassembles at a release;ci-statusrefuses a pull request intodevthat is not documentation only and has no news file, or no review file whose verdict begins "Lands", committed after its last code, by a reviewer other than its author. A documentation-only push onto a green head re-uses that head's run, and the weekly run opens an issue when it is red. - A release is a pull request from
devtomain(spec 138, step 4): its run is the whole workflow on the commitmainwill receive (a set built on macOS and never reused, both rebuilds and same-bytes, every shard of both tiers on three systems,verifyon three andquick), and its gather has every goal: noPLAN, G7 holding the newest acceptance to the run's own set, clean and whole, and each release-tier row that moved since the last release listed with the reason of a news file or of the release-tier ledger. Its ownaccept-recordaccepts the record of both tiers, and.github/workflows/accept-release.yml, started by that run's completion ondev's own context, pushes the acceptance ontodevwhiledev's tip is the head judged, red when it is not. When the acceptance lands, the release pull request's next run re-uses the green one. The release pull request is refused any commit that changes the accepted state and is not an acceptance, and its build job checks that the version's section is dated, its tag free andmain's tip an ancestor.release/version.py datedates the section ondevbefore the release pull request, joins any news file merged afterbumpand moves the ledger's blocks under the version;release/version.py releasecommits nothing, tagsmain's tip and pushes the tag alone, so the commit tagged is the one judged plus its acceptance.bump,dateand the release's two reviews land in one pull request intodev, the release's preparation, which the decision job judges as documentation, since its one change outside documentation is the version line and its change to...
wasi-python 0.1.0
-
A coverage badge from CI (spec 124): the release workflow's new
coveragejob runs the fast tier serially under coverage.py (7.17.0, pinned in thedevgroup) against the set the build job made, and uploads the Cobertura report to Codecov with the repository'sCODECOV_TOKEN; the README carries Codecov's badge, andtest_readme_carries_the_codecov_badgeholds that it is this repository's.uv run python tests/harness/coverage.py --measuretakes the same measurement locally intobuild/coverage/. The-Vsentence and numpy's integer width are now held ondocs/reference/limitations.md, not the README. -
The wheel's LICENSE carries every component licence (spec 125): before each wheel build the release workflow runs
release/licences.py append, which rewrites the checkout's LICENSE as the Apache-2.0 text followed by everyLICENSES/*.txtof the set in name order, as Pillow's wheels do; the build job checks the wheel it built, and the judge's wheel check compares.dist-info/licenses/LICENSEwith the same text. FreeType's licence ships there, and the combined LICENSE's introduction gives FreeType's credit in words, so the README no longer carries it;release/licences.py restoreputs LICENSE back after a wheel built by hand;test_the_wheel_carries_every_component_licencereplaces the README credit check. -
A short README and a clean tree (spec 122): the README is cut to about 170 lines (what it is, a five-minute quickstart, what is safe by default, what runs, hosts, the stability promise, verifying a download, building, licences), every link absolute for PyPI. The generated API reference moves to
docs/reference/python-api.md(uv run python tests/harness/api.py --writewrites it,--pagenames another); the command's options are generated from its parser intodocs/reference/cli.mdbytests/harness/cli_doc.py, andtest_generated_pages_are_currentholds both pages current. The 560 known gaps and suite entries whose closer was the README, and pandas' excluded marker, now namedocs/reference/limitations.md, a new page of the platform's limits with a reason for each; six that named the questions file name the roadmap. The scans for document citations and embedder names read the README and every page underdocs/.pyproject.tomlsays "Development Status :: 4 - Beta", names all seven compiled packages, gains a Documentation URL and adocsdependency group (MkDocs 1.6.1, Material 9.7.7);mkdocs.ymland.github/workflows/docs.ymlbuild the site strictly on every pull request and push to main, and deploy it from a release tag.context/questions-for-the-owner.mdis deleted; its last paragraph moves tocontext/decisions.md. -
The documentation site (spec 123): a Diátaxis tree under
docs/(get started, how-to guides, reference, explanation, contributing), built with MkDocs and served athttps://claygendron.github.io/wasi_python/from each release tag. -
The release runs the pure-Python packages' own tests (spec 067): each whole suite before a publication, a smoke group per package on every run, for the 42 pure-Python packages the product ships; 39 test sources (each package's sdist, or its release tag's archive where the sdist carries no tests, and pypdf's sample files) are pinned and named in
MANIFEST, and the judge ties each to the bytes that ship. Four test-source patches are carried, each making one test-only import lazy or optional so the rest of its file runs:recipes/patches/pypdf-6.21.1/0001-tests-import-ssl-where-it-is-used.patch,recipes/patches/cloudpickle-3.1.2/0001-tests-import-psutil-where-it-is-used.patch,recipes/patches/formulaic-1.2.2/0001-tests-skip-the-pyarrow-check-without-pyarrow.patch,recipes/patches/jinja2-3.1.6/0001-tests-run-the-asyncio-cases-without-trio.patch. sympy's doctests run under sympy's own doctest runner, output checker and printer setup (the setup once per session, with the stdlib's finder: module globals, anddoctest_depends_onnot read); six sympy tests that do not end within 2.9 to 4.5 T of fuel, sympy'stest_W25, and networkx'stest_asadpour_integral_held_karp, whose path is not the same on Linux, are traps, named with their reason. -
Two tiers of tests (spec 102). The slow tests run before a publication, never left out: numpy's and pandas'
slowcases, contourpy's slow andrandom_bigcases and CPython'scpu-gated tests form a release tier, judged on the tag, on the dry run on main and in the lane's release acceptance (--tier release); every other run judges the normal tier, about 4 T lighter. A row a run's tier did not run keeps what was accepted, a check that moves between tiers needs--accept-tier-move, andcontext/release-tier-claims.tomlholds the claims between two releases. -
The release runs scipy's, scikit-learn's and statsmodels' own tests (spec 095). Each suite runs whole in the release tier, 261 groups in 104 Linux release shards, about 86 T of guest fuel; a smoke group per package (two for scipy) runs in the normal tier at every check, 855 G in two Linux shards, chosen so every subpackage with compiled modules and every carried patch is touched, a rule the judge holds against the product's builtin modules. Every known failure is a class by test id with its closer; every file not run is named with its reason. The judge gains three keys for these families (the ancestors'
__init__.pystaged, test data scipy's build makes from its sdist made in the guest, a family's environment) and a clock step for a release group whose one test times a call. -
One module touches wasmtime-py's private API (spec 103).
src/wasi_python/_wasmtime_ffi.pyis the engine seam: the runner and the mounts drive the engine through its named operations, it prefers wasmtime-py's public API wherever the installed release has it, and at the firstrun()it raises oneImportErrornaming the wasmtime-py version and every private name that moved. A rules check holds that no other.pyundersrc/ortests/reaches the private API; a second holds the debt sheetcontext/upstream/34-wasmtime-py-public-api-for-the-runner/facts.mdequal to the names the seam needs. No behaviour, fuel or errno changes. -
The runner is a package (spec 104; the runner design review's H1, M5, L1).
runner.py(6,241 lines) is eleven modules, one per concern (_limits,_platform,_artifacts,_engines,_private_dirs,_zip,_places,_home,_call,_sessions, andrunneras the thin entry), the text moved verbatim, in a written import order with no cycle;__init__.pyno longer importsrunnerlast; a unit check finds every module-level lock and proves the fork hook re-makes it; a rule holds every module undersrc/to 1,500 lines. Nothing the embedder sees changes: no public name, no message, no fuel count. -
The runner's process state has one owner (spec 105): every lock, cache, counter and registry of the runner is an attribute of one
_Processobject, reset for a fork by introspection over its locks and released at exit by one method; the judge's seams are fields of that object, not module globals; two rules checks keep locks and mutables out of module level. No public name changes. -
The runner's checked call is a frozen typed record, a session's and a home's life are transition tables the judge drives whole, and
mypy --strictruns oversrc/wasi_pythonin the judge (spec 106). -
The runner's mount table, platform choice, file stamps and comments explain themselves (spec 107; the design review's H4, M3, M4 and M2).
mounts.pyasks nine questions of a call-context protocol and imports nothing from the runner; one_platform.pyholds every POSIX, macOS and Windows difference behind one interface chosen once at import; the digest and version caches in front of the artifact check key on the full file stamp, so a same-size, same-time replacement is hashed again;src/names no specification, memo, review, finding ID orcontext/path, every reason kept in words, held by one rules check (the owner's rule, "code speaks for itself";tests/,scripts/andrelease/follow in spec 109); andmypy --strictis clean over the whole package. No public name, artifact byte or guest-visible errno changes. -
A structural review is a stage (spec 108; the design review of 2026-10-20). A change that adds a module-level mutable or lock, a use of wasmtime-py's private API, a platform branch or a test seam outside the one place specs 103 to 107 give each, or takes a module past the size ceiling or a function past 80 lines, is reviewed before it is built; every release gets a structural review of
src/against the design review's eight-row rubric, beside its security review, which the judge requires of a released version; and two rules checks hold that the judge patches the runner only through its process object and that no function is over 80 lines, each with an allowance list that only shrinks. Nothing in the artifact set or the wheel changes. -
Code speaks for itself outside
src/(spec 109). Comments and docstrings undertests/,scripts/,release/and the release workflow give their reasons in words, with no specification number, review, finding identifier orcontext/path; a rules check holds it, with the judge's allowance files, the guest's own sources and thesrc/check's own documentation of its shapes named as the exceptions. No code, behaviour, fuel or artifact byte changes;MANIFEST's recipe hashes ofscripts/move with their comments. -
numpy's linear algebra,
dotandmatmulrun on the set's OpenBLAS (spec 100; questions 106 and 107). numpy was built with no BLAS and ran its bundledlapack_lite, an f2c LAPACK 3.2.2. It now finds the OpenBLAS 0.3.34 scipy uses, through numpy's own build options; a one-line recipe header (recipes/numpy_lapack_abi.h) and a file of shims (recipes/numpy_lapack_shims.c) bridge numpy'sintdeclarations to Op...
wasi-python 0.1.0a7
- 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 bui...
- Character encodings are wasi-libc's iconv, not GNU libiconv: windows-1252, ISO-8859-2
wasi-python 0.1.0a6
-
The x86_64 rebuild runs natively (spec 059). Every compile carries
-ffp-contract=off,
set once in the toolchain's ownclang.cfgandclang++.cfgby step 10, so wasi-sdk's x86_64
and aarch64 builds of clang emit the same bytes, and the x86_64 Linux rebuild compiles with its
own clang instead of the arm64 clang under qemu (an hour less per release run). Run-time
arithmetic does not change (wasm32 fuses nothing); a constant the compiler folds moves to the
value the run time computes.python.wasm, the snapshot andMANIFESTchange once; the zip
stays. The judge refuses any compiler option that would override the file (-ffp-model=,
-ffast-math,-Ofast,-funsafe-math-optimizations,--no-default-config,--config,
another-ffp-contract=) in the recipe and in every build record, greps the source trees for
a pragma that turns contraction on, and probes the compiler's own-cc1line. Spec 040's
emulated compiler stays in the tree, switched off by one line inscripts/line.conf, until
the first alpha whose native x86_64 rebuild passed has shipped. The LLVM issue draft is
narrowed tollvm.fmuladdonfp128. -
Async tools and gates (spec 053). A tool or a gate may be an
async def, or a function
that returns an awaitable. Underrun_asyncit is awaited on the event loop that awaits
run_async, so an MCP client session or an async HTTP client works as it is; underrun, on a
loop of the call's own, on a thread started at the call's first awaitable and closed when the
call ends. A call answered before its coroutine started never starts it (spec 056's fix,
extended to async tools); every later end of the call (timeout_s, the deadline, a cancel,
Ctrl-C) cancels it at its nextawait. AKeyboardInterrupt,SystemExitor
CancelledErrora tool raises, and a loop that has closed or stopped, are faults in the record,
never exceptions in the embedder's loop. The guest, the protocol and the artifact set do not
change. The README's MCP recipe is now anasync def. An async generator is refused as a tool
or a gate. -
Changed:
host=given anasync def(or an async generator, apartialof one, an
object with an async__call__) is refused withTypeErrorbefore any work. The raw door is
synchronous; it used to answer every frame with a host fault and never await the coroutine. -
A provider may raise
OSError(spec 061). Its errno, matched by name (never by number,
which differs by host), is the guest's answer, as theErrnoof that name would be. With no
errno at all,FileNotFoundError,FileExistsError,IsADirectoryError,
NotADirectoryError,PermissionErrorandTimeoutErroranswerENOENT,EEXIST,
EISDIR,ENOTDIR,EACCESandETIMEDOUT. An errno outside the closed set (EINTR,
EAGAINand the rest), whatever the class, and any other exception stay a fault,EIO. A
mapped raise leaves no fault in the record. The guest never sees the exception's message, and
the record keeps nothing of it. -
The judge runs on several cores (spec 052).
uv run pytest -n 6gives the serial
answer in about a third of the time: pytest-xdist 3.8.0 is pinned in the development group,
each check's record is made on its worker and carried to the controller, which alone writes the
run record and accepts, and an upstream group runs with its classes in one worker. The lane's
accepting run stays serial;tests/harness/sameness.pycompares two run records. Five checks
no longer depend on the order or speed of the run, and the judge's zips folder is safe when
several processes make it at once. Nothing in the artifact set or the wheel changes. -
Security: a per-run budget of tool request bytes (spec 060; the 0.1.0a4 security review's
F3; spec 049, Decision 8, exception 2).max_tool_request_bytes=onrunandrun_async, and
Defaults.tool_request_bytes, default 16 MiB, counted from each frame's length before it is
read, whatever its answer; a frame that would pass it is refused unread, and the guest gets
ToolBudgetExceededand runs on. Parsing a 1 MiB frame costs the host a few tenths of a second
of CPU, so until now only the deadline bounded one call's total. The tool list is encoded once
per call, not once perlistframe.tool()finds a partial's bound keywords through a
__call__chain of any depth and through a metaclass's__call__(the review's N8), and
refuses a__call__cycle withTypeError, where Python 3.14 raisedRecursionError. -
Changed: a new, tighter default (spec 060; spec 049, Decision 8, exception 1): a call that
sends more than 16 MiB of tool requests passes a largermax_tool_request_bytes, orNone. -
matplotlib, the first milestone (spec 023). matplotlib 3.11.2 is built in:
import matplotlib; matplotlib.use("agg"),plt.plot(...)andsavefigto PNG, PDF, PS, EPS or SVG
in a granted folder. A fixed line plot's RGBA buffer equals native matplotlib's at the same pins,
and its PNG decodes to those pixels (the judge's golden jobplot_line). Linked in: FreeType
2.14.3, HarfBuzz 14.1.0, SheenBidi 3.0.0 and libraqm 0.10.5 (matplotlib's own wrap versions, for
native pixel parity), matplotlib'sft2font,_path,_image,_backend_agg,
_c_internal_utilsand_tri, kiwisolver 1.5.1 and Pillow 13.0.0's_imaging,_imagingmath
and_imagingmorph(zlib only: JPEG and other codecs raiseOSError). In the zip: matplotlib,
mpl_toolkits, PIL, kiwisolver, fontTools 4.67.0, cycler 0.12.1, pyparsing 3.3.3 and packaging
26.3, each with its METADATA. Not yet: Delaunay triangulation (_qhullis left out of the link;
explicit triangles work) andcontour(contourpy), both the next milestone; the snapshot does not
pre-import matplotlib (the owner's question 34).- matplotlib's data travels inside the zip as one member,
matplotlib/mpl-data.zip. The
runner extracts it once per process into its private zip folder, checks every file as it
writes it, compares the whole tree with its record on every call, and grants it read-only at
/usr/local/lib/python315.zip/matplotlib/mpl-data, the path matplotlib computes. The zip
itself now sits in that folder'slib/, so/usr/local/libstill lists only the zip
(spec 017's Decision 2, amended). An embedder without the runner extracts and preopens it
itself (the README says how). - Three more carried patches.
recipes/patches/matplotlib-3.11.2/0001-ft2font-enums-without-python.patchis upstream's
merged commit8e5591bc(pull request #30291, matplotlib 3.12):ft2font's enums no longer
take the GIL during static initialization, which hung a statically linked reactor before Python
started.recipes/patches/pillow-13.0.0/0001-arrow-capsule-signatures.patchand
recipes/patches/kiwisolver-1.5.1/0001-callback-signatures.patchgive two Pillow methods and
21 kiwisolver methods and getters the signatures CPython calls them with: spec 051's callback
scan found them in the first matplotlib link, and kiwisolver's trapped the guest at
Solver.addConstraint. Facts sheets for the owner:context/upstream/17-…(nothing to send),
18-pillow-arrow-capsule-signatures/and19-kiwisolver-callback-signatures/. - The build: two new steps, 37 (the plot libraries) and 55 (Pillow, kiwisolver and
matplotlib); the pinnedpkgconfwheel in step 10;scripts/wasi-ccdrops-pthread, which
would have compiled+atomicsin; step 60 checks the static constructors and target features
of every link against the judge's reviewed lists and stops on a new one. Eighteen new pins,
among them the Unicode License V3 for HarfBuzz's tables, pinned at a commit of Unicode's own
repository. - The judge: the milestone job and its PNG decoder (stdlib only);
savefig(dpi=-1)and a
kiwisolverUnsatisfiableConstraint; the data grant refuses every write; the static
constructor list; the ten target features; fifteen source trees judged against their tarballs
(the release workflow's count is 15); every C++ build's exception flags; the licence notices of
every new component. The callback scan no longer reads a record whose function sits at a parser
rule's table index as a method definition (FreeType's BDF property table). Two known gaps,
closed by the research memo's M2.
- matplotlib's data travels inside the zip as one member,
-
Security: the release signs and publishes only what its build job made (spec 057; the
0.1.0a4 security review's F4, N1 and N3's workflow side; it closes the one-release allowance the
0.1.0a5 addendum gave F4, before this release). The build job hands on its upload's artifact id
and the SHA-256 of itsSHA256SUMS. Every job that checks, signs or publishes refuses an empty
id, downloads by that id withdigest-mismatch: error, and checksSHA256SUMS, every file, and
that nothing else is there, before it acts; until now each fetched whatever artifact was named
release-assets. The two Linux rebuilds are two jobs instead of a matrix, so one cannot hand on
the other's ids. The download step fails, naming the file, when a fetched file has no pin.
publishnames thereleaseenvironment,setup-uv's cache is off in every job, and every job
states its own least permissions under a workflow-levelpermissions: {}. The judge checks all
of it, runs the workflow's own check and guard steps, and runs the download step on fake pins.
It also refuses any other download intorelease-assetsor into a job that holds it, any
continue-on-error:, and apypijob that could run beforepublishor outside a pushed tag.
python.wasm, the snapshot and the zip do not change.MANIFESTchanges in one line, because
it records the download script's hash, so the set'sSHA256SUMSand the wheel change too.
Verify a download (gh logged in; see the README's Verifying a download):
gh...
wasi-python 0.1.0a5
The first release since 0.1.0a3. 0.1.0a4 was tagged but never published (see its section):
everything listed under 0.1.0a4 ships in this release too, including the security fixes of specs
048 and 056.
- Read-only code on the import path (spec 054). Added:
run(import_path=[…])and
run_async(import_path=[…]), andwasi-python run --import-path GUEST. Each entry, a folder
or a zip file inside a read-only grant, goes onsys.pathafter the stdlib zip. The runner
refuses, before any work, an entry the root, a writable grant or a mount serves, a writable
grant or a mount under an entry, and a writable grant or root inside the grant serving one.
No.pycis written. A module the runtime has is always the runtime's; one it looks for and
lacks (_hashlib,pytz, …) is taken from an entry, on the plain binary also for packages
the snapshot pre-imported (README, The import path). Fixed (docs): the README no longer says
a writable mount at/puts/on a-cprogram'ssys.path; nothing writable is ever
there. - A Windows grant renames a folder over a file (spec 058). The 0.1.0a4 release run stopped
on Windows, where a writable grant letsos.renamereplace a file with a folder; a mount
answersENOTDIR(54) there as on every host, and as a Linux or macOS grant does. The judge
now asserts that row's grant answer, and the listing that follows from it, exactly per system,
and the mount's answer everywhere; Linux's and macOS's assertions are kept, and each gains an
exact value. The README names it, and a second Windows difference the same run showed: a
Windows grant cannot remove or rename a file the guest holds open, sotempfile.TemporaryFile
fails there. Nothing in the runtime or the artifact set changes. - The public API is frozen and documented (spec 049).
- Added:
defaults()andset_defaults(), which replace assigning
wasi_python.runner.DEFAULTS(that still works, unpromised, until the internal name
changes);Defaultsandstats()at the top level;PROTOCOLin
wasi_python.tools.__all__;py.typed, so a type checker reads the package's hints;tool's
two overloads, so a function decorated with@toolkeeps a type; and the README's "API
reference", generated from the same render astests/public_api.txt, the judge's snapshot of
the surface. - Changed: the seventeen other
wasi_python.toolsconstants (TOOL_REQUEST_BYTESand the
rest) andwasi_python'sARTIFACT_DIR,ARTIFACTS_ENVandCHECKOUT_ARTIFACT_DIRare
private, each with a leading_; the numbers stay in the README.Defaults'memory_bytes,
deadline_s,output_bytesandfile_callsare annotated… | None,hostas returning
bytes | bytearray | memoryview, andMount.provideras aFileProvider, as the code
already accepts. A call reads the defaults once, when it is made, so aset_defaultson
another thread never gives it a mix. The README no longer nameswasi_python.runneror
DEFAULTS. - The promise, from 0.1.0, the first final release: a patch release changes nothing
public; a minor release may add; a deprecated thing keeps working for at least two final
minor releases (deprecated in 0.N, removable in 0.N+2); a tighter default, in a minor release,
and a security fix, in any release, need no deprecation. The README's "API reference" says
what is promised and what is not.
- Added:
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.0a5 \
--source-ref refs/tags/v0.1.0a5 --deny-self-hosted-runners
wasi-python 0.1.0a3
- Breaking: nothing is writable by default (the owner's rule, constitution §4.15; spec 025).
A call given no root and no grants has no/, no/tmpand noHOME: it can import,
compute, print, read the stdlib and call the door, and nothing else. Before, it got a fresh
private writable/. To keep that, passroot=True(--private-rooton the command line);
or grant the folders the program needs.root=Nonenow means no root, likeFalse. - Security: a
rootis prepared only through the folder that was checked (the 0.1.0a3
security review, S1). The runner maderoot/usr/local/libandroot/tmpby path after
checking the root, so a guest of another call with a writable folder above it could swap that
folder for a link in between, and the runner removed a file namedusr,local,libor
tmp, or made folders, outside every grant. It now works through the walked folder's
descriptor; the guest never saw the wrong folder before, and still does not. A writable grant
at, inside or above the host Python's ownstdliborsite-packagesis now refused (S2). The
patch applier refuses a source tree that holds.git(S3). np.saveandnp.loadof files work (the first carried patch,
recipes/patches/numpy-2.5.2/0001-npy-files-without-dup.patch): they raisedOSError58,
because numpy'stofile/fromfileneedos.dup, which WASI lacks. The patch is the fix numpy
is asked to take (its pull request is drafted for the owner) and goes when numpy releases it.
ndarray.tofile,np.fromfileon real files and memory maps still fail. The next §5.6 security
review reads this patch.- Carried patches (the owner's decision; constitution §4.1): a fix the project cannot wait
for may now be carried as a patch to a pinned project's source, underrecipes/patches/, with
its upstream pull request drafted undercontext/upstream/patches/. Each is reviewed, hashed in
MANIFEST, named here, and deleted once upstream releases the fix. None is carried yet; the
release check is now "the source trees are their tarballs plus the recorded patches". - Typed grants:
run(grants=[Grant(host, guest, writable=False), ...])gives the guest
more host folders, each read-only unless it sayswritable=True, nested in any order;
root=Falsegives it no/, only its grants. Every grant androotis walked folder by folder
and checked before any work: no link a guest could have made, no read-only grant inside a
writable one, no writable grant at, inside or above the runtime's own folders. After wasmtime
opens each folder, the runner checks it opened the one it checked.HOMEis made in the folder
the guest sees at/tmp, and there is none without one. - The stdlib zip is read-only and no longer copied into a root: every call sees it read-only
at/usr/local/lib, from one folder per process that its exit removes (a killed process's is
swept like its other folders). Apython315.zipthat 0.1.0a2 left in a reused root is never
read again and may be deleted. - Some
root=values are now refused (a change): a root at, inside or above the runtime's
own folders, such as the home folder, a project folder holding.venvorbuild/artifacts,
or the temporary folder itself, since its guest could change files later calls trust; a root
holdingwasi_python's or wasmtime-py's own code; a root still to be made where the module
cache will be made; and a root reached through a link in a folder you can write. A missing
root is still made. No grant, read-only either, may hold the temporary folder, which holds
every call's private folders andstdin. - Grants on the command line:
wasi-python run --grant GUEST=HOST,--grant-writable GUEST=HOST,--root HOSTand--no-root, each one ofrun's arguments. wasi-python run: a command that runs one program and prints its result as one line of
JSON, so any language can run agent code by spawning it (python -m wasi_python runis the
same). Each option is one ofrun's arguments; without options the call hasrun's defaults.- A killed process's files are cleaned up: a process killed mid-call left its private root
(the guest's files) and its stdin folder (the caller'sstdin) in the temporary folder for
good. Each now has a locked lock file beside it naming the kernel that made it; the next
wasi_python process on that kernel removes them at its first call, and never touches a live
call's folders or another kernel's. On a temporary folder without file locks, calls go on
and such folders stay, as before. trappednames a trap by its kind (a change to what it says): a stack overflow now reads
"stack overflow", not wasmtime's heading"error while executing at wasm backtrace:", and so
do"unreachable","memory out of bounds"and every other trap with a code. An error with no
trap code keeps its first line.- The released wheel is the bytes a second host rebuilt: the release workflow rebuilds the
wheel on arm64 Linux and publishes only a wheel equal to it, byte for byte, whose artifacts
match the set'sSHA256SUMS. - Run from async code:
run_asyncruns a call on a worker thread, so an async server's
event loop never waits for a guest. Cancelling the awaiting task stops the guest (checked
every 50 ms while it computes, at once when it waits or calls the door, later when other
threads hold the GIL) and frees its worker beforeCancelledErroris raised. Calls share one
pool per process ofDEFAULTS.async_workersthreads (the CPU count, at most 8).
run(cancel=event)stops a guest from any thread (trapped == "cancelled"). A process forked
after it ran a guest now refuses to run guests, instead of aborting (macOS) or losing its
deadlines; at exit,run_async's guests are stopped and its queued calls dropped. - sqlite3, bz2 and lzma are built in, from three new pins (SQLite 3.54.1, bzip2 1.0.8,
liblzma from xz 5.8.4), with no upstream patch.bz2,lzmaandtarfile'sbz2andxz
modes write the same bytes as native;sqlite3gives native's results, except the last digits
of its math functions (trigonometric, hyperbolic,atan2,log10, one-argumentlog), which
follow the guest's C library, and what changed since native's SQLite 3.53.1, such as JSON's
float text; it has the native feature set (FTS3/4/5, R-tree, Geopoly, math functions,
dbstat, percentile).shelveanddbmnow createdbm.sqlite3files, as native does.-V
now says "no ctypes, mmap, sockets, ssl or zstd":compression.zstdis the compression module
still missing. - SQLite's limits in the guest (the README's sqlite3 in the guest): one connection holds
the database, through a<db>.lockdirectory beside it, while it writes or has a statement
open, so a second connection gets "database is locked" after its busy timeout (5 s by
default);journal_mode=walstaysdeleteunlesslocking_mode=exclusive; a guest stopped
inside a transaction (killed, out of fuel, past its deadline, or byos._exit) leaves the
lock and a hot journal in a reused root for the embedder to remove; SQLite's own
random(),randomblob()and temporary names are seeded from the clock's second, so use
secrets,uuid.uuid4()oros.urandomfor IDs; a database in a read-only directory opens
only through a URI that takes no lock, such asfile:<path>?immutable=1; no
load_extension;sqlite3.threadsafetyis 0. - Sizes: the three libraries add about 2 MB to
python.wasm(SQLite is most of it); the zip
is unchanged.import bz2andimport lzmacost nothing more on the snapshot, which already
holds them;import sqlite3is not pre-imported.
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.0a3 \
--source-ref refs/tags/v0.1.0a3 --deny-self-hosted-runners
wasi-python 0.1.0a2
The second alpha moves the line to CPython 3.15.0 and wasi-sdk 33, so a C++ exception inside a
package becomes a Python exception instead of ending the guest.
- The line: CPython 3.15.0, built with wasi-sdk 33; numpy 2.5.2, pandas 3.0.6 and the
pure-Python packages are unchanged. The line's versions are named once, in
scripts/line.conf. - C++ exceptions work: numpy and pandas' C++ is compiled for WebAssembly exception handling
in its standard form (exnref). A C++ error such as pocketfft's failed allocation now raises
MemoryErrorin the program, which goes on. - Hosts must support exception handling. wasmtime 47 or later runs it by default (as
wasmtime-py does); wasmer, wazero and Node need a switch; WAMR and wasmi cannot run it. The
README's Hosts lists them. For a host without it, use0.1.0a1. - The stdlib zip's guest name is now
/usr/local/lib/python315.zip. An embedder that lays
out the root by hand must follow it;wasi_python.rundoes. - The snapshot is made by the pinned wasmtime's own
wasmtime wizer(the standalone Wizer
pin is gone), and pre-imports_colorize, which 3.15 loads lazily, so the first traceback in
a run does not pay for it. - Measured: a snapshot call costs about 6.6% more guest instructions than on 3.14 (81.9M
against 76.8M: CPython 3.15's own start-up and finalization); the stdlib zip is 6% larger. - Safe by default (
runchanges for existing callers):- with no
root, a call runs in a fresh private directory, removed afterwards; - a call has a 60 s deadline, a 1 GiB memory cap and a 16 MiB output cap per stream unless it
passes others (Nonemeans no limit); a longer job now ends withtrapped == "deadline"; - the guest's environment starts from
TMPDIR=/tmp,PYTHONTZPATH=and aHOMEmade
fresh for each call (under/tmp, removed after it), withenv=laid over it, so
os.environis no longer exactly whatenv=gave; - a call's store is closed when the call ends, trapped or not, so a trapped call no longer
holds its guest memory until Python's garbage collector runs; Completedgainstruncated, last, defaulting to().runrefuses a limit of the wrong type (TypeError) or out of range (ValueError) before
any work, where 0.1.0a1 passed it on: a negativefuelormemory_bytes, or a
memory_bytesof 263 or more, meant no limit;Truemeant 1; a deadline of 0 or less ran
one tick. Integer limits take any integer type (numpy's included) from 0 to 263 - 1; a
deadline is a positive real number of seconds (notDecimal), at most about 4.6e16.runrefuses anargv,envorstdinthe guest would see differently, before any work: a
NUL in an argument, key or value, or text that is not valid UTF-8 (ValueError); an=in a
key, or an empty key (ValueError); astrasargv, a non-stritem or entry, or anenv
that is not aMapping(TypeError).stdinmust be bytes-like:"",0,Falseand
[], which meant empty input, are now aTypeError, as any otherstralready was. Messages
never quote a value.
- with no
- Planted code never runs: only the stdlib zip is on
sys.pathandsitedoes not run
(sys.flags.no_site == 1), so a module,.pthorpyvenv.cfgone call leaves in a root is
never run by the next. The plain reactor ran them before. - The door is bounded: a frame past guest memory, a response over 2 GiB or a failing host is
a host fault (−2);runno longer raises for them. - Fuel counts the guest only:
fuel_usedand thefuelcap start after instantiation, so
they no longer include wasmtime's copy of the memory image (42.35M instructions for the
snapshot, 5.05M for the plain reactor, charged on macOS when compiled in the process and on
Windows always). Figures from 0.1.0a1 are that much higher. - A sleeping guest keeps its deadline:
time.sleepandselectno longer hold a call past
its deadline; the runner answers the guest's waits itself, stopping at the deadline. One
poll_oneofftakes at most 4,096 subscriptions (more answerENOMEM:select.poll()on more
than 2,047 descriptors), and a bad pointer answersEFAULT. - Concurrent calls share nothing: calls from many threads no longer corrupt wasmtime-py's
callback tables (after a few hundred concurrent calls, every later call raised, and a guest
could reach another call's host or output). The host functions are defined once per engine,
and outputs use the runner's own table. An exception a host raises, and Ctrl-C, now come back
to the call that caused them; Ctrl-C while a guest computed could abort the whole process. A
trap or exit inside the guest's allocator during a door call is that call's result, and its
fuel_usedisNone. Concurrent first calls compile the module once. - A guest cannot make
runraise by exhausting host file descriptors: the call's store,
which holds every descriptor the guest opened, is now closed before anything else is cleaned
up. Before,runraisedOSError(Errno 24) and left the stdin scratch file behind. - Release provenance: from this release on, every asset is attested by the release workflow
and verified before it is published; the notes say how to verify a download. - The snapshot honours extra preopens by name and in any order, as the plain reactor does;
before, it ignored them or swapped them. - The same bytes from any build host: arm64 macOS and arm64 Linux rebuild the same artifact
set from the pins, byte for byte, and the release workflow rebuilds it on an arm64 Linux runner
and comparesSHA256SUMSbefore anything is signed. x86_64 Linux is pinned and builds, but its
python.wasmis not yet byte-identical (one numpy function compiles differently there).
Every host links wasi-sdk's
host-neutral sysroot and builtins, and the build sorts in the C locale, sopython.wasm's hash
changes. Inside the guest,sysconfigandnumpy.show_config()name the build machine
build.MANIFEST'spinlines now list every supported build host's inputs.
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.0a2 \
--source-ref refs/tags/v0.1.0a2 --deny-self-hosted-runners
wasi-python 0.1.0a1
The first alpha: CPython 3.14.8 for wasm32-wasip1 with numpy 2.5.2 and pandas 3.0.6 compiled
in, built from pinned sources with no upstream patches.
- The artifact set:
python.wasm(a WASI reactor),python-snapshot.wasm(the same binary
with numpy and pandas pre-imported by Wizer), the bytecode-only stdlib zip (with
python-dateutil, tzdata and six),MANIFEST,SHA256SUMSand the licence notices. Each
rebuilds to the same bytes on one host. - The guest: two imports only, WASI preview1 and one host door (
host::call);runfollows
CPython's command line (-c,-m, a script, stdin,-u,-V). - The runner:
wasi_python.run(argv, root=...)runs one program in one call, with a host
function for the door, fuel, a deadline, a memory cap and a private compiled-module cache
(therunextra). - Known limits: numpy's default integer is 32-bit; no threads, sockets, ssl, sqlite3, bz2 or
lzma; a C++ exception aborts the guest (wasi-sdk 24); compute is about 1.3 to 2.3 times slower
than native. Built and judged on arm64 macOS; the release workflow also checks it on Linux
and Windows.