diff --git a/crates/bashkit-cpython-wasm/artifacts/MANIFEST b/crates/bashkit-cpython-wasm/artifacts/MANIFEST index 449af0507..8a1236412 100644 --- a/crates/bashkit-cpython-wasm/artifacts/MANIFEST +++ b/crates/bashkit-cpython-wasm/artifacts/MANIFEST @@ -3,5 +3,5 @@ wasi_sdk=24 wasmtime_wizer=49.0.1 zlib=1.3.1 sqlite=sqlite-amalgamation-3500400 -77058ae157db6f10e3fe9e73745147627a05bca03ee6a6c50a0a5f04ea1c7fa6 python.wasm.xz -bfe72099bf1ab706b9de9a8856f509ac5db4fb1c86c29a49c622d37490ce388e python314.zip +93b66c1e1357a3b77481fee8e14448b55ca83a0dec4b05ec0284b33b58099b75 python.wasm.xz +a2fbfc15b11fe851fffe3a84369f4d8c73992904ea23bbc045d89373737e15ea python314.zip diff --git a/crates/bashkit-cpython-wasm/artifacts/python.wasm.xz b/crates/bashkit-cpython-wasm/artifacts/python.wasm.xz index 875554ba6..bc36effeb 100644 Binary files a/crates/bashkit-cpython-wasm/artifacts/python.wasm.xz and b/crates/bashkit-cpython-wasm/artifacts/python.wasm.xz differ diff --git a/crates/bashkit-cpython-wasm/artifacts/python314.zip b/crates/bashkit-cpython-wasm/artifacts/python314.zip index bda9a819d..2a6676701 100644 Binary files a/crates/bashkit-cpython-wasm/artifacts/python314.zip and b/crates/bashkit-cpython-wasm/artifacts/python314.zip differ diff --git a/crates/bashkit-cpython-wasm/guest/build.sh b/crates/bashkit-cpython-wasm/guest/build.sh index 96688dca2..d90d28221 100755 --- a/crates/bashkit-cpython-wasm/guest/build.sh +++ b/crates/bashkit-cpython-wasm/guest/build.sh @@ -131,12 +131,21 @@ cp -r "$HERE/requests" "$WORK/stdlib/requests" cd "$WORK/stdlib" # Not usable or not useful in a sandboxed, single-threaded, headless guest. rm -rf test idlelib tkinter turtledemo ensurepip venv pydoc_data turtle.py \ - _pyrepl/__pycache__ curses mailbox.py dbm/gnu.py dbm/ndbm.py multiprocessing concurrent/futures/process.py + _pyrepl/__pycache__ curses mailbox.py multiprocessing concurrent/futures/process.py # No FFI, TLS, sockets or TTY in the guest: these can never work. rm -rf ctypes ssl.py ftplib.py imaplib.py poplib.py smtplib.py socketserver.py \ http/server.py wsgiref xmlrpc webbrowser.py _pyrepl pdb.py bdb.py pydoc.py \ _aix_support.py _android_support.py _ios_support.py _osx_support.py - # Debugger stand-in: doctest imports pdb; breakpoint() prints a notice. + # Low-use for the main workload (agents running file-processing and glue + # scripts): test/profiling/packaging tooling, macOS/legacy formats, and + # modules whose C backends the guest lacks (_bz2, _lzma, _zstd, termios, + # _interpreters). Dropped so they cost no crate bytes or preload budget. + rm -rf unittest doctest.py cProfile.py profile.py pstats.py trace.py tabnanny.py \ + pyclbr.py modulefinder.py pickletools.py compileall.py zipapp.py dbm shelve.py \ + plistlib.py wave.py netrc.py cmd.py rlcompleter.py pty.py tty.py this.py \ + antigravity.py __hello__.py __phello__ concurrent/interpreters \ + bz2.py lzma.py compression/bz2.py compression/lzma.py compression/zstd + # Debugger stand-in: breakpoint() prints a notice and continues. cp "$HERE/pdb.py" pdb.py # HTTP goes through the host's egress pipeline, not sockets # (_bashkit_http.py); http.client patches itself when first imported. diff --git a/crates/bashkit-cpython-wasm/guest/pdb.py b/crates/bashkit-cpython-wasm/guest/pdb.py index f3c980073..c47ef381d 100644 --- a/crates/bashkit-cpython-wasm/guest/pdb.py +++ b/crates/bashkit-cpython-wasm/guest/pdb.py @@ -1,9 +1,9 @@ """pdb stand-in for bashkit's CPython guest. There is no interactive terminal in the sandbox, so the real debugger is -not shipped. This stub keeps modules that import pdb (doctest, unittest -helpers) working: `breakpoint()` / `pdb.set_trace()` print a notice and -continue; `pdb.run*` raise RuntimeError. +not shipped. This stub keeps scripts that call it running: `breakpoint()` +and `pdb.set_trace()` print a notice and continue; `pdb.run*` raise +RuntimeError. """ import sys @@ -12,7 +12,7 @@ class Pdb: - """Constructible (doctest creates one per run); debugging is a no-op.""" + """Constructible; debugging is a no-op.""" def __init__(self, *args, **kwargs): pass diff --git a/crates/bashkit/docs/cpython.md b/crates/bashkit/docs/cpython.md index a3f171b5f..f7ad4e7eb 100644 --- a/crates/bashkit/docs/cpython.md +++ b/crates/bashkit/docs/cpython.md @@ -56,7 +56,7 @@ return a value. Scripts written by people and agents expect a Python | | Monty (`python` feature) | CPython (`cpython` feature) | |---|---|---| | Language | Subset (no classes, limited stdlib) | Full Python 3.14 | -| Stdlib | `math`, `pathlib`, `os.getenv`, `sys`, `typing`, ... | Full pure-Python stdlib plus `json`, `re`, `csv`, `sqlite3`, `zlib`, `hashlib`, `decimal`, `datetime`, `asyncio`, ... | +| Stdlib | `math`, `pathlib`, `os.getenv`, `sys`, `typing`, ... | Pure-Python stdlib for scripts (see [Limitations](#limitations)) plus `json`, `re`, `csv`, `sqlite3`, `zlib`, `hashlib`, `decimal`, `datetime`, `asyncio`, ... | | CLI | `-c`, file, `-` | `-c`, `-m`, file, directory with `__main__.py`, `-`, stdin, `-x`, `-W`, `-V`, `-h` | | Errors | Monty-specific text | CPython tracebacks, exit codes, `sys.exit` semantics | | Isolation | In-process Rust interpreter | WebAssembly sandbox (memory-safe boundary), fresh instance per call | @@ -210,6 +210,12 @@ with httpx.Client(base_url="https://api.example.com", headers={"X-Key": "..."}) keeps full tracebacks. Non-HTTP network clients and servers (`smtplib`, `ftplib`, `http.server`, `xmlrpc`, ...) are not shipped since the guest has no sockets. +- **Trimmed for scripts**: the stdlib targets agents running file-processing + and glue scripts. Test, profiling and packaging tools (`unittest`, + `doctest`, `cProfile`, `profile`, `trace`, `compileall`, `zipapp`, ...), + `dbm`/`shelve`, `plistlib`, `wave`, `netrc`, `cmd`, `tty`/`pty`, and + `bz2`/`lzma`/`compression.zstd` (no C codec in the guest) are not + shipped. `gzip`, `zipfile` (deflate) and `tarfile` (plain or gzip) work. - **Symlinks are not followed**, like everywhere in the Bashkit VFS. - **`errno` numbers are WASI's** (`ENOENT` is 44, not 2). Exception types (`FileNotFoundError`, ...) and messages are correct; code comparing @@ -246,7 +252,7 @@ Nothing is compiled at run time with either option. The `cpython` feature adds about 45 MB to a binary: the precompiled interpreter snapshot (~41 MB, mostly the pre-initialized 40 MB heap image so -it can be mapped copy-on-write) and the zipped stdlib bytecode (~3.5 MB) are embedded, +it can be mapped copy-on-write) and the zipped stdlib bytecode (~3.2 MB) are embedded, plus the Wasmtime runtime. Pages are mapped on demand, so resident memory per process is far smaller. diff --git a/crates/bashkit/tests/integration/cpython_integration_tests.rs b/crates/bashkit/tests/integration/cpython_integration_tests.rs index 5377549c4..886191737 100644 --- a/crates/bashkit/tests/integration/cpython_integration_tests.rs +++ b/crates/bashkit/tests/integration/cpython_integration_tests.rs @@ -529,50 +529,37 @@ async fn environ_is_replaced_each_call() { /// L-CPY-009: the stdlib is precompiled bytecode (importing a module outside /// the snapshot must not compile source on Pulley), and modules that can -/// never work in the guest are not shipped. +/// never work in the guest or are low-use for agent scripts are not shipped. #[tokio::test] async fn stdlib_is_bytecode_only() { let out = ok("python3 -c ' import email.message, http.client, xml.dom.minidom, json print(json.__file__.endswith(\".pyc\"), email.message.__file__.endswith(\".pyc\")) -for m in (\"ctypes\", \"ssl\", \"smtplib\", \"bdb\", \"pydoc\", \"_pydecimal\", \"mailbox\"): +for m in (\"ctypes\", \"ssl\", \"smtplib\", \"bdb\", \"pydoc\", \"_pydecimal\", \"mailbox\", + \"unittest\", \"doctest\", \"cProfile\", \"dbm\", \"shelve\", \"bz2\", \"lzma\", \"tty\"): try: __import__(m) print(m, \"present\") except ModuleNotFoundError: pass -import decimal, datetime +import decimal, datetime, shutil, tarfile, zipfile, gzip print(decimal.Decimal(\"1.10\") + 1, datetime.datetime.strptime(\"2020\", \"%Y\").year) '") .await; assert_eq!(out, "True True\n2.10 2020\n"); } -/// The real debugger is not shipped; a stand-in keeps doctest importable and -/// makes `breakpoint()` print a notice and continue. +/// The real debugger is not shipped; a stand-in makes `breakpoint()` print a +/// notice and continue. #[tokio::test] -async fn doctest_works_and_breakpoint_continues() { +async fn breakpoint_continues() { let r = Bash::builder() .cpython() .build() - .exec( - "cat > /m.py <<'EOF' -def add(a, b): - \"\"\" - >>> add(1, 2) - 3 - \"\"\" - return a + b -EOF -cd / && python3 -c 'import doctest, m; print(doctest.testmod(m)); breakpoint(); print(\"after\")'", - ) + .exec("python3 -c 'breakpoint(); import pdb; pdb.set_trace(); print(\"after\")'") .await .unwrap(); - assert_eq!( - r.stdout, "TestResults(failed=0, attempted=1)\nafter\n", - "{}", - r.stderr - ); + assert_eq!(r.stdout, "after\n", "{}", r.stderr); assert!(r.stderr.contains("debugger not available"), "{}", r.stderr); } diff --git a/knowledge/log.md b/knowledge/log.md index 518e8ac58..46ed254bd 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,7 @@ ## 2026-10-07 +* **Decision**: CPython's workload priority is agents running `python3` for scripted logic (processing a file, transforming data). Low-use stdlib modules are no longer shipped: `unittest`, `doctest`, profilers, `compileall`/`zipapp`, `dbm`/`shelve`, `plistlib`, `wave`, `netrc`, `cmd`, `tty`/`pty`, and `bz2`/`lzma`/`compression.zstd` (no C codec in the guest anyway). Stdlib zip 3.58 to 3.16 MB. L-CPY-009 updated. See [CPython WebAssembly Runtime](runtimes/cpython-wasm.md#workload-priority). * **Feature**: `yes` and `seq` stream into pipelines. As a producer stage they write chunks into the pipe as they go (`Context::stdout_stream`), so `yes | head -1` and `seq 1000000 | head -1` stop after the first chunk with `PIPESTATUS` `141 0` instead of running to the output cap and printing a truncation notice. Step 2 of the roadmap (streaming builtin I/O), starting with the generators. * **Feature**: `sudo` and `busybox`, the top missing commands in the gap replay (`sudo tee /etc/x <