Skip to content

wasi-python 0.1.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 14:21
· 35 commits to main since this release
Immutable release. Only release title and notes can be modified.
  • A coverage badge from CI (spec 124): the release workflow's new coverage job runs the fast tier serially under coverage.py (7.17.0, pinned in the dev group) against the set the build job made, and uploads the Cobertura report to Codecov with the repository's CODECOV_TOKEN; the README carries Codecov's badge, and test_readme_carries_the_codecov_badge holds that it is this repository's. uv run python tests/harness/coverage.py --measure takes the same measurement locally into build/coverage/. The -V sentence and numpy's integer width are now held on docs/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 every LICENSES/*.txt of 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/LICENSE with 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 restore puts LICENSE back after a wheel built by hand; test_the_wheel_carries_every_component_licence replaces 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 --write writes it, --page names another); the command's options are generated from its parser into docs/reference/cli.md by tests/harness/cli_doc.py, and test_generated_pages_are_current holds both pages current. The 560 known gaps and suite entries whose closer was the README, and pandas' excluded marker, now name docs/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 under docs/. pyproject.toml says "Development Status :: 4 - Beta", names all seven compiled packages, gains a Documentation URL and a docs dependency group (MkDocs 1.6.1, Material 9.7.7); mkdocs.yml and .github/workflows/docs.yml build the site strictly on every pull request and push to main, and deploy it from a release tag. context/questions-for-the-owner.md is deleted; its last paragraph moves to context/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 at https://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, and doctest_depends_on not read); six sympy tests that do not end within 2.9 to 4.5 T of fuel, sympy's test_W25, and networkx's test_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' slow cases, contourpy's slow and random_big cases and CPython's cpu-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, and context/release-tier-claims.toml holds 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__.py staged, 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.py is 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 first run() it raises one ImportError naming the wasmtime-py version and every private name that moved. A rules check holds that no other .py under src/ or tests/ reaches the private API; a second holds the debt sheet context/upstream/34-wasmtime-py-public-api-for-the-runner/facts.md equal 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, and runner as the thin entry), the text moved verbatim, in a written import order with no cycle; __init__.py no longer imports runner last; a unit check finds every module-level lock and proves the fork hook re-makes it; a rule holds every module under src/ 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 _Process object, 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 --strict runs over src/wasi_python in 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.py asks nine questions of a call-context protocol and imports nothing from the runner; one _platform.py holds 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 or context/ path, every reason kept in words, held by one rules check (the owner's rule, "code speaks for itself"; tests/, scripts/ and release/ follow in spec 109); and mypy --strict is 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 under tests/, scripts/, release/ and the release workflow give their reasons in words, with no specification number, review, finding identifier or context/ path; a rules check holds it, with the judge's allowance files, the guest's own sources and the src/ check's own documentation of its shapes named as the exceptions. No code, behaviour, fuel or artifact byte changes; MANIFEST's recipe hashes of scripts/ move with their comments.

  • numpy's linear algebra, dot and matmul run on the set's OpenBLAS (spec 100; questions 106 and 107). numpy was built with no BLAS and ran its bundled lapack_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's int declarations to OpenBLAS's void subroutines, and lapack_lite's f2c code leaves the binary. A matrix of nan raises LinAlgError in svd, lstsq and eigh as natively, where it raised ValueError: On entry to DLASCL … (and lstsq, with spec 099 alone, SystemError); numpy and scipy agree on which matrices are singular; a 200×200 inverse, decomposition or product takes 23 to 55 percent less fuel, and a 3×3 product 28 percent more (OpenBLAS's call costs more than numpy's own loop at that size); python.wasm is 1,240,316 bytes smaller and the snapshot 1,237,935. A matrix singular only in exact arithmetic is a rounding question, and the README says how the guest differs there (measured on macOS arm64), and what OpenBLAS's buffer does under a tight memory cap. The oracle's numpy on macOS arm64 is now the OpenBLAS wheel.

  • tools.py is three modules behind one public facade (spec 110). wasi_python.tools keeps every public name and nothing else of weight; the schema rules (what a tool's schema may say, how one is derived from a signature, how a value is checked) live in _tool_schema.py and the host side of a call (the frames, the thread, the budget, the slot table) in _tool_host.py, each under the 1,500-line ceiling, importing in one written direction; tools.py leaves the size allowance and its two functions leave the long-function allowance, split at a seam each. No public name, message, artifact byte or fuel count changes.

  • The runner's filesystem reads along the WASI interface's seams (spec 111; the design review's section 5 and R3, R7). mounts.py keeps the public types, their two private rows and the call-context protocol; the preview1 bytes (_fs_memory.py), the call's table and its provider bridge (_fs_table.py) and the descriptor and path calls (_fs_descriptors.py, _fs_paths.py, free functions over the table, dispatched by a table equal to the shadowed signatures) are one concern each, every module under the size ceiling and every function under 80 lines; the ceiling's allowance loses mounts.py and the filesystem's import rule holds all five modules in a chain. No public name, artifact byte or guest-visible errno changes.

  • A singular matrix raises LinAlgError (spec 099; spec 066's survey). wasm has no floating-point status flags, and numpy reports a failed LAPACK call by one, so numpy.linalg.inv, solve, tensorinv, tensorsolve and matrix_power(a, -1) of a singular matrix, and cholesky of one not positive definite, returned nans. A recipe header, recipes/npy_fenv_flags.h, keeps the signals numpy raises itself in a software flag word: these raise LinAlgError whenever numpy's LAPACK (lapack_lite then; OpenBLAS since spec 100, above) finds the matrix singular; for a matrix singular only in exact arithmetic that is not always where native numpy does (LAPACKs round differently; exact parity is not a goal in wasm). The same word makes integer division by zero, integer overflow in division and float16 overflow warn, or raise under np.errstate, as natively: a widening of the owner's question, named here. Signals only the hardware gives (float division by zero, log(0), invalid casts) stay silent; the README says so.

  • A faster release run (spec 101). A dispatch on a branch other than main reuses a set the cache holds under its commit's key (release/set_key.py: every pin, every tracked file under scripts/ and recipes/, the constructor gate with its imports, the build's environment), and the same run still rebuilds the set from the pins three times on Linux and requires the same bytes; a pushed tag and main always build. The build shard runs on a fresh arm64 Linux build on every run, tags included. The macOS judge splits its two longest shards (own-hostile, own-embedding-a). .github/workflows/set-cache.yml fills main's cache on each push to main. The artifact set does not change.

  • The release runs the compiled packages' own tests (spec 066): matplotlib's, contourpy's, Pillow's, kiwisolver's, lxml's and regex's own suites, every test of each (contourpy's random_big, --runslow and --runtext cases included, and Pillow's checks/ and selftest.py), in 40 groups, 17,972 passes, each package a family with its own grant, stage and budget (caps 1.5 times the measured fuel, 1.517×10¹³ in all, each held to its recorded measurement by the rules check), in fourteen new Linux release shards; known failures are 23 classes by node id, each a gap with its closer. tests/upstream/ package_drive.py drives them; numpy's and pandas' driver and verdict are unchanged. tests/upstream/packages.toml: the six are suites; scipy's suite now closes with spec 095.

  • The OpenBLAS patch script no longer fails when macOS rewrites a .DS_Store in the unpacked tree (spec 097). scripts/make_openblas_void_patch.sh leaves host litter out of its git add by pathspec, so git never opens a litter file, and removes a staged one with git update-index --force-remove, which a rewrite cannot refuse; the carried patches are unchanged. The judge's guest folders live outside the home folder on macOS, in the per-user temporary folder, where macOS's folder-size cache does not write, so a long run's writable root is no longer voided by host litter; the sealed stages stay under build/harness, and Linux and Windows are unchanged.

  • A garbage cycle holding a kiwisolver object is collected, not a trap (spec 096; spec 066's survey). kiwisolver's Variable, Term, Expression and Constraint declared tp_clear as returning nothing, so the collector's call ended the guest in indirect call type mismatch; carried patch recipes/patches/kiwisolver-1.5.1/0002-tp-clear-returns-int.patch is upstream's fix (#245), until a kiwisolver release has it. import lxml.html.diff works: its plain-Python form lacked cython.final without Cython; carried patch recipes/patches/lxml-6.1.3/0001-fake-cython-final.patch is upstream's line (#477), until lxml 7.0. The judge's callback scan reads type slots: static type objects and their method suites, PyType_Slot arrays and module definitions, 5,893 functions on the set with scipy, where spec 051's read method and getset tables.

  • statsmodels 0.15.0 built in, with formulaic 1.2.2, interface-meta 2.0.1 and wrapt 2.5.1 (spec 045). Its 26 compiled modules are builtins and its Python package is in the zip, with native statsmodels' answers on a golden job (statistical_models): regressions, discrete and generalised linear models, tests, ARIMA and the state-space models, STL, exponential smoothing, LOWESS and KDE, with formulas in patsy's or formulaic's syntax. formulaic, its second formula engine, comes with interface-meta and wrapt, all pure wheels (wrapt runs its Python classes). One carried patch reads the datasets as package resources when they are not files, so they load from the zip: recipes/patches/statsmodels-0.15.0/0001-datasets-from-a-zip.patch (its facts sheet for the owner, context/upstream/patches/statsmodels-0.15.0/). Sixteen public-domain datasets ship; the data files of thirteen whose stated terms are not approved stay out, and their loaders raise FileNotFoundError. The state-space smoothers' and _tools' init names are renamed per file (recipes/statsmodels-renames/). What the sandbox lacks raises: X-13ARIMA-SEATS (X13NotFoundError), get_rdataset and webuse (no network), hdrboxplot (no processes). LICENSES/statsmodels.txt carries its licence, the bundled code's notices and each dataset's terms.

  • scikit-learn 1.9.1 built in (spec 044). Its 69 compiled modules are builtins and its Python package and six bundled datasets are in the zip, with native scikit-learn's answers on a golden job (machine_learning). It runs serially with the same results: no OpenMP, and every n_jobs in the calling thread. One carried patch reads its HTML representation's style sheets as package resources, so import sklearn works from the zip: recipes/patches/scikit_learn-1.9.1/0001-html-repr-styles-from-a-zip.patch (its facts sheet for the owner, context/upstream/patches/scikit_learn-1.9.1/). The names that would clash in one static link are renamed per file by recipe headers (recipes/sklearn-renames/), and step 60 gains two checks for every package: no weak definition two archives share unless a review allowed it (scripts/weak_duplicates.py, recipes/weak-duplicates-allowed.txt), and the link map shows scikit-learn's three random generators under their own names. The fetch_* datasets fail without a network (with no root, FileNotFoundError on '~'; with a root, URLError); the two sample photographs are left out, so their loaders raise ModuleNotFoundError. LICENSES/scikit-learn.txt carries each bundled component's notice and the attribution of the three datasets UCI publishes under CC BY 4.0 (digits, breast cancer, wine).

  • scipy 1.18.1, with OpenBLAS 0.3.34 as its BLAS and LAPACK, built in (spec 037). All of scipy but scipy.odr (it needs Fortran; importing it says so): linalg, sparse (SuperLU, ARPACK, PROPACK, csgraph), optimize (HiGHS's linprog and milp), integrate, interpolate, signal, fft, special, stats, spatial (qhull, cKDTree), ndimage, cluster, io and the rest, with native scipy's answers on three golden jobs. No Fortran compiler is used: OpenBLAS's own C translation of LAPACK is the LAPACK. Carried patches: recipes/patches/OpenBLAS-0.3.34/0001-c-lapack-subroutines-return-void-1-of-2.patch and recipes/patches/OpenBLAS-0.3.34/0002-c-lapack-subroutines-return-void-2-of-2.patch (OpenBLAS develop's convention, every C LAPACK subroutine void, made by our own script; merged upstream after 0.3.34), recipes/patches/OpenBLAS-0.3.34/0003-ilaver-returns-void.patch, and recipes/patches/scipy-1.18.1/0001-uarray-callback-signatures.patch (five uarray callbacks) and recipes/patches/scipy-1.18.1/0002-sqrtm-triu-imports-its-error-where-it-raises.patch (so scipy.linalg._matfuncs_sqrtm_triu imports first, which pydoc's apropos and help('modules word') do with every builtin). scipy.spatial links the one qhull matplotlib's _qhull links (step 37's 8.0.2, now with a qhull_r.pc), and scipy's own fix to its vendored copy is carried for both: recipes/patches/qhull-8.0.2/0001-format-strings.patch (upstream's merged pull request 143, unreleased). The reactor refuses freopen (qhull's TI <file> option, its one caller, fails as an unopenable file does), so the fd_renumber import matplotlib's _qhull brought goes again. OpenBLAS's kernels use wasm SIMD: hosts need simd128 (every supported wasmtime has it). Known gaps until spec 038: mmread and mmwrite raise (a thread pool); qmc.Sobol with nothing writable gives wrong numbers with exit 0 and an "Exception ignored" message (unscrambled all zeros, scrambled one point repeated; differential_evolution(init='sobol') and shgo(sampling_method='sobol') with it; give a temporary folder); geninvgauss.cdf, genhyperbolic.cdf, studentized_range and loadarff raise OSError from the ctypes stand-in. fft's workers= runs serially; differential_evolution and brute with workers other than 1, and cKDTree.query(workers=...) other than 1, raise. numpy's own LAPACK (lapack_lite) now carries a symbol prefix so it cannot meet OpenBLAS's; numpy computes as before. (Later in this release numpy links the same OpenBLAS and lapack_lite leaves the binary: spec 100, above.) The set is the same bytes from every build host with no exception: wasi-sdk 34 (spec 086) compiles scipy's ducc0 FFT to one object, so the same-bytes exception this specification built on wasi-sdk 33 was withdrawn before it landed.

  • scipy's sandbox edges work (spec 038; spec 037's three known gaps closed). With nothing writable and no thread: scipy.io.mmread and mmwrite read and write serially, since Matrix Market's default parallelism is 1 on WASI (recipes/patches/scipy-1.18.1/0003-io-matrix-market-serial-without-threads.patch); stats.qmc.Sobol, qmc_quad, sobol_indices and the Sobol options of differential_evolution and shgo read the direction numbers' bytes from the zip without a temporary file, so they no longer give wrong numbers with exit 0 (recipes/patches/scipy-1.18.1/0004-stats-sobol-without-a-temporary-file.patch); and geninvgauss.cdf, genhyperbolic.cdf, studentized_range, tukey_hsd and loadarff work. Changed: the ctypes stand-in holds values: an instance of a numeric type (c_int(5).value, c_ulong(-1).value == 4294967295, c_float rounded to single precision) or of c_void_p (a 32-bit integer, or None), and cast(x, c_void_p) of an integer, None or a c_void_p, where each raised OSError before; numpy's ndarray.ctypes.data and data_as(ctypes.c_void_p) give the array's address. An instance is false when its value is zero and unhashable, as in CPython; c_void_p takes only an int; its bytes cannot be read (bytes() and memoryview() of it raise OSError). Nothing has an address: byref, pointer, addressof, typed pointers, strings, structures and every library load still raise OSError, and -V still says "no ctypes".

  • Added: seaborn 0.13.2 and reportlab 5.0.1 (spec 042; the owner's decision 43), pure Python, with native Python's output on two golden jobs: seaborn's relational, distribution, categorical, grid, matrix and seaborn.objects plots, its regression, kernel densities, violins and clustering (through scipy, whose import import seaborn now pays); reportlab's canvas and platypus PDFs, charts through renderPDF and renderSVG, Unicode text and images. seaborn comes from its sdist, whose licences/ folder holds the notices of the five pieces of code it vendors; LICENSES/seaborn.txt carries them. reportlab is its wheel, without its fonts (their licences are not approved, and reportlab cannot read its own files from a zip on any Python from 3.13; for Unicode text register DejaVu Sans from matplotlib.get_data_path()) and, until a later change ships it, without its barcode folder. LICENSES/reportlab.txt carries the third-party notices its code holds only as comments: Tyler C. Sarna's (the barcode modules), Adobe's (the glyph list) and the QR encoder's with the MIT text. reportlab writes PDFs in memory with nothing writable (invariant=1 for repeatable bytes); renderPM raises RenderPMError (no rlPyCairo). seaborn needs a writable MPLCONFIGDIR, as matplotlib does; sns.load_dataset cannot fetch (through the runner with a writable root, its cache folder goes under the runner's fresh HOME in /tmp, removed after the call; an embedder that gives a writable root and no HOME gets a folder /~/.cache/seaborn in that root): read a granted CSV with pd.read_csv instead.

  • The judge: spec 084's read-only removal rows are blocked on Windows by a named need (spec 094). A Windows guest deletes a read-only file itself (084's C2, settled by wave 01's dry run), so the rows that check a removal keeps such a file now name the need read-only files, unmet on Windows alone, instead of skipping without a need; the release's gather allows that block on Windows only. Nothing changes for the product.

  • A desktop's files never change the build (spec 090). The build skips one shared list of names an operating system writes into folders (.DS_Store, AppleDouble ._x files, Icon\r, Thumbs.db, desktop.ini, ehthumbs.db and a volume's own folders such as .TemporaryItems), where it skipped three: in the stdlib zip's walk, the licences and the artifact set's list. The release tarball packs exactly the files the set's SHA256SUMS lists, never the folder. The judge's set and wheel checks forgive those names and no others, and the wheel may carry nothing the list does not name. Step 20 removes the bytecode CPython's build writes into Lib/ (its build-Python runs with -E, and its WASI interpreter under wasmtime sees no host variable), and the test suites' left_out fact no longer records bytecode or litter, so two trees built from the same pins record the same facts. The artifacts' bytes do not change.

  • Each guest runs on a stack of its own (spec 078; question 102). The runner runs every guest on a thread it makes for the call, with 10 MiB of stack, 8 MiB of it for wasm (it was 512 KiB on the caller's thread), and checks that thread's stack before any wasm runs. Recursion in C over nested data (repr, ==, json both ways), the parser, deep exception groups, expat's callbacks, XPath and XSLT now raise instead of ending the call in a trap; freeing nested structures, deep hash, long expressions, C14N, RelaxNG and very high recursion limits still trap, 10 to 16 times deeper (the README lists them). A recursing guest can no longer kill a host whose calling thread is small, such as musl's 128 KiB threads and run_async's pool there. Your host function and providers still run on the thread that called run. Ctrl-C now stops a guest that computes without calling back, and an embedder's SIGINT handler that raises stops the call instead of being lost. Spec 065's nine trap entries close: their tests run in their groups, test_traceback whole. Changed: each running guest holds one more thread (10 MiB of stack reserved; committed on Windows); a door or provider call costs one thread hand-off each way; every call's epoch deadline is checked every 50 ms. On CPython 3.14.0 on musl every call is refused (use 3.14.1 or later), and so is every call on a platform whose thread stack the runner cannot read (FreeBSD; any but Linux, Android, macOS and Windows). No public name, signature or default changes; fuel does not change.

  • The release's gather checks what it trusts (spec 085; the 0.1.0a7 security review's F5, F6, F7 and N5). A shard's record may report a check blocked only by a need that check's own code asks for, and only on a system that lacks it (F6). Its unit-test tally must equal its share, as whole numbers, and every passing check must carry each metric the lane accepted for it, as a number; every record must name the wasmtime uv.lock names (F7). Each acceptance's history line now records the hash of the artifact set's own SHA256SUMS, the build job hands the same hash on, and the gather requires the two to agree, so the lane's acceptance covers the snapshot, the zip, MANIFEST and the licences as well as python.wasm (F5). The rebuild jobs print the runner's own python3 --version (N5). The gather checks consistency, not authenticity: a compromised judge job could still write consistent records, though never change the bytes that are signed or published.

  • Fixed: a guest could make its private root outlive the call (spec 084; the 0.1.0a7 security review's F1). A guest given root=True that built a tree one level at a time through dir_fd, deeper than the process's free descriptors (247 levels at macOS's default soft limit of 256), left its private root and lock file behind, silently, and the next process's sweep failed the same way, so disk use added up across calls; a deep fresh HOME was left in the caller's root the same way. The runner now removes its folders with a walk that holds two descriptors at any depth, creates nothing, never follows a link and changes no mode, so content the host made read-only is left, never deleted, even when the guest moves it in. A folder a removal leaves is named in one RuntimeWarning per process; a private folder keeps its lock file and is retried by the next call's sweep (three times per process, then by the next process). A HOME in a caller's root, which another call on that root may be writing, gets a budget of about a million system calls. No public name, default or field changes.

  • Fixed: on CPython 3.11 a guest could make run raise RecursionError and keep its private root's lock for the process's life (spec 084). 3.11's shutil.rmtree recurses one frame per level, so a private root of more than about 995 levels (1,010 at Linux's common soft limit of 1,024) made the removal raise out of run. The new removal does not recurse, and a removal that fails in any way now leaves the folder with its lock free for the sweep.

  • The abandoned-turn check holds its guest instead of racing it (spec 093). Spec 078's check that a turn forced out of its home keeps its session busy read busy while the stopped guest was ending, and on Windows the guest ended first. It now holds the guest thread on a gate until it has looked, and checks that the teardown runs on the guest thread. The runner does not change.

  • The judge compares a release run with a local one (spec 092). The release's merged record now carries what the comparison needs (the checks no shard ran, the files collected, each shard's unit tally, anything that steered a shard, and which system's run each check's row comes from), and tests/harness/sameness.py reads it as the darwin run it contains. With --claims, it prints only the differences no specification's claim list explains. Judge only: no product change.

  • Changed: the toolchain is wasi-sdk 34 (clang 23.1.0, wasi-libc 2e6fb9d8 with musl 1.2.6, libc++ 23.1.0; spec 086). wasi-sdk 33's clang nested try_table blocks by heap address in one pass of its WebAssembly backend, so some C++ (scipy's FFT, coming) compiled to different bytes on every run; LLVM 23 fixed it. Every artifact's bytes change once, and every build host still gives the same SHA256SUMS, with no exception. The judge compiles the reduced case 64 times with each pinned clang and expects one object, and records every #define of the guest's pyconfig.h. Programs now see os.dup2, os.pipe2 and time.pthread_getcpuclockid, each raising OSError ENOTSUP, and select.PIPE_BUF (4096): wasi-libc 34 declares them as stubs. os.dup keeps ENOTSUP: wasi-libc 34's fcntl answers its two dup commands EINVAL on WASI preview1 while its dup answers ENOTSUP, so a new link-time refusal, recipes/fcntl_dup_refusal.c, gives dup's answer (a facts sheet for wasi-libc, context/upstream/25-wasi-libc-fcntl-dupfd/). GB18030 stays refused: musl 1.2.6's decoder still has the bug.

  • Changed: strings hash with SipHash-1-3 (spec 086; the 0.1.0 security review's S5). CPython's configure falls back to 32-bit FNV when a cross build cannot run its aligned-access probe, and the guest hashed str, bytes and memoryview with FNV, which crafted keys can flood whatever its key (PEP 456). Step 20 now passes --with-hash-algorithm=siphash13, CPython's own default: sys.hash_info.algorithm is 'siphash13' on both binaries, kept in 32 bits on wasm32. The snapshot's key stays fixed and public, which the README's Not protected list now says.

  • Fixed: a missing stdlib module says why. The zip now carries _missing_stdlib_info, which CPython's build generates beside _sysconfigdata, so import _tkinter fails with "Unsupported platform for standard library module '_tkinter'" as natively, not "was not found" (spec 086; spec 078's build review, F2).

  • The judge's inline pytest sessions keep wasmtime's signal handlers (spec 082; spec 080's review, F1). A pytest session the judge runs inside its own process ended by switching faulthandler off and on again, which dropped wasmtime's handlers on Linux, so the runner's guard refused 16 later unit tests in a serial run. The judge's pytester now runs every such session with -p no:faulthandler, and a check holds it. The runner does not change.

  • CPython's own tests in the judge and the release (spec 065). The judge runs CPython's own regrtest suite in the guest: 542 of its 557 modules in five groups, 28,109 passing tests, each known failure a class by test id with its closer, each test it does not run named, each result that differs by host recorded per system (26, most of them Windows file rules), and the eight tests where freeing or parsing deep structures still traps the guest (spec 078) each a gap. The judge states each guest clock's resolution instead of passing the host's through. The release runs the five groups on Linux in its existing upstream shards, and the file-system group on macOS and Windows too. python now names its suite in tests/upstream/packages.toml. The hostile-guest checks that read MANIFEST are blocked, not failed, without an artifact set.

  • The judge answers a file's times and inode number itself (spec 083). Its guest driver forwards the guest's file-stat and listing calls to wasmtime and answers the times from its own stepped clock and the inode numbers from a count, so guest fuel no longer moves with the host's file times or inode numbers (it did, in steps of 17 instructions), and a file the guest writes is no longer dated after the guest's clock. Each run gets its own copy of the stdlib zip instead of a hard link to the shared one, so no run sees another's link count. CPython's two test_utime_current tests now pass in the judge. The product runner is unchanged: programs run through wasi_python.run still see the host's real times and inode numbers.

  • Fixed: regex's timeout= fires past 2.1 seconds (spec 087; the 0.1.0a7 security review's F3): a second carried patch, recipes/patches/regex-2026.9.29/0002-timeout-ticks.patch, keeps regex's clock ticks in 64 bits (wasm32's Py_ssize_t is 32 bits, and wasi-libc counts 10**9 ticks a second); timeout=inf now means no timeout. Checks now hold the README's lxml DENY_ALL sentences and each 512 KiB recursion row, on an engine at wasmtime's defaults.

  • Fixed: an installed copy never loads a neighbouring artifact set (spec 087, Decision 10; the 0.1.0 security review's S3). Only a source checkout of wasi-python (src/wasi_python/ beside a pyproject.toml naming it) prefers its build/artifacts/; a copy installed with pip install --target, or vendored, inside a project uses its own set. run and Session check the set they find against its SHA256SUMS before using it, compile and grant only the bytes they checked, and raise ArtifactsMissing on a difference; a set passed as artifacts= is the caller's own, as before. No public name, signature or default changes.

  • Added: tier A of the default profile, thirty pure-Python packages (spec 039; the owner's decisions 43 to 47). By family: Excel (openpyxl 3.1.5, et-xmlfile 2.0.0, xlsxwriter 3.2.9, xlrd 2.0.2); PDF (pypdf 6.21.1); HTML and XML (beautifulsoup4 4.15.0, soupsieve 3.0.3, html5lib 1.1, webencodings 0.6.1, defusedxml 0.7.1, xmltodict 1.0.4); text formats (pyyaml 6.0.3, tomli-w 1.2.0, jinja2 3.1.6, markupsafe 3.0.4, tabulate 0.10.0, charset-normalizer 3.5.2); modelling (networkx 3.7, patsy 1.0.3); symbolic (sympy 1.14.0, mpmath 1.3.0); utilities (attrs 26.1.0, more-itertools 11.2.1, toolz 1.2.0, tqdm 4.70.1, typing-extensions 4.16.0, joblib 1.6.0, cloudpickle 3.1.2, threadpoolctl 3.7.0, narwhals 2.27.1). Each is pinned by URL and SHA-256; 28 are pure wheels (charset-normalizer's pure wheel, not its mypyc ones), and pyyaml and markupsafe, which publish no pure wheel, come from their sdists' pure package folders, so pyyaml runs without libyaml (yaml.__with_libyaml__ is False, no CLoader) and markupsafe through _native. Each family's golden job equals native Python's output. The zip grows by 2,014 members and 16.5 MB (38.7 MB); the plain binary is unchanged; a plain -c pass costs 50.6M more guest instructions (218M) and a snapshot call 4.7M more (20.4M). No package is pre-imported. Their licence notices are in LICENSES/; tqdm (MPL-2.0 AND MIT) ships with the full MPL-2.0 text and its source, the pinned wheel unchanged, as LICENSES/tqdm-4.70.1-source.whl, and sympy's notice carries the MIT notice of its LaTeX grammar.

  • Changed: the ctypes stand-in's ctypes.util.dllist() answers [] (spec 039): the guest is one statically linked module and loads no shared library, so threadpoolctl finds no native thread pools (threadpool_info() is []) instead of raising OSError.

  • The default profile's edges, documented (spec 039; the README's What the default profile carries): with nothing writable, .xlsx is written only through xlsxwriter in memory (engine_kwargs={"options": {"in_memory": True}}); a writable /tmp grant makes every writer work (decision 46). pypdf's AES needs cryptography, which is not carried (DependencyError). joblib runs serially. tqdm warns once that it cannot start its monitor thread (tqdm.tqdm.monitor_interval = 0 silences it). Two known gaps until spec 041: pandas' Styler (df.style, df.to_latex()) raises TemplateNotFound, and networkx's graph atlas raises TypeError, each reading its files by path from the zip. With jinja2 present, .style raises TemplateNotFound (a LookupError) instead of AttributeError, so hasattr(df, "style"), getattr(df, "style", None) and inspect.getmembers(df) now raise until spec 041. yaml.unsafe_load, sympy.sympify on a string, plain jinja2 templates and pickle run code from their input inside the guest, as natively; the safe forms refuse. pd.read_html of a GB18030 page whose tables lxml cannot read now falls back to bs4 and raises UnicodeDecodeError (ImportError before).

  • Fixed: pandas' Styler and networkx's graph atlas read their files from the zip (spec 041). df.style (to HTML, LaTeX, text and Typst), df.to_latex(), Styler.from_custom_template and nx.graph_atlas, nx.graph_atlas_g work, with native Python's output, where they raised TemplateNotFound and TypeError; hasattr(df, "style"), getattr(df, "style", None) and inspect.getmembers(df) work again. Two carried patches, each an upstream bug in any Python that imports the package from a zip, with a facts sheet drafted for the owner under context/upstream/patches/: recipes/patches/pandas-3.0.6/0001-styler-templates-from-a-zip.patch (jinja2's PackageLoader when the templates folder is not a directory) and recipes/patches/networkx-3.7/0001-graph-atlas-from-a-zip.patch (open the atlas resource, then decompress the stream).

  • Changed: networkx is built from its sdist (spec 041), pinned in place of its wheel, so its carried patch has a source tree: its package folder, METADATA and entry_points.txt are the wheel's byte for byte, but for networkx/conftest.py, pytest's configuration, which the zip no longer holds. make_stdlib_zip.py --entry-points packs an sdist's entry_points.txt beside its METADATA.

  • Fixed: a file a program leaves open keeps its buffered bytes, in every mode (spec 073). On the snapshot, run lost them when __main__ defined a function, or when the program hung the file on a pre-imported stdlib module or class (json.JSONEncoder.default = …); a script, a directory and -m lost them on both binaries. Now every program runs in a fresh __main__, a script and -m run in it as CPython's own entry points run them, and run on the snapshot undoes the snapshot's frozen heap before it finalizes, as a session's close does (the owner's answer to question 103), so the snapshot finalizes as CPython does: a ZipFile left open gets its directory written too. The price is the finalization: a bare snapshot call costs about 43.8M guest instructions instead of 15.6M, most of it after the program has ended. So a fuel= cap on a snapshot call must cover about 28M more (more after large imports, up to about 90M), or the call ends "out of fuel", with exit_code None, after its output was written.

  • Changed: a script's, a directory's and -m's __main__ are CPython's own (spec 073). A script's __package__ is None and its __loader__ a SourceFileLoader (a SourcelessFileLoader for a .pyc), and its __file__ is removed when it ends, as CPython does; __builtins__ is the builtins module in every mode; an atexit handler finds the program's __main__, its classes and functions; -m of a missing module prints …/python.wasm: No module named …, a directory without __main__.py prints can't find '__main__' module in …, and an -m traceback shows runpy's _run_module_as_main and _run_code frames, each as CPython does. No public name, signature or default changes.

  • matplotlib's second milestone: triangulations, contours, SVG and PDF (spec 043). matplotlib.tri.Triangulation(x, y) (Delaunay, through matplotlib's _qhull and qhull 8.0.2), and so tricontour, tricontourf, tripcolor and triplot on bare points, now work, and so do contour, contourf and clabel, through contourpy 1.4.0, built in as contourpy._contourpy with its package in the zip. A golden job saves a figure with all of them as SVG and PDF: the SVG's bytes are native matplotlib's, and the PDF's are too but for its font subset tags, which matplotlib derives from hash(), 32 bits wide in the guest. One more carried patch, recipes/patches/matplotlib-3.11.2/0002-qhull-without-devnull-on-wasi.patch: on WASI, _qhull sends qhull's messages to a stream that writes nowhere instead of opening /dev/null, which raised RuntimeError: Could not open devnull in every Delaunay triangulation and, in a writable root holding a dev folder, would have made a file there; its facts sheet is for the owner to send (matplotlib refuses AI-written pull requests). contourpy runs one thread (max_threads() is 1). The binary imports one more WASI preview1 call, fd_renumber (qhull's freopen), which the runner already shadows and counts. python.wasm grows by 1.08 MB and the zip by 19 members; import matplotlib.pyplot costs what it did. The notices gain contourpy's licence with matplotlib's LICENSE_YORICK (GIST's notice, for contourpy's mpl2005 code; question 81) and qhull's COPYING.txt.

  • HOME is never the root (spec 114; the 0.1.0 security review's H2, L1, L2 and L3). Where the runner makes no fresh HOME (no root, a read-only grant or a mount at /tmp, a mount at /), the guest's HOME is now an empty, read-only folder of the call's own, so ~ never falls back to the working folder, a writable root. Before, a reused root let one call plant ~/.reportlab_mods or ~/.reportlab_settings and a later call's bare import reportlab ran it. The stdlib zip also carries empty reportlab_mods and reportlab_settings modules, which reportlab imports before it would read either file, so it reads neither, whatever HOME the embedder passes. A reportlab_mods of the embedder's own on import_path is now shadowed; use RL_* variables. The README names what an env= HOME in a reused root still carries (pdb's .pdbrc, scikit-learn's pickled caches), matplotlib's ./matplotlibrc and a planted /etc/xml/catalog.

  • Every module cache is guarded, whatever a call's own cache_dir (spec 113; the 0.1.0 security review's H1). A call with cache_dir=False or a cache of its own no longer passes a writable grant or root over the cache other calls and processes use: the judge now refuses one at, inside or above the call's own cache, $WASI_PYTHON_CACHE and every platform default a process of this user could compute (with or without $XDG_CACHE_HOME or %LOCALAPPDATA%, under $HOME and the system's home), made or not, and through every folder on the cache's path as written, in run, run_async and Session. Before, a guest given such a grant could write a compiled module under the snapshot's public name that the next call loaded as native code. On POSIX a cache folder is used only at 0700 and a .cwasm loaded only at 0600 with one name, as the runner makes them; a file that is not is compiled again and replaced, never loaded. That stops a file a guest makes, not one it rewrites in place: the refusal is the guard. On Windows the cache refuses links and second names; it cannot tell who wrote a file, and the README says so. No public name, artifact byte or guest-visible errno changes.

  • Strings speak for themselves (spec 112). The judge's messages, help texts and titles, the goal marker, the build's refusal, the release workflow's step names and the comment in the generated inittab source give their reasons in words, with no specification number, review or constitution section; a rules check over string literals holds it, with a rule's own name, a scanner's fixture, a gap's closer and one fuel-recording guest program named as the exceptions. No behaviour, fuel or binary byte changes; MANIFEST's recipe hashes of two scripts/ files move.

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.0 \
  --source-ref refs/tags/v0.1.0 --deny-self-hosted-runners