Report vulnerabilities via GitHub security advisories on this repository (preferred) or a private issue to the maintainer. Please do not open public issues for unpatched problems.
pyproc runs real CPython inside the browser tab: Chrome's renderer sandbox plus WASM isolation. That boundary protects the user from the code (escape hardening), not your secrets from the user, and not the tab from resource exhaustion. Products still own CPU, memory, and network budgets.
A .pymachine is live interpreter state plus a boot manifest whose setup runs on open.
Treat it exactly like an executable download. open(blob, trustOpts) refuses untrusted files:
either the file carries a signature verifiable by a key you pass in trustedPublicKeys,
or you explicitly accept the risk with trust: true. Integrity is a full-envelope SHA-256
(header and payload both authenticated; the v1 format that authenticated only the delta is
rejected). Signing is WebCrypto ECDSA P-256; fingerprintStatePublicKey (from pyproc/history) gives the
stable sha256:<hex> shown in approval UIs. Signature verifies origin, not safety:
key distribution and permission UI belong to the product
(see trustPermissions, Korean).
- npm publishing uses Trusted Publishing (OIDC) with provenance; manual publishes are disabled by policy.
- The
pyproc-assetsCLI emits an SRI manifest over the worker/service-worker import graph;verifyPyProcAssetIntegrityenforces it before any worker spawns, andregisterPyProcServiceWorkerregisters the service worker only from a verified graph. - Engine boot supports
engineScriptIntegrity/coreIntegrity(fail-closed SRI on the Pyodide script and core assets) plus an OPFS offline cache that re-verifies on read.
boot({ deterministic: true }) stubs crypto.getRandomValues, Date.now, and performance.now
for the duration of the boot so replays are byte-identical; the stub is tab-global while it lasts.
Product code running concurrently in the same tab during that window would read the stub
entropy. pyproc serializes all of its own global-patching windows behind one internal
mutex, and reseeds Python's random immediately after the boundary (cp0) is captured.
If your product generates keys or nonces at page start, do it before or after machine
boot, not concurrently with it.
MachineJail's Python chokepoints are cooperative (bypassable via import js); the real
wall is the CSP (connect-src) the product applies to the jail context. Do not present
the Python tier alone as a security boundary.
Local execution and no-exfiltration are separate claims. boot() keeps computation in the
browser, but it does not by itself stop executed code from using the network. The Agent and MCP
sandbox examples finish trusted engine and package preparation, install enableJail({ net: false }),
then apply the returned jail CSP before accepting agent code. The MCP page keeps only same-origin
control traffic open. Its browser gate attempts an external request through import js and
fetch and requires the controlled receiver to observe zero requests.
The default engine distribution is fetched from a CDN before that example policy closes. Self-host
the engine when even trusted boot must make no external request. connect-src 'self' also treats the
product origin as trusted; protect same-origin endpoints with their own authorization and input
validation. MCP tool results intentionally cross that trusted control channel, so this policy is not
a confidentiality boundary against the MCP client; products still constrain and review returned
data. A same-origin parent remains a separate side channel, as described in the jail contract.
Journal recovery refuses foreign state: a generation whose replay fingerprint (h0) does
not match the current engine/manifest fails with PYPROC_REPLAY_MISMATCH instead of
silently corrupting the heap, and corrupted stores fail loudly rather than masquerading
as a first boot. A sent RPC is re-asked after leader loss only when the caller controller
can prove both a durable generation and a proxy-free session. Ordinary followers cannot
inspect the leader session and fail closed with PYPROC_RPC_OUTCOME_UNKNOWN; live-leader
timeouts and caller loss do too. The full resend and result boundary is the
durable RPC state table.