This document defines the install, import, type, asset, and runtime boundaries of the published package.
The roles are split.
- This document: install, version pinning, import boundaries, runtime-asset deployment, and runtime consistency.
- capabilityMatrix.md: per-capability intrinsic value, contract state, prerequisites, runnable surface, verification, boundaries.
- trustPermissions.md:
.pymachinepublic keys, signer fingerprints, the permission UI. - resumeCatalog.md: policy for reopening product resources after a revival.
- Take it from the npm registry at an exact version. No floating ranges (
^,~,latest): an exact version plus a lockfile is what guarantees reproducibility. To move up, deliberately re-pin to a new release version. A release is one bundle of version bump, tag, GitHub Release, and npm publish (release.md). - There is no build step (native ESM). It works from
<script type="module">with no bundler. - Source pin (optional): to run a specific commit, pin a SHA with
"pyproc": "github:eddmpython/pyproc#<commit-sha>". The canonical runtime path remains an exact installed package plus same-origin engine and worker assets; a CDN module alone is not a complete Machine deployment.
Only the public package entry and named subpaths form the product contract. The per-capability export list is canonical in capabilityMatrix.md; the type contract is the bundled index.d.ts.
| Specifier | Purpose |
|---|---|
pyproc |
The six root values: boot, open, createWebComputer, checkEnvironment, PyProcError, PYPROC_ERROR_CODES |
pyproc/runtime |
For adopting a self-booted Pyodide: the Runtime value, bootRuntime (which gives a Runtime, not a machine), MemoryCapability, FileSystem, and the EngineContract/RuntimeContract checks. Available from 0.0.11 |
pyproc/assets |
Runtime-asset manifest and SRI preflight: getPyProcAssetManifest, verifyPyProcAssetIntegrity, registerPyProcServiceWorker |
pyproc/history |
The state kernel plus the store, bundle, and signature contracts |
pyproc/machine |
Web Machine host, device, store, and guest assembly detail |
pyproc/worker |
Only when a bundler or product build must reference the worker entrypoint explicitly |
pyproc/gpu, pyproc/socket, pyproc/wasi |
Demoted Experimental and Research surfaces. New Experimental subpaths are frozen |
Forbidden boundaries:
- No deep imports from
src/.... Internal file layout may change between releases. - Do not consume
Runtime.raw,HEAPU8, or Pyodide's internal FS directly. File IO goes throughRuntime.fsand heap access stays behindMemoryCapability. - Dependencies flow one way only: products depend on pyproc. pyproc imports no product UI and no domain logic.
- Do not put product UI or domain policy into pyproc. pyproc provides runtime and capabilities only.
- Worker and Service Worker files, which the browser requires to be same-origin, are handled as a deployment-asset contract rather than as public JavaScript imports.
pyprocSw.js is not a module you import; it is a static asset you register on your own origin (a Service Worker must be same-origin). Place it on your own deployment path, register it, and turn features on with query parameters (they compose):
import { registerPyProcServiceWorker } from "pyproc/assets";
const assetIntegrity = await fetch("/vendor/pyproc-assets.json").then((r) => r.json());
// Offline core cache and virtual origin (a Python server at a real URL) together
await registerPyProcServiceWorker(assetIntegrity, {
cache: true,
asgi: "/pyproc/",
coreIntegrity: "/vendor/pyodide-integrity.json",
scope: "/",
});
new VirtualOrigin(asgiServer).bind(); // now fetch("/pyproc/api/...") reaches the kernel
// Opening SAB (the process OS) on hosting that cannot set headers, such as GitHub Pages:
// register, then reload once
await registerPyProcServiceWorker(assetIntegrity, { coi: true, scope: "/" });asgi matches on the pathname prefix only, never on a substring of the whole URL. At root scope, asgi: "/pyproc/" intercepts only /pyproc/api/...; under a narrower scope it also supports pyproc/... beneath that scope. It never touches package asset paths such as /node_modules/pyproc/.... Registering at root scope requires the server to send the Service-Worker-Allowed: / header (see examples/serve.mjs).
Keeping this file in the same folder as virtualOrigin.js is a path contract. Do not assemble a navigator.serviceWorker.register() string yourself: that lets the manifest you verified and the file you actually registered drift apart.
PyProc, MachineContainer, WasiSession, and VirtualOrigin each have a Worker, SharedWorker, or Service Worker entrypoint the browser opens directly. Those files fail if they exist only at a cross-origin CDN URL, so a product deploys them on its own origin while preserving the package's src/ relative import structure.
import { getPyProcAssetManifest } from "pyproc/assets";
const manifest = getPyProcAssetManifest({ baseURL: "/vendor/pyproc/" });
// manifest.assets:
// - processWorker src/processOs/worker.js
// - machineWorker src/processOs/machineWorker.js
// - wasiWorker src/runtime/engines/wasi/wasiWorker.js
// - pyprocServiceWorker src/capabilities/pyprocSw.jsThis manifest is canonical for runtime-asset paths. A deployment pipeline uses the list as its copy set, its same-origin check, and the input to an SRI/hash manifest. v1 is a path, role, and policy contract; the actual worker import graph is connected all the way to a runtime preflight through the pyproc-assets output and the assetIntegrity option below.
In a Node deployment pipeline, the bundled CLI follows the relative import graph beyond the entrypoint and computes a per-file sha256-... SRI.
npx pyproc-assets --baseURL /vendor/pyproc/ --out public/vendor/pyproc-assets.json --copy-to public/vendor/pyprocIn the output JSON, entrypoints[].graph is the local import graph each Worker or SW actually pulls in, and files[] is the copy set with its SRI. --copy-to copies those graph files preserving their relative paths.
At runtime, hand that JSON straight in and the role's graph is fetched and SHA-256 verified before any worker is created.
import { boot } from "pyproc";
import { verifyPyProcAssetIntegrity } from "pyproc/assets";
const assetIntegrity = await fetch("/vendor/pyproc-assets.json").then((r) => r.json());
await verifyPyProcAssetIntegrity(assetIntegrity, { roles: ["processWorker"] }); // explicit preflight
const machine = await boot({ assetIntegrity });
const os = await machine.proc({ lanes: 4 });boot({ assetIntegrity }) stores the manifest on the Runtime, and the SyscallBridge and MachineContainer created from that Runtime inherit it. Things used without a Runtime - PyProc, JobControl, bootWasi - take assetIntegrity in their own options. Because a browser cannot attach SRI attributes to the sub-imports of a module Worker, this verification is a preflight before spawn, and it assumes immutable deployment assets on your own origin. For the Service Worker, registerPyProcServiceWorker() verifies the pyprocServiceWorker graph first, and pyprocSw.js?cache=1&coreIntegrity=<manifest> re-verifies script, module, wasm, and zip fetches at the SW layer.
open() is the canonical product entry. It opens the default named OPFS Machine; open({ name })
selects another durable identity shared by same-origin tabs. boot() is the explicit transient path.
import { open } from "pyproc";
const kernel = await open({
name: "workspace",
manifest: { packages: ["numpy"], setup: "import numpy", assetIntegrity },
});
await kernel.run("counter = 41");
console.log(kernel.status());This default path returns a KernelElection handle, not the transient PyprocMachine returned by
boot() and the bundle/session forms of open. Its run is asynchronous and auto-commits by
default; commit() remains an explicit force boundary.
KernelElectionis the lower contract providing one Web Locks leader, BroadcastChannel RPC, a unique participant ID, and a persistent OPFS epoch. The leader kernel lives in its own document, so it keepscrossOriginIsolatedalong with the SAB and JSPI capabilities.MachineJournalputs the WASM heap delta,/home/web, and forwarded command outcome into one generation. A completedrunsettles only after that generation commits. Commit failure becomes non-retryablePYPROC_RPC_OUTCOME_UNKNOWNbecause the live effect may already exist.- The SharedWorker-based alternative (
SharedKernel) was removed. A SharedWorker iscrossOriginIsolated=false, so it could not offer SAB interrupts, snapshot-fork, or persistent epoch recovery;open()is the single canonical multi-tab path. - A request not yet sent can wait for a ready leader and then be sent once. A sent request follows the durable RPC state table below;
durablealone never authorizes a resend. status()providesparticipantId,leaderId,epoch,role,phase,recovered,lastCommitAt,participantCount,pendingRequests,durable,autoCommit, and a conciserpcSemanticsprojection. Two leaders in the same epoch fail withPYPROC_SPLIT_BRAIN.manifest.packagesandmanifest.setupare the contract by which a new leader deterministically reproduces the same prepared environment. They are not a promise to revive, as they were, a native package installed mid-run, an open socket, a file descriptor, a DB connection, a Promise, or an arbitrary Python stack. Reopen external resources withresume.py.
This table is the semantic SSOT for a sent KernelElection request. "Portable known" means the caller controller owns a session and can prove that hostProxySurfaces() is empty. A normal follower does not own the leader's session, so its value is unknown even when the machine is durable. "Recorded" means the request outcome is in the recovered journal generation, not merely in the former leader's RAM cache.
| Event after send | Durable generation | Portable known | Outcome in recovered generation | Caller alive | Resend | Result and execution bound | Error |
|---|---|---|---|---|---|---|---|
| Response matches leader and epoch | Any | Any | Any | Yes | No | Resolve or reject with that response; one leader delivery | Leader result |
| Same request ID is delivered again to the same leader | Any | Any | RAM cache | Yes | Client does not initiate one | Return the served-cache response; do not execute again | Leader result |
| Leader changes | No | Any | No durable record | Yes | No | The former effect may or may not have run; no durable conclusion | PYPROC_RPC_OUTCOME_UNKNOWN, retryable=false |
| Leader changes | Yes | No or proxy present | Any | Yes | No | Fail closed because successor usability is not proven | PYPROC_RPC_OUTCOME_UNKNOWN, retryable=false |
| Leader changes | Yes | Yes | Yes | Yes | Once, with the same request ID | Successor returns the recorded result; no second execution | Recorded result |
| Leader changes | Yes | Yes | No | Yes | Once, with the same request ID | Successor executes against the last committed generation. The former leader may have executed only in discarded, uncommitted state; one effect enters durable history | Successor result |
| Leader stays live and the caller timer expires | Any | Any | Any | Yes | No | A late response is ignored; whether the leader ran is unknown | PYPROC_RPC_OUTCOME_UNKNOWN, retryable=false |
| Caller leaves or its browsing context disappears | Any | Any | Any | No | No | No participant continues the Promise. The leader may finish the one delivery | PYPROC_RPC_OUTCOME_UNKNOWN while leave() can still reject; otherwise no observer |
A request still waiting for a ready leader has not crossed the send boundary: PYPROC_LEADER_UNAVAILABLE is retryable there. PYPROC_RPC_OUTCOME_UNKNOWN is never retryable. A product must not issue a new request ID for the same effect unless it has its own idempotency policy. The installed-package browser gate fixes the normal follower boundary: forced leader loss rejects the in-flight call, does not replay it, and continues from the last commit. The structure gate fixes the conditional portable resend, outcome-record lookup, unsafe-heap refusal, fencing, and ordering branches.
Virtual origin boundaries (the honest wall): these are synthetic SW responses, so they differ from a real origin. tests/attempts/runtimeParity/virtualOriginBoundaryProbe.html keeps measuring this boundary in a browser. (1) Set-Cookie is not exposed as a response header and is not stored. Do not depend on cookie sessions; use explicit tokens such as an Authorization header, a bearer token, or a signed URL. (2) A WebSocket upgrade is not intercepted by the Service Worker fetch event, so it never reaches ASGI dispatch. Design bidirectional streams with a separate relay or the SocketBridge family. (3) For streaming and SSE, AsgiServer accumulates the http.response.body chunks and returns one Response, so a product needing chunk-by-chunk UI updates must not depend on this path. (4) Endpoints must be async def; there is no synchronous dispatch.
After a revival - journal, session, or image open - process resources such as file handles and DB connections are not guaranteed by a heap delta alone. A .pymachine restores the Python heap and the /home/web file bytes, but open file descriptors, sockets, and DB connections must be reopened through Init.resume(reason) and /home/web/resume.py. The resource policy is canonical in resumeCatalog.md. A signature is provenance verification, not a sandbox permission grant, so public-key distribution and the permission UI are managed separately; that policy is canonical in trustPermissions.md.
npm testchecks thatpackage.jsonexports expose only approved stable specifiers, that the public examples consume only the root API or subpath exports, and thatindex.d.tscovers the public type contract.npm run test:installedverifies the installed-package contract from an isolated browser fixture that has no repo-relative imports and exposes only the installednode_modules/pyproc.- That installed-package browser gate exercises argument-free
open(),DeviceFsfile devices, theJobControljob lifecycle, theMachineContainerchild-machine lifecycle, a force-removedopen({ name })leader across three independent browsing contexts, and a cold reopen of auto-committed heap plus/home/webplus prepared environment. It also covers the permission-jail manifest, signed.pymachineexport and open, trusted public key and wrong-key rejection, signer fingerprints, and reopening a SQLite connection from/home/web/resume.py. pyproc/runtimeis the public Runtime wrapper from 0.0.11. The internalruntime.jscore handles only the engine wrapper andRuntime.fs; the composition rootsrc/composition/runtimeApi.jsinstalls theruntimeBindings.jsregistry to provide opt-in capability factories such asenableReactive.- The
restoreLiveexecution boundary is machine-verified. Respect the boundary and restore is immediate with zero rehashing; violate it and the violation is detected automatically and promoted to the rehash path. Check which path ran through the returnedrehashed.
npm run test:package and npm run test:installed look only at the installed tarball's public specifiers, never at doc links or repo-relative imports. This table is the public surface actually verified against the installed package. The table data is canonical in installedPackageCoverage.mjs.
| Gate | Exposed specifiers | Actual public surface | Contract verified |
|---|---|---|---|
| package surface | pyproc, pyproc/assets, pyproc/history, pyproc/machine |
boot, open, createWebComputer, checkEnvironment, getPyProcAssetManifest, verifyPyProcAssetIntegrity, registerPyProcServiceWorker, a commitState/openState kernel round trip, pyproc-assets and pyproc-engine bins |
package exports, stable subpath, index.d.ts, npm files, engine preparation, CLI graph copy and SRI manifest |
| installed package - asset path | pyproc, pyproc/assets |
getPyProcAssetManifest, verifyPyProcAssetIntegrity, registerPyProcServiceWorker |
An asset manifest rooted at /node_modules/pyproc/, worker graph SRI, registration of the installed pyprocSw.js, and rejection of a bad worker SRI before spawn |
| installed package - runtime/server | pyproc |
boot, the machine runtime's enableAsgiServer, ASGI delegation wiring of the installed pyprocSw.js |
Machine boot from the installed package, a Python ASGI app, a fetch("/pyproc/...") virtual-origin round trip, the S3 timing source |
| installed package - device filesystem | pyproc |
machine runtime enableDeviceFs |
Reading and writing /dev/productState and /proc/meminfo through the Python open() file contract on an installed-package machine |
| installed package - process OS | pyproc |
the machine's proc() pool |
Running pool map and terminate on the installed worker graph, rejection of a bad worker SRI before spawn, and no collision between the SRI and the ASGI Service Worker prefix |
| installed package - shell jobs | pyproc |
fork/repl/signal on a proc({ replay }) pool |
Building an interactive namespace on the installed worker graph and running the expr &, fg, kill, terminate job lifecycle |
| installed package - machine container | pyproc |
child kernels of the machine's proc() (a setup manifest plus exec/kill) |
Spawning, running, measuring heapLen, killing a child machine on the installed worker graph, and rejecting calls after the kill |
| installed package - crash resume | pyproc |
boot({ deterministic: true }), machine history.commit/history.recover |
Leaving a reactive boundary on an installed-package deterministic machine with history.commit() and recovering product state in a new machine with history.recover() |
| installed package - immortal python machine | pyproc |
open() / open({ name }), the KernelElection handle |
The default durable auto-commit Machine plus three independent browsing contexts sharing one Python state and prepared environment, continuing after forced leader removal, and cold-reopening automatically committed heap plus /home/web after every context closes |
| installed package - permission policy | pyproc |
the machine runtime escape hatch (the setGlobal chokepoint plus the CSP connect-src) |
Enforcement of a product permission manifest (net=false, clipboard=false, home=true, workers=false) and of the Python chokepoints |
| installed package - portable machine | pyproc, pyproc/history |
boot({ deterministic: true }), open(blob), createStateKeyPair, exportStatePublicKey, fingerprintStatePublicKey, machine history.export({ signingKey }), Runtime enableInit |
Signed .pymachine plus /home/web export, signer fingerprint, untrusted and wrong-key rejection, trusted open, reopening the resume.py SQLite resource, the S4 timing source |
| installed package - web computer | pyproc |
createWebComputer |
Assembling a browser computer from the installed package alone: booting the Python guest, running code, and stopping the whole thing |
- Package-internal paths are private. Only root and documented subpath exports are public.
- UI and domain logic do not go into pyproc. pyproc provides runtime and capabilities only.
- Support: Chromium/Edge only (JSPI + SharedArrayBuffer + crossOriginIsolated). The page needs COOP/COEP headers.
- Default Pyodide: v314.0.2 (CPython 3.14) at the verified same-origin
/vendor/pyodide/path. A supplied loader owns its version and trust contract. - Owned distribution:
npx pyproc-engine --out <static-root>/vendor/pyodidedownloads the pinned full release, verifies six catalog anchors, then verifies every package file named by the trusted lock. The browser gate boots from that path and rejects third-party engine traffic.indexURLis recorded on the kernel so child workers use the same point. A CDN URL exists only as an explicit evaluation override. - Boot asset SRI: default boot pins the
pyodide.jsscript SRI and verifies fetched core assets against the built-in trust anchor.engineScriptIntegrityandcoreIntegritymay replace that policy; explicitfalsedisables it.assetIntegrityseparately verifies pyproc's Worker, SharedWorker, and WASI worker graph before spawn.registerPyProcServiceWorker()binds the Service Worker registration file to the same manifest. - The WASI session (bootWasi/WasiSession) is a separate async surface on the
pyproc/wasisubpath. It is additive and independent of the Pyodide-based surfaces (boot/Runtime/PyProc/ReactiveController). It is an opt-in for proving engine independence, withwasmURLsupplied by the caller and self-hosted under COOP/COEP. Constraints: the value bridge is JSON-serializable only (no FFI), native extensions are impossible (static linking), and a cross-engine.pymachineis not possible. For production Python the Pyodide surface is canonical. - Bundler contract: types resolve under
moduleResolution: "Bundler"withallowJs: false, and Vite emitsnew Worker(new URL(...))as a worker chunk under the installed-package gate.
This repository records only the package contract and its executable gates. Root boot() returns a
PyprocMachine; pyproc/runtime provides Runtime-only boot and loaded-engine adoption; the
public pyproc-assets executable emits hosted runtime assets. Package-internal paths are never public.
If a worker already has a self-booted Pyodide, adopt that instance rather than calling pyproc's boot() a second time:
This pattern imports pyproc/runtime, available from 0.0.11.
// In a worker that already owns a self-booted Pyodide
const py = await loadPyodide({ indexURL });
// Layer pyproc capabilities on top of it (do not create a second interpreter)
import { Runtime } from "pyproc/runtime";
const rt = new Runtime(py); // Runtime(py) wraps a loaded Pyodide instance
const asgi = rt.enableAsgiServer({ app: "app" }); // the in-kernel server
rt.setInterruptBuffer(interruptSab); // cancel a runaway synchronous UDF (SIGINT)
const raw = rt.getGlobal("myUdf");
const fn = rt.toHostValue(raw, { proxyMode: "copy", fallback: null }); // normalize to a host function
const out = fn(1, 2);
rt.destroyHostValue(raw);new Runtime(py)wraps a loaded Pyodide in an adapter. A custom engine must declareengineContractVersion,engineKind,capabilities(), and the required methods.setInterruptBuffer(sab): write a signal number into[0]of that SAB (2 is SIGINT) and running Python is cancelled. This is reachable through the contract, with no engineraw.getGlobal(name)returns the engine proxy as is. Normalize the return value into a host value withtoHostValue(raw, options)and release it withdestroyHostValue(raw)when done.toHostValue(value, { proxyMode, fallback })is the engine-neutral value bridge.proxyModeiscopyorpreserve, and the adapter translates it into engine-specific options. Without afallback, a conversion failure propagates as a throw.- The Pyodide
RuntimeandWasiSessionimplement a minimum RuntimeContract sharingruntimeContractVersion=1,runtimeKind,capabilities(),runAsync, and the global and value bridges. Synchronous execution and heap access are capability differences. - The WASI session (
bootWasi) is a separate async surface with a JSON-only value bridge. Products that depend on C extensions such as polars and pyarrow use the Pyodide engine path.
The runtime wrapper, engine contracts, and installed-package gates are the maintained wiring record.