Guest filesystem¶
This page describes the file system a guest program sees: which paths exist, which are writable, where HOME and /tmp are, and how Python finds modules. Use it when a program cannot find a file, cannot write one, or imports the wrong module.
The guest is the Python program running inside the sandbox. The host is your process and your machine. The guest sees no host path unless you hand it one.
By default: nothing writable¶
With no arguments, a call has no /. It sees only two read-only places:
| guest path | what it is |
|---|---|
/usr/local/lib |
The stdlib zip, as /usr/local/lib/python315.zip: the standard library and every package in the set. |
/usr/local/lib/python315.zip/matplotlib/mpl-data |
matplotlib's data (fonts, matplotlibrc, styles), the path matplotlib itself computes. |
The program can import, compute, print and read the stdlib. A relative path fails, because there is no working folder to resolve it against. open('x', 'w'), tempfile, np.save and df.to_csv('x.csv') all fail with OSError.
You add places with three arguments: root=, grants= and mounts=.
The root: /¶
root= |
what the guest sees at / |
|---|---|
False or None (the default) |
No / at all. |
True |
A fresh private folder, writable, removed after the call (or when a session ends). |
| a host path | That host folder, writable, made if it is missing. |
The runner prepares a root's usr/local/lib and tmp itself. The stdlib zip is never in a root: every call sees it read-only at /usr/local/lib, whatever the root holds. A write, a create or a rename at or under /usr/local/lib answers EPERM, however the path is spelled.
On the command line, --private-root is root=True, --root HOST is a host path, and --no-root (or neither) is no root.
Grants: host folders at guest paths¶
A grant hands the guest one host folder at a guest path:
from pathlib import Path
from wasi_python import Grant, run
data = Path.home() / "project" / "data"
done = run(
["-c", "import os; open('/out/n.txt', 'w').write(str(len(os.listdir('/data'))))"],
grants=[Grant(data, "/data"), Grant("/tmp/job-17", "/out", writable=True)],
)
- A grant is read-only unless it says
writable=True. A read-only grant refuses every write withEPERM, which Python raises asPermissionError. - A guest path must be absolute, with no empty,
.or..part and no trailing/. It cannot be/(that is the root), nor/usr/local/libor anything under it. No two grants may share a guest path. - Grants may nest (
/dataread-only with/data/outwritable), in any order. The guest's C library picks the longest matching guest path. ..never crosses from one grant into another, and a grant does not appear in a listing of its parent.
See Grants for the checks on the host side.
Mounts: files served by your code¶
A mount is like a grant, but your own Python object, a provider, answers each file call instead of a host folder. A mount's guest path is absolute, or / for the guest's whole file system (not together with root=). A mount is read-only unless it says writable=True.
See Mounts.
/tmp, HOME and the environment¶
The guest's environment starts from three variables. Anything you pass in env= is laid over them.
| variable | value |
|---|---|
TMPDIR |
/tmp |
PYTHONTZPATH |
empty, so zoneinfo reads the tzdata in the stdlib zip |
HOME |
a fresh, empty folder for each call, removed after it (see below) |
There is no PATH variable.
/tmp is writable only when something writable serves it: a writable root (its own tmp) or a writable grant at /tmp. Where /tmp is writable, the runner makes HOME fresh inside it for each call. Where it is not, HOME is an empty, read-only folder of the call's own, so ~ never falls back to the working folder. A mount at /tmp, writable or not, gets the read-only HOME too. Pass HOME in env= if you need a writable one, at a folder no untrusted call writes.
A Session makes its HOME once, for the whole session.
Paths the guest does not have¶
The guest has no /dev/null and no /dev/urandom. Use os.urandom, secrets or uuid.uuid4() for randomness; they use real entropy.
The import path¶
Python looks for modules on sys.path. In the guest it holds one entry by default, the stdlib zip at /usr/local/lib/python315.zip.
The interpreter runs in isolated mode with site off. So PYTHONPATH in env= is not read, and no module, .pth file or pyvenv.cfg in a root runs at start-up. The runner never puts the root, a writable grant or a mount on the import path.
To import your own code, put it in a read-only grant and name its guest path in import_path= (or --import-path GUEST on the command line):
lib = Path.home() / "agent-lib" # holds helpers.py, and tools.zip with a package toolbox
done = run(
["-c", "import helpers, toolbox; print(helpers.VERSION)"],
grants=[Grant(lib, "/lib")],
import_path=["/lib", "/lib/tools.zip"],
)
- Entries go on
sys.pathafter the stdlib zip, in the order given. Each is a folder or a zip file inside a read-only grant. - Because they come after the runtime, a module the runtime has (the stdlib's, numpy's, pandas', the tool protocol's
host) is always the runtime's. Name your packages apart. - A module the runtime looks for and lacks is taken from an entry: C accelerators such as
_decimal, other platforms' modules such asfcntl, and optional dependencies such aspytz. Ship such a name only on purpose. - A missing entry, or a file that is not a zip, is skipped, as Python skips a bad
PYTHONPATHentry. - No
.pycis ever written. A valid.pycalready in an entry, made for CPython 3.15, is used.
The runner refuses, before any work, an entry served by the root, a writable grant or a mount; a writable grant or mount under an entry; and a writable grant or root inside the grant that serves an entry. See Grants.
Error numbers¶
Errors inside the guest carry WASI's numbers, not Linux's. A missing path is [Errno 44] (ENOENT), where Linux says [Errno 2]. open('') raises IsADirectoryError, not FileNotFoundError. See Results and errors.
In one line¶
By default the guest sees only the read-only stdlib at /usr/local/lib; root=, grants= and mounts= add the rest, and only read-only grants named in import_path= can add code.